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 authentication, and request proxying for both raw TCP and HTTP traffic.
- TCP and HTTP/HTTPS reverse proxy modes
- HTTP/2 support with ALPN negotiation
- Configurable TLS versions and cipher suites (server and backend)
- SNI-based certificate selection for multi-domain hosting
- Certificate hot-reload via SIGHUP (zero-downtime cert rotation)
- Mutual TLS (mTLS) with client certificate require/verify
- Let's Encrypt automatic certificate provisioning
- Prometheus metrics for connections, bytes transferred, and errors
- Health check endpoint (HTTP mode)
- Structured logging via
log/slogwith configurable levels and destinations - Graceful shutdown on SIGINT/SIGTERM
- Config validation with
--validateflag - Docker support with multi-stage Alpine-based image
Create a config file 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 the proxy:
tlspxy -config config.yamlValidate config without starting:
tlspxy -config config.yaml -validate# Build
make docker
# Run
docker run -v /path/to/config.yaml:/etc/tlspxy.yaml \
-p 8443:8443 \
elcolio/tlspxy:latest -config /etc/tlspxy.yamlConfiguration is loaded in layers, with each layer overriding the previous:
- Built-in defaults
- YAML files in the working directory (auto-discovered by
#tlspxyheader) - YAML files/directories specified via
-config - Environment variables
- CLI flags
#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)
timeouts:
read: "0s" # Read timeout per connection (e.g. 30s, 5m)
write: "0s" # Write timeout per connection
idle: "300s" # Idle timeout before closing connection
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)
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)
tls:
enable: true # Use TLS when connecting to the backend
verify: true # Verify backend certificate
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 (use with caution)
destination: "stdout" # Log destination: stdout, file path, or syslog://address
metrics:
enable: false # Enable Prometheus metrics
addr: ":9090" # Metrics server listen address
path: "/metrics" # Metrics endpoint pathAll environment variables use the TLSPXY_ prefix to avoid collisions with standard variables (e.g., REMOTE_ADDR, PATH). The prefix is stripped, then dots are replaced by underscores, all uppercase:
| Config Key | Environment Variable |
|---|---|
server.addr |
TLSPXY_SERVER_ADDR |
server.type |
TLSPXY_SERVER_TYPE |
server.http2 |
TLSPXY_SERVER_HTTP2 |
server.tls.minversion |
TLSPXY_SERVER_TLS_MINVERSION |
remote.addr |
TLSPXY_REMOTE_ADDR |
remote.tls.enable |
TLSPXY_REMOTE_TLS_ENABLE |
remote.tls.verify |
TLSPXY_REMOTE_TLS_VERIFY |
log.level |
TLSPXY_LOG_LEVEL |
metrics.enable |
TLSPXY_METRICS_ENABLE |
Any config key follows the same pattern: add the TLSPXY_ prefix, replace . with _, and uppercase.
Flag names use dashes instead of dots:
tlspxy \
-server-addr ":8443" \
-server-type tcp \
-remote-addr "127.0.0.1:8080" \
-remote-tls-enable=false \
-log-level debugUse -config to specify one or more config files or directories:
tlspxy -config /etc/tlspxy/config.yaml
tlspxy -config /etc/tlspxy.d/ # loads all #tlspxy YAML files in directoryUse -version to print version and commit info. Use -validate to check config and exit.
server:
tls:
cert: "/path/to/server.crt"
key: "/path/to/server.key"Both cert and key must be provided together. If neither is set (and Let's Encrypt is disabled), the server runs without TLS.
server:
tls:
minversion: "1.2" # Minimum TLS version (1.0, 1.1, 1.2, 1.3)
maxversion: "1.3" # Maximum TLS version
ciphersuites: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"When left empty, Go defaults are used (TLS 1.2 minimum, system-selected cipher suites). Cipher suite names must match Go's crypto/tls naming (e.g., TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384). TLS 1.3 cipher suites are not configurable -- Go always uses the mandatory TLS 1.3 suites.
server:
tls:
alpn: "h2,http/1.1" # Advertised protocolsWhen TLS certificates are loaded from files (not Let's Encrypt), tlspxy supports zero-downtime certificate rotation via SIGHUP:
# Replace cert files on disk, then signal the process
kill -HUP $(pidof tlspxy)On SIGHUP, tlspxy reloads the default certificate and all SNI certificates from disk. Existing connections continue using the old certificate; new connections use the reloaded one. If a certificate fails to load, the old certificate is preserved and an error is logged.
Serve different certificates based on the client's requested hostname:
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.key
- hostname: "api.example.com"
cert: /path/to/api.crt
key: /path/to/api.keyLookup order: exact hostname match, then default certificate. SNI configuration is YAML-only (not available via flags or env vars). All SNI certificates are reloaded on SIGHUP alongside the default certificate.
server:
tls:
cert: "/path/to/server.crt"
key: "/path/to/server.key"
ca: "/path/to/client-ca.crt"
require: true # Require a client cert (any valid cert)
verify: true # Require AND verify against the CA (overrides require)require: true-- clients must present a certificate, but it is not verified against the CAverify: true-- clients must present a certificate that is valid against the configured CA
server:
tls:
letsencrypt:
enable: true
domain: "proxy.example.com"
cachedir: "/var/cache/letsencrypt"Automatically obtains and renews TLS certificates from Let's Encrypt. The server must be reachable on port 443 for the ACME challenge. Certificate hot-reload via SIGHUP is not available when using Let's Encrypt (it manages its own certificate lifecycle).
Enable HTTP/2 support for HTTP/HTTPS proxy modes:
server:
type: https
http2: true
tls:
cert: "/path/to/server.crt"
key: "/path/to/server.key"
alpn: "h2,http/1.1"When http2: true is set, both the server and the upstream transport are configured for HTTP/2. This requires server.type to be http or https (not tcp). Set server.tls.alpn to advertise HTTP/2 support to clients.
remote:
addr: "backend:443"
tls:
enable: true
verify: true
sysroots: trueremote:
tls:
enable: true
verify: true
ca: "/path/to/backend-ca.crt"
sysroots: falseremote:
tls:
enable: true
cert: "/path/to/client.crt"
key: "/path/to/client.key"remote:
tls:
enable: true
minversion: "1.2"
maxversion: "1.3"
ciphersuites: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
alpn: "h2,http/1.1"remote:
tls:
enable: true
verify: falseSets InsecureSkipVerify: true on the backend connection. Not recommended for production.
remote:
tls:
enable: falseEnable Prometheus metrics:
metrics:
enable: true
addr: ":9090"
path: "/metrics"Available metrics:
| 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 to a path to enable a health check endpoint:
server:
type: http
healthcheck: "/healthz"Requests to that path return 200 OK with {"status":"ok"}. All other requests are proxied normally.
log:
level: "info" # debug, info, warning, error
destination: "stdout" # stdout, /path/to/file, or syslog://address
contents: false # log proxied data (debug level)Destinations:
stdout-- write to standard output (default)/path/to/file-- append to the specified filesyslog://address-- send to a syslog server (supported on Linux, macOS, Windows)
Setting contents: true logs the actual proxied data at debug level. This generates significant output and should only be used for debugging.
See contrib/examples/ for complete example configurations:
| 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+.
# Build binary
make build
# Run tests
make test
# Build Docker image
make dockerThe binary is output to bin/ and is statically compiled with CGO disabled.