Hardening TLS Termination for OGC Endpoints
This guide hardens the TLS configuration at a portal’s edge without breaking the long-lived, rarely-updated clients that agency portals accumulate. It sits under Security Boundary Mapping for OGC Services, within the Core Portal Architecture & Security Boundaries framework.
Prerequisites
- Nginx 1.24 or newer (or an equivalent proxy) terminating TLS in front of the OGC services.
- Certificate issuance already automated, or a documented manual renewal with a named owner.
- Access to a month of edge logs, so the client population can be measured before anything is tightened.
- A staging endpoint with the same configuration, for testing before production.
Measure the clients before choosing the policy
A public web application can adopt a modern TLS policy and let the small tail of old browsers fail. A geospatial portal usually cannot, because its long tail is not old browsers — it is desktop GIS installations, embedded devices, and integrations written years ago by contractors who have moved on. Those clients are frequently the ones performing statutory work, and their failure is silent to you and total to them.
The third row is where the damage is done. A server-side integration failing a TLS handshake produces no user-visible error at all: the job logs a connection failure into a file nobody reads, and the first symptom is a dataset that has not updated in three weeks.
Step-by-step implementation
1. Log the handshake before changing it
# Add protocol and cipher to the edge log for a month before tightening anything.
log_format tlsinfo '$remote_addr "$http_user_agent" '
'$ssl_protocol $ssl_cipher $ssl_session_reused '
'$status "$request"';
access_log /var/log/nginx/tls.log tlsinfo;
# Who would a TLS 1.2 floor actually remove?
awk '{print $(NF-4)}' /var/log/nginx/tls.log | sort | uniq -c | sort -rn
# expect: TLSv1.3 and TLSv1.2 dominant; anything older is your migration list
# Which user agents are on the oldest protocol — this is the list to contact
awk '$(NF-4) ~ /TLSv1(\.[01])?$/ {print $2, $3}' /var/log/nginx/tls.log \
| sort | uniq -c | sort -rn | head -20
2. Set the protocol floor and cipher policy
server {
listen 443 ssl;
http2 on;
server_name portal.example.gov;
ssl_certificate /etc/ssl/portal/fullchain.pem;
ssl_certificate_key /etc/ssl/portal/privkey.pem;
# TLS 1.2 floor keeps desktop GIS working; 1.3 is preferred automatically.
ssl_protocols TLSv1.2 TLSv1.3;
# Forward secrecy only, and no ciphers whose failure modes need explaining.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off; # let 1.3 clients pick; ordering is theirs
# Session resumption matters here: a slippy map opens many connections.
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 4h;
ssl_session_tickets off; # tickets weaken forward secrecy without rotation
# OCSP stapling removes a round trip and a third-party dependency per client.
ssl_stapling on;
ssl_stapling_verify on;
resolver 10.0.0.10 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
3. Understand what session resumption is worth here
Tile traffic makes handshake cost unusually visible. A map opening a viewport establishes many connections in a burst, and a full handshake on each is measurable in the time-to-first-tile that readers experience as sluggishness.
Session tickets are disabled above deliberately. They allow resumption across servers, which is attractive behind a load balancer, but a ticket key that is never rotated undermines forward secrecy for every session it covers. Either rotate ticket keys frequently and automatically, or use the shared session cache and accept that resumption is per-node.
4. Make expiry a non-event
Certificate expiry remains one of the most common causes of a portal outage, and it is entirely preventable. Automate issuance, alert on remaining lifetime rather than on failure, and verify from outside the network that the served chain is complete.
# Alert at 21 days, not at expiry: renewals fail for boring reasons and need slack.
END=$(openssl s_client -connect portal.example.gov:443 -servername portal.example.gov </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=$(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 ))
echo "days remaining: $DAYS"
test "$DAYS" -gt 21 || echo "ALERT: certificate expires in $DAYS days"
The middle row is the one that catches teams out after a certificate change: browsers cache intermediate certificates and will happily complete a chain the server failed to send, so a portal can look perfectly healthy in a browser while every server-side integration fails verification.
One more property is worth verifying explicitly after any change here: that a client which fails the handshake produces a log line you can attribute. A rejected connection that leaves nothing but a counter is indistinguishable from a client that never tried, and the population most affected by a tightening — unattended integrations — is exactly the one that will never call to complain. Keep the handshake log for a full renewal cycle after each change, and read it once a week for the first month.
Verification
# 1. Only the intended protocols are offered
nmap --script ssl-enum-ciphers -p 443 portal.example.gov | grep -E 'TLSv1|ciphers'
# expect: TLSv1.2 and TLSv1.3 only
# 2. The served chain is complete, verified from a host with no cached intermediates
openssl s_client -connect portal.example.gov:443 -servername portal.example.gov \
-verify_return_error </dev/null 2>&1 | grep -E 'Verify return code|depth'
# expect: Verify return code: 0 (ok)
# 3. Stapling is actually working
openssl s_client -connect portal.example.gov:443 -status </dev/null 2>&1 | grep -A2 'OCSP Response Status'
# expect: successful
# 4. Session resumption is happening for repeat connections
openssl s_client -connect portal.example.gov:443 -reconnect </dev/null 2>&1 | grep -c 'Reused'
# expect: a non-zero count
# 5. An OGC request still works from a representative old client
curl --tlsv1.2 --tls-max 1.2 -s -o /dev/null -w '%{http_code}\n' \
"https://portal.example.gov/geoserver/wms?service=WMS&request=GetCapabilities"
# expect: 200
Troubleshooting matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| A nightly integration stopped after a TLS change | Its runtime does not support the new protocol floor | Identify it from the handshake log, contact the owner, and stage the change |
| Browsers work but a server client reports verification failure | Incomplete chain; browsers filled in the intermediate | Serve the full chain and verify from a host with no certificate cache |
| Handshake latency high for distant users | Resumption not working, or the session cache is too small | Increase the cache, confirm reuse, and consider rotating ticket keys instead |
| Stapling silently disabled | Resolver unset or unreachable, so the proxy cannot fetch the response | Configure resolver, and alert on stapling status rather than assuming |
| Certificate renewed but the old one is still served | The proxy was not reloaded after renewal | Add a reload hook to the renewal, and verify the served expiry, not the file |
| Some hostnames work and others do not | The certificate covers only the primary name | Enumerate every published hostname and check each in monitoring |
| A strict security scan flags session tickets | Tickets enabled with a static key | Disable tickets, or rotate keys automatically on a short cycle |
FAQ
Should TLS 1.0 and 1.1 be disabled immediately?
Disable them, but on a schedule you control and after measuring who uses them. Turning them off without notice in an agency portal reliably breaks a statutory integration whose owner had no warning; turning them off after a month of measurement and a directed conversation with the handful of affected clients almost never does.
Is TLS 1.3-only a reasonable target?
For a portal serving only modern browsers, yes. For one serving desktop GIS and long-lived integrations, not yet — the population data almost always shows a meaningful share on 1.2, and those clients update on organisational rollout cycles rather than on yours. Revisit annually with fresh measurements rather than on principle.
Where should TLS terminate — the edge, or all the way to the service?
Terminate at the edge, and use mutual TLS or a service mesh for the hop behind it if the network between them is not trusted. Terminating at each service multiplies the certificate management problem by the number of services and moves the cipher policy out of one reviewable place.
Does HSTS create a risk for a government portal?
It creates a commitment: once a browser has seen the header, it will refuse plain HTTP for that domain for the duration. That is the intent, and the risk is scope — includeSubDomains covers every subdomain, including ones run by other teams that may not yet be on HTTPS. Confirm the whole domain is ready before enabling it, and start with a shorter max-age.
How does this interact with client certificates for partner access?
They are separate mechanisms on the same connection: the policy here governs the protocol and ciphers, while client certificates govern who the caller is. If a partner uses mutual TLS, the protocol floor still applies to them, and their certificate infrastructure is often the oldest in the estate — measure before tightening, as with everything else here.
Related
- Terminating Mutual TLS for Agency WFS Clients — adding client identity to the same edge.
- Securing MapProxy with Nginx and ModSecurity — the inspection layer behind this termination point.
- Configuring HAProxy for WMS Load Balancing — the routing tier this sits in front of.
Up one level: Security Boundary Mapping for OGC Services.