Skip to content

Review of 3972 - #4075

Closed
fhanik wants to merge 133 commits into
cloudfoundry:developfrom
fhanik:review/pr3972
Closed

fhanik wants to merge 133 commits into
cloudfoundry:developfrom
fhanik:review/pr3972

Conversation

@fhanik

@fhanik fhanik commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review notes: RFC 8705 mTLS client authentication

I spent some time on this branch and wrote a set of adversarial MockMvc tests against /oauth/mtls/token to check the boundaries rather than reason about them. Everything below is something the tests actually produced, unless marked (from reading).

The trust model is carefully built — the XFCC-spoofing defenses in particular are better than I expected, and the RawPeerCertificateCaptureFilter / mapper-parse-failure guard is a real subtlety that's handled correctly. The findings below are mostly about the blast radius of the new endpoint rather than the certificate validation itself.

What I verified as working

Worth stating up front, because these are the parts under the most load:

  • A certificate that doesn't chain to the client's tls-client-auth-ca is rejected cleanly: 401 invalid_client, "tls_client_auth: certificate chain validation failed: Path does not chain with any of the trust anchors". Same for expired certs and a malformed CA PEM.
  • Claim mappings cannot forge scope, client_id or zid — NON_ADDITIONAL_ROOT_CLAIMS in UaaTokenServices does its job.
  • tls-client-auth-required-claims correctly rejects a valid certificate from the same CA in the wrong space. The shared-CA mitigation the docs describe holds.
  • An mTLS-configured client can't fall back to its client_secret, can't mix cert+secret on one request, and a CA certificate presented as the leaf is rejected by validateEndEntityConstraints.

1. The mTLS endpoint serves every grant type, including password and refresh_token

POST /oauth/mtls/token with grant_type=password, authenticating with a valid instance-identity certificate, returns 200. Decoded access token:

{ "client_auth_method": "tls_client_auth",
  "cnf": { "x5t#S256": "YMmciXQkCQXoo0RJtoqsVM6pH39epZebkh6YnfNVHhk" },
  "app_id": "b1-app",
  "grant_type": "password",
  "user_id": "d78bfe26-78ca-4400-b44f-15243c9b2e3d",
  "user_name": "...", "email": "...", "origin": "uaa",
  "scope": ["uaa.user"] }

The refresh token it returns is then usable at the same endpoint (200, "revocable": true, amr: ["pwd"]).

The concern isn't that the client had password in its authorized_grant_types — it's what the resulting token claims. cnf.x5t#S256 is RFC 8705 §3 sender-constraint: a resource server reading it concludes the presenter holds that certificate. The same token also carries a full user identity. So a Diego app instance certificate ends up certificate-binding a user's token, and app_id (certificate-derived) sits beside user_id (password-derived) with nothing marking their provenance.

mtlsTokenEndpointSecurity installs BackwardsCompatibleTokenEndpointAuthenticationFilter exactly as tokenEndpointSecurity does, so every grant is reachable. Was that deliberate? If the endpoint is meant for workload identity, restricting it to client_credentials (and maybe jwt-bearer) would match the feature's purpose, and would keep cnf meaning what RFC 8705 says it means.

2. The same validation failure returns 500 with a stack trace, depending on which client-auth filter runs

Authorization: Basic base64("clientId:") — empty secret, which still satisfies ObjectUtils.isEmpty(authentication.getCredentials()) in ClientDetailsAuthenticationProvider. ClientParametersAuthenticationFilter.wrapClientCredentialLogin skips whenever an Authorization header is present, so this is handled by ClientBasicAuthenticationFilter, which catches only AuthenticationException:

"http.path":"/oauth/mtls/token","http.status_code":"500"

Uncaught Exception:
error="invalid_client", error_description="tls_client_auth: certificate chain validation failed: ..."
	at TlsClientAuthentication.validateClientCert(TlsClientAuthentication.java:343)
	at ClientDetailsAuthenticationProvider.validateTlsClientAuth(ClientDetailsAuthenticationProvider.java:217)
	at ClientDetailsAuthenticationProvider.additionalAuthenticationChecks(ClientDetailsAuthenticationProvider.java:95)

Identical credentials and certificate as the 401 case above — only the filter differs. The clean 401 comes from AbstractClientParametersAuthenticationFilter.performClientAuthentication's catch (Exception e) -> BadCredentialsException, which is incidental rather than by design.

InvalidClientDetailsException is a UaaException → OAuth2Exception → RuntimeException, not a Spring AuthenticationException, so it escapes the security chain to SecurityFilterChainPostProcessor's UaaLoggingFilter, which logs ERROR "Uncaught Exception:" and sends 500. That's an unauthenticated endpoint writing a stack trace per attempt.

Smallest fix is in validateTlsClientAuth: catch and convert rather than depending on a downstream filter's catch-all.

3. /oauth/mtls/token is an unrestricted alias of /oauth/token

A client with no tls-client-auth-ca at all, authenticating with client_secret_basic and presenting no certificate, gets a normal token from /oauth/mtls/token (200). Since mtls_endpoint_aliases.token_endpoint now advertises this path in discovery, it's advertising an endpoint that accepts non-mTLS authentication.

4. tls-client-auth-sub-template with no placeholders forges an arbitrary sub

MtlsClaimsEnhancer.renderTemplate returns a template containing no {placeholder} verbatim, and UaaTokenServices.createJWTAccessToken re-applies sub after its own defaults. ClientAdminEndpointsValidator.validateTemplatePlaceholders only checks that placeholders that are present are declared — it never requires one.

Configuring tls-client-auth-sub-template: "00000000-0000-0000-0000-000000000000" produced a token with exactly that sub. It needs clients.write, so it's client-admin → subject-spoofing rather than an anonymous hole, but sub is the canonical identity claim for every downstream resource server, and user_id/cid are protected while sub deliberately is not. Requiring at least one placeholder would close it.

5. Claim mappings can set amr and acr

"amr": "mfa", "acr": "urn:example:high"

NON_ADDITIONAL_ROOT_CLAIMS protects UAA's own claims but not the authentication-context claims that downstream policy engines read, and the validator applies no allowlist to claim names. An allowlist (or a reserved-name denylist) at config time would be better than relying on the token-builder denylist.

6. With uaa.mtls-enabled=false, the endpoint still resolves

The security chain is @ConditionalOnProperty, but UaaTokenEndpoint's @RequestMapping lists /oauth/mtls/token unconditionally and both new servlet filters are registered unconditionally. With the feature off, a POST returns:

{"error":"Could not verify the provided CSRF token because no token was found to compare."}

That's the uiSecurity catch-all's CSRF filter — so the request reaches the browser login chain rather than 404ing. It fails closed here, but by accident. A disabled feature's endpoint should not resolve.

Related (from reading): clientCertificateMapperFilter() reflectively instantiates a package-private third-party class at startup unconditionally and throws IllegalStateException if it's absent — a hard startup failure for a feature that's off.

7. token-endpoint-auth-method rejection breaks unrelated clients

ClientAdminEndpointsValidator.checkMtlsClientConfigAllowed throws on the mere presence of that key in additionalInformation, for every client create/update, mTLS or not. Creating a plain client carrying it returns 400. That string appears nowhere else in UAA, but hyphenated unknown keys from a BOSH oauth.clients manifest land in additionalInformation, so deployments may already have clients carrying it — previously ignored, now uncreatable and unupdatable. Is this a leftover from an earlier iteration?

Smaller things, from reading rather than testing

  • MtlsClientAuthTomcatCustomizer.customize iterates all connectors and all SSLHostConfigs with no port scoping, so enabling the feature makes UAA send a CertificateRequest on every TLS connection including the login page — browsers holding a client cert will prompt end users — and moves the whole connector to BCJSSE in FIPS mode. Worth scoping to a dedicated connector, or documenting loudly.
  • ClientDetailsAuthenticationProvider.getTlsClientAuthConfiguration and MtlsClaimsEnhancer.loadTlsConfig are ~60 lines of identical parsing in different modules. One decides authentication, the other decides what identity claims go in the token; if they drift, those two disagree. A single static factory on TlsClientAuthConfiguration would be safer.
  • parsePemCertificate does one parser.readObject(), so an operator pasting a root+intermediate bundle into tls-client-auth-ca silently gets only the first certificate as trust anchor.
  • setRevocationEnabled(false) is reasonable for short-lived instance certs but deserves a line in the docs, since the same config accepts arbitrary long-lived certificates.
  • The new endpoint inherits allowQueryStringForTokens (default true), so GET /oauth/mtls/token?grant_type=... works.
  • ClientCredentialsTokenGranter.isAllowedAuthMethod is public static and called only from a test.

Offer

I have the MockMvc tests that produced all of the above — roughly 17 tests covering credential confusion, grant types at the endpoint, validation-failure semantics, and claim-mapping reach, using real BouncyCastle-generated certificates rather than mocks. Happy to push them to the branch if useful; several of them are worth keeping as regression coverage regardless of what you decide about the findings, particularly the wrong-CA case and the protected-claim guard, which aren't currently covered.

rkoster added 30 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.
…filter chain

Add FilterChainOrder.OAUTH_11 (211) so the mTLS security chain can be
ordered before the OAUTH_10 catch-all token endpoint chain.

Add mtlsTokenEndpointSecurity @order(OAUTH_11) that:
- matches /oauth/mtls/token
- runs client-credentials authentication with tls_client_auth support
- disables CSRF (stateless machine-to-machine endpoint)
- uses BasicAuthenticationEntryPoint so Spring returns 401, not a redirect

Without this chain the /oauth/mtls/token path falls through to the
LoginSecurityConfiguration catch-all which rejects requests with
CSRF-related 403 errors.
… expose /oauth/mtls/token

getTlsClientAuthConfiguration previously handled only TlsClientAuthConfiguration
(in-memory) and Map (Jackson-deserialized BOSH config stored as nested object).
BOSH flat config stores tls-client-auth-ca as a plain PEM string and
tls-client-auth-claim-mappings as a JSON array string — add an
'instanceof String pem' branch that parses both.

UaaTokenEndpoint @RequestMapping previously covered only /oauth/token.
After Gorouter sanitize_set was set, client authentication started succeeding
for /oauth/mtls/token but Spring MVC returned 404 because no controller was
mapped to that path. Add /oauth/mtls/token to the value array so the same
endpoint handles both paths.
…ulti-valued RDNs

Two bugs prevented CF identity claims from appearing in tokens issued via
/oauth/mtls/token:

1. DB-loaded clients: UaaClientDetails.getTlsClientAuthConfiguration() returns
   null for clients loaded via JDBC because the tlsClientAuthConfiguration field
   is only set through JSON deserialization, not through the JDBC row-mapper path
   (which populates additionalInformation instead). Switch to loadTlsConfig()
   which mirrors ClientDetailsAuthenticationProvider.getTlsClientAuthConfiguration
   and reads from additionalInformation directly, handling the
   TlsClientAuthConfiguration/Map/String cases.

2. Multi-valued RDNs in Diego instance-identity certs: Diego encodes all three
   OU attributes (app:, space:, organization:) as a single multi-valued RDN
   using '+' as separator (RFC 2253 §2.2). Splitting only on ',' left the entire
   '+OU=space:..+OU=org:..' string attached to the app GUID. Fix extractOus()
   and extractRdnValue() to split on '+' within each RDN component.
- UaaClientDetails.setTlsClientAuthConfiguration() now syncs to
  additionalInformation so JDBC-loaded clients see the typed value
- MtlsClaimsEnhancer checks the typed getTlsClientAuthConfiguration()
  field first, falls back to loadTlsConfig(additionalInformation)
- ClientDetailsAuthenticationProvider and MtlsClaimsEnhancer now handle
  rawMappings instanceof List<?> (Jackson parses JSON arrays natively
  from JDBC; not always a String)
- Move java-buildpack-client-certificate-mapper-jakarta to Gradle
  version catalog (libs.versions.toml)
- SpringServletXmlFiltersConfiguration: remove class-level
  @SuppressWarnings and raw FilterRegistrationBean; add comment
  explaining why reflection is required (ClientCertificateMapper is
  package-private)
…lient_auth

- Add tls_client_auth to tokenAMR assertion in wellKnownEndpoint tests
- Assert mtls_endpoint_aliases.token_endpoint in both MockMvc test variants
  (SUBDOMAIN and ZONE_PATH zone resolution modes)
- Document mtls_endpoint_aliases.token_endpoint in REST Docs snippet
…n mTLS auth

Thread 8: Replace @JsonProperty(TLS_CLIENT_AUTH_CA) with @JsonIgnore on
tlsClientAuthConfiguration in UaaClientDetails. The field is set only
programmatically; JSON wire format for tls-client-auth-ca flows through
additionalInformation via @JsonAnyGetter/@JsonAnySetter, avoiding the
nested serialisation and flat-PEM deserialization failure.

Thread 9: Add getCertificateChainFromRequest() returning the full
X509Certificate[] from the request attribute. Add chain-aware overload
validateClientCert(X509Certificate[], TlsClientAuthConfiguration) that
builds CertPath from Arrays.asList(chain) so intermediate CAs are
included in PKIX path validation. ClientDetailsAuthenticationProvider
now uses the chain-based methods. Single-cert overload kept for
backward compat (delegates to chain overload).
After adding @JsonIgnore to UaaClientDetails.tlsClientAuthConfiguration,
JSON round-trips populate additionalInformation (via @JsonAnySetter) rather
than the typed getter. Update the test to assert on additionalInformation
directly, which is the actual contract authentication providers depend on.
…plates win

UaaTokenServices.createJWTAccessToken() was unconditionally setting sub and
aud after spreading additionalRootClaims, causing MtlsClaimsEnhancer's
rendered sub/aud templates to be silently discarded.

Move the additionalRootClaims.putAll() to after all UAA-default claims are
set (including sub and aud) so that enhancer claims take precedence.
Explicit excluded claims are still removed last so operator exclusions win.

Add regression test: WhenTokenEnhancerOverridesSubAndAud verifies that an
enhancer-supplied sub and aud survive into the final JWT.
@fhanik

fhanik commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Example fixes here #4076

At this point, I'm not comfortable merging this because it wasn't a pure TDD pattern. Tests were changed along with production code, making it more difficult to review.

@fhanik

fhanik commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #4076

@fhanik fhanik closed this Sep 24, 2026
@rkoster

rkoster commented Sep 24, 2026

Copy link
Copy Markdown

Pushed 14 follow-up commits to #3972, from 471ad57 through a80e1c9, addressing this review and the example fixes in #4076.

The mTLS endpoint serves every grant type, including password and refresh_token

38357eb restricts the endpoint to client_credentials. This is the workload-identity endpoint policy; RFC 8705 itself permits certificate-bound user tokens.

The same validation failure returns 500 with a stack trace, depending on which client-auth filter runs

433728d converts certificate failures to authentication exceptions, yielding consistent 401 / invalid_client responses for Basic and parameter authentication.

/oauth/mtls/token is an unrestricted alias of /oauth/token

e4ca968 requires mTLS client configuration at the dedicated endpoint, including descendants and zone-prefixed paths.

tls-client-auth-sub-template with no placeholders forges an arbitrary sub

343cf61 requires a declared placeholder in nonblank subject templates. Invalid constant/oversized subjects reaching token generation fail issuance. Literal audiences remain supported for federation. Placeholder presence does not guarantee subject uniqueness.

Claim mappings can set amr and acr

8741802 protects reserved root claims at configuration and enhancement time, including dotted mappings such as acr.level.

With uaa.mtls-enabled=false, the endpoint still resolves

6f3988e explicitly returns 404 before browser security. cb5fbd8 avoids loading or registering the mapper when disabled, verified with the mapper class unavailable.

token-endpoint-auth-method rejection breaks unrelated clients

b3c51f6 restores acceptance as inert metadata; it cannot select or bypass mTLS authentication.

Smaller things, from reading rather than testing

  • 09f1992 consolidates configuration parsing into one factory.
  • ede683e parses every certificate in CA trust bundles, supporting old/new overlap rotation for workload and proxy CAs. Malformed PEM entries reject the entire bundle; every entry is an anchor.
  • 1cc3ed4 documents disabled CRL/OCSP checks.
  • aa075f5 makes mTLS POST-only, returning 405 with Allow: POST for authenticated GET requests.
  • 067fac0 removes the test-only helper and verifies actual granter behavior.
  • a80e1c9 distinguishes missing certificates from required-claims failures without exposing claim values.
  • Connector-wide TLS effects are explicitly documented in both references.

Tests were changed along with production code, making it more difficult to review.

The follow-ups are split by finding. Behavioral fixes were exercised with failing regressions before implementation; refactors used tests run before and after extraction/removal. Tests and production changes are committed together, so history is not a separate red/green sequence. Existing test changes reflect the explicit policies above and new diagnostic messages; positive issuance, ordinary-endpoint, literal-audience, and rotation coverage remains.

Targeted suites passed at each step; the latest run passed 17 provider tests, 59 HTTP regressions, and the mTLS REST Docs test. Affected API snippets were regenerated. Full repository/deployment testing was not performed in this round. Full HTML generation remains blocked locally by missing Ruby/gem; pre-existing Markdownlint findings remain.

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 30, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 30, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 2, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 14, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 16, 2026
…red)

rkoster's cloudfoundry#4075 follow-up ede683e fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

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.

G1 pins the first-CA case, which already passes today, so the pair localises
the defect to the anchor set rather than to bundle parsing generally. G2 fails
today: a certificate from the second CA in the bundle is refused with
"certificate chain validation failed".
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 16, 2026
rkoster's cloudfoundry#4075 follow-up aa075f5 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

/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. G3 fails today: it expects 405 with an Allow: POST header
and gets the request served instead. G4 guards the blast radius -- both paths
are mapped by the same controller method, so restricting one must not
restrict the other; it already passes.

Registers MtlsEndpointAvailabilityFilter in this test class's MockMvc chain,
which is what actually lets G3 observe the endpoint's real behaviour instead
of whatever Spring Security would otherwise do with an unauthenticated GET.
fhanik added a commit to fhanik/uaa that referenced this pull request Oct 16, 2026
… (red)

rkoster's cloudfoundry#4075 follow-up cb5fbd8 fixed this on cloudfoundry#3972 independently; ours
diverged before that commit, so port the gap in intent, with our own tests
and implementation (next commit).

The buildpack certificate mapper 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.

E7 fails today: with uaa.mtls-enabled=false, clientCertificateMapperFilter is
still enabled.
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.

2 participants