Skip to content

feat(spiffe): JWT-SVID signing endpoint (RFC UAA-SPIFFE-001) - #3968

Closed
rkoster wants to merge 9 commits into
cloudfoundry:developfrom
rkoster:spiffe-signer
Closed

rkoster wants to merge 9 commits into
cloudfoundry:developfrom
rkoster:spiffe-signer

Conversation

@rkoster

@rkoster rkoster commented Jun 29, 2026

Copy link
Copy Markdown

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/sign authenticates a SPIFFE Agent (client credentials), verifies
the 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, JwtSvidSigner
  • InstanceIdentityVerifier, CertificateOuParser, ProofOfPossessionVerifier
  • SpiffeId, CfInstanceIdentity, SpiffeProperties, SpiffeConfiguration,
    SpiffeSecurityConfiguration
  • Full unit-test coverage for each component (9 focused commits).

Context

Part of RFC UAA-SPIFFE-001 — UAA as a SPIFFE Identity Server for Cloud Foundry.

Cross-repo dependencies

  • cloudfoundry/uaa-release spiffe-signer — exposes uaa.spiffe.* (trust
    domain, instance-identity CA) and the signer client.
  • Consumed by the SPIFFE Agent in cloudfoundry/diego-release spiffe-agent.

Status

Draft / POC. End-to-end verified on bosh-lite: a CF app received a valid JWT-SVID
with cf org/space/app/process_type claims.

rkoster added 9 commits June 26, 2026 19:26
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.
Comment on lines +49 to +59
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)

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.

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 High severity · 3 Medium severity · 2 Low severity

Open (6)
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/sign request 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.

Comment on lines +30 to +32
if (Math.abs(Instant.now().getEpochSecond() - timestamp) > properties.popFreshnessSeconds()) {
return false;
}
Comment on lines +77 to +90
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");
}
}
Comment on lines +63 to +64
KeyInfo activeKey = keyInfoService.getActiveKey();
String svid = JwtHelper.encode(claims, activeKey).getEncoded();
Comment on lines +9 to +10
@ConfigurationProperties(prefix = "uaa.spiffe")
public record SpiffeProperties(
this.properties = properties;
}

@PostMapping(value = "/jwt-svid/sign", consumes = "application/json", produces = "application/json")
Comment on lines +50 to +54
.securityMatcher("/jwt-svid/**")
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/**").hasAuthority("uaa.resource");
auth.anyRequest().denyAll();
})
@strehle

strehle commented Sep 22, 2026

Copy link
Copy Markdown
Member

@rkoster @beyhan which RFC references this feature ? SPIFFE not equal to mTLS, but for us not clear in which context UAA should do what

@strehle strehle added the clarification needed The issue is not accepted but we need clarification label Sep 22, 2026
@marcohelmerich

Copy link
Copy Markdown

@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 VCAP_SERVICES, and have to be rotated by hand. GCP Workload Identity Federation (WIF) solves this if the workload can present a short-lived, signed JWT with a stable subject and an aud equal to the WIF provider URI.

What we tried:

  1. UAA as the issuer (per-app client, client_credentials). It fails because aud can't be controlled: resource_ids is always overwritten with ["none"], and aud is derived from client_id plus scopes. As far as we can tell that's by design. It just doesn't fit OIDC federation, where aud has to match a pre-registered provider URI exactly.
  2. Per-cell daemon on the host network. It fails because containers can't reach their cell's IP, which is the intended isolation. And we didnt't want to change that behaviour.
  3. What we found working (PoC): a small JWT issuer deployed as a CF app and reached over C2C (apps.internal). It authenticates callers via mTLS with the Diego instance identity cert and parses the org/space/app GUIDs from the OUs. It signs its own RS256 JWT (sub=cf-app-<app-guid>, aud=<WIF provider>). The JWKS is uploaded statically to the GCP WIF provider, so GCP never calls back into the foundation. A supply buildpack adds a sidecar that refreshes the token file, plus an external_account config, so the Google SDKs pick it up with zero app changes. This works end-to-end on a real foundation.

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 aud. With /jwt-svid/sign plus the agent's socket, CF could offer GCP WIF out of the box, and likely other OIDC-federating providers too (AWS, Azure, Vault's JWT auth, …).

Happy to test this on our foundations against GCP WIF once there's something deployable.

@rkoster

rkoster commented Sep 24, 2026

Copy link
Copy Markdown
Author

@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.

fhanik added a commit to fhanik/uaa that referenced this pull request Sep 28, 2026
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.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 28, 2026
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.
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 28, 2026
…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).
fhanik added a commit to fhanik/uaa that referenced this pull request Sep 28, 2026
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.
@fhanik

fhanik commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Closing for now. No use case requires SPIFFE at this very moment

@fhanik fhanik closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clarification needed The issue is not accepted but we need clarification

Projects

Development

Successfully merging this pull request may close these issues.

6 participants