Reverse Proxies and TLS Termination

A reverse proxy can simplify certificate management, publish private services, route multiple applications through one address, and add health checks or access controls. It can also create confusing failures when the application no longer knows the original scheme, hostname, or client address.

Where the Reverse Proxy Sits

Internet client
      |
      v
DNS and public IP
      |
      v
Firewall / NAT
      |
      v
Reverse proxy or load balancer
      |      |      |
      v      v      v
   App A  App B  App C
    

The client connects to the proxy. The proxy chooses an upstream and creates a second connection to the application. These are separate transport and TLS sessions, even when the user experiences them as one website.

Common uses

  • Publish multiple applications using name-based virtual hosting.
  • Terminate public TLS certificates in one place.
  • Keep application servers on private addresses.
  • Load-balance across multiple upstreams.
  • Add authentication, rate limits, security headers, or maintenance pages.
  • Perform blue/green or migration cutovers.
  • Normalize old applications that cannot manage modern TLS themselves.

Understand the Trust Boundaries

A proxy receives untrusted client input and then creates headers that the upstream may trust. The upstream must distinguish headers set by the trusted proxy from headers supplied directly by a client.

Client to proxy

Public TLS policy, certificate, HTTP limits, WAF or rate-limit behavior, and source-address handling apply here.

Proxy to upstream

May be HTTP on a private network, HTTPS using public or private trust, a Unix socket, FastCGI, or another protocol.

Management plane

Configuration API, dashboard, SSH, certificate storage, and reload permissions should not be exposed like the public listener.

Application trust

The application should trust forwarded headers only from known proxy addresses or networks.

Do not “fix” internal TLS by disabling verification

If the proxy uses HTTPS to an upstream, verify the upstream certificate. A name mismatch or private certificate should be solved by using the correct SNI name, a private CA trust store, or a valid internal certificate. Disabling verification creates encryption without identity and hides configuration mistakes.

Forwarded Headers

HeaderPurposeRisk when wrong
HostOriginal requested hostnameWrong virtual host, redirects, certificate assumptions, or tenant selection
X-Forwarded-ForClient and proxy address chainIncorrect logging, access control, or rate limiting
X-Forwarded-ProtoOriginal scheme such as HTTPSRedirect loops, mixed content, insecure cookies
X-Forwarded-HostOriginal public hostIncorrect generated URLs
ForwardedStandardized forwarding informationSame trust concerns as the X-Forwarded family
PROXY protocolTransport-level source address metadataListener failure or spoofing if accepted from untrusted sources

The mixed-content failure pattern

Browser requests:        https://monitor.example.com/
Proxy connects upstream: http://10.10.10.25:8080/
Application believes:    request scheme is HTTP
Application generates:   http://monitor.example.com/assets/app.css
Browser blocks asset:    mixed content
    

The fix is usually to pass and trust the original HTTPS state, then configure the application or FastCGI environment to use it. Simply rewriting every response body is fragile.

TLS Termination Patterns

Terminate at the proxy, use HTTP internally

This is simple and avoids internal certificate management, but the internal network path must be appropriately trusted and isolated.

Client -- HTTPS --> Proxy -- HTTP --> Application
    

Terminate and re-encrypt

Use when internal traffic crosses less-trusted networks, policy requires encryption, or the application identity must be verified end to end.

Client -- HTTPS --> Proxy -- HTTPS --> Application
                      |                |
                  public cert      public or private CA cert
    

TLS pass-through

The proxy or load balancer forwards encrypted traffic without terminating it. This preserves application-controlled TLS but limits HTTP-aware routing and header manipulation.

Certificate operations

  • Know which system requests and renews each certificate.
  • Monitor expiration from outside the proxy.
  • Protect private keys and restrict configuration access.
  • Validate full certificate chains and SNI behavior.
  • Plan ACME HTTP-01 or DNS-01 validation paths.
  • Document how certificates are restored after a proxy rebuild.
  • Remove unused certificates and old hostnames.

Configuration Examples

Caddy: basic reverse proxy

app.example.com {
    reverse_proxy 10.20.30.40:8080
}
    

With a public hostname and reachable validation path, Caddy can automate public certificate issuance and renewal. The application still needs correct proxy awareness.

Caddy: HTTPS upstream with private trust

app.example.com {
    reverse_proxy https://10.20.30.40:8443 {
        transport http {
            tls_server_name app-internal.example.net
            tls_trust_pool file /etc/caddy/internal-ca.pem
        }
    }
}
    

Caddy: multiple upstreams and health checking

app.example.com {
    reverse_proxy 10.20.30.41:8080 10.20.30.42:8080 {
        health_uri /healthz
        lb_try_duration 5s
    }
}
    

NGINX: basic proxy with forwarding headers

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    location / {
        proxy_pass http://10.20.30.40:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
    

NGINX: WebSocket-capable location

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name realtime.example.com;

    location / {
        proxy_pass http://10.20.30.50:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
    

FastCGI behind an HTTPS proxy

When NGINX or another web server passes a request to PHP-FPM, ensure the HTTPS state and forwarded protocol reach PHP correctly.

fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO https;
fastcgi_param HTTP_X_FORWARDED_SSL on;
    

Only set these unconditionally when that server block is reached exclusively through the trusted HTTPS proxy path. Otherwise derive values safely from trusted input.

Avoid Making the Proxy the New Single Point of Failure

Publishing several redundant applications through one proxy moves the failure domain. Consider:

  • Two proxy nodes with a virtual IP, routed service IP, or external load balancer
  • Configuration synchronization and independent validation
  • Certificate storage and renewal coordination
  • Health checks that validate the actual application, not only the TCP port
  • DNS failover limitations and TTL behavior
  • Separate management and public interfaces
  • Capacity for TLS handshakes, large uploads, streaming, and request buffering

Health checks need meaning

A health endpoint that always returns 200 even when the database is unavailable sends traffic to a broken node. A health endpoint that performs every expensive dependency check on every request may create load. Design separate liveness and readiness semantics where the application supports them.

Troubleshooting by Status and Layer

SymptomLikely checks
502 Bad GatewayUpstream address, listener, firewall, DNS, socket permissions, TLS verification, application crash
504 Gateway TimeoutUpstream response time, proxy timeouts, database delay, network loss, application deadlock
Redirect loopX-Forwarded-Proto, application base URL, HTTP-to-HTTPS logic, CDN/proxy chain
Wrong client IPTrusted proxies, forwarded-header order, PROXY protocol configuration
Wrong certificateDNS target, SNI, default virtual host, certificate selection, stale proxy node
Large uploads failClient body limit, request buffering, upstream limits, timeout, temporary filesystem space
WebSocket disconnectsHTTP version, upgrade headers, idle timeout, intermediary support
Assets load over HTTPApplication proxy awareness, scheme headers, cached base URL, FastCGI parameters

Useful tests

# Test public DNS and certificate presentation
dig app.example.com A
dig app.example.com AAAA
openssl s_client -connect app.example.com:443 -servername app.example.com

# Show response headers and redirect chain
curl -I https://app.example.com/
curl -IL https://app.example.com/

# Test the upstream directly from the proxy
curl -v http://10.20.30.40:8080/
curl -vk https://10.20.30.40:8443/

# Validate configuration before reload
caddy validate --config /etc/caddy/Caddyfile
nginx -t
    

Official References