Terminating Mutual TLS for Agency WFS Clients
This guide adds certificate-based client authentication for partner organisations consuming WFS, terminated at the proxy and mapped to the same tenant identity the rest of the portal uses. It belongs to Reverse Proxy Configuration for WMS/WFS, within the Infrastructure Orchestration & Configuration Management framework.
Prerequisites
- A hardened TLS termination point, per hardening TLS termination for OGC endpoints.
- A certificate authority for issuing client certificates, or the partner’s own CA to trust.
- A tenant model to map certificates onto, per implementing RBAC for multi-tenant GIS portals.
- A separate hostname for the mutual-TLS endpoint, so browser traffic is unaffected.
When mutual TLS is the right answer
Client certificates are a heavyweight mechanism and they suit one situation particularly well: a small number of long-lived, machine-to-machine integrations between organisations, where the consuming system cannot participate in an interactive login and the relationship is formal enough to justify certificate management.
Step-by-step implementation
1. Use a separate hostname
Requiring a client certificate on the main hostname breaks every browser and every ordinary client. Give the mutual-TLS endpoint its own name so the requirement applies only where it belongs.
server {
listen 443 ssl;
server_name partners.portal.example.gov;
ssl_certificate /etc/ssl/portal/partners-fullchain.pem;
ssl_certificate_key /etc/ssl/portal/partners-privkey.pem;
# Require a client certificate signed by a CA we trust for this purpose.
ssl_client_certificate /etc/ssl/partners/trusted-cas.pem;
ssl_verify_client on;
ssl_verify_depth 2;
# Revocation must be checked, or a withdrawn certificate keeps working.
ssl_crl /etc/ssl/partners/revoked.crl;
location /geoserver/wfs {
proxy_pass http://geoserver_backend;
}
}
2. Map the certificate to a tenant
The certificate proves who connected; the portal needs to know which tenant that is. Map explicitly from a stable field, and refuse anything unmapped rather than defaulting.
# Map the client certificate's subject DN to a tenant. Explicit, and no default.
map $ssl_client_s_dn $partner_tenant {
default "";
"CN=highways-integration,O=County Highways,C=GB" "tenant-highways";
"CN=planning-etl,O=District Planning,C=GB" "tenant-planning";
}
server {
# ... TLS configuration from step 1
location /geoserver/wfs {
# An unmapped but validly signed certificate must not be treated as a tenant.
if ($partner_tenant = "") { return 403 '{"error":"certificate_not_mapped"}'; }
proxy_set_header X-Tenant-Id $partner_tenant;
proxy_set_header X-Client-Serial $ssl_client_serial;
# Never let a caller supply these themselves.
proxy_set_header X-Forwarded-Client-Cert "";
proxy_pass http://geoserver_backend;
}
}
Mapping on the subject DN rather than on the certificate fingerprint is deliberate: a renewal keeps the DN and changes the fingerprint, so a fingerprint map turns every routine renewal into an outage requiring a configuration change on your side.
3. Log the identity, including the serial
log_format mtls '$remote_addr $partner_tenant "$ssl_client_s_dn" '
'serial=$ssl_client_serial verify=$ssl_client_verify '
'expires="$ssl_client_v_end" $status "$request"';
access_log /var/log/nginx/partners.log mtls;
The serial and expiry in the log are what make a support question answerable: which certificate was this, and was it the one we think they are using.
Expiry is the operational problem, not authentication
Client certificates work reliably and expire silently. The partner’s certificate is managed by the partner, often by somebody who has left, and the first indication of an expiry is usually a nightly integration that stops without anybody on either side noticing for days.
# A scheduled check over the trusted client certificates, from the mapping file
# rather than from memory. Alerts at the three points above.
for CERT in /etc/ssl/partners/clients/*.pem; do
END=$(openssl x509 -in "$CERT" -noout -enddate | cut -d= -f2)
DAYS=$(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 ))
CN=$(openssl x509 -in "$CERT" -noout -subject | sed 's/.*CN=\([^,]*\).*/\1/')
case $DAYS in
[0-7]) echo "CRITICAL $CN expires in $DAYS days" ;;
[8-9]|[12][0-9]|30) echo "WARN $CN expires in $DAYS days" ;;
*) [ "$DAYS" -le 60 ] && echo "INFO $CN expires in $DAYS days" ;;
esac
done
Onboarding a partner without a two-week email thread
Most of the friction in mutual TLS is not technical; it is the exchange of files and expectations between two organisations. A short, written onboarding sequence removes nearly all of it.
Verification
# 1. A request without a certificate is refused
curl -s -o /dev/null -w '%{http_code}\n' \
"https://partners.portal.example.gov/geoserver/wfs?service=WFS&request=GetCapabilities"
# expect: 400 or 496 — the handshake requires a certificate
# 2. A valid, mapped certificate is accepted and carries its tenant
curl -s --cert highways.pem --key highways.key -o /dev/null -w '%{http_code}\n' \
"https://partners.portal.example.gov/geoserver/wfs?service=WFS&version=2.0.0&request=GetCapabilities"
# expect: 200, and X-Tenant-Id: tenant-highways in the upstream log
# 3. A valid but unmapped certificate is refused, not defaulted
curl -s --cert unmapped.pem --key unmapped.key -w '\n%{http_code}\n' \
"https://partners.portal.example.gov/geoserver/wfs?service=WFS&request=GetCapabilities"
# expect: 403 certificate_not_mapped
# 4. A revoked certificate stops working
openssl crl -in /etc/ssl/partners/revoked.crl -noout -text | grep -c "$REVOKED_SERIAL"
curl -s -o /dev/null -w '%{http_code}\n' --cert revoked.pem --key revoked.key \
"https://partners.portal.example.gov/geoserver/wfs?service=WFS&request=GetCapabilities"
# expect: rejected at the handshake
# 5. The upstream cannot be told a tenant by the caller
curl -s --cert highways.pem --key highways.key -H "X-Tenant-Id: tenant-planning" \
-o /dev/null -w '%{http_code}\n' \
"https://partners.portal.example.gov/geoserver/wfs?service=WFS&request=GetCapabilities"
# expect: 200, and the upstream sees tenant-highways — the header was overwritten
Check 5 is the one that matters most. A proxy that forwards a caller-supplied tenant header alongside the derived one has made the whole mechanism decorative.
Troubleshooting matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Browsers prompt for a certificate on the main site | Client verification enabled on the shared hostname | Use a separate hostname for the mutual-TLS endpoint |
| A partner’s renewal breaks their access | Mapping keyed on fingerprint rather than subject DN | Map on the DN; treat a DN change as a deliberate re-onboarding |
| A revoked certificate still works | CRL not configured, or stale | Configure the CRL and refresh it on a schedule; alert if it is old |
| A validly signed but unknown certificate gets access | A default in the mapping |
Refuse unmapped certificates explicitly |
| Handshake fails with an intermediate CA | ssl_verify_depth too shallow for the partner’s chain |
Raise the depth deliberately; confirm which CAs are actually trusted |
| The upstream sees the wrong tenant | Caller-supplied header forwarded | Clear inbound identity headers at the proxy before setting them |
| An integration fails days after expiry with no alert | No expiry monitoring for client certificates | Run the scheduled check; notify the partner contact, not only the platform team |
FAQ
Should the portal issue the certificates or accept the partner’s?
Issuing them yourself is simpler to control — you set the lifetime, the naming and the revocation — and it means distributing a private key to the partner, which needs a secure channel and a record. Accepting their CA avoids that distribution and means trusting their issuance practices for everything that CA signs. For a small number of formal partners, issuing is usually the better trade.
How long should a client certificate live?
Long enough to avoid constant renewal friction and short enough to bound an undetected compromise: one year is a common compromise for partner integrations, with the notification schedule above. Anything longer tends to outlive the person who arranged it.
Can mutual TLS coexist with API keys and OIDC?
Yes, and it usually should. Different consumers have different capabilities: a partner ETL system with a certificate, a web map with an origin-restricted key, an internal service with a token. What matters is that all three resolve to the same tenant identity before authorisation, so the policy layer has one concept to reason about.
Does terminating at the proxy weaken the guarantee?
It moves it. Everything behind the proxy trusts the proxy’s assertion, which is why the derived headers must be set by the proxy and cleared from inbound requests. If the network behind the proxy is not trusted, extend mutual TLS inward as a separate hop rather than passing the client certificate through.
What should happen when a partner’s certificate expires anyway?
Fail clearly and tell somebody. The handshake failure is not visible to the partner’s application as anything more than a connection error, so the portal side should alert on a partner whose traffic has stopped — an absence-of-traffic alert per partner is cheap and catches this, along with several other silent failures.
Is this suitable for many small consumers?
No. The certificate lifecycle cost is per consumer and falls on both sides, so it scales badly past a handful of relationships. For a long tail of small integrations, scoped API keys are the proportionate mechanism, with mutual TLS reserved for the formal, high-volume, machine-to-machine cases.
How should a compromised partner certificate be handled?
Revoke it, publish the updated revocation list, and confirm at the proxy that the certificate is actually refused — a revocation that the proxy has not reloaded is a policy statement rather than a control. Then issue a replacement through the normal onboarding steps rather than reusing the subject, and record the incident against the partner relationship so the next review knows it happened.
Related
- Hardening TLS Termination for OGC Endpoints — the transport configuration underneath this.
- Issuing Scoped API Keys for OGC Clients — the proportionate mechanism for smaller consumers.
- Header Preservation and the OGC Trust Boundary — why inbound identity headers are cleared.
Up one level: Reverse Proxy Configuration for WMS/WFS.