PingAuthorize Talk to us

Workload identity · key manager · trust manager · servlet

SPIFFE mTLS

Gives a PingAuthorize connection handler a SPIFFE workload identity. The server presents its own X.509-SVID, accepts only clients whose SVIDs chain to the SPIRE trust bundle - optionally pinned to listed SPIFFE IDs - and a servlet reads the caller's ID off the verified certificate. SVIDs rotate in place; nothing restarts.

The thing to understand first

On PingAuthorize, mTLS is not something a servlet does

Jetty terminates TLS in the HTTP Connection Handler, which builds one SSLContext from the key manager provider and trust manager provider it is configured with. So this module is three Server SDK extensions and one handler - and the servlet is the smallest part.

Underneath, java-spiffe 0.8.12 does the SPIFFE work: its X.509 source streams SVID and bundle updates from the Workload API, and its key and trust managers read the current values on every handshake. What this module adds is the Server SDK lifecycle around them, the allow-list, and one design choice: both providers read through a live source, so dsconfig can move the socket or change the allow-list and the managers already handed to Jetty follow on the next handshake.

PieceWhat it does
SpiffeKeyManagerProvider
Third-Party Key Manager Provider
The server's TLS certificate is its X.509-SVID from the Workload API, served under the alias spiffe. Read on every handshake, so SPIRE's rotation needs no handler restart.
SpiffeTrustManagerProvider
Third-Party Trust Manager Provider
Client chains are validated against the bundle the Workload API keeps current. With accepted-spiffe-id set, an unlisted workload cannot finish the handshake.
SPIFFE Who Am I
HTTP Servlet Extension
GET /spiffe/whoami echoes the peer's SPIFFE ID - proof the SPIRE → handler → servlet path works end to end.
PeerSpiffeId
helper
Reads the verified chain off the request and returns its URI SAN. The AuthZEN Adapter reads its caller the same way.

Front doors · live

Which handler serves what

Registering the module adds a way in; it does not close the old one. Out of the box the HTTPS handler serves both decision endpoints to anyone holding the shared secret. Lock-down is a change to the handlers' servlet lists plus one loopback handler for the caller that lives inside the server. Switch between the two states.

One connection

Handshake to servlet

  1. At handler start, both providers open the Workload API. SVID and trust bundle stream in and rotate in place.
  2. The PEP fetches its own X.509-SVID from the SPIRE agent and sends a ClientHello to :8444.
  3. The handler asks the key manager for the current SVID chain and key, and presents it with a CertificateRequest.
  4. The PEP answers with its client SVID. The handler hands the chain to the trust manager: chain against the bundle, then SPIFFE ID against accepted-spiffe-id.
  5. Handshake complete. Jetty's SecureRequestCustomizer puts the verified chain on every request as the X509Certificate attribute.
  6. GET /spiffe/whoami → 200 {"spiffe_id":"spiffe://bank.example/ns/payments/sa/payments-gateway", "trust_domain":"bank.example", "path":"/ns/payments/sa/payments-gateway"}
  7. An unlisted workload, or a chain from another CA with the same trust-domain name, gets a CertificateException at step 4 and a TLS alert. No request is ever seen - it never reaches a 401.

Grounded in PingAuthorize's own handler code

The handler behaviour this module relies on was confirmed in the PingAuthorize 10.1.0.0 and 11.1.0.0 server libraries: the handler calls getKeyManagers() and getTrustManagers(), builds an SSLContext for Jetty's SslContextFactory.Server, and maps ssl-client-auth-policy to want or need client auth.

Ping's own documentation says HTTP client certificates are used "for TLS mutual authentication only" - the server maps them to no identity. That gap is exactly what PeerSpiffeId fills. Leave the handler's ssl-cert-nickname unset: the SVID is served under java-spiffe's own alias.

Every TLS property on a handler is read when it starts, so changing one means disabling and re-enabling the handler. SVID rotation needs neither; nor does changing the allow-list or the socket on a provider.

Recipe

Locking the governance engine to mTLS

These commands take a server from the as-registered state to the locked-down one shown above. Afterwards, from outside the pod, the native decision APIs - JSON PDP API and PDP Endpoint - need a listed SVID and their shared secret, and the AuthZEN Adapter needs a SPIFFE ID listed in its own accepted-spiffe-id. Inside the pod the adapter reaches the engine on 127.0.0.1 only. Before you start, set accepted-spiffe-id on the SPIFFE Trust Manager Provider so unlisted workloads fail the handshake.

# 1. a loopback-only handler for the AuthZEN Adapter's own call to the engine
dsconfig create-connection-handler --handler-name "Loopback PDP Connection Handler" --type http \
  --set enabled:true --set listen-address:127.0.0.1 --set listen-port:1444 \
  --set use-ssl:false --set allowed-client:127.0.0.1 \
  --set "http-servlet-extension:JSON PDP API"

# 2. point the adapter at it - extension-argument is multi-valued: remove + add, never --set
dsconfig set-http-servlet-extension-prop --extension-name "AuthZEN Adapter" \
  --remove "extension-argument:pdp-url=https://localhost:1443/governance-engine" \
  --add    "extension-argument:pdp-url=http://127.0.0.1:1444/governance-engine" \
  --remove "extension-argument:query-url=https://localhost:1443/governance-engine/query" \
  --add    "extension-argument:query-url=http://127.0.0.1:1444/governance-engine/query"

# 3. serve the adapter and the native decision APIs on the mTLS handler, then restart it
dsconfig set-connection-handler-prop --handler-name "SPIFFE mTLS Connection Handler" \
  --add "http-servlet-extension:AuthZEN Adapter" \
  --add "http-servlet-extension:JSON PDP API" --add "http-servlet-extension:PDP Endpoint"
dsconfig set-connection-handler-prop --handler-name "SPIFFE mTLS Connection Handler" --set enabled:false
dsconfig set-connection-handler-prop --handler-name "SPIFFE mTLS Connection Handler" --set enabled:true

# 4. admit only listed workloads to the adapter, with no api-key fallback
dsconfig set-http-servlet-extension-prop --extension-name "AuthZEN Adapter" \
  --add "extension-argument:accepted-spiffe-id=spiffe://bank.example/ns/payments/sa/payments-gateway" \
  --add "extension-argument:spiffe-required=true"

# 5. take the adapter and the decision APIs off the public HTTPS handler, then restart it
dsconfig set-connection-handler-prop --handler-name "HTTPS Connection Handler" \
  --remove "http-servlet-extension:AuthZEN Adapter" \
  --remove "http-servlet-extension:JSON PDP API" --remove "http-servlet-extension:PDP Endpoint"
dsconfig set-connection-handler-prop --handler-name "HTTPS Connection Handler" --set enabled:false
dsconfig set-connection-handler-prop --handler-name "HTTPS Connection Handler" --set enabled:true
# repeat step 5 for any other handler that serves the adapter, e.g. an HTTP handler behind a TLS edge

Step 4 comes after step 3 because spiffe-required refuses every request that arrives without a listed SVID - on a handler that asks for no client certificate, that is every request. Restarting a handler drops its open connections. A Kubernetes NetworkPolicy limiting :8444 to the PEP namespaces is the layer under all of it.

Deploy

Where the socket comes from

The PingAuthorize container needs the SPIRE agent's Workload API socket. On Kubernetes, the spiffe-csi driver projects it into the pod; a ClusterSPIFFEID gives the PingAuthorize service account an ID such as spiffe://bank.example/ns/paz/sa/pingauthorize, and one for each PEP that will call it.

volumeMounts:
  - name: spiffe-workload-api
    mountPath: /spiffe-workload-api
    readOnly: true
volumes:
  - name: spiffe-workload-api
    csi:
      driver: csi.spiffe.io
      readOnly: true

Needs the socket. Platforms that cannot mount the Workload API socket into the container cannot use workload identity; there, the AuthZEN Adapter's bearer-key front door is the way in.

ArgumentOnMeaning
spiffe-socketboth providersWorkload API endpoint, default unix:///spiffe-workload-api/spire-agent.sock. Changing it re-opens the stream; managers in service follow.
accepted-spiffe-idtrust managerRepeatable. Only these workloads complete the handshake; unset admits any SVID that chains to the bundle. Applies on the next handshake.

A provider fails to initialise, naming the socket it tried, when the Workload API is unreachable within 15 seconds. dsconfig refuses a blank socket or an allow-list entry that is not a SPIFFE ID up front.

# verify from a workload with its own SVID
spire-agent api fetch x509 -socketPath /spiffe-workload-api/spire-agent.sock -write /tmp/svid
curl --cert /tmp/svid/svid.0.pem --key /tmp/svid/svid.0.key \
     --cacert /tmp/svid/bundle.0.pem https://pingauthorize:8444/spiffe/whoami

Assurance and requirements

Tested against the certificates SPIRE issues

The test suite mints a certificate authority and leaf certificates shaped exactly like SPIRE's X.509-SVIDs - a URI SAN, not a CA, digitalSignature only - because java-spiffe refuses anything else. The trust manager is then driven with real chains: a listed workload, an unlisted one, an impostor CA using the same trust-domain name, and a trust domain the bundle has never seen.

The build fails unless configuration handling, the key and trust decisions and the whoami servlet have full line and branch test coverage. The Workload API socket and the handler itself need a live cluster and are outside those tests.

  • PingAuthorize 10 or 11. Built against Server SDK 10.1 and 11.1; the version 11 build reads the certificate from jakarta.servlet.request.X509Certificate. Confirm your exact release with ID Partners.
  • A SPIRE agent. Its Workload API socket mounted into the PingAuthorize container - a Unix domain socket, with no TCP fallback.
  • Linux x86_64 or aarch64. The bundled java-spiffe 0.8.12 carries the native transport for both.
  • One bundled dependency. java-spiffe ships in the bundle as a single self-contained jar.