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.

Client populations and the TLS capability each brings Four populations from modern browsers to embedded devices, each with its typical protocol support, update cadence, and what its failure looks like from the portal side. POPULATION SUPPORTS FAILURE LOOKS LIKE modern browsers TLS 1.3, updated weekly a visible error the user reports desktop GIS TLS 1.2, updated yearly a layer that will not load server integrations whatever the runtime had a nightly job that stops, silently field devices fixed at ship date nothing — until somebody is in the field

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.

Handshake cost across one viewport of tiles Two rows comparing full handshakes on every connection against one full handshake followed by resumed sessions. Without session resumption six full handshakes, six round trips the cost is paid again on every new connection, and a map opens many at once With session resumption one full handshake, then abbreviated resumptions largest benefit for distant clients, where a saved round trip is tens of milliseconds each time

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"
Three certificate faults, and the check that finds each Expiry, incomplete chain and hostname mismatch, each with its symptom, the detecting check, and where that check must run. FAULT WHO IT BREAKS CHECK expiry everyone, at a known moment days-remaining alert at 21 days incomplete chain strict clients only — integrations verify from a clean host, no cache hostname mismatch everyone, immediately check every published hostname

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.

Up one level: Security Boundary Mapping for OGC Services.