A lightweight TLS-terminating TCP and HTTP reverse proxy written in Go.
tlspxy sits in front of your services and handles TLS termination, mutual TLS, and request proxying for both raw TCP and HTTP traffic.
- TCP and HTTP/HTTPS reverse proxy modes, with HTTP/2 and ALPN
- SNI-based certificate selection and zero-downtime cert reload (SIGHUP or automatic file watching)
- Mutual TLS on both sides: client cert require/verify, backend client certs
- Let's Encrypt automatic certificates
- HAProxy PROXY protocol (v1/v2) to preserve client IPs in TCP mode
- Prometheus metrics, health check endpoint, structured logging
- Single static binary
Create config.yaml:
#tlspxy
server:
addr: ":8443"
type: tcp
tls:
cert: /path/to/server.crt
key: /path/to/server.key
remote:
addr: "127.0.0.1:8080"
tls:
enable: falseNote: Config files must start with
#tlspxyon the first line to be auto-discovered.
Run it:
tlspxy -config config.yamlAdd -validate to check the config and print the resolved settings without starting. -version prints version and commit.
Configuration is loaded in layers, each overriding the previous:
- Built-in defaults
- YAML files in the working directory (auto-discovered by
#tlspxyheader) - YAML files/directories given via
-config(a directory loads all its#tlspxyfiles) - Environment variables: prefix
TLSPXY_, dots become underscores, uppercase (remote.addr→TLSPXY_REMOTE_ADDR) - CLI flags: dots become dashes (
remote.addr→-remote-addr)
#tlspxy
server:
addr: ":9898" # Listen address
type: "tcp" # Proxy mode: tcp, http, or https
healthcheck: "" # Health check path (HTTP mode only, e.g. "/healthz")
maxconns: 0 # Max concurrent connections (0 = unlimited)
http2: false # Enable HTTP/2 (http/https modes only)
trustxff: false # Append to inbound X-Forwarded-For (only enable behind a trusted upstream proxy)
timeouts:
read: "0s" # Read timeout per connection (e.g. 30s, 5m)
write: "0s" # Write timeout per connection
idle: "300s" # Idle timeout before closing connection
handshake: "10s" # Client TLS handshake timeout (TLS listeners only)
tls:
cert: "" # Path to server TLS certificate
key: "" # Path to server TLS private key
ca: "" # Path to CA cert for client verification
require: false # Require client certificates
verify: false # Require AND verify client certificates (overrides require)
autoreload: false # Watch cert/key files and reload automatically on change
minversion: "" # Minimum TLS version: 1.0, 1.1, 1.2, 1.3 (default: 1.2)
maxversion: "" # Maximum TLS version (default: Go default, currently 1.3)
ciphersuites: "" # Comma-separated cipher suite names (default: Go defaults)
alpn: "" # Comma-separated ALPN protocols (e.g. "h2,http/1.1")
letsencrypt:
enable: false # Enable automatic Let's Encrypt certificates
domain: "example.org" # Domain for the certificate
email: "" # Email for expiry notifications
cachedir: "/tmp/letsencrypt" # Certificate cache directory
sni: # SNI-based certificate selection (YAML only)
- hostname: "app.example.com"
cert: /path/to/app.crt
key: /path/to/app.key
remote:
addr: "" # Backend address (host:port for TCP, URL for HTTP)
proxyprotocol: "" # Send HAProxy PROXY protocol header to backend: "", v1, or v2 (TCP mode only)
timeouts:
dial: "10s" # Backend dial (connection establishment) timeout; also bounds backend TLS handshake
tls:
enable: true # Use TLS when connecting to the backend
verify: true # Verify backend certificate (false = InsecureSkipVerify; not for production)
cert: "" # Client certificate for backend mTLS
key: "" # Client key for backend mTLS
ca: "" # Custom CA for backend verification
sysroots: true # Include system CA roots
minversion: "" # Minimum TLS version for backend
maxversion: "" # Maximum TLS version for backend
ciphersuites: "" # Comma-separated cipher suites for backend
alpn: "" # Comma-separated ALPN protocols for backend
log:
level: "info" # Log level: debug, info, warning, error
contents: false # Log proxied data content at debug level (very verbose)
destination: "stdout" # stdout, /path/to/file, or syslog://address
metrics:
enable: false # Enable Prometheus metrics
addr: ":9090" # Metrics server listen address
path: "/metrics" # Metrics endpoint pathNotes:
- Durations use Go syntax (
30s,5m); invalid values fail validation at startup."0s"/ empty means unbounded or disabled. - Cipher suite names must match Go's
crypto/tlsnaming (e.g.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384). TLS 1.3 suites are not configurable — Go always uses the mandatory ones. server.maxconnslimits accepted connections. In HTTP/2 mode multiple streams share one connection; use request-level limits if you need per-request control.- SNI config is YAML-only (no flag/env equivalent).
Set server.tls.cert and server.tls.key together to terminate TLS. If neither is set (and Let's Encrypt is disabled), the server runs without TLS. If TLS is requested but the cert/key/CA fails to load, startup fails — there is no silent fallback to plaintext.
For TLS listeners, tlspxy completes the client handshake (bounded by server.timeouts.handshake) before dialing the backend, so bare TCP probes and port scanners never consume a backend connection.
Certificates loaded from files can be rotated with zero downtime — existing connections keep the old cert, new connections get the new one, and a cert that fails to load is ignored in favor of the previous one. Two mechanisms:
- SIGHUP: replace the files on disk, then
kill -HUP $(pidof tlspxy). Reloads the default and all SNI certificates. - Automatic (
server.tls.autoreload: true): watches the cert/key files and reloads on change, debounced. Directory-level watching means atomic rename and symlink swaps are detected — recommended for Kubernetes mounted secrets and certbot-style rotation.
Neither applies under Let's Encrypt, which manages its own certificate lifecycle.
server:
tls:
cert: "/path/to/default.crt"
key: "/path/to/default.key"
sni:
- hostname: "app.example.com"
cert: /path/to/app.crt
key: /path/to/app.keyExact hostname match first, then the default certificate.
server:
tls:
cert: "/path/to/server.crt"
key: "/path/to/server.key"
ca: "/path/to/client-ca.crt"
verify: truerequire: true demands a client certificate without verifying it; verify: true demands one that validates against the configured CA (and overrides require).
server:
tls:
letsencrypt:
enable: true
domain: "proxy.example.com"
cachedir: "/var/cache/letsencrypt"Certificates are obtained and renewed automatically. The server must be reachable on port 443 for the ACME challenge.
Backend connections use TLS by default, verified against system roots. Common variations, all under remote.tls:
- Custom CA: set
cato the CA file; setsysroots: falseto trust only that CA. - Backend mTLS: set
certandkeyto present a client certificate. - Plaintext backend:
enable: false. - Skip verification:
verify: false(setsInsecureSkipVerify; not for production).
Version, cipher suite, and ALPN constraints use the same keys and syntax as the server side.
The proxy sets X-Real-IP to the client peer address, X-Forwarded-Host to the inbound Host, and X-Forwarded-Proto to https when the client connection was TLS-terminated.
By default tlspxy treats itself as the trust boundary and replaces X-Forwarded-For with the real peer, discarding any client-supplied (spoofable) value. Set server.trustxff: true only when tlspxy sits behind another trusted proxy — then the peer is appended to the inbound header instead.
In TCP mode the backend normally sees only tlspxy's address. Set remote.proxyprotocol to v1 (text) or v2 (binary) to prepend a HAProxy PROXY protocol header to each backend connection, carrying the real client source and destination addresses:
server:
type: tcp
remote:
addr: "127.0.0.1:8080"
proxyprotocol: "v1"The backend must be configured to accept it (nginx proxy_protocol, HAProxy accept-proxy). TCP mode only; validation rejects it for http/https.
server:
type: https
http2: true
tls:
cert: "/path/to/server.crt"
key: "/path/to/server.key"
alpn: "h2,http/1.1"Configures both the server and the backend transport for HTTP/2. Requires server.type http/https; set alpn so clients negotiate h2.
Enable with metrics.enable: true (served on metrics.addr at metrics.path):
| Metric | Type | Description |
|---|---|---|
tlspxy_connections_active |
Gauge | Currently active proxy connections |
tlspxy_connections_total |
Counter | Total connections accepted |
tlspxy_bytes_sent_total |
Counter | Total bytes sent to the backend |
tlspxy_bytes_received_total |
Counter | Total bytes received from the backend |
tlspxy_errors_total |
Counter (labeled) | Errors by type (connection, http) |
In HTTP/HTTPS mode, set server.healthcheck: "/healthz" to serve 200 OK / {"status":"ok"} on that path. This is a proxy liveness check — it does not probe the backend. All other paths are proxied normally.
Structured logging via log/slog. log.destination accepts stdout, a file path, or syslog://address (Linux, macOS, Windows). log.contents: true logs proxied payloads at debug level — very verbose, debugging only.
Complete configurations in contrib/examples/:
| Example | Description |
|---|---|
basic-tcp.yml |
Minimal TCP proxy with TLS termination |
http-reverse-proxy.yml |
HTTP reverse proxy with health check |
letsencrypt.yml |
Let's Encrypt automated certificates |
mutual-tls.yml |
mTLS with client cert verification |
sni-multi-domain.yml |
SNI-based multi-domain certs |
http2.yml |
HTTP/2 with ALPN and TLS backend |
strict-tls.yml |
Hardened TLS 1.3 only |
Requires Go 1.24+.
make build # static binary in bin/
make test # go test -race ./...Commits follow Conventional Commits (linted in CI); releases are tagged and published automatically from commit history.