<p class="wp-block-paragraph">Depending on management cost and user experience requirements, it might be more sensible to configure internal service as https or as http.</p>
<p class="wp-block-paragraph">I give example of 2 backend exposing NodePort on Kubernetes, just to keep proxy concern clearly separated.</p>
<figure class="wp-block-image size-large"><img src="https://smartango.com/wp-content/uploads/2025/12/proxy-termination-t-1024x682.png" alt="" class="wp-image-825"/></figure>
<h2 class="wp-block-heading"><strong>SSL Termination at the Proxy (HTTP Mode)</strong></h2>
<h3 class="wp-block-heading"><strong>Concept</strong></h3>
<ul class="wp-block-list">
<li>The proxy <strong>terminates TLS</strong>, handling encryption and certificate validation.</li>
<li>Backends receive plain HTTP (or optionally HTTP/2) traffic.</li>
<li>The proxy can inspect and modify headers (X-Forwarded-*) and perform routing, load balancing, caching, etc.</li>
</ul>
<pre class="wp-block-code"><code>Browser <--HTTPS--> Proxy (SSL Termination) <--HTTP--> Backend</code></pre>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h3 class="wp-block-heading"><strong>HAProxy Example (HTTP/2 Termination)</strong></h3>
<pre class="wp-block-code"><code>frontend https_frontend
bind *:443 ssl crt /etc/haproxy/ssl/wildcard_beerme.pem alpn h2,http/1.1
mode http
option httplog
# Match hostnames
acl host_redmine hdr(host) -i redmine.beerme
acl host_odoo hdr(host) -i odoo.beerme
# Route to backend
use_backend redmine_backend if host_redmine
use_backend odoo_backend if host_odoo
backend redmine_backend
mode http
balance roundrobin
option forwardfor
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
server r1 10.4.1.11:30080 check
server r2 10.4.1.12:30080 check
backend odoo_backend
mode http
balance roundrobin
option forwardfor
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
server o1 10.4.1.11:32036 check
server o2 10.4.1.12:32036 check</code></pre>
<p class="wp-block-paragraph"><strong>Notes:</strong></p>
<ul class="wp-block-list">
<li>alpn h2,http/1.1 enables HTTP/2 between browser and proxy.</li>
<li>Backend sees plain HTTP. Browser multiplexing happens <strong>between browser and proxy only</strong>.</li>
<li>The backend receives headers indicating original HTTPS.</li>
</ul>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h3 class="wp-block-heading"><strong>NGINX Example (HTTP/2 Termination)</strong></h3>
<pre class="wp-block-code"><code>server {
listen 443 ssl http2;
server_name redmine.beerme;
ssl_certificate /etc/nginx/ssl/wildcard_beerme.crt;
ssl_certificate_key /etc/nginx/ssl/wildcard_beerme.key;
location / {
proxy_pass http://redmine_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
upstream redmine_backend {
server 10.4.1.11:30080;
server 10.4.1.12:30080;
}</code></pre>
<p class="wp-block-paragraph"><strong>Notes:</strong></p>
<ul class="wp-block-list">
<li>http2 in listen enables browser-side HTTP/2.</li>
<li>Backends only receive HTTP/1.1 requests unless proxy_http_version 2 is supported (NGINX default to HTTP/1.1 upstream).</li>
<li>Headers inform backend of original HTTPS protocol.</li>
</ul>
<h2 class="wp-block-heading"><strong>SSL Passthrough (TCP/Stream Mode)</strong></h2>
<ul class="wp-block-list">
<li>Proxy <strong>does not terminate TLS</strong>; it simply forwards TCP based on SNI.</li>
<li>Browser connects directly to the backend TLS session.</li>
<li>End-to-end HTTP/2 multiplexing is preserved.</li>
</ul>
<h3 class="wp-block-heading"><strong>HAProxy TCP Passthrough Example</strong></h3>
<pre class="wp-block-code"><code>frontend ssl_passthrough
bind *:443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
acl redmine_sni req.ssl_sni -i redmine.beerme
acl odoo_sni req.ssl_sni -i odoo.beerme
use_backend redmine_tcp_backend if redmine_sni
use_backend odoo_tcp_backend if odoo_sni
backend redmine_tcp_backend
mode tcp
server r1 10.4.1.11:30080 check
server r2 10.4.1.12:30080 check
backend odoo_tcp_backend
mode tcp
server o1 10.4.1.11:32036 check
server o2 10.4.1.12:32036 check</code></pre>
<h3 class="wp-block-heading"><strong>NGINX Stream Passthrough Example</strong></h3>
<pre class="wp-block-code"><code>stream {
map $ssl_preread_server_name $backend_name {
redmine.beerme redmine_backend;
odoo.beerme odoo_backend;
default blackhole;
}
upstream redmine_backend {
server 10.4.1.11:30080;
server 10.4.1.12:30080;
}
upstream odoo_backend {
server 10.4.1.11:32036;
server 10.4.1.12:32036;
}
server {
listen 443;
proxy_pass $backend_name;
ssl_preread on;
}
}</code></pre>
<p class="wp-block-paragraph"><strong>Notes:</strong></p>
<ul class="wp-block-list">
<li>Backend certificates must match domain names (redmine.beerme, odoo.beerme) or use a wildcard certificate.</li>
<li>Browser-side HTTP/2 multiplexing is fully preserved.</li>
<li>Proxy cannot inspect HTTP headers.</li>
</ul>
<h2 class="wp-block-heading"><strong>Browser Performance</strong></h2>
<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Mode</strong></th><th><strong>Browser HTTP/2 Multiplexing</strong></th><th><strong>Backend Certificate Needed</strong></th><th><strong>Pros</strong></th><th><strong>Cons</strong></th></tr></thead><tbody><tr><td>HTTP SSL Termination</td><td>Only browser↔proxy</td><td>Proxy cert only</td><td>Header manipulation, routing, caching</td><td>Multiplexing not end-to-end</td></tr><tr><td>SSL Passthrough</td><td>Browser↔backend (full)</td><td>Backend cert valid</td><td>Full HTTP/2 performance</td><td>Backend must handle TLS, no header inspection</td></tr></tbody></table></figure>
<p class="wp-block-paragraph"><strong>Key takeaway:</strong></p>
<ul class="wp-block-list">
<li>For maximum <strong>page serving speed and real end-to-end HTTP/2</strong>, passthrough is superior.</li>
<li>SSL termination simplifies backend management and centralizes certs, but may limit multiplexing performance.</li>
</ul>
<h2 class="wp-block-heading">SSL Termination in Proxy, but talking with HTTP/2 backend (Stateless REST)</h2>
<p class="wp-block-paragraph">To take advance from stateless nature of REST requests, the better option is to let the proxy reuse the http/2 backend connection for more clients. This also reduce the total number of connection kept by the backend server, and the total number of TLS handshake required:</p>
<ul class="wp-block-list">
<li>proxy keep one connection per backend</li>
<li>proxy negotiate one TLS per backend</li>
<li>proxy reuse the same backend for requests arriving from different clients</li>
</ul>
<p class="wp-block-paragraph">For making nginx talk http/2 to the backend:</p>
<pre class="wp-block-code"><code>server {
listen 443 ssl http2;
server_name redmine.beerme;
ssl_certificate /etc/nginx/ssl/self.crt;
ssl_certificate_key /etc/nginx/ssl/self.key;
location / {
proxy_pass http://backend_redmine;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Enable HTTP/2 to backend
proxy_http_version 2;
proxy_set_header Connection "";
}
}</code></pre>
<p class="wp-block-paragraph"></p>
Depending on management cost and user experience requirements, it might be more sensible to configure internal service as https or as http.
I give example of 2 backend exposing NodePort on Kubernetes, just to keep proxy concern clearly separated.
SSL Termination at the Proxy (HTTP Mode)
Concept
The proxy terminates TLS , handling encryption and certificate validation.
Backends receive plain HTTP (or optionally HTTP/2) traffic.
The proxy can inspect and modify headers (X-Forwarded-*) and perform routing, load balancing, caching, etc.
Browser <--HTTPS--> Proxy (SSL Termination) <--HTTP--> Backend
HAProxy Example (HTTP/2 Termination)
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/ssl/wildcard_beerme.pem alpn h2,http/1.1
mode http
option httplog
# Match hostnames
acl host_redmine hdr(host) -i redmine.beerme
acl host_odoo hdr(host) -i odoo.beerme
# Route to backend
use_backend redmine_backend if host_redmine
use_backend odoo_backend if host_odoo
backend redmine_backend
mode http
balance roundrobin
option forwardfor
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
server r1 10.4.1.11:30080 check
server r2 10.4.1.12:30080 check
backend odoo_backend
mode http
balance roundrobin
option forwardfor
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
server o1 10.4.1.11:32036 check
server o2 10.4.1.12:32036 check
Notes:
alpn h2,http/1.1 enables HTTP/2 between browser and proxy.
Backend sees plain HTTP. Browser multiplexing happens between browser and proxy only .
The backend receives headers indicating original HTTPS.
NGINX Example (HTTP/2 Termination)
server {
listen 443 ssl http2;
server_name redmine.beerme;
ssl_certificate /etc/nginx/ssl/wildcard_beerme.crt;
ssl_certificate_key /etc/nginx/ssl/wildcard_beerme.key;
location / {
proxy_pass http://redmine_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
upstream redmine_backend {
server 10.4.1.11:30080;
server 10.4.1.12:30080;
}
Notes:
http2 in listen enables browser-side HTTP/2.
Backends only receive HTTP/1.1 requests unless proxy_http_version 2 is supported (NGINX default to HTTP/1.1 upstream).
Headers inform backend of original HTTPS protocol.
SSL Passthrough (TCP/Stream Mode)
Proxy does not terminate TLS ; it simply forwards TCP based on SNI.
Browser connects directly to the backend TLS session.
End-to-end HTTP/2 multiplexing is preserved.
HAProxy TCP Passthrough Example
frontend ssl_passthrough
bind *:443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
acl redmine_sni req.ssl_sni -i redmine.beerme
acl odoo_sni req.ssl_sni -i odoo.beerme
use_backend redmine_tcp_backend if redmine_sni
use_backend odoo_tcp_backend if odoo_sni
backend redmine_tcp_backend
mode tcp
server r1 10.4.1.11:30080 check
server r2 10.4.1.12:30080 check
backend odoo_tcp_backend
mode tcp
server o1 10.4.1.11:32036 check
server o2 10.4.1.12:32036 check
NGINX Stream Passthrough Example
stream {
map $ssl_preread_server_name $backend_name {
redmine.beerme redmine_backend;
odoo.beerme odoo_backend;
default blackhole;
}
upstream redmine_backend {
server 10.4.1.11:30080;
server 10.4.1.12:30080;
}
upstream odoo_backend {
server 10.4.1.11:32036;
server 10.4.1.12:32036;
}
server {
listen 443;
proxy_pass $backend_name;
ssl_preread on;
}
}
Notes:
Backend certificates must match domain names (redmine.beerme, odoo.beerme) or use a wildcard certificate.
Browser-side HTTP/2 multiplexing is fully preserved.
Proxy cannot inspect HTTP headers.
Browser Performance
Mode Browser HTTP/2 Multiplexing Backend Certificate Needed Pros Cons HTTP SSL Termination Only browser↔proxy Proxy cert only Header manipulation, routing, caching Multiplexing not end-to-end SSL Passthrough Browser↔backend (full) Backend cert valid Full HTTP/2 performance Backend must handle TLS, no header inspection
Key takeaway:
For maximum page serving speed and real end-to-end HTTP/2 , passthrough is superior.
SSL termination simplifies backend management and centralizes certs, but may limit multiplexing performance.
SSL Termination in Proxy, but talking with HTTP/2 backend (Stateless REST)
To take advance from stateless nature of REST requests, the better option is to let the proxy reuse the http/2 backend connection for more clients. This also reduce the total number of connection kept by the backend server, and the total number of TLS handshake required:
proxy keep one connection per backend
proxy negotiate one TLS per backend
proxy reuse the same backend for requests arriving from different clients
For making nginx talk http/2 to the backend:
server {
listen 443 ssl http2;
server_name redmine.beerme;
ssl_certificate /etc/nginx/ssl/self.crt;
ssl_certificate_key /etc/nginx/ssl/self.key;
location / {
proxy_pass http://backend_redmine;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Enable HTTP/2 to backend
proxy_http_version 2;
proxy_set_header Connection "";
}
}