Repository navigation
RFC: Service Accounts for Cloud Foundry Workload Identity #1645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rkoster
wants to merge
21
commits into
cloudfoundry:main
Choose a base branch
from
rkoster:rfc-service-accounts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f1bb1d3
Add draft RFC for space-owned workload service accounts
rkoster eb255d7
Link service-account RFC to its draft pull request
rkoster d468a22
Make service-account RFC self-contained and align creation permissions
rkoster bddccad
Propose service account limits in space quotas
rkoster 5688075
Propose admin-only recovery of tombstoned service account names
rkoster e53872b
Present service-account RFC through developer workflows and diagrams
rkoster cd0ec87
Lead service-account RFC with federation and broker use cases
rkoster 7fcc6b9
Clarify credential issuance and lead with external object storage
rkoster ccfe2e4
Describe OSBAPI and route-policy integration in service-account RFC
rkoster 165edb8
Use cf:svc prefix for service-account route-policy sources
rkoster af452de
Explain account route access in developer-facing terms
rkoster 79837f0
Remove routing implementation detail from service-account RFC
rkoster 7a146ca
Distinguish agreed UAA authentication from remaining account profile …
rkoster 05a59db
Illustrate certificate SAN matching against account route policy
rkoster 1d6707d
Remove distracting route-policy diagram from RFC
rkoster 0cfb83d
Keep UAA review follow-ups out of RFC delivery summary
rkoster e73e347
Focus RFC next step on community review and approval
rkoster cdf2620
Explain account disable and enable lifecycle explicitly
rkoster 5c797fc
Reference RFC 1123 label syntax for account names
rkoster 8d5aff3
Define configurable creation budget with platform admin exemption
rkoster a1647ff
Specify cascading service-account cleanup on space and org deletion
rkoster File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,353 @@ | ||
| # Meta | ||
| [meta]: #meta | ||
| - Name: Service Accounts for Cloud Foundry Workload Identity | ||
| - Start Date: 2026-10-06 | ||
| - Author(s): @rkoster | ||
| - Status: Draft | ||
| - RFC Pull Request: [community#1645](https://github.com/cloudfoundry/community/pull/1645) | ||
| - Related RFCs: [RFC 0055: Identity-Aware Routing for GoRouter](rfc-0055-identity-aware-routing-for-gorouter.md) | ||
| - Affected Component(s): Cloud Controller, BBS, Diego, UAA, CF CLI, service brokers; later GoRouter | ||
|
|
||
| ## Summary | ||
|
|
||
| Give applications a stable identity they can share, without distributing a shared | ||
| secret. Developers assign a **service account**. Cloud Foundry issues and rotates | ||
| each app instance's certificate, which the app uses to obtain short-lived JWTs | ||
| from UAA. External services can trust those JWTs directly or exchange them for | ||
| their own credentials. | ||
|
|
||
| The primary use case is **workload identity federation (WIF)** to off-platform | ||
| services, especially services managed by service brokers. | ||
|
|
||
| **One account, multiple apps, independent keys, explicit permissions.** | ||
|
|
||
| ## Problem | ||
|
|
||
| A payments API stores uploaded invoices in a broker-managed cloud object store; | ||
| a background worker reads them for processing. Both need access to that external | ||
| service, without storing a long-lived access key in their service bindings. | ||
| The cloud provider supports WIF: exchange a JWT from a trusted issuer for | ||
| short-lived service credentials. The missing piece is a stable workload identity | ||
| that the provider can trust, without distributing another secret. | ||
|
|
||
| **Broker-managed services are a natural fit.** A broker already provisions the | ||
| service and its access. An identity-aware binding could configure trust and grants | ||
| for the app's service account, returning federation/connection metadata instead | ||
| of a long-lived password. Developers would retain the familiar service-binding UX. | ||
|
|
||
| Cloud Foundry already gives every instance its own certificate. However, instance | ||
| identities change on replacement, and app identities do not describe an intentional | ||
| group of apps. The team needs a durable **payments identity**, independent of which | ||
| instances or blue/green apps currently implement it. | ||
|
|
||
| **Next, coding agents running as CF apps** may need to push the applications they | ||
| generate. Explicit CAPI roles can give an agent that ability in a chosen space. | ||
| This is an emerging use case; off-platform federation is the primary motivation. | ||
|
|
||
| ## Proposal | ||
|
|
||
| ### 1. Create once, assign to apps | ||
|
|
||
| First, give the payments apps an identity that the object store's federation | ||
| provider can recognize across deployments. The account belongs to the targeted | ||
| space: each app can use one account, and multiple apps in that space can share | ||
| it. Cross-space assignment is excluded. Service access is granted in the next step. | ||
|
|
||
| ```sh | ||
| cf target -o acme -s payments | ||
| cf push payments-api --no-start | ||
| cf push payments-jobs --no-start | ||
|
|
||
| cf create-service-account payments-worker | ||
| cf bind-service-account payments-api payments-worker | ||
| cf bind-service-account payments-jobs payments-worker | ||
|
|
||
| cf start payments-api | ||
| cf start payments-jobs | ||
| cf service-account payments-worker | ||
| ``` | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| CLI["Developer: create and bind"] --> CAPI["CAPI: account and assignments"] | ||
| CAPI -->|"first bind: managed OAuth client"| UAA["UAA"] | ||
| CAPI -->|"typed account identity"| Diego["BBS / Diego"] | ||
| subgraph Space["Space: payments"] | ||
| API["payments-api<br/>instance key A"] | ||
| Jobs["payments-jobs<br/>instance key B"] | ||
| end | ||
| Diego -->|"issue certificate"| API | ||
| Diego -->|"issue certificate"| Jobs | ||
| API -.-> SAN["Shared DNS SAN:<br/>payments-worker.svc.identity"] | ||
| Jobs -.-> SAN | ||
| ``` | ||
|
|
||
| First bind provisions one secretless UAA client and a roleless CAPI principal; | ||
| subsequent binds reuse them. The CLI waits for provisioning jobs, including retries. | ||
| No account certificate is issued before authorized provisioning is ready. | ||
|
|
||
| ### 2. Bind a broker-managed service using federation | ||
|
|
||
| Binding an account answers **“Who am I?”**, not **“What may I do?”** For a service | ||
| whose broker supports identity federation, the proposed UX is: | ||
|
|
||
| ```sh | ||
| # Proposed option; requires broker and client-library support | ||
| cf bind-service payments-api payments-store --authentication service-account | ||
| cf bind-service payments-jobs payments-store --authentication service-account | ||
| ``` | ||
|
|
||
| CAPI conveys the platform-owned account identity to the broker, which configures | ||
| the service's federation grants. Services may also be configured for federation | ||
| outside a broker. | ||
|
|
||
| #### OSBAPI impact | ||
|
|
||
| Propose a versioned service-plan capability, `service_account_binding`, and an | ||
| additive `bind_resource.service_account` object in the OSBAPI binding request. | ||
| These are **proposed extensions requiring OSBAPI agreement**, not existing fields. | ||
| An explicitly negotiated CF context extension can pilot the same semantics. | ||
|
|
||
| Illustrative request fragment supplied by CAPI, never by app binding parameters: | ||
|
|
||
| ```json | ||
| { | ||
| "bind_resource": { | ||
| "app_guid": "<app-guid>", | ||
| "service_account": { | ||
| "version": "1.0", | ||
| "id": "<account-guid>", | ||
| "issuer": "https://uaa.example.org/oauth/token", | ||
| "subject": "cf:service-account:payments-worker" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The broker accepts only configured issuer trust domains and returns non-secret | ||
| connection/federation metadata through the binding's `credentials` object: | ||
|
|
||
| ```json | ||
| { | ||
| "credentials": { | ||
| "uri": "https://storage.example.org/payments", | ||
| "authentication": { | ||
| "type": "oauth2-bearer-jwt", | ||
| "issuer": "https://uaa.example.org/oauth/token", | ||
| "audience": "https://identity.example.org/federation/cf-payments" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| CAPI approves and reconciles audience/scope targets before marking the binding | ||
| ready; libraries perform token acquisition and provider-specific exchange. | ||
| Unsupported brokers reject the requested mode; ordinary bindings stay unchanged. | ||
| Retries/async completion retain the binding's identity snapshot; failed setup or | ||
| cleanup remains visible and retryable. Grants and token targets are reference-counted | ||
| per binding so one unbind cannot remove another binding's access. | ||
|
|
||
| Account reassignment requires removing identity-based service bindings first. | ||
| All apps sharing an account share its grants. The OSBAPI originating-identity header | ||
| continues to identify the operation's caller, not the assigned workload account. | ||
|
|
||
| Creating accounts follows service-instance creation permissions: **Space Developer | ||
| or platform admin**, with readable/writable-space checks and operator controls. | ||
| Other lifecycle management initially requires space manager/admin; assignment | ||
| requires app-write permission. Ownership alone grants no account resource roles. | ||
|
|
||
| ### 3. From app certificate to off-platform access | ||
|
|
||
| The app discovers its client ID and token endpoint through non-secret | ||
| `VCAP_SERVICE_ACCOUNT` metadata. Its library reads `CF_INSTANCE_CERT` and | ||
| `CF_INSTANCE_KEY`, reloading them when acquiring tokens after credential rotation. | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant App as payments-api | ||
| participant UAA | ||
| participant IdP as External federation provider | ||
| participant Service as Broker-managed service | ||
| App->>UAA: HTTPS POST /oauth/mtls/token + instance certificate | ||
| Note over App,UAA: grant_type=client_credentials<br/>client_id=cf:service-account:payments-worker | ||
| UAA->>UAA: Validate chain, key possession and exact account SAN | ||
| UAA-->>App: Signed JWT for the approved federation audience | ||
| App->>IdP: HTTPS token exchange with JWT | ||
| IdP->>IdP: Verify issuer, signature, audience and subject grant | ||
| IdP-->>App: Short-lived service credentials | ||
| App->>Service: Request using exchanged credentials | ||
| Service-->>App: Authorized data, or access denied | ||
| ``` | ||
|
|
||
| Illustrative **decoded UAA JWT** for an approved federation target (the provider's | ||
| required audience and exchange protocol must be configured and tested): | ||
|
|
||
| ```json | ||
| { | ||
| "iss": "https://uaa.example.org/oauth/token", | ||
| "sub": "cf:service-account:payments-worker", | ||
| "client_id": "cf:service-account:payments-worker", | ||
| "aud": ["https://identity.example.org/federation/cf-payments"], | ||
| "iat": 1791288000, | ||
| "exp": 1791288300 | ||
| } | ||
| ``` | ||
|
|
||
| Both apps have the same subject, but authenticate with different keys. The token | ||
| is limited to authorized audiences/scopes and expires within five minutes **and | ||
| no later than the certificate used to obtain it**. The external provider controls | ||
| the exchanged credentials' permissions and lifetime. Caller attribution should | ||
| also be retained in platform-controlled claims; claim names remain to be agreed. | ||
|
|
||
| There is deliberately **no `cnf`**: [RFC 8705][mtls] separates mTLS client | ||
| authentication from certificate-bound tokens. UAA must honor the client policy | ||
| `tls_client_certificate_bound_access_tokens: false`. The certificate is needed | ||
| to obtain the token, not to present it to the federation provider. Token issuance needs no live CAPI | ||
| assignment lookup; consumers validate issuer, signature, audience, expiry and | ||
| permissions independently. | ||
|
|
||
| ### 4. Coding agents: the same identity, a CAPI target | ||
|
|
||
| An agent app can instead request a CAPI-targeted token. An authorized role manager | ||
| grants its account `SpaceDeveloper` in the space where generated apps may be pushed: | ||
|
|
||
| ```sh | ||
| cf set-space-role cf:service-account:app-builder acme generated-apps SpaceDeveloper --client | ||
| ``` | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant Agent as Coding agent app | ||
| participant UAA | ||
| participant CAPI as Cloud Foundry API | ||
| Agent->>UAA: mTLS token request for approved CAPI target | ||
| UAA-->>Agent: JWT with aud cloud_controller and CAPI scopes | ||
| Agent->>CAPI: Create and push generated app over HTTPS with bearer JWT | ||
| CAPI->>CAPI: Validate token and explicit SpaceDeveloper role | ||
| CAPI-->>Agent: Deployment result | ||
| ``` | ||
|
|
||
| This is a separate token target, not an all-purpose multi-audience token. CAPI | ||
| roles do not authorize external services, and federation grants confer no CAPI roles. | ||
|
|
||
| ### 5. Authorize app-to-app routes with the same identity | ||
|
|
||
| Extend RFC 0055 route options with the source format | ||
| `cf:svc:<service-account-name>`, for example `cf:svc:payments-worker`. | ||
| The proposed CLI resolves the following to that route-option value: | ||
|
|
||
| ```sh | ||
| cf add-route-policy apps.identity --hostname invoices --source-service-account payments-worker | ||
| ``` | ||
|
|
||
| Both payments apps can now call the invoices route. GoRouter verifies the caller's | ||
| certificate and checks for `payments-worker.svc.identity`; an app without that | ||
| identity does not qualify for this grant. No JWT or token exchange is needed. | ||
|
|
||
| Existing route policies keep working. An account grant does not bypass a domain's | ||
| org/space restriction: the **calling app** must still be in the allowed org or | ||
| space. Without a matching grant, access remains denied. | ||
|
|
||
| **Disabling a service account stops new UAA tokens, not route access.** Route access | ||
| uses the certificate directly, so an already-issued certificate may still work | ||
| until expiry. Remove the route grant to withdraw that permission, and remove | ||
| account route grants before deleting the account. | ||
|
|
||
| ### 6. Manage the account's lifecycle | ||
|
|
||
| | Intent | Command | Effect | | ||
| | --- | --- | --- | | ||
| | Inspect | `cf service-accounts` | List accounts in the targeted space | | ||
| | Remove assignment | `cf unbind-service-account payments-api` | Changes desired identity; restart the app to apply | | ||
| | Stop new tokens | `cf disable-service-account payments-worker` | Disables issuance for all apps sharing it | | ||
| | Resume | `cf enable-service-account payments-worker` | Re-enables issuance, retaining roles | | ||
| | Delete | `cf delete-service-account payments-worker` | Requires workload references removed; retains a name tombstone | | ||
| | Recover a deleted name | `cf create-service-account payments-worker --reuse-name` | Explicit, audited platform-admin override | | ||
|
|
||
| **Space/org deletion cascades owned service accounts, as it does service instances.** | ||
| CAPI cleans up account-backed service bindings and route grants, deletes the | ||
| workloads and their account references, and removes managed UAA registrations and | ||
| account roles before deleting the accounts. Name tombstones remain. Pending or | ||
| failed cleanup prevents final space/org deletion and is visible through the | ||
| deletion job; users need not manually delete each account first. | ||
|
|
||
| **Disabling an account is a token-issuance switch.** | ||
| `cf disable-service-account payments-worker` disables its managed UAA client for | ||
| all apps sharing the account; the CLI waits until reconciliation completes. | ||
| The account, app assignments and CAPI roles remain, and apps keep running. | ||
| Already-issued JWTs remain valid until expiry, and certificates can still grant | ||
| route access. `cf enable-service-account payments-worker` restores token issuance | ||
| with the retained assignments and roles. In the POC, disabling deletes the UAA | ||
| client registration and enabling recreates it. | ||
|
|
||
| Running instances retain their launch identity—including renewal—until restart; | ||
| new tasks use the desired assignment. Staging never receives the account identity. | ||
| Unbind before assigning a different account. Zero-app accounts retain their client | ||
| and roles until explicitly disabled/deleted. | ||
|
|
||
| **Unbind, restart and disable do not revoke existing tokens or certificates.** | ||
| Copied credentials can remain usable until expiry while the client is enabled; | ||
| without restart, a running old assignment can keep renewing. | ||
|
|
||
| Names are immutable and unique within a foundation. They follow | ||
| [RFC 1123 hostname-label syntax][names], restricted to lowercase ASCII and 3–63 | ||
| characters: letters, digits and hyphens, beginning and ending with a letter or digit. | ||
| The SAN suffix creates no DNS record or route; external identity is the trusted | ||
| `(issuer, subject)` pair. Admin `--reuse-name` creates a new UUID without restoring | ||
| bindings/roles, preserves audit history and cannot bypass live-name conflicts or | ||
| quotas. Its warning explains that external grants and valid old certificates may | ||
| still apply to the reused identity. | ||
|
|
||
| ### 7. Extend space quotas | ||
|
|
||
| Add a maximum account count to space quotas, with this proposed V3 fragment: | ||
|
|
||
| ```json | ||
| { | ||
| "service_accounts": { | ||
| "total_service_accounts": 10 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Count existing accounts, including disabled/unprovisioned ones, not bindings or | ||
| tombstones. Creation checks the limit atomically; deletion frees capacity. Lowering | ||
| the limit blocks further creation rather than deleting accounts. Default/unlimited | ||
| behavior follows existing space-quota conventions. | ||
|
rkoster marked this conversation as resolved.
|
||
|
|
||
| Separately, operators configure a foundation-wide creation budget per authenticated | ||
| principal over a rolling seven-day window, with an explicit **unlimited** option. | ||
| This constrains name-reservation abuse on public platforms; internal platforms may | ||
| choose unlimited. The budget applies across all spaces to users and automation | ||
| clients, but **platform admins are exempt**, including automation authenticated with | ||
| platform-admin privileges. Each successful non-exempt creation consumes budget | ||
| atomically; failed requests do not. Deletion remains available and does not replenish | ||
| the budget. Admin-only `--reuse-name` remains subject to live-name and space-quota | ||
| checks, but is exempt from this creation budget. | ||
| This bounds per-principal creation rates, not total retained tombstones. | ||
|
|
||
| ### Delivery | ||
|
|
||
| **The UAA authentication foundation is already implemented and agreed with the | ||
| UAA team, pending final review in [UAA #4076][uaa].** This covers certificate-based | ||
| client authentication and registered subject/SAN matching. This RFC builds on | ||
| that work; it does not propose starting UAA mTLS support from scratch. | ||
|
|
||
| [CAPI][capi], [release wiring][release], [BBS][bbs], [Diego][diego] and [CLI][cli] | ||
| drafts already demonstrate the shared two-app identity, explicit roles, | ||
| disable/enable and unbind/restart against CAPI. Finish creation permissions, | ||
| quotas/name reuse, cascading space/org cleanup, contract publication and compatible | ||
| rollout before release. | ||
|
|
||
| The next step is community review and RFC approval of the proposed identity model | ||
| and developer experience. Detailed implementation and test evidence live in the | ||
| linked PRs. | ||
|
|
||
| [mtls]: https://www.rfc-editor.org/rfc/rfc8705.html#section-3.4 | ||
| [names]: https://www.rfc-editor.org/rfc/rfc1123.html#section-2.1 | ||
| [capi]: https://github.com/cloudfoundry/cloud_controller_ng/pull/5520 | ||
| [release]: https://github.com/cloudfoundry/capi-release/pull/702 | ||
| [bbs]: https://github.com/cloudfoundry/bbs/pull/168 | ||
| [diego]: https://github.com/cloudfoundry/diego-release/pull/1216 | ||
| [cli]: https://github.com/cloudfoundry/cli/pull/3875 | ||
| [uaa]: https://github.com/cloudfoundry/uaa/pull/4076 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.