Skip to content

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

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

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

a7b77e3/bf6149ae8 made tls_client_auth and mtls_endpoint_aliases only
advertised in the OIDC discovery document when uaa.mtls-enabled is true
(default false), correctly closing a gap where a discovery client could
select an authentication method the server can't actually perform. Three
pre-existing tests in the uaa module -- OpenIdConnectEndpointDocs (a REST
Docs test, which broke the generate-api-docs CI job with a SnippetException
since the documented mtls_endpoint_aliases.token_endpoint field was no
longer present in the default-config response) and
OpenIdConnectEndpointsMockMvcTests/OpenIdConnectEndpointsMockMvcZonePathTests
-- were not updated at the time and started failing under the default
(disabled) configuration once that fix landed, since they assert on
tls_client_auth/mtls_endpoint_aliases being present unconditionally.

Adds @TestPropertySource(properties = "uaa.mtls-enabled=true") to all three,
matching the same pattern already used in TokenEndpointDocs, so these tests
continue to exercise and document the mTLS-enabled discovery document shape
they were originally written for.

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

Suppressed comments (4)

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

server/src/main/java/org/cloudfoundry/identity/uaa/client/ClientAdminBootstrap.java:223

  • Bootstrapped oauth.clients bypass the two validators that call validateTlsClientAuthClaimConfig. This path only checks the feature flag, so an invalid regex or null mapping is persisted and later fails token issuance inside certificate-claim extraction. Validate the claim configuration here before registering the client.
            client.setAdditionalInformation(info);
            ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, clientId);

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

  • This early return also skips validation when tls-client-auth-required-claims is configured without any claim mappings. Such a client is accepted but can never authenticate because extraction always yields an empty map, so every required claim fails. Treat missing mappings as an empty declaration set and continue validating dependent properties.
    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneEndpointsClientDetailsValidator.java:60

  • Presence of the map key is not equivalent to a configured CA. A blank/null tls-client-auth-ca currently bypasses the required-secret check, so the zone API accepts a secretless client that can never pass TlsClientAuthConfiguration.isConfigured during authentication. Base this decision on a nonblank, valid CA configuration instead.

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

  • A syntactically valid pattern with no capture group is accepted here, but matchFirstOu only emits group(1). For example, ^app:.*$ saves successfully yet silently produces no claim (and can make required-claim authentication permanently fail). Reject patterns that do not declare at least one capture group.
            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 uaa/slateCustomizations/source/index.html.md.erb Outdated
Comment thread docs/UAA-Client-Authentication.md Outdated
@rkoster
rkoster marked this pull request as draft August 26, 2026 06:43

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 2 comments.

Suppressed comments (3)

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

  • Copying a JDBC/JSON-loaded UaaClientDetails drops its mTLS CA: such instances have the flat configuration only in additionalInformation and a null typed field, so this setter removes the key copied on line 107. InMemoryClientDetailsService.addClientDetails uses this copy constructor, leaving the copied client unable to authenticate. Only invoke the setter when the typed value is non-null so the flat representation is preserved.
            this.setTlsClientAuthConfiguration(uaa.getTlsClientAuthConfiguration());

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

  • Returning as soon as tls-client-auth-claim-mappings is absent also skips validation of tls-client-auth-required-claims. A client can therefore persist required claims without any mapping capable of producing them; authentication then always fails because extraction returns an empty map. Parse an absent mapping list as empty and continue validating dependent properties so this invalid configuration is rejected at creation time.
    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

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

  • Compiling the regex is insufficient validation for the runtime contract. TlsClientAuthentication.matchFirstOu only emits group(1), so a valid regex with no capture group is accepted here but silently produces no claim. In addition, patterns on subject_cn and subject_o are accepted here but ignored by extraction. Either apply capture-group patterns consistently to all supported fields or reject configurations that runtime cannot honor.
            String pattern = mapping.getPattern();
            if (pattern != null && !pattern.isBlank()) {
                try {
                    Pattern.compile(pattern);

Comment on lines +478 to +491
SecurityFilterChain chain = http
.securityMatcher(RawPeerCertificateCaptureFilter.MTLS_TOKEN_PATH,
RawPeerCertificateCaptureFilter.MTLS_TOKEN_PATH + "/**")
.authenticationManager(clientAuthenticationManager)
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/**").access(anyOf().fullyAuthenticated());
auth.anyRequest().denyAll();
})
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(getClientParameterAuthenticationFilter(), BasicAuthenticationFilter.class)
.addFilterAt(clientAuthenticationFilter.getFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(tokenEndpointAuthenticationFilter.getFilter(), BasicAuthenticationFilter.class)
.anonymous(AnonymousConfigurer::disable)
.csrf(CsrfConfigurer::disable)

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 3 comments.

Suppressed comments (3)

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

  • The admin validator only validates the top-level map, but UaaClientDetails.setTlsClientAuthConfiguration() serializes the supported typed form as a nested object under tls-client-auth-ca. Unlike ZoneEndpointsClientDetailsValidator (lines 66–67), this path never validates that nested object's mappings/templates, so malformed configurations accepted by the API can later produce wrong claims or fail token issuance. Normalize and validate all three supported forms here as well.
        checkMtlsClientConfigAllowed(client.getAdditionalInformation(), mtlsEnabled, client.getClientId());
        validateTlsClientAuthClaimConfig(client.getAdditionalInformation(), client.getClientId());

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

  • Returning whenever tls-client-auth-claim-mappings is absent skips validation of independently supplied tls-client-auth-sub-template, tls-client-auth-aud-templates, and tls-client-auth-required-claims. For example, required claims without mappings are accepted at registration but can never match, making every mTLS authentication fail; unresolved template placeholders are silently dropped. Treat missing/null mappings as an empty declared-claim set and still validate dependent properties.
    public static void validateTlsClientAuthClaimConfig(Map<String, Object> additionalInfo, String clientId) {
        if (additionalInfo == null
                || !additionalInfo.containsKey(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_CLAIM_MAPPINGS)) {
            return;

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

  • This accepts any compilable pattern for every supported field, but TlsClientAuthentication.extractClaimMappingValues applies the pattern only to subject_ou; subject_cn and subject_o ignore it entirely. It also accepts zero-capture patterns even though OU extraction only returns group 1. Such accepted configurations emit untransformed or missing claims and can make required-claim authentication fail. Apply patterns consistently to all fields and reject a supplied pattern without a capture group.
            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);
                }

.contentType(APPLICATION_FORM_URLENCODED)
.param(CLIENT_ID, clientId)
.param(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS)
.param(REQUEST_TOKEN_FORMAT, OPAQUE.getStringValue())
--cacert /path/to/uaa-server-ca.pem \
--request POST \
--header 'Accept: application/json' \
--data 'grant_type=client_credentials&client_id=<client-id>&token_format=opaque' \
| `tls-client-auth-ca` | yes | PEM-encoded CA certificate. This is the per-client mTLS selector: requests to the fixed `/oauth/mtls/token` endpoint authenticate with a presented leaf certificate only when it chains to this CA. |
| `tls-client-auth-trusted-proxy-ca` | conditional | PEM-encoded CA certificate the Gorouter's own backend mTLS certificate must chain to. Configuring this switches the client to the Gorouter/XFCC-forwarding-only topology (requiring the `X-Forwarded-Client-Cert` header) -- see "Deployment topology" above. Leave unset for a direct-connection-only client. |
| `tls-client-auth-required-claims` | no | Map of `claimName -> requiredValue`, checked against the values already produced by `tls-client-auth-claim-mappings`. When configured, authentication fails unless every entry matches exactly -- e.g. `{space_guid: "<specific-space-guid>"}` scopes this client to a single CF space, even if other clients share the same `tls-client-auth-ca`. |
| `tls-client-auth-claim-mappings` | no | List of `{field, pattern, claim}` mappings from certificate subject fields (`subject_cn`, `subject_ou`) to JWT claim names, optionally extracting a capture group via `pattern`. |
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