Repository navigation
Conversation
Reject process_type not matching [A-Za-z0-9_-]{1,63} and audience that is blank, over 512 chars, or contains control characters (400). process_type is concatenated into the SPIFFE ID path and every proof-of-possession message field must be newline-free, so this closes SPIFFE-ID injection and PoP message ambiguity. Adds characterization tests pinning existing fail-closed behaviour: malformed PEM, future-dated and malformed-base64 PoP, and a not-yet-valid instance cert.
| SecurityFilterChain chain = http | ||
| .securityMatcher("/jwt-svid/**") | ||
| .authorizeHttpRequests(auth -> { | ||
| auth.requestMatchers("/**").hasAuthority("uaa.resource"); | ||
| auth.anyRequest().denyAll(); | ||
| }) | ||
| .authenticationManager(clientAuthenticationManager) | ||
| .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
| .addFilterAt(clientAuthenticationFilter.getFilter(), BasicAuthenticationFilter.class) | ||
| .anonymous(AnonymousConfigurer::disable) | ||
| .csrf(CsrfConfigurer::disable) |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved proof-of-possession, signing, configuration, validation, and documentation issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (6)
Prevent timestamp overflow from bypassing proof freshness checks · New Validate null or blank instance certificates before parsing · New Enforce RSA/RS256 keys for JWT-SVID signing · New Document SPIFFE configuration properties and defaults · New Add REST Docs coverage for the JWT-SVID signing endpoint · New Add integration coverage for JWT-SVID endpoint authentication · New
What changed in this PR
Adds an optional UAA SPIFFE JWT-SVID signing endpoint for authenticated Cloud Foundry workloads, including certificate validation, proof-of-possession checks, and JWT issuance.
Changes:
- Adds protected
/jwt-svid/signrequest and response handling. - Adds SPIFFE configuration, security, identity parsing, and certificate verification.
- Adds proof-of-possession validation and JWT-SVID signing.
- Adds focused unit tests and certificate fixtures.
| File | Description |
|---|---|
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeTestCerts.java |
Provides test certificates and keys. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/SpiffePropertiesTests.java |
Tests configuration defaults. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeIdTests.java |
Tests SPIFFE ID formatting. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeConfigurationTests.java |
Tests CA parsing. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/ProofOfPossessionVerifierTests.java |
Tests proof-of-possession validation. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidSignerTests.java |
Tests JWT-SVID signing. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidControllerTests.java |
Tests endpoint behavior and validation. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/InstanceIdentityVerifierTests.java |
Tests certificate trust and validity. |
server/src/test/java/org/cloudfoundry/identity/uaa/spiffe/CertificateOuParserTests.java |
Tests certificate OU extraction. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeSecurityConfiguration.java |
Protects the signing endpoint. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeProperties.java |
Defines SPIFFE configuration. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeId.java |
Formats workload SPIFFE IDs. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/SpiffeConfiguration.java |
Parses the configured CA. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/ProofOfPossessionVerifier.java |
Verifies proof-of-possession signatures. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidSigner.java |
Creates signed JWT-SVIDs. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidResponse.java |
Defines signing responses. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidRequest.java |
Defines signing requests. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/JwtSvidController.java |
Implements the signing endpoint. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/InstanceIdentityVerifier.java |
Validates instance certificates. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/CfInstanceIdentity.java |
Stores Cloud Foundry identity attributes. |
server/src/main/java/org/cloudfoundry/identity/uaa/spiffe/CertificateOuParser.java |
Extracts identity values from certificate OUs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (Math.abs(Instant.now().getEpochSecond() - timestamp) > properties.popFreshnessSeconds()) { | ||
| return false; | ||
| } |
| private static void validateRequest(JwtSvidRequest request) { | ||
| String processType = request.processType(); | ||
| if (processType == null || !PROCESS_TYPE_PATTERN.matcher(processType).matches()) { | ||
| throw new BadSvidRequestException("process_type must match [A-Za-z0-9_-]{1,63}"); | ||
| } | ||
| String audience = request.audience(); | ||
| if (audience == null || audience.isBlank() | ||
| || audience.length() > MAX_AUDIENCE_LENGTH | ||
| || containsControlCharacter(audience)) { | ||
| throw new BadSvidRequestException( | ||
| "audience must be non-blank, at most " + MAX_AUDIENCE_LENGTH | ||
| + " characters, and free of control characters"); | ||
| } | ||
| } |
| KeyInfo activeKey = keyInfoService.getActiveKey(); | ||
| String svid = JwtHelper.encode(claims, activeKey).getEncoded(); |
| @ConfigurationProperties(prefix = "uaa.spiffe") | ||
| public record SpiffeProperties( |
| this.properties = properties; | ||
| } | ||
|
|
||
| @PostMapping(value = "/jwt-svid/sign", consumes = "application/json", produces = "application/json") |
| .securityMatcher("/jwt-svid/**") | ||
| .authorizeHttpRequests(auth -> { | ||
| auth.requestMatchers("/**").hasAuthority("uaa.resource"); | ||
| auth.anyRequest().denyAll(); | ||
| }) |
|
@strehle @rkoster — a concrete use case from the operator side, in case it helps... What we need: CF apps accessing Google Cloud without service-account keys. Today the keys come from a service broker, sit long-lived in What we tried:
It works, but it's a second issuer next to UAA, with its own key management, JWKS lifecycle and HA story. This PR would make that redundant: the instance-identity check is the same one we do, and it fixes the one thing that blocked us in (1), a caller-chosen Happy to test this on our foundations against GCP WIF once there's something deployable. |
|
@marcohelmerich thanks for the feedback. The focus is now on getting #3972 across the finish line. Which adds support for RFC 8705 to the UAA. This should also allow doing what you are describing. |
Review comment cloudfoundry#13 on PR cloudfoundry#3968: the endpoint mints a credential with no audit record, so there is no trail of which agent obtained which workload's identity, when. Introduces the JwtSvidIssuedEvent audit event (AuditEventType code 43) and wires an ApplicationEventPublisher into JwtSvidController, but the controller does not yet publish the event -- the new AuditEvents test is intentionally red until the next commit.
Wires the ApplicationEventPublisher added in the previous commit into JwtSvidController.sign(): every successful issuance now publishes a JwtSvidIssuedEvent, giving UAA's audit framework a record of which agent client obtained which SPIFFE ID, for which audience, and when. Closes review comment cloudfoundry#13 on PR cloudfoundry#3968. Also injects IdentityZoneManager to source the event's zone id, since the static IdentityZoneHolder is deprecated.
…ning Review comment cloudfoundry#14 on PR cloudfoundry#3968: uaa.spiffe.* configuration is global, but the endpoint is reachable from any identity zone via subdomain routing, and JwtSvidSigner takes the issuer from IdentityZoneHolder, so the same trust domain gets served under differing issuers per zone. Adds JwtSvidController.ZoneNotSupportedException (404, so the endpoint's existence isn't revealed outside the default zone) plus a unit test and an end-to-end MockMvc test asserting a request from a non-default zone is refused. Both are red: the controller does not yet perform the check, and the end-to-end test proves that today a request from another zone is actually signed and returned (200).
Wires the ZoneNotSupportedException added in the previous commit into JwtSvidController.sign(): a request that arrives via a non-default zone's subdomain is now refused with 404 before any other processing. Closes review comment cloudfoundry#14 on PR cloudfoundry#3968 -- uaa.spiffe.* stays a global, default-zone-only feature rather than being silently reachable, and re-issuable under a different issuer, from every zone.
|
Closing for now. No use case requires SPIFFE at this very moment |



Summary
Adds a JWT-SVID signing endpoint to UAA so it can act as a SPIFFE identity
server for Cloud Foundry workloads (offline-OIDC federation).
POST /jwt-svid/signauthenticates a SPIFFE Agent (client credentials), verifiesthe caller's CF instance-identity certificate against the configured CA,
parses org/space/app GUIDs from the cert OUs, checks a proof-of-possession
signature for freshness, and mints an RS256 JWT-SVID signed with UAA's active key
and OIDC issuer (so it verifies offline against
/token_keys).Key changes (all under
org.cloudfoundry.identity.uaa.spiffe)JwtSvidController,JwtSvidRequest/JwtSvidResponse,JwtSvidSignerInstanceIdentityVerifier,CertificateOuParser,ProofOfPossessionVerifierSpiffeId,CfInstanceIdentity,SpiffeProperties,SpiffeConfiguration,SpiffeSecurityConfigurationContext
Part of RFC UAA-SPIFFE-001 — UAA as a SPIFFE Identity Server for Cloud Foundry.
Cross-repo dependencies
spiffe-signer— exposesuaa.spiffe.*(trustdomain, instance-identity CA) and the signer client.
spiffe-agent.Status
Draft / POC. End-to-end verified on bosh-lite: a CF app received a valid JWT-SVID
with
cforg/space/app/process_type claims.