When a reverse proxy or load balancer sits in front of your applications, you have to decide where HTTPS is decrypted. There are three strategies: terminate TLS at the proxy and talk plain HTTP to the backends (offloading), forward the encrypted stream untouched (passthrough), or terminate at the proxy and open a new TLS connection to the backend (re-encryption). This guide compares the three, shows a working configuration for each with Nginx and HAProxy on Ubuntu 24.04, and explains how to choose.
Prerequisites
To try the examples you need:
- A proxy server and at least one backend server running Ubuntu 24.04 LTS, for example CubePath VPS instances connected through a private network.
- A non-root user with
sudoprivileges on each server. - A domain with an A record pointing to the proxy, and a certificate for it (for example from Let's Encrypt with Certbot).
- Nginx (
sudo apt install nginx, version 1.24 on Ubuntu 24.04) or HAProxy (sudo apt install haproxy, version 2.8), depending on the example.
In the examples, the proxy is reachable as your_domain and the backend has the private IP 10.0.0.11. Replace both with your own values.
The three strategies at a glance
| Offloading | Passthrough | Re-encryption | |
|---|---|---|---|
| Where TLS is decrypted | Proxy only | Backend only | Proxy and backend |
| Proxy to backend traffic | Plain HTTP | Encrypted (client session) | Encrypted (new session) |
| Where the public certificate lives | Proxy | Every backend | Proxy (backends use an internal certificate) |
| Proxy sees HTTP (paths, headers, cookies) | Yes | No, only the SNI hostname | Yes |
| Path routing, header rewrites, caching, WAF | Yes | No | Yes |
| Client IP at the backend | X-Forwarded-For header | PROXY protocol | X-Forwarded-For header |
| TLS CPU cost | Proxy | Backends | Proxy and backends |
| Client certificate (mTLS) checked by | Proxy | Backend | Proxy |
Strategy 1 - TLS offloading
With offloading, the proxy holds the certificate, decrypts every request and forwards plain HTTP to the backend. This is the most common setup: certificates are managed in one place, backends do no TLS work, and the proxy can route by path, add headers, cache and compress.
The trade-off is that traffic between the proxy and the backends travels unencrypted. That is acceptable only when that path is trusted, such as a private network or the same host.
Configure it in Nginx on the proxy:
sudo nano /etc/nginx/sites-available/app.conf
upstream app_plain {
server 10.0.0.11:8080;
keepalive 16;
}
server {
listen 80;
listen [::]:80;
server_name your_domain;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app_plain;
proxy_http_version 1.1;
proxy_set_header Connection "";
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 https;
}
}
X-Forwarded-Proto https is important: without it, many frameworks think the request arrived over HTTP and generate http:// links or redirect loops. Configure the application to trust these headers only from the proxy's IP address.
Enable the site and reload:
sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Make sure the backend port 8080 only accepts connections from the proxy. On the backend:
sudo ufw allow from 10.0.0.5 to any port 8080 proto tcp
Here 10.0.0.5 is the proxy's private IP.
Strategy 2 - TLS passthrough
With passthrough, the proxy does not decrypt anything. It reads the SNI hostname from the unencrypted TLS ClientHello, picks a backend and forwards the raw TCP stream. The backend holds the certificate and private key and terminates TLS itself.
Use passthrough when the proxy must not see the traffic (for example a shared load balancer in front of tenants that manage their own certificates), when a backend must verify client certificates itself, or for non-HTTP TLS services. You lose everything that needs HTTP visibility: no path routing, no header injection, no caching, and no HTTP health checks on the real content.
HAProxy handles SNI routing in TCP mode:
sudo nano /etc/haproxy/haproxy.cfg
Append:
frontend tls_passthrough
bind :443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
use_backend app1_tls if { req.ssl_sni -i app1.your_domain }
default_backend app2_tls
backend app1_tls
mode tcp
server app1 10.0.0.11:443 check send-proxy-v2
backend app2_tls
mode tcp
server app2 10.0.0.12:443 check send-proxy-v2
tcp-request inspect-delay and req.ssl_hello_type 1 make HAProxy wait until the ClientHello has arrived so that req.ssl_sni is available.
Because the connection comes from the proxy, the backend would see the proxy's IP as the client. send-proxy-v2 prepends a PROXY protocol header with the real client address. The backend must be configured to expect it, or every connection fails. On a backend running Nginx:
server {
listen 443 ssl proxy_protocol;
server_name app1.your_domain;
ssl_certificate /etc/letsencrypt/live/app1.your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app1.your_domain/privkey.pem;
set_real_ip_from 10.0.0.5;
real_ip_header proxy_protocol;
root /var/www/html;
}
Validate and reload HAProxy on the proxy:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
NoteWith passthrough, each backend must obtain and renew its own certificate. The HTTP-01 challenge needs port 80 forwarded to the right backend as well, so DNS-01 validation is often simpler.
Strategy 3 - Re-encryption
Re-encryption (also called TLS bridging or end-to-end TLS) terminates the client connection at the proxy, then opens a separate TLS connection to the backend. The proxy keeps full HTTP visibility and traffic stays encrypted on the internal network. This is what compliance frameworks such as PCI DSS usually expect when traffic crosses networks you do not fully control.
The backend presents a certificate for an internal name, for example app.internal, issued by a private CA or any CA the proxy trusts. The public certificate stays on the proxy.
Configure Nginx on the proxy to connect over HTTPS and verify the backend certificate:
upstream app_tls {
server 10.0.0.11:8443;
keepalive 16;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
location / {
proxy_pass https://app_tls;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/internal-ca.crt;
proxy_ssl_name app.internal;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}
}
proxy_ssl_verify onis the key line. Nginx does not verify upstream certificates by default, so without it an attacker on the internal network could impersonate the backend.proxy_ssl_trusted_certificatepoints to the CA that issued the backend certificate.proxy_ssl_nameis the name checked against the backend certificate, andproxy_ssl_server_name onsends it as SNI.keepalivewithproxy_http_version 1.1reuses TLS connections to the backend, which removes most of the extra handshake cost.
The HAProxy equivalent is the ssl verify required ca-file parameters on the server line:
backend app_tls
mode http
server app1 10.0.0.11:8443 check ssl verify required ca-file /etc/haproxy/internal-ca.crt sni str(app.internal) verifyhost app.internal
Verifying which strategy is in effect
Check which certificate a client actually receives:
openssl s_client -connect your_domain:443 -servername your_domain </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
subject=CN = your_domain
issuer=C = US, O = Let's Encrypt, CN = R11
With offloading and re-encryption, this is the proxy's certificate. With passthrough, it is the certificate installed on the backend.
For re-encryption, test the backend directly from the proxy with the internal CA to confirm the proxy can verify it:
curl --cacert /etc/nginx/internal-ca.crt --resolve app.internal:8443:10.0.0.11 https://app.internal:8443/ -o /dev/null -w "%{http_code}\n"
200
If this fails with a certificate error, Nginx will fail too and return 502 Bad Gateway, logging upstream SSL certificate verify error in /var/log/nginx/error.log.
Performance considerations
On modern CPUs with AES-NI, symmetric encryption is cheap. The expensive part is the handshake, and both HTTP keepalive and TLS session resumption avoid most handshakes. In practice:
- Offloading puts all TLS work on the proxy, which is the easiest place to scale and tune.
- Passthrough spreads the TLS work across the backends and makes the proxy very light, since it only copies bytes.
- Re-encryption doubles the handshakes, but with upstream
keepalivethe proxy-to-backend handshakes happen rarely and the overhead is small.
Measure on your own hardware before choosing a strategy for performance reasons alone. Security and operational needs usually matter more.
Which strategy should you choose
- Choose offloading when the proxy and backends share a trusted private network or host, and you want simple certificate management and full HTTP features. This covers most single-tenant web applications.
- Choose re-encryption when traffic between the proxy and backends crosses a network you do not fully trust (another data center, a shared network, a cloud provider's backbone) or a compliance standard requires encryption in transit everywhere.
- Choose passthrough when the proxy must not see plaintext, when each backend manages its own certificate, when the backend authenticates clients with mTLS itself, or when you balance non-HTTP TLS protocols.
Mixing strategies on one proxy is common: offload the public website, re-encrypt traffic to an API in another region, and pass through a tenant's traffic untouched.
Conclusion
Offloading, passthrough and re-encryption differ in one decision: where TLS is decrypted. That choice determines where certificates live, what the proxy can do with the traffic, how the backend learns the client IP and whether internal traffic is encrypted. Start with offloading on a private network, and move to re-encryption or passthrough when your security or tenancy model requires it.
As next steps, automate certificate renewal with Certbot on whichever servers hold the public certificates, set up a small private CA for internal backend certificates, and consider mutual TLS between the proxy and the backends for service-to-service authentication.
