Skip to content

feat: RFC 8705 mutual-TLS client authentication for CF app instance identity - #3972

Closed
rkoster wants to merge 146 commits into
cloudfoundry:developfrom
rkoster:feat/rfc8705-mtls-client-auth
Closed

rkoster wants to merge 146 commits into
cloudfoundry:developfrom
rkoster:feat/rfc8705-mtls-client-auth

Conversation

@rkoster

@rkoster rkoster commented Jul 3, 2026 •

Copy link
Copy Markdown

Summary

Implements RFC 8705 mutual-TLS client
authentication for Cloud Foundry app instance identity, enabling workload identity
federation with AWS, GCP, Azure, and any OIDC-aware service.

CF app instances already receive a short-lived X.509 certificate from the Diego
Cell (instance.crt / instance.key). This change lets an app exchange that cert
for a UAA JWT containing verified app_guid, space_guid, org_guid, and
cf_instance_guid claims — without secrets or user credentials.

How it works

CF app ──cert──▶ Gorouter (sanitize_set) ──XFCC──▶ UAA /oauth/mtls/token
                                                         │
                                            ClientCertificateMapper
                                            (XFCC → X509Certificate attr)
                                                         │
                                            ClientDetailsAuthenticationProvider
                                            (tls_client_auth: PKIX chain validation)
                                                         │
                                            MtlsClaimsEnhancer
                                            (cert OU → app/space/org_guid claims)
                                                         │
                                            ◀── JWT with CF identity claims ──

Changes (this PR — 18 commits)

Model layer:

  • ClientAuthentication: add tls_client_auth constant
  • TokenConstants: add CLIENT_AUTH_TLS_CLIENT_AUTH
  • TlsClientAuthConfiguration: per-client CA PEM + claim-mapping model
  • UaaClientDetails: add tlsClientAuthConfiguration field
  • OpenIdConfiguration: add mtls_endpoint_aliases to OIDC discovery

Server layer:

  • ClientDetailsAuthenticationProvider: isTlsClientAuth(), validateTlsClientAuth(), getTlsClientAuthConfiguration() — handles in-memory, Map (Jackson), and flat String PEM (BOSH) config forms
  • TlsClientAuthentication: PKIX cert chain validation against per-client CA
  • ClientCertificateMapper registration: SpringServletXmlFiltersConfiguration registers the java-buildpack-client-certificate-mapper-jakarta filter for /oauth/mtls/* to materialise X-Forwarded-Client-Cert as a jakarta.servlet.request.X509Certificate attribute
  • ClientCredentialsTokenGranter: allow tls_client_auth alongside client_secret
  • MtlsClaimsEnhancer: UaaTokenEnhancer that reads cert subject OU fields and maps them to JWT claims per per-client configuration; handles DB-loaded clients (reads additionalInformation directly) and Diego multi-valued RDNs
  • FilterChainOrder.OAUTH_11 + mtlsTokenEndpointSecurity: dedicated security filter chain for /oauth/mtls/token with CSRF disabled
  • UaaTokenEndpoint: add /oauth/mtls/token to @RequestMapping
  • OIDC discovery: expose mtls_endpoint_aliases

Deployment notes

Requires the Gorouter to be configured with forwarded_client_cert: sanitize_set
so it validates the TLS session cert and injects it as X-Forwarded-Client-Cert.

The UAA client for an app must be configured with:

tls-client-auth-ca: <instance-identity CA certificate PEM>
tls-client-auth-trusted-proxy-ca: <Gorouter backend mTLS CA certificate PEM, e.g. service_cf_internal_ca>
tls-client-auth-claim-mappings:
  - field: subject_cn
    claim: cf_instance_guid
  - field: subject_ou
    pattern: "app:(.+)"
    claim: app_guid
  - field: subject_ou
    pattern: "space:(.+)"
    claim: space_guid
  - field: subject_ou
    pattern: "organization:(.+)"
    claim: org_guid

tls-client-auth-trusted-proxy-ca switches this client to the Gorouter/XFCC-forwarding-only
topology: UAA then requires the X-Forwarded-Client-Cert header to actually be present and its
immediate TLS peer to have presented a certificate signed by that CA during the handshake --
preventing a direct caller (bypassing the Gorouter) from replaying a harvested certificate it
doesn't hold the private key for, or a direct connection from being silently accepted instead.
For a client that connects to UAA directly (e.g. permitted by Application Security Group
configuration, bypassing the Gorouter), omit this property entirely -- configuring it at all
makes the client reject direct connections. See
docs/UAA-Client-Authentication.md
for both cases; two separate UAA clients are needed to support both patterns for the same
workload.

Proof of concept

End-to-end verified on a real CF deployment: a Go app pushes its Diego instance cert
to POST /oauth/mtls/token, and the returned JWT contains:

{
  "app_guid":         "b0bff1c2-a258-4060-981d-601f22e6bcf8",
  "space_guid":       "02700fa7-8db7-4598-b015-9a5fc73d4656",
  "org_guid":         "8deb6c47-8460-4501-8a86-246b774d97e4",
  "cf_instance_guid": "86bf36e4-af79-4d7a-6484-0d89",
  "client_auth_method": "tls_client_auth",
  "cnf": { "x5t#S256": "rk4P4d0DXNJDpZeOotKRUzmoaomqSqPQn8OzyKQhMuw" }
}

All GUIDs verified against cf app, cf org, and cf space --guid.

Related

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds RFC 8705 mutual-TLS client authentication support for Cloud Foundry app instance identity by introducing a dedicated /oauth/mtls/token endpoint, validating instance certificates against per-client CA configuration, and enriching issued JWTs with CF identity claims derived from certificate subject fields. It also updates OIDC discovery to advertise mTLS endpoint aliases and tls_client_auth as a supported token endpoint authentication method.

Changes:

  • Introduces /oauth/mtls/token with a dedicated Spring Security filter chain and request-to-certificate mapping via ClientCertificateMapper.
  • Adds TLS client certificate validation (TlsClientAuthentication) and a token enhancer (MtlsClaimsEnhancer) to emit cnf.x5t#S256 plus configured subject-derived claims.
  • Extends client auth method support across model/constants and OIDC discovery metadata (tls_client_auth, mtls_endpoint_aliases).

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthenticationTest.java Adds unit coverage for null inputs and malformed CA handling in TLS cert validation.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/MtlsClaimsEnhancerTest.java Verifies OU/CN claim extraction and cnf.x5t#S256 behavior for mTLS tokens.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/tls/ClientCertificateMapperFilterTest.java Confirms servlet filter registration for mapping XFCC to request X509Certificate attribute on /oauth/mtls/*.
server/src/test/java/org/cloudfoundry/identity/uaa/oauth/provider/client/ClientCredentialsTokenGranterTests.java Ensures tls_client_auth is allowed for client_credentials.
server/src/test/java/org/cloudfoundry/identity/uaa/authentication/UaaClientAuthenticationProviderTest.java Updates provider wiring to include TlsClientAuthentication.
server/src/test/java/org/cloudfoundry/identity/uaa/authentication/ClientDetailsAuthenticationProviderTests.java Adds tests for mtls path detection and TLS config deserialization behavior.
server/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpointsTest.java Validates discovery document includes mtls_endpoint_aliases.token_endpoint.
server/src/main/java/org/cloudfoundry/identity/uaa/web/FilterChainOrder.java Adds a new security chain order slot (OAUTH_11) for the mTLS token endpoint chain.
server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlFiltersConfiguration.java Registers ClientCertificateMapper filter for /oauth/mtls/*.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/UaaTokenEndpoint.java Expands token endpoint mapping to include /oauth/mtls/token.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/TlsClientAuthentication.java Adds per-client CA-based PKIX validation and request certificate extraction helper.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/tls/MtlsClaimsEnhancer.java Implements JWT enrichment from cert subject + cnf.x5t#S256 for the mTLS flow.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/provider/client/ClientCredentialsTokenGranter.java Allows tls_client_auth as a valid auth method for client credentials.
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointSecurityConfiguration.java Adds dedicated security filter chain for /oauth/mtls/token (stateless + CSRF disabled).
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointBeanConfiguration.java Wires TlsClientAuthentication into ClientDetailsAuthenticationProvider bean construction.
server/src/main/java/org/cloudfoundry/identity/uaa/authentication/ClientDetailsAuthenticationProvider.java Detects mTLS path, validates certs, and parses per-client TLS configuration from additional info.
server/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConnectEndpoints.java Populates mtls_endpoint_aliases in OIDC discovery.
server/build.gradle.kts Adds dependency on the Gorouter client certificate mapper (Jakarta).
model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json Updates fixture to include tls_client_auth in supported auth methods.
model/src/test/java/org/cloudfoundry/identity/uaa/constants/ClientAuthenticationTest.java Adds tests for tls_client_auth support, secret requirements, and validity rules.
model/src/test/java/org/cloudfoundry/identity/uaa/client/UaaClientDetailsTest.java Adds JSON round-trip test for TLS client auth config and adjusts hashCode assertion.
model/src/test/java/org/cloudfoundry/identity/uaa/client/TlsClientAuthConfigurationTest.java Adds unit tests for TLS auth config JSON round-tripping and equality semantics.
model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java Updates supported auth methods expectations and adds tests for mTLS aliases field.
model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/TokenConstants.java Exposes CLIENT_AUTH_TLS_CLIENT_AUTH constant.
model/src/main/java/org/cloudfoundry/identity/uaa/constants/ClientAuthentication.java Adds TLS_CLIENT_AUTH constant and updates supported/valid method logic and calculation.
model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java Introduces tlsClientAuthConfiguration field and includes it in equals/hashCode.
model/src/main/java/org/cloudfoundry/identity/uaa/client/TlsClientAuthConfiguration.java Adds model for trusted CA PEM + claim mapping configuration.
model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java Adds mtls_endpoint_aliases and includes tls_client_auth in supported methods.

Comment thread server/build.gradle.kts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

Comment thread model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.

rkoster added 15 commits August 18, 2026 08:50
Add TlsClientAuthConfiguration field serialized as tls-client-auth-ca in
client JSON, following the clientJwtConfig pattern. Includes getter/setter,
copy constructor support, equals/hashCode. Fix fragile isPositive() hash
code assertion to isNotZero().
…dpoint

The BOSH ERB template emits 'mtls.endpoint' (from the nested mtls.endpoint
YAML block) but the @value annotation was reading 'uaa.mtls_endpoint_path',
a key never emitted by the template. Align the annotation to the actual
Spring property so operator-configured paths are honoured.
@strehle

strehle commented Sep 22, 2026

Copy link
Copy Markdown
Member

there is a 2nd #4076

fhanik added a commit to fhanik/uaa that referenced this pull request Sep 24, 2026
@fhanik

fhanik commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Superseded by #4076

fhanik added a commit to fhanik/uaa that referenced this pull request Sep 24, 2026
@rkoster rkoster mentioned this pull request Sep 24, 2026
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 24, 2026
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 25, 2026
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 25, 2026
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 25, 2026
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 29, 2026
…round

rkoster pushed 14 follow-up commits to cloudfoundry#3972 (471ad57..a80e1c9) addressing
the cloudfoundry#4075 review. 471ad57 is the last commit squashed into this branch's
base, so none of those 14 are here -- both branches fixed most of the same
findings independently. Comparing them surfaced three gaps on our side. These
tests fail today and are made to pass in the following commit.

G2  CA rotation is silently broken. A platform CA has to be rotatable without
    an outage, which means publishing a bundle holding the outgoing and
    incoming CA together. Registration already accepts one --
    PemCertificateParser.parseCertificateChain reads every entry -- but
    TlsClientAuthentication has its own private parser that takes only the
    first PEM object and builds a single TrustAnchor. So the rotation appears
    to configure successfully and then half the workloads fail. Observed: a
    certificate from the second CA in the bundle is refused with
    "certificate chain validation failed". G1 pins the first-CA case, which
    already works, so the pair localises the defect to the anchor set.

G3  The mTLS endpoint serves GET. /oauth/mtls/token shares a @RequestMapping
    with /oauth/token and inherits its GET support, so a token request can be
    made with the parameters in the query string, where access logs and every
    proxy in front of UAA record them. /oauth/token keeps GET for backwards
    compatibility; this endpoint is new and has no such debt. Expects 405 with
    an Allow: POST header. G4 guards the blast radius -- both paths are mapped
    by the same controller method, so restricting one must not restrict the
    other.

E7  The buildpack certificate mapper is registered even when mTLS is disabled.
    It exists only to turn Gorouter's X-Forwarded-Client-Cert header into the
    servlet X509Certificate attribute for this one endpoint, so with the
    feature off it is a third-party filter in every request's path for no
    purpose. It is also built reflectively from a package-private class and
    throws IllegalStateException on failure, which couples UAA's ability to
    start to that jar being present even for operators who will never enable
    mTLS.

Not covered by a test, and fixed in the following commit: revocation
behaviour (setRevocationEnabled(false) -- no CRL, no OCSP) is not mentioned
anywhere under docs/.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 29, 2026
…mapper

Makes the three tests from the previous commit pass, and documents revocation.
Ported in intent from rkoster's cloudfoundry#3972 follow-ups (ede683e, aa075f5,
cb5fbd8, 1cc3ed4); the implementations here are our own, since the two
branches have diverged.

CA rotation (G1, G2)
  TlsClientAuthentication had a private single-certificate PEM parser and built
  a one-element TrustAnchor set, while registration validated the value with
  PemCertificateParser, which reads every certificate. A rotation bundle
  therefore registered successfully and then only the first CA was trusted.
  Both call sites -- the client CA and the trusted-proxy CA -- now use the same
  shared parser, and every certificate in the value becomes an anchor, so an
  operator can publish outgoing+incoming CAs for the overlap window. The
  private parser is deleted, which removes the asymmetry rather than papering
  over it. C3's expected message moves with it: the shared parser says "Not a
  PEM-encoded X.509 certificate" where the private one said "No PEM object
  found in tls-client-auth-ca"; same status, same error code, same distinction
  between a bad CA and a certificate that simply did not validate.

POST-only (G3, G4)
  Enforced in MtlsEndpointAvailabilityFilter rather than in UaaTokenEndpoint.
  "Method not allowed" is a property of the resource, not of the caller, so
  deciding it ahead of Spring Security means an unauthenticated GET is answered
  405 -- with an Allow header, per RFC 9110 -- instead of 401, and PUT/DELETE
  are covered by the same check. UaaTokenEndpoint is left untouched, so
  /oauth/token keeps the GET support it has for backwards compatibility. The
  disabled check still runs first: a 404 endpoint must not disclose which
  methods it would have accepted.

Certificate mapper gating (E7)
  clientCertificateMapperFilter now takes uaa.mtls-enabled and returns a
  disabled registration when the feature is off. Beyond not placing a
  third-party filter in every request's path for no reason, this stops a
  deployment that will never use mTLS from depending on the buildpack jar
  merely to boot -- the class is package-private and loaded reflectively, and
  its absence throws. Existing callers in the server tests pass true; a new
  case pins the disabled branch.

Revocation documentation
  docs/UAA-Client-Authentication.md gains an "Operating tls_client_auth"
  section recording that PKIX validation runs with setRevocationEnabled(false)
  -- no CRL, no OCSP -- what that means for a stolen certificate, and that
  removing tls-client-auth-ca is the way to cut a client off. The same section
  documents the rotation procedure the fix above enables. No new markdownlint
  findings (21 before, 21 after; all pre-existing).

Verified: 50 mTLS MockMvc tests and 101 server-module tls/token tests pass.
@fhanik

fhanik commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

superseded by #4076

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clarification needed The issue is not accepted but we need clarification in_review The PR is currently in review

Projects

Development

Successfully merging this pull request may close these issues.

5 participants