mTLS Gateway Authentication Flow
Detailed architecture diagrams for mTLS certificate-bound token validation in the STOA Gateway (Rust).
Related ADRs: ADR-027 (X509 Headers), ADR-028 (RFC 8705 Binding), ADR-029 (Certificate Lifecycle), ADR-039 (Rust Gateway mTLS)
Linear: CAB-864
1. End-to-End mTLS Flowβ
STOA Platform (K8s Cluster)
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
mTLS β HTTP + X-SSL-* headers β
ββββββββββββ (TLS 1.2/1.3) βββββββββββ βββββββββββββββββββββ β
β β ββββββββββββββββββΊ β β βββββββββββββββββΊ β β β
β Client β Client cert + β F5 β X-SSL-Client-* β STOA Gateway β β
β App β Bearer token β BigIP β + Authorization β (Rust/axum) β β
β β ββββββββββββββββββ β β βββββββββββββββββ β β β
ββββββββββββ Response βββββββββββ Response βββββββββ¬ββββββββββββ β
β β β
β β JWKS fetch β
β ββββββββββΌβββββββββ β
β β Keycloak β β
β β (JWKS + cnf) β β
β βββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step-by-Step Detailβ
Step 1: TLS Handshake + Client Certificate Presentation
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Client F5 BigIP
β β
βββββ ClientHello βββββββββββββββββββΊβ
βββββ ServerHello + ServerCert βββββββ
βββββ CertificateRequest βββββββββββββ F5 requests client cert
βββββ ClientCertificate βββββββββββββΊβ Client sends its X.509 cert
βββββ CertificateVerify βββββββββββββΊβ Client proves private key ownership
βββββ Finished ββββββββββββββββββββββΊβ
βββββ Finished βββββββββββββββββββββββ
β β
β TLS session established β
Step 2: F5 Validates Client Certificate
βββββββββββββββββββββββββββββββββββββββ
F5 BigIP (internal validation)
β
βββ Verify signature against CA chain
β Root CA βββΊ Intermediate CA βββΊ Client cert
β
βββ Check validity period
β NotBefore <= now <= NotAfter
β
βββ Check revocation
β βββ CRL download (if configured)
β βββ OCSP request (if configured)
β
βββ Result: SUCCESS or FAILED:<reason>
β
βββ FAILED β F5 returns 403 to client, request does NOT reach Gateway
Step 3: F5 Injects Headers and Forwards Request
ββββββββββββββββββββββββββββββββββββββββββββββββ
F5 βββββββββββΊ STOA Gateway
Headers injected:
Authorization: Bearer <jwt> (from client)
X-SSL-Client-Verify: SUCCESS (from F5)
X-SSL-Client-S-DN: CN=acme-consumer,O=Acme Corp (from F5)
X-SSL-Client-I-DN: CN=STOA Platform CA,O=STOA (from F5)
X-SSL-Client-Serial: 0A:1B:2C:3D:4E:5F (from F5)
X-SSL-Client-Fingerprint: a1b2c3d4e5f6... (from F5, SHA-256 hex)
X-SSL-Client-NotAfter: 2027-01-01T00:00:00Z (from F5)
Step 4: Gateway Extracts Certificate Metadata (auth/mtls.rs)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
extract_certificate_from_headers(&headers, &config)
β
βββ Read X-SSL-Client-Verify
β "SUCCESS" β continue
β anything else β return Err(MTLS_CERT_INVALID)
β
βββ Read X-SSL-Client-Fingerprint
β normalize: strip colons, lowercase
β compute base64url: hex β bytes β base64url_encode
β
βββ Read X-SSL-Client-S-DN β subject_dn
βββ Read X-SSL-Client-I-DN β issuer_dn
β check allowed_issuers (if configured)
βββ Read X-SSL-Client-Serial β serial
βββ Read X-SSL-Client-NotAfter β not_after
β check expiry: not_after > now
β
βββ Return Ok(CertificateInfo { ... })
β stored in request.extensions()
Step 5: Gateway Validates JWT + Extracts cnf (existing + claims.rs extension)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Existing JWT flow (unchanged):
β
βββ Extract Bearer token from Authorization header
βββ Decode JWT header β get kid
βββ Fetch JWKS from Keycloak (moka cache, 5min TTL)
βββ Verify RS256 signature
βββ Validate iss, aud, exp, leeway
βββ Deserialize Claims struct
β
βββ NEW field: claims.cnf: Option<CnfClaim>
β
βββ CnfClaim { x5t_s256: Option<String> }
= base64url-encoded SHA-256 of client cert DER
Step 6: Gateway Verifies Certificate-Token Binding (auth/mtls.rs)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
verify_certificate_binding(&cert_info, &cnf_claim)
β
βββ Get cert_info.fingerprint_b64url (computed in Step 4)
β "obLD1N5..."
β
βββ Get cnf_claim.x5t_s256
β "obLD1N5..."
β
βββ Decode both to bytes
β
βββ Timing-safe comparison (subtle::ConstantTimeEq)
β cert_bytes.ct_eq(&token_bytes)
β
βββ MATCH β continue to RBAC + handler
β
βββ MISMATCH β 403 MTLS_BINDING_MISMATCH
β
βββ No cert + mtls_required β 401 MTLS_CERT_REQUIRED
β
βββ No cnf + cert present + require_binding β 403 MTLS_BINDING_REQUIRED
2. Consumer Onboarding (Certificate Registration)β
This extends the CAB-1121 Phase 2 consumer onboarding with certificate binding.
ββββββββββββββββ
β Operator β
β (Console) β
ββββββββ¬ββββββββ
β
β POST /api/v1/consumers
β Body: { external_id, display_name, tenant_id, certificate_pem }
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Control Plane API β
β β
β 1. Validate certificate (existing: certificates.py) β
β βββ Parse PEM β X.509 β
β βββ Check expiry (warn if < 30 days) β
β βββ Verify minimum key size (2048 RSA / P-256 EC) β
β βββ Extract: subject, issuer, SAN, key size, fingerprint β
β β
β 2. Compute certificate thumbprint for RFC 8705 β
β βββ DER-encode the certificate β
β βββ SHA-256 hash β
β βββ Base64url encode β x5t_s256 β
β β
β 3. Create Keycloak client (existing: CAB-1121 Phase 2) β
β βββ Client ID: {tenant}-{consumer_external_id} β
β βββ Grant type: client_credentials β
β βββ Service account: enabled β
β βββ Client attribute: x5t_s256 = <thumbprint> β
β βββ Client attribute: x509.certificate.sha256 = <hex> β
β (for Keycloak x509 authenticator, ADR-027) β
β β
β 4. Configure cnf protocol mapper on client β
β βββ Mapper type: Hardcoded claim β
β βββ Token claim name: cnf β
β βββ Claim JSON type: JSON β
β βββ Value: {"x5t#S256": "<x5t_s256>"} β
β βββ Add to access token: true β
β βββ Add to ID token: false β
β β
β 5. Store consumer record β
β βββ consumer_id, tenant_id, external_id β
β βββ keycloak_client_id β
β βββ certificate_fingerprint (SHA-256 hex) β
β βββ certificate_fingerprint_b64url (for binding check) β
β βββ certificate_subject_dn, certificate_not_after β
β βββ mtls_enabled = true β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3. Bulk Onboarding Flow (Phase 3)β
Operator prepares CSV (max 100 rows):
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β external_id,display_name,tenant_id,certificate_pem β
β acme-svc-001,Acme Service 1,tenant-acme,-----BEGIN CERT... β
β acme-svc-002,Acme Service 2,tenant-acme,-----BEGIN CERT... β
β ... β
β acme-svc-100,Acme Service 100,tenant-acme,-----BEGIN CERT... β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β POST /api/v1/admin/consumers/bulk
β Content-Type: multipart/form-data
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Control Plane API β
β β
β Per row (sequential, each row is atomic): β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. Validate certificate PEM β β
β β 2. Compute x5t_s256 thumbprint β β
β β 3. Create Keycloak client + cnf protocol mapper β β
β β 4. Store consumer record in DB β β
β β β β
β β On success: { row, status: "success", consumer_id } β β
β β On failure: rollback row, { row, status: "error", ... } β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β All rows processed (failures do not stop batch). β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
Response:
{
"total": 100,
"success": 97,
"failed": 3,
"results": [
{ "row": 1, "status": "success", "consumer_id": "...", "client_id": "..." },
{ "row": 42, "status": "error", "error": "certificate expired" },
...
]
}
4. Failure Modesβ
| # | Failure | Where | HTTP | Error Code | Recovery |
|---|---|---|---|---|---|
| F1 | Client cert expired | F5 | TLS error (no HTTP) | N/A | Client renews cert with CA |
| F2 | Client cert revoked | F5 | TLS error (no HTTP) | N/A | Re-issue cert, update CRL |
| F3 | Unknown CA | F5 | TLS error (no HTTP) | N/A | Import CA into F5 trust store |
| F4 | F5 verify = FAILED | Gateway | 403 | MTLS_CERT_INVALID | Check F5 logs for reason |
| F5 | No cert headers + mtls required | Gateway | 401 | MTLS_CERT_REQUIRED | Client must present cert |
| F6 | JWT expired | Gateway | 401 | TOKEN_EXPIRED | Refresh token |
| F7 | JWT missing cnf claim | Gateway | 403 | MTLS_BINDING_REQUIRED | Register cert on Keycloak client, get new token |
| F8 | Thumbprint mismatch | Gateway | 403 | MTLS_BINDING_MISMATCH | Token was issued for different cert |
| F9 | Cert rotated, old token | Gateway | 403 | MTLS_BINDING_MISMATCH | Get new token (ADR-029 grace period) |
| F10 | Issuer not in allowed list | Gateway | 403 | MTLS_ISSUER_DENIED | Add issuer to STOA_MTLS_ALLOWED_ISSUERS |
| F11 | Header spoofing | Gateway | 403 | MTLS_CERT_INVALID | Enforce K8s NetworkPolicy (ADR-027) |
| F12 | Keycloak mapper missing | Keycloak | 403 | MTLS_BINDING_REQUIRED | Configure cnf protocol mapper on client |
5. Certificate Rotation (ADR-029 Integration)β
Day 0 Day 0+grace_period
β β
βΌ βΌ
ββββββββββββ Rotate ββββββββββββββββββββββββ Grace expires ββββββββββββ
β Cert A β βββββββββββ β Cert A + Cert B β βββββββββββββββ β Cert B β
β (active) β β (both valid) β β (active) β
ββββββββββββ ββββββββββββββββββββββββ ββββββββββββ
PUT /api/v1/consumers/{id}/certificate
β
βββ Validate new certificate
βββ Compute new x5t_s256
βββ Store old fingerprint as certificate_fingerprint_previous
βββ Set previous_cert_expires_at = now + grace_period (default: 24h)
βββ Update Keycloak client attribute with new x5t_s256
βββ During grace period: Gateway accepts tokens bound to EITHER thumbprint
6. Network Securityβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Kubernetes Cluster β
β β
β βββββββββββββββ NetworkPolicy ββββββββββββββββββββ β
β β F5 BigIP ββββ(ingress: allow)βββΊβ STOA Gateway β β
β β (pod) β X-SSL-* trusted β (pod) β β
β βββββββββββββββ ββββββββββββββββββββ β
β β
β βββββββββββββββ NetworkPolicy ββββββββββββββββββββ β
β β Other Pod ββββ(BLOCKED)ββββXβββββΊβ STOA Gateway β β
β β β X-SSL-* stripped β (pod) β β
β βββββββββββββββ ββββββββββββββββββββ β
β β
β Defense in depth: Gateway strips X-SSL-* from non-F5 β
β sources even if NetworkPolicy is misconfigured. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
7. Observabilityβ
Prometheus Metricsβ
stoa_gateway_mtls_requests_total{status="success|binding_mismatch|cert_invalid|cert_required|binding_required"}
stoa_gateway_mtls_binding_duration_seconds{quantile="0.5|0.9|0.99"}
stoa_gateway_mtls_cert_expiry_days{consumer_id, tenant_id}
Structured Log Fieldsβ
{
"event": "mtls_auth",
"status": "success",
"cert_subject_dn": "CN=acme-consumer,O=Acme Corp,C=FR",
"cert_serial": "0A:1B:2C:3D:4E:5F",
"cert_not_after": "2027-01-01T00:00:00Z",
"binding_match": true,
"user_id": "acme-consumer-001",
"tenant_id": "tenant-acme",
"trace_id": "abc123"
}
Alertsβ
| Alert | Condition | Severity |
|---|---|---|
MtlsBindingMismatchRate | binding_mismatch rate > 0.1/s over 5m | Warning |
MtlsCertExpiringSoon | cert_expiry_days < 30 | Warning |
MtlsCertExpired | cert_expiry_days <= 0 | Critical |
MtlsHighFailureRate | failure rate > 5% over 5m | Critical |