feat(pki-server): support trusted reverse-proxy authentication

Support explicit direct-mTLS and trusted-reverse-proxy authentication
modes with mutually authenticated backend transport.

Keep proxy and end-client principals separate, validate forwarded
certificates independently, and enforce narrowly scoped forwarding
authority for RFC 9440 and NGINX escaped-PEM profiles.
This commit is contained in:
2026-08-04 23:07:01 +02:00
parent 5b896ee2a2
commit 7328f075dd
29 changed files with 1954 additions and 72 deletions

View File

@@ -11,13 +11,57 @@ zeroecho-pki-server --config server-config.json
Use `--help` and `--version` for launcher information. Validation parses the strict versioned JSON and checks finite security bounds without opening a listener, realm, store, provider, or audit sink.
The loopback and production examples are [pki-server-loopback-example.json](pki-server-loopback-example.json) and [pki-server-production-example.json](pki-server-production-example.json). They contain no credentials or key material. Replace every illustrative commitment and identity with provisioned values before use. The mapped principal must already exist in the durable server-control store and have explicit scoped grants; a certificate mapping establishes identity only and never grants authority.
The direct examples are [pki-server-loopback-example.json](pki-server-loopback-example.json) and [pki-server-production-example.json](pki-server-production-example.json). Trusted-proxy examples are [pki-server-trusted-proxy-rfc9440-example.json](pki-server-trusted-proxy-rfc9440-example.json) and [pki-server-trusted-proxy-nginx-example.json](pki-server-trusted-proxy-nginx-example.json). They contain no credentials or key material. Replace every illustrative commitment and identity with provisioned values before use. Mapped principals must already exist in the durable server-control store and have explicit scoped grants; a certificate mapping establishes identity only and never grants authority.
## TLS and authentication
## TLS and authentication modes
Administrative access has no anonymous mode. The listener always requires a client certificate. The built-in `jsse-pkcs12` TLS provider reads server identity and client trust stores from configured files, while passwords are resolved exclusively from named environment variables. Provider exceptions, aliases, references, certificate subjects, SANs, fingerprints, and paths are not returned to clients or written to request audit details.
Administrative access has no anonymous mode. Schema version 2 requires exactly one explicit mode: `DIRECT_MTLS` or `TRUSTED_REVERSE_PROXY`. The listener always requires a client certificate in either mode. Schema version 1 is rejected; there is no inferred direct-mode compatibility decoder.
In `DIRECT_MTLS`, the TLS peer is the end-client principal. Forwarded certificate headers are rejected. The built-in `jsse-pkcs12` TLS provider reads server identity and direct administrative-client trust stores from configured files, while passwords are resolved exclusively from named environment variables.
In `TRUSTED_REVERSE_PROXY`, the TLS peer is a dedicated `SERVICE` transport principal. Its only enabled allowing authority must be `FORWARD_AUTHENTICATED_CLIENT_IDENTITY`, explicitly scoped to the realm. ZeroEcho first validates backend mutual TLS and that permission, then independently parses and validates the forwarded external client certificate against `administrativeClientTrust`. Authorization, approvals, break-glass ownership and PII auditing use only the mapped end-client principal. Neither identity inherits the other identity's permissions.
The preferred forwarding representation is RFC 9440 `Client-Cert` plus optional `Client-Cert-Chain`. The compatibility representation `NGINX_ESCAPED_PEM_V1` accepts one explicitly named header containing only the URL-escaped, canonical single-certificate PEM emitted by `$ssl_client_escaped_cert`. Formats are never auto-detected. Duplicate leaf or chain headers, mixed representations, malformed escapes, raw PEM, non-canonical Base64, concatenated objects and oversized values fail closed.
Client certificates map to persisted principals through an exact SHA-256 certificate, canonical SPKI, or issuer-and-positive-serial commitment. Proxy and forwarded-client mappings additionally require an exact certificate or SPKI commitment and cannot overlap. Subject and SAN strings, source addresses, `X-Forwarded-For`, `Forwarded`, `X-Real-IP`, PROXY protocol data and certificate-header possession are not trust identities. An absent, ambiguous, expired, malformed, untrusted, wrong-usage, unauthorized or disabled mapping fails before typed operation decoding.
Direct mode obtains proof of possession from ZeroEcho's TLS handshake. Proxy mode relies on the mutually authenticated and narrowly authorized proxy's attestation that external mutual-TLS proof of possession succeeded. Independently parsing and validating the forwarded certificate does not itself prove possession.
Provider exceptions, aliases, references, certificate subjects, SANs, fingerprints, forwarded header values and paths are not returned to clients or written to request audit details. Safe audit records the authentication mode and keeps the transport and end-client principal identities separate.
## Trusted NGINX topology
The secure topology is:
```text
administrator --mTLS--> NGINX --mTLS--> ZeroEcho
```
RFC 9440 is preferred where the proxy can emit it. For the explicit NGINX compatibility mode, the essential NGINX policy is conceptually:
```nginx
server {
listen 443 ssl;
ssl_verify_client on;
ssl_client_certificate /external/reference/admin-client-trust.pem;
location /admin/ {
proxy_set_header Client-Cert "";
proxy_set_header Client-Cert-Chain "";
proxy_set_header X-ZeroEcho-Client-Cert "";
proxy_set_header X-ZeroEcho-Client-Cert $ssl_client_escaped_cert;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /external/reference/backend-server-trust.pem;
proxy_ssl_certificate /external/reference/proxy-client-certificate.pem;
proxy_ssl_certificate_key /external/reference/proxy-client-key.pem;
proxy_pass https://zeroecho_backend;
}
}
```
The paths and upstream name above are placeholders, not deployment values. The proxy must remove or replace all inbound forwarded-certificate headers, verify the external client certificate, and use a dedicated backend client certificate mapped to the narrowly authorized proxy principal. `proxy_pass` must remain HTTPS. `X-Forwarded-For` and the client source address are never authorization evidence. Separate mapping sets identify the proxy transport principal and forwarded end clients.
Client certificates map to persisted principals through an exact SHA-256 certificate, canonical SPKI, or issuer-and-positive-serial commitment. Subject and SAN strings are not trust identities. An absent, ambiguous, expired, malformed, untrusted, or disabled mapping fails before typed operation decoding.
## API

View File

@@ -882,6 +882,141 @@ Each server remains authoritative for its own realm.
Cross-realm UI aggregation is presentation and orchestration, not shared PKI authority.
## Trusted reverse-proxy TLS termination
The ZeroEcho PKI server MUST support exactly these two explicit administrative authentication deployment modes:
```text
DIRECT_MTLS
TRUSTED_REVERSE_PROXY
```
### Direct mutual TLS
In `DIRECT_MTLS` mode, ZeroEcho terminates TLS, validates the client certificate, verifies possession through the TLS handshake, and maps the validated certificate to one persisted `SecurityPrincipal`.
Forwarded client-identity headers MUST be rejected in this mode.
### Trusted reverse proxy
In `TRUSTED_REVERSE_PROXY` mode, a configured TLS-terminating reverse proxy such as NGINX validates the external client certificate and forwards the authenticated client identity to ZeroEcho.
The proxy-to-ZeroEcho connection MUST itself be authenticated.
The preferred deployment is:
```text
administrative client
-- mutual TLS -->
trusted reverse proxy
-- separate mutual TLS -->
ZeroEcho
```
The proxy MUST authenticate using a dedicated infrastructure identity that is distinct from every end-user principal.
The server MUST distinguish:
```text
transport principal
end-client principal
```
The transport principal identifies the trusted proxy. The end-client principal is used for administrative authorization.
The proxy principal MUST NOT inherit the permissions of the end-client principal. It MAY only receive the narrowly scoped infrastructure authority required to forward an authenticated client identity.
That infrastructure authority MUST be an explicit realm-scoped permission equivalent to `FORWARD_AUTHENTICATED_CLIENT_IDENTITY`. It grants no PKI, identity-administration, PII, approval, or end-client authority, and break-glass access MUST NOT create implicit forwarding authority.
### Forwarded certificate formats
The preferred forwarding format is the RFC 9440 `Client-Cert` header and optional `Client-Cert-Chain` header.
ZeroEcho MAY additionally support one explicitly configured NGINX compatibility format based on URL-escaped PEM, but it MUST NOT auto-detect arbitrary proxy-specific identity headers.
Every accepted format MUST have:
* one stable configuration identifier;
* strict bounded decoding;
* duplicate-header rejection;
* exact certificate consumption;
* canonical certificate validation;
* no subject-string-only identity mapping.
### Header sanitization
The trusted reverse proxy MUST remove or overwrite every incoming forwarded client-certificate header.
A request for which external mutual TLS authentication did not succeed MUST NOT contain a forwarded client identity.
ZeroEcho MUST reject:
* forwarded identity received from an untrusted transport principal;
* duplicate client-certificate headers;
* malformed certificate or chain data;
* absent client identity for an administrative operation;
* zero or multiple principal mappings;
* conflicting identity representations.
### Independent backend validation
ZeroEcho MUST independently validate the forwarded certificate and, when provided, its chain using the configured administrative-client trust policy.
ZeroEcho MUST validate:
* strict canonical certificate DER;
* certificate validity;
* configured trust;
* allowed certificate usage;
* exact certificate or SPKI commitment mapping;
* persisted principal enabled state.
The proof of possession for the external client certificate is attested by the trusted reverse proxy. Therefore, proxy authentication and transport integrity are part of the authentication authority in this mode.
### Backend transport policy
Plain unauthenticated HTTP over TCP MUST NOT be an enterprise-grade trusted-proxy mode, including over loopback.
A production trusted-proxy deployment MUST use either:
* mutually authenticated TLS from proxy to ZeroEcho; or
* a future explicitly supported operating-system-authenticated local transport such as a permission-protected Unix domain socket.
Client source-address headers, `X-Forwarded-For`, `Forwarded`, or PROXY protocol information MUST NOT establish authentication or authorization.
### Audit
Safe authentication audit MUST record:
* authentication mode;
* transport-principal identity;
* end-client principal identity when resolved;
* request correlation ID;
* safe result classification.
It MUST NOT record:
* raw certificates;
* subjects or SAN values;
* forwarded certificate headers;
* private material;
* proxy credentials;
* unrestricted parser or provider errors.
### Security invariant
Possession of a forwarded identity header is never authentication authority by itself.
Forwarded client identity is trusted only when all of the following hold:
```text
configured trusted-proxy mode
+ authenticated authorized transport principal
+ valid forwarded-certificate format
+ successful certificate and principal validation
```
## 18. Server implementation constraints
The future server implementation MUST:

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"serverName": "zeroecho-admin",
"realm": {
"realmId": "production",
@@ -56,7 +56,8 @@
"maximumBodyBytes": 1048576
},
"authentication": {
"mappings": [{
"mode": "DIRECT_MTLS",
"directClientMappings": [{
"mappingId": "bootstrap-administrator",
"principalId": "bootstrap-admin",
"certificateSha256": "0000000000000000000000000000000000000000000000000000000000000000"

View File

@@ -1,5 +1,5 @@
{
"version": 1,
"version": 2,
"serverName": "zeroecho-admin",
"realm": {
"realmId": "production",
@@ -56,7 +56,8 @@
"maximumBodyBytes": 1048576
},
"authentication": {
"mappings": [{
"mode": "DIRECT_MTLS",
"directClientMappings": [{
"mappingId": "bootstrap-administrator",
"principalId": "bootstrap-admin",
"certificateSha256": "0000000000000000000000000000000000000000000000000000000000000000"

View File

@@ -0,0 +1,44 @@
{
"version": 2,
"serverName": "zeroecho-admin-nginx",
"realm": {
"realmId": "production",
"displayName": "ZeroEcho Production",
"authorityExposure": {"mode":"ALL_REALM_AUTHORITIES","authorityIds":[],"creationPermitted":false},
"authorizationCommitment": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"approvalCommitment": "96f85d99cb556b16af9b963833a832f905010681f437a757d94aab4a22e4c29c",
"disclosureCommitment": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"disclosureDefaults": {"rootCa":"PUBLIC","intermediateCa":"PUBLIC","caChain":"PUBLIC","crl":"PUBLIC","leaf":"OWNER_ONLY","sensitiveLeaf":"RESTRICTED"},
"controlLog": "state/server-control.log",
"controlStoreId": "0123456789abcdef0123456789abcdef",
"approvalPolicy": {"policyId":"high-risk","threshold":1,"eligibleApprovers":["bootstrap-approver"],"requiredRoleTemplateIds":[],"requesterSeparation":true,"lifetimeMillis":3600000,"justificationRequired":true},
"pkiSession": {
"version": 1,
"store": {"id":"fs","properties":{"root":"state/pki"}},
"audit": {"id":"file","properties":{"root":"state/audit"}},
"publishers": [],
"bindingProviders": []
}
},
"listener": {
"address": "127.0.0.1",
"port": 8443,
"tlsProvider": {"id":"jsse-pkcs12","properties":{"keyStore":"tls/server-identity.p12","keyStorePasswordEnvironment":"ZEROECHO_TLS_KEYSTORE_PASSWORD","trustStore":"tls/proxy-transport-trust.p12","trustStorePasswordEnvironment":"ZEROECHO_PROXY_TRUSTSTORE_PASSWORD"}},
"clientCertificateRequired": true,
"maximumHeaderBytes": 65536,
"maximumBodyBytes": 1048576
},
"authentication": {
"mode": "TRUSTED_REVERSE_PROXY",
"proxyTransportMappings": [{"mappingId":"proxy-transport","principalId":"trusted-proxy","certificateSha256":"1111111111111111111111111111111111111111111111111111111111111111"}],
"forwardedClientMappings": [{"mappingId":"forwarded-admin","principalId":"bootstrap-admin","subjectPublicKeyInfoSha256":"2222222222222222222222222222222222222222222222222222222222222222"}],
"trustedProxyPrincipalIds": ["trusted-proxy"],
"forwardedCertificateFormat": "NGINX_ESCAPED_PEM_V1",
"forwardedCertificateHeaderName": "X-ZeroEcho-Client-Cert",
"administrativeClientTrust": {"id":"jsse-pkcs12-client-trust","properties":{"trustStore":"tls/administrators-trust.p12","trustStorePasswordEnvironment":"ZEROECHO_ADMIN_TRUSTSTORE_PASSWORD"}},
"maximumForwardedCertificateBytes": 65536,
"maximumForwardedChainBytes": 524288
},
"execution": {"transportWorkers":8,"transportQueueCapacity":64,"operationWorkers":4,"operationQueueCapacity":32,"maximumAdmittedRequests":96,"defaultDeadlineMillis":30000,"maximumDeadlineMillis":120000,"gracefulShutdownMillis":30000,"forcedShutdownMillis":10000},
"runtime": {}
}

View File

@@ -0,0 +1,45 @@
{
"version": 2,
"serverName": "zeroecho-admin-proxy",
"realm": {
"realmId": "production",
"displayName": "ZeroEcho Production",
"authorityExposure": {"mode":"ALL_REALM_AUTHORITIES","authorityIds":[],"creationPermitted":false},
"authorizationCommitment": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"approvalCommitment": "96f85d99cb556b16af9b963833a832f905010681f437a757d94aab4a22e4c29c",
"disclosureCommitment": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"disclosureDefaults": {"rootCa":"PUBLIC","intermediateCa":"PUBLIC","caChain":"PUBLIC","crl":"PUBLIC","leaf":"OWNER_ONLY","sensitiveLeaf":"RESTRICTED"},
"controlLog": "state/server-control.log",
"controlStoreId": "0123456789abcdef0123456789abcdef",
"approvalPolicy": {"policyId":"high-risk","threshold":1,"eligibleApprovers":["bootstrap-approver"],"requiredRoleTemplateIds":[],"requesterSeparation":true,"lifetimeMillis":3600000,"justificationRequired":true},
"pkiSession": {
"version": 1,
"store": {"id":"fs","properties":{"root":"state/pki"}},
"audit": {"id":"file","properties":{"root":"state/audit"}},
"publishers": [],
"bindingProviders": []
}
},
"listener": {
"address": "127.0.0.1",
"port": 8443,
"tlsProvider": {"id":"jsse-pkcs12","properties":{"keyStore":"tls/server-identity.p12","keyStorePasswordEnvironment":"ZEROECHO_TLS_KEYSTORE_PASSWORD","trustStore":"tls/proxy-transport-trust.p12","trustStorePasswordEnvironment":"ZEROECHO_PROXY_TRUSTSTORE_PASSWORD"}},
"clientCertificateRequired": true,
"maximumHeaderBytes": 65536,
"maximumBodyBytes": 1048576
},
"authentication": {
"mode": "TRUSTED_REVERSE_PROXY",
"proxyTransportMappings": [{"mappingId":"proxy-transport","principalId":"trusted-proxy","certificateSha256":"1111111111111111111111111111111111111111111111111111111111111111"}],
"forwardedClientMappings": [{"mappingId":"forwarded-admin","principalId":"bootstrap-admin","subjectPublicKeyInfoSha256":"2222222222222222222222222222222222222222222222222222222222222222"}],
"trustedProxyPrincipalIds": ["trusted-proxy"],
"forwardedCertificateFormat": "RFC9440",
"forwardedCertificateHeaderName": "Client-Cert",
"forwardedCertificateChainHeaderName": "Client-Cert-Chain",
"administrativeClientTrust": {"id":"jsse-pkcs12-client-trust","properties":{"trustStore":"tls/administrators-trust.p12","trustStorePasswordEnvironment":"ZEROECHO_ADMIN_TRUSTSTORE_PASSWORD"}},
"maximumForwardedCertificateBytes": 65536,
"maximumForwardedChainBytes": 524288
},
"execution": {"transportWorkers":8,"transportQueueCapacity":64,"operationWorkers":4,"operationQueueCapacity":32,"maximumAdmittedRequests":96,"defaultDeadlineMillis":30000,"maximumDeadlineMillis":120000,"gracefulShutdownMillis":30000,"forcedShutdownMillis":10000},
"runtime": {}
}