Skip to content

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

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

feat: RFC 8705 mutual-TLS client authentication for CF app instance identity#3972
rkoster wants to merge 132 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.

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 70 out of 70 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:141

  • The client-admin path validates only top-level claim settings. UaaClientDetails.setTlsClientAuthConfiguration stores the typed configuration as a nested object under tls-client-auth-ca, and the runtime authentication/enhancer code explicitly accepts that shape, so malformed nested mappings/templates and a malformed nested trusted-proxy CA can be persisted without these checks. Normalize and validate the nested shape here as ZoneEndpointsClientDetailsValidator does, including its trusted-proxy CA.
        checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, client.getClientId());
        validateTlsClientAuthClaimConfig(client.getAdditionalInformation(), client.getClientId());

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:475

  • This accepts any syntactically valid regex for every field, but runtime applies pattern only to subject_ou and emits a value only when capture group 1 exists. A pattern on subject_cn/subject_o, or an OU pattern with no capture group, is therefore accepted and then silently ignored, producing unexpected or missing claims. Reject patterns for non-OU fields and require at least one capture group during validation.
            String pattern = mapping.getPattern();
            if (pattern != null && !pattern.isBlank()) {
                try {
                    Pattern.compile(pattern);

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 70 out of 70 changed files in this pull request and generated 4 comments.

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 71 out of 71 changed files in this pull request and generated 3 comments.

Suppressed comments (2)

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:142

  • This validates only the outer additionalInformation map, although checkMtlsClientConfigAllowed explicitly accepts tls-client-auth-ca as a nested Map or TlsClientAuthConfiguration. Claim mappings, templates, required claims, and the trusted-proxy CA inside that nested object therefore bypass validation and can be persisted malformed; the zone validator handles this by validating a normalized nested map as well. Normalize and validate the nested configuration here so all client-admin entry points enforce the same contract.
        checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, client.getClientId());
        validateTlsClientAuthClaimConfig(client.getAdditionalInformation(), client.getClientId());

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:499

  • Syntax compilation does not make these client-configurable Java regular expressions safe. At token time the pattern is compiled and matched against certificate OU data with no input bound or timeout, so a catastrophic expression such as ^(a+)+$ can consume excessive CPU on every authentication attempt. Restrict mappings to a safe regex grammar/engine, or enforce defensible pattern and certificate-field bounds before accepting the configuration.
            if (claim == null || claim.isBlank()) {
                throw new InvalidClientDetailsException(
                        "tls-client-auth-claim-mappings entry has a blank claim for client_id=" + clientId);
            }
            String pattern = mapping.getPattern();

Comment thread uaa/slateCustomizations/source/index.html.md.erb Outdated
Comment thread docs/UAA-Configuration-Reference.md

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 71 out of 71 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

Previously missed (2) — in code that hasn't changed since the last review.

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:491

  • A mapping such as aud.app or sub.id is accepted here and converted by MtlsClaimsEnhancer into a map-valued top-level aud/sub. UaaTokenServices explicitly reapplies those two roots after defaults, so the resulting JWT violates the required string-or-array/string claim types. Reject dotted mappings under these registered claim names; callers can use the dedicated templates for valid overrides.
            String claim = mapping.getClaim();
            if (claim == null || claim.isBlank()) {
                throw new InvalidClientDetailsException(
                        "tls-client-auth-claim-mappings entry has a blank claim for client_id=" + clientId);

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:537

  • Blank audience-template entries are accepted, but MtlsClaimsEnhancer renders them as empty strings and then replaces UAA's normal audience with aud: [""]. That yields a token with no usable audience rather than treating the configuration as invalid. Reject blank entries just as null entries are rejected.
                    if (!template.isBlank()) {

model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java:110

  • Copying a deserialized or JDBC-style UaaClientDetails removes its mTLS CA. Such clients keep the flat tls-client-auth-* values in additionalInformation while the ignored typed field is null; after line 107 copies that map, this call invokes the setter with null and deletes the CA. InMemoryClientDetailsService.addClientDetails() uses this constructor, so the copied client can no longer authenticate with mTLS. Preserve the flat representation when no typed configuration exists.
            this.setTlsClientAuthConfiguration(uaa.getTlsClientAuthConfiguration());

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:495

  • This validates only regex syntax, but runtime extraction applies patterns only to subject_ou and returns a value only when capture group 1 exists. Consequently, a pattern on subject_cn/subject_o is silently ignored, while an OU pattern without a capture group silently emits no claim. Reject those configurations here so accepted client settings match the documented/runtime behavior.
                    Pattern.compile(pattern);

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 71 out of 71 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java:392

  • Including this @JsonIgnore field in equality makes a JSON round trip unequal: the original object has the typed field set, but deserialization restores only the flattened additionalInformation entries. Since those entries already fully represent the configuration, compare that single representation (and remove the matching field contribution from hashCode at line 426) or ensure deserialization reconstructs the typed field.
        if (!Objects.equals(clientJwtConfig, other.clientJwtConfig)) {
            return false;
        }
        return Objects.equals(tlsClientAuthConfiguration, other.tlsClientAuthConfiguration);

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:500

  • This accepts a pattern for subject_cn/subject_o and also accepts patterns with no capture group, but extraction applies patterns only to subject_ou and returns only group 1. Such configurations therefore silently emit an unfiltered value or no claim at all. Reject nonblank patterns unless field is subject_ou, and require at least one capturing group; update the existing validator tests that currently treat a CN pattern as valid.
            String pattern = mapping.getPattern();
            if (pattern != null && !pattern.isBlank()) {
                try {
                    Pattern.compile(pattern);
                } catch (PatternSyntaxException e) {
                    throw new InvalidClientDetailsException(
                            "tls-client-auth-claim-mappings entry has an invalid pattern '" + pattern
                                    + "' for client_id=" + clientId + ": " + e.getMessage(), e);
                }

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 71 out of 71 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java:394

  • tlsClientAuthConfiguration is derived into additionalInformation, but it is @JsonIgnore and remains null on JSON/JDBC reload. Two clients with identical persisted mTLS settings therefore compare unequal solely because one was built through the typed setter. Equality should use the persisted additionalInformation representation and not compare this duplicate transient field.
        if (!Objects.equals(clientJwtConfig, other.clientJwtConfig)) {
            return false;
        }
        return Objects.equals(tlsClientAuthConfiguration, other.tlsClientAuthConfiguration);

model/src/main/java/org/cloudfoundry/identity/uaa/client/UaaClientDetails.java:428

  • Including the derived, non-serialized tlsClientAuthConfiguration in the hash gives different hashes to otherwise identical persisted/reloaded clients. Remove it along with the redundant equality comparison so equals/hashCode remain stable across serialization and database loading.
        result = prime * result + (tlsClientAuthConfiguration == null ? 0 : tlsClientAuthConfiguration.hashCode());

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:388

  • A valid tls-client-auth-trusted-proxy-ca is accepted even when tls-client-auth-ca is absent. Such a client is not mTLS-configured (TlsClientAuthConfiguration.isConfigured requires the primary CA), so this documented proxy-only topology silently has no effect and can leave secret authentication active. Reject the dependent proxy CA unless a nonblank primary CA is also supplied.
        if (additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_TRUSTED_PROXY_CA)) {
            try {
                PemCertificateParser.parseCertificate((String) additionalInfo.get(
                        TlsClientAuthConfiguration.TLS_CLIENT_AUTH_TRUSTED_PROXY_CA));

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminEndpointsValidator.java:496

  • Patterns are only applied at runtime for subject_ou, and matchFirstOu requires at least one capture group. This validation currently accepts patterns on subject_cn/subject_o (where they are silently ignored) and OU patterns with no capture group (which silently produce no claim). Reject both forms so accepted configuration matches the documented/runtime behavior.
            String pattern = mapping.getPattern();
            if (pattern != null && !pattern.isBlank()) {
                try {
                    Pattern.compile(pattern);
                } catch (PatternSyntaxException e) {

@rkoster
rkoster force-pushed the feat/rfc8705-mtls-client-auth branch from 3e58161 to 471ad57 Compare September 4, 2026 08:40
@rkoster
rkoster marked this pull request as ready for review September 7, 2026 14:42
@rkoster
rkoster requested review from a team September 7, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants