Skip to content

Implement org-owned service accounts with space entitlements and workload identity - #5520

Open
rkoster wants to merge 168 commits into
cloudfoundry:mainfrom
rkoster:service-accounts-poc
Open

rkoster wants to merge 168 commits into
cloudfoundry:mainfrom
rkoster:service-accounts-poc

Conversation

@rkoster

@rkoster rkoster commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implement the Cloud Controller portion of Service Accounts for Cloud Foundry Workload Identity (community #1645), aligned with the RFC document at its merged revision.

Service accounts are org-owned identities, shared by apps only in explicitly entitled spaces of the same org. Each org has an admin-allocated, immutable, foundation-unique service_account_prefix; account names are immutable and unique within that org. Both use lowercase RFC 1123 labels of 3–63 characters.

For prefix acme-payments and account payments-worker:

Identity Value
UAA client ID / token subject cf:service-account:acme-payments:payments-worker
Instance-certificate DNS SAN payments-worker.acme-payments.svc.identity
Route-policy source cf:svc:acme-payments:payments-worker

One app has at most one account; many apps can share an account. Assignment grants no permissions: account use, CAPI roles, external grants and route policies remain explicit and independent.

Intentional RFC divergence: independent CAPI user lifecycle

Unlike RFC Appendix D, first bind provisions only the managed UAA client; it does not create a roleless CAPI user. CAPI users and their role grants remain independently managed. The existing CAPI authentication flow can lazily create a roleless OAuth user on its first API request; explicit roles are still required for resource access. No new explicit-user-creation command or authentication behavior is introduced here.

Disable, enable and service-account retirement do not create, disable or delete CAPI users or their roles, nor do they change external WIF identities/grants. Account deletion removes the managed UAA client and account resource. Ordinary space/org deletion still removes roles on deleted resources. If an identity is explicitly reused, independently retained CAPI roles can apply to the same client ID, just as retained external grants can; remove those grants independently when retiring access. This avoids coupling workload identity lifecycle to CAPI user lifecycle and requires no account-to-principal ownership field.

Cloud Controller implementation

Accounts, namespaces and permissions

  • Add /v3/service_accounts lifecycle, metadata, filtering, app listing and space-entitlement APIs, plus GET/PATCH /v3/apps/:guid/relationships/service_account.
  • Add organization service_account_prefix on creation or one-time unset-to-value update. Allocation is admin-only and atomic; an allocated prefix cannot be renamed, cleared or automatically released on org deletion. An unset prefix prevents account creation, entitlement expansion and assignment.
  • Space Developers create accounts with an initial entitlement to their space. Org Managers/admins oversee all accounts in the org, including accounts with no entitled spaces. Developers manage accounts entitled to their active spaces; sharing requires management authority or Developer access to an existing entitled space and the destination space. Cross-org entitlements are rejected; app writers assign only in entitled spaces. No new roles are introduced.
  • Add service_accounts.total_service_accounts to org quotas and an optional rolling seven-day creation budget per authenticated user/client across spaces. Admins are exempt from the budget; failed creation rolls back consumption and deletion does not refund it. Concurrent creation and first-bind provisioning are serialized, including MySQL repeatable-read handling.
  • Keep org-local deleted-name reservations and timestamped deletion history independently of audit retention. Explicit Org Manager/admin reuse_name creates a new UUID, is audited, and warns that external grants and still-valid old certificates may apply. Live collisions and quotas remain enforced.

Lifecycle, UAA registration and runtime identity

  • Provision a secretless UAA client on first bind through retryable pollable jobs, without creating a CAPI user. Reuse active provisioning jobs, reject registration collisions and human-user shadowing, and reconcile enable, disable and deletion. Independently managed OAuth users and their roles are left untouched.
  • Register the exact qualified SAN and owning-org claim restriction, reference an operator-defined UAA CA trust bundle, request bearer issuance with tls_client_certificate_bound_access_tokens: false, configure a maximum 300-second token validity, and map instance/app/space/org attribution claims. Support the named allowed-resource-policy reference for federation audiences; developers do not control registration inputs.
  • Add typed BBS account name/prefix properties and generated Ruby bindings. Preserve existing certificate attribution; expose only non-secret discovery through VCAP_SERVICE_ACCOUNT. Snapshot process identity until explicit restart; new runtime tasks use the current assignment; staging excludes account identity.
  • Retain accounts when spaces are deleted, removing their entitlements. Org deletion cleans up accounts and managed UAA clients retryably; ordinary resource deletion removes roles on deleted orgs/spaces, while CAPI user records remain independently managed. Failures retain the parent for retry. Direct deletion/unentitlement checks desired and active process/task references and identity-bearing service bindings.
  • Preserve pre-existing unqualified POC identities and historical reservations without silently converting their external identities. New accounts use the RFC's prefix-qualified namespace.

Explicit authorization and integrations

  • Resolve qualified service-account principals locally for existing CAPI role APIs. Binding does not grant resource roles.
  • Support qualified cf:svc:<prefix>:<name> route-policy sources, account resolution and domain org/space restrictions while preserving existing source semantics.
  • Snapshot account identity on service bindings and add { issuer, subject } to the CF platform context.service_account of broker bind requests for ready accounts; service keys do not receive it. Identity-based bindings must be removed before app reassignment. Existing bindings require rebind to adopt the account. Broker-side shared-grant lifecycle remains the broker's responsibility.
  • Add lifecycle, prefix allocation, entitlement, assignment and name-reuse audit evidence, API documentation, migrations and regression coverage.

Verification

  • All 18 GitHub checks pass at 498d40b4c, including normal and backward-compatibility suites on MySQL 8.0/8.2 and PostgreSQL 14/15/17, OpenAPI request conformance, docs, RuboCop, CodeQL and CLA. Main test run · backward compatibility.
  • Full local OpenAPI request-conformance run: 8,164 examples, zero failures before the final database-specific corrections; subsequent CI OpenAPI conformance also passes at the current head.
  • Full migration/creation-budget regression runs on MySQL 8.2 and PostgreSQL 17 passed with platform-specific pending examples; additional MySQL creation, quota, assignment and entitlement regressions passed 68 examples. Meaningful failing-test RED and passing-implementation GREEN commits are preserved, including deterministic concurrency and FK/index rollback regressions.
  • Lab validation with the companion native CLI/CATs demonstrates shared qualified account identity across apps in entitled spaces, independent instance keys, bearer HTTPS CAPI access with explicit roles, wrong/unbound identity denials, disable/enable, restart-sensitive assignment, current-identity tasks, staging exclusion, and Org Manager recovery after deletion of the last entitled space. Account-routing cases exercise certificate-only access/denials, disable semantics and domain space scope. Companion acceptance tests.
  • UAA lab checks demonstrate attribution claims, bearer tokens without cnf, named resource-policy audience selection/denials, and policy-only updates without re-registering account clients. These are integration evidence, not proof of every external provider's token requirements.

Dependencies, rollout and remaining validation

  • Companion changes: capi-release #702, BBS #168, Diego #1216, CLI #3875, and CATs #2193. Generated BBS consumers and deployment/module pins must agree on the qualified account contract.
  • UAA mTLS baseline is merged in UAA #4109, superseding the earlier build(deps): bump json_pure from 2.7.6 to 2.8.1 #4076 baseline. Named federation resource policies are tracked by UAA #4125 / implementation #4137; build(deps-dev): bump rubocop from 1.69.1 to 1.69.2 #4137 is the tested lab baseline.
  • Provisioning/runtime activation is opt-in. Qualified runtime identities require service_account_runtime_contract_version: 2 and compatible BBS/Diego cells before activation; this is an operator assertion, not automatic capability negotiation. The app-facing token endpoint must support reachable, trusted mTLS, either directly or through UAA's authenticated trusted-proxy mode.
  • Deploy compatible routing enforcement before enabling CAPI account route-policy producers. There is no router-version configuration gate; parallel release updates do not establish this ordering.
  • Remaining integration criteria include the near-leaf-expiry token lifetime cap, long-running credential renewal, real cloud-provider federation (including AWS azp/audience semantics), broker-managed shared grants end to end, Windows runtime, and namespace-limited UAA client administration. CAPI registration alone does not demonstrate these UAA/provider/broker properties.
  • Disable stops new token issuance when effective; unbind/restart, entitlement changes and deletion are not immediate cryptographic revocation of existing certificates, JWTs or exchanged provider credentials. Certificate-only route access requires removal of the relevant route policy to withdraw its grant.

The RFC document's recorded status is Draft, although community #1645 is merged. This PR implements the CAPI scope and records its cross-component dependencies; it does not claim complete end-to-end RFC acceptance.

@rkoster
rkoster force-pushed the service-accounts-poc branch from c303938 to f9d5880 Compare November 11, 2026 10:44
@rkoster
rkoster marked this pull request as ready for review November 11, 2026 12:44
Copilot AI balanced review requested due to automatic review settings November 11, 2026 12:44
@rkoster rkoster changed the title Draft: implement space-owned service-account lifecycle and runtime identity Implement org-owned service accounts with space entitlements and workload identity Nov 11, 2026

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.

🟡 Changes recommended

Critical ownership and concurrency flaws can reuse or delete unrelated OAuth principals and permit service-account assignment after entitlement revocation.

10 open issues
What changed in this PR

Adds experimental service-account lifecycle and workload identity support to Cloud Controller. The implementation uses organization-owned accounts with explicit space entitlements, integrating account identities with UAA provisioning, Diego workloads, service brokers and route policies.

Changes:

  • Adds account APIs, organization namespaces, entitlements, quotas, creation budgets and audit events.
  • Adds asynchronous provisioning and cleanup, launch identity snapshots, broker context and account-based route sources.
  • Adds database migrations, rollout documentation and request/unit coverage.
File Description
spec/​unit/​service_account_migration_spec.rb Tests MySQL partial-migration collation recovery.
spec/​unit/​models/​runtime/​service_account_ownership_spec.rb Tests ownership, entitlements and reservation statistics.
spec/​unit/​models/​runtime/​service_account_model_spec.rb Tests identity validation and name reservations.
spec/​unit/​models/​runtime/​service_account_assignment_spec.rb Tests assignment and ownership constraints.
spec/​unit/​lib/​services/​service_brokers/​v2/​client_spec.rb Tests broker identity context and missing-account rejection.
spec/​unit/​lib/​cloud_controller/​service_account_provisioner_configuration_spec.rb Tests provisioning configuration and token refresh.
spec/​unit/​lib/​cloud_controller/​diego/​task_recipe_builder_spec.rb Tests task certificate identity propagation.
spec/​unit/​lib/​cloud_controller/​diego/​task_environment_spec.rb Tests task account discovery metadata.
spec/​unit/​lib/​cloud_controller/​diego/​service_account_identity_spec.rb Tests runtime identity validation and serialization.
spec/​unit/​lib/​cloud_controller/​diego/​qualified_service_account_identity_spec.rb Tests qualified identities and contract gating.
spec/​unit/​lib/​cloud_controller/​diego/​environment_spec.rb Tests authoritative runtime discovery metadata.
spec/​unit/​lib/​cloud_controller/​diego/​app_recipe_builder_spec.rb Tests preservation of launch identity after unassignment.
spec/​unit/​lib/​cloud_controller/​backends/​staging_environment_builder_spec.rb Tests exclusion of account discovery from staging.
spec/​unit/​jobs/​v3/​service_account_provision_spec.rb Tests job serialization, retries and lifecycle guards.
spec/​unit/​actions/​task_create_spec.rb Tests task identity snapshots and entitlement validation.
spec/​unit/​actions/​space_delete_spec.rb Tests account retention during space deletion.
spec/​unit/​actions/​service_credential_binding_app_create_spec.rb Adjusts the concurrent binding fixture.
spec/​unit/​actions/​service_account_registration_profile_spec.rb Tests UAA attribution and resource-policy registration.
spec/​unit/​actions/​service_account_quota_concurrency_spec.rb Tests quota freshness and concurrent creation.
spec/​unit/​actions/​service_account_provision_spec.rb Tests canonical registration, collisions and retries.
spec/​unit/​actions/​service_account_org_lifecycle_spec.rb Tests organization-owned account cleanup and retention.
spec/​unit/​actions/​service_account_creation_budget_spec.rb Tests concurrent per-principal creation limits.
spec/​unit/​actions/​service_account_binding_guard_spec.rb Tests identity-bearing binding lifecycle guards.
spec/​unit/​actions/​process_restart_spec.rb Tests restart-only identity changes.
spec/​unit/​actions/​organization_service_account_prefix_spec.rb Tests atomic namespace allocation.
spec/​unit/​actions/​organization_delete_spec.rb Tests account cleanup and deletion retries.
spec/​unit/​actions/​app_assign_service_account_spec.rb Tests assignment permissions and provisioning-job reuse.
spec/​request/​spaces_spec.rb Updates embedded organization expectations.
spec/​request/​service_accounts_spec.rb Tests account APIs, budgets and lifecycle operations.
spec/​request/​service_account_route_policies_spec.rb Tests account route sources and grant cleanup.
spec/​request/​service_account_roles_spec.rb Tests explicit resource-role authorization.
spec/​request/​service_account_quotas_spec.rb Tests organization account quotas.
spec/​request/​service_account_namespaces_spec.rb Tests qualified names, reuse and deletion history.
spec/​request/​service_account_entitlements_spec.rb Tests entitlement authorization and revocation.
spec/​request/​roles_spec.rb Updates embedded organization expectations.
spec/​request/​organizations_spec.rb Adds namespace fields to response expectations.
spec/​request/​organization_service_account_prefix_spec.rb Tests namespace allocation, validation and permissions.
spec/​request/​organization_quotas_spec.rb Adds account quota response expectations.
spec/​request/​apps_spec.rb Updates embedded organization expectations.
spec/​request/​app_service_accounts_spec.rb Tests app assignment APIs and asynchronous provisioning.
spec/​migrations/​service_account_namespace_rollback_spec.rb Tests namespace and assignment rollback.
spec/​migrations/​20261109121000_add_service_account_organization_ownership_spec.rb Tests ownership and entitlement backfill.
lib/​services/​service_brokers/​v2/​client.rb Sends captured account identity in broker context.
lib/​diego/​bbs/​models/​certificate_properties_pb.rb Adds generated account certificate fields.
lib/​cloud_controller/​uaa/​service_account_client.rb Adds retrying UAA client-management operations.
lib/​cloud_controller/​diego/​task_recipe_builder.rb Adds task certificate identity properties.
lib/​cloud_controller/​diego/​task_environment.rb Adds authoritative task account discovery.
lib/​cloud_controller/​diego/​service_account_identity.rb Builds and validates runtime identity metadata.
lib/​cloud_controller/​diego/​environment.rb Adds process account discovery metadata.
lib/​cloud_controller/​diego/​app_recipe_builder.rb Adds process certificate identity properties.
lib/​cloud_controller/​dependency_locator.rb Constructs the configured account provisioner.
lib/​cloud_controller/​config_schemas/​worker_schema.rb Adds provisioning and runtime settings.
lib/​cloud_controller/​config_schemas/​clock_schema.rb Adds runtime identity settings.
lib/​cloud_controller/​config_schemas/​api_schema.rb Adds account feature and provisioning settings.
lib/​cloud_controller/​backends/​staging_environment_builder.rb Removes account discovery from staging environments.
errors/​v3.yml Adds account conflict and budget errors.
docs/​v3/​source/​index.html.md Includes service-account API documentation.
docs/​v3/​source/​includes/​resources/​organizations/​_update.md.erb Documents namespace allocation restrictions.
docs/​v3/​source/​includes/​resources/​organizations/​_object.md.erb Documents organization namespace fields.
docs/​v3/​source/​includes/​resources/​organizations/​_create.md.erb Documents namespace creation semantics.
docs/​v3/​source/​includes/​api_resources/​_service_accounts.md Documents account APIs, integrations and rollout.
docs/​v3/​source/​includes/​api_resources/​_organizations.erb Updates organization response examples.
db/​migrations/​20261109131000_snapshot_service_binding_account.rb Adds binding identity snapshots.
db/​migrations/​20261109130000_preserve_legacy_account_namespace.rb Preserves legacy global name reservations.
db/​migrations/​20261109125000_create_service_account_deletion_history.rb Adds durable account deletion history.
db/​migrations/​20261109124000_add_org_service_account_quota.rb Adds organization account quota storage.
db/​migrations/​20261109123000_scope_service_account_names_to_organizations.rb Introduces organization-scoped names and identity prefixes.
db/​migrations/​20261109122000_allow_accounts_without_original_space.rb Allows accounts to survive original-space deletion.
db/​migrations/​20261109121000_add_service_account_organization_ownership.rb Adds organization ownership and space entitlements.
db/​migrations/​20261109120000_add_organization_service_account_prefixes.rb Adds immutable namespace reservations.
db/​migrations/​20261007120000_create_service_account_creations.rb Adds the creation-budget ledger.
db/​migrations/​20261005120400_add_process_service_account_snapshot.rb Adds process launch identity storage.
db/​migrations/​20261005120300_add_task_service_account_snapshot.rb Adds task launch identity storage.
db/​migrations/​20261005120200_add_service_account_metadata.rb Adds descriptions, labels and annotations.
db/​migrations/​20261005120100_add_app_service_account.rb Adds the app-account relationship and index.
db/​migrations/​20261005120000_create_service_accounts.rb Creates the initial account registry.
config/​routes.rb Registers account and assignment endpoints.
app/​repositories/​service_account_event_repository.rb Records account and assignment audit events.
app/​presenters/​v3/​service_account_presenter.rb Presents account identities and relationships.
app/​presenters/​v3/​route_policy_presenter.rb Presents account route-source relationships.
app/​presenters/​v3/​organization_quota_presenter.rb Exposes account quota limits.
app/​presenters/​v3/​organization_presenter.rb Exposes organization namespaces.
app/​models/​services/​service_binding.rb Captures account identity during binding creation.
app/​models/​runtime/​task_model.rb Exposes captured task identity.
app/​models/​runtime/​service_account_model.rb Implements ownership, reservations and lifecycle references.
app/​models/​runtime/​service_account_label_model.rb Adds account label associations.
app/​models/​runtime/​service_account_annotation_model.rb Adds account annotation associations.
app/​models/​runtime/​route_policy.rb Resolves qualified account route sources.
app/​models/​runtime/​process_model.rb Captures account identity when starting processes.
app/​models/​runtime/​organization.rb Validates and reserves immutable namespaces.
app/​models/​runtime/​app_model.rb Adds account assignment and entitlement validation.
app/​models.rb Loads account and metadata models.
app/​messages/​service_accounts_list_message.rb Validates account list filters.
app/​messages/​service_account_update_message.rb Validates account metadata and state updates.
app/​messages/​service_account_create_message.rb Validates creation and name-reuse requests.
app/​messages/​route_policy_create_message.rb Accepts qualified account route sources.
app/​messages/​route_policies_list_message.rb Accepts account source filters.
app/​messages/​organization_update_message.rb Validates organization namespace input.
app/​messages/​organization_quotas_update_message.rb Validates account quota limits.
app/​messages/​app_service_account_update_message.rb Validates assignment relationship payloads.
app/​jobs/​v3/​service_account_provision.rb Runs retryable account lifecycle operations.
app/​decorators/​include_route_policy_source_decorator.rb Includes readable account route sources.
app/​controllers/​v3/​service_accounts_controller.rb Implements account management and entitlement APIs.
app/​controllers/​v3/​route_policies_controller.rb Authorizes and filters account route sources.
app/​controllers/​v3/​organizations_controller.rb Restricts namespace allocation to administrators.
app/​controllers/​v3/​app_service_accounts_controller.rb Implements app-account relationship endpoints.
app/​collection_transformers/​username_populator.rb Resolves managed account principal names.
app/​actions/​task_create.rb Captures task identity under account locks.
app/​actions/​space_delete.rb Retains accounts while removing original-space references.
app/​actions/​service_account_provision.rb Reconciles UAA clients and CAPI principals.
app/​actions/​service_account_creation_budget.rb Enforces atomic rolling creation budgets.
app/​actions/​route_policy_create.rb Locks account sources during grant creation.
app/​actions/​process_restart.rb Refreshes identity snapshots on restart.
app/​actions/​organization_update.rb Applies namespace allocation and reports conflicts.
app/​actions/​organization_quotas_update.rb Updates account quota limits.
app/​actions/​organization_quotas_create.rb Initializes account quota limits.
app/​actions/​organization_delete.rb Coordinates account cleanup during organization deletion.
app/​actions/​organization_create.rb Allocates namespaces during organization creation.
app/​actions/​app_assign_service_account.rb Enforces assignment guards and queues provisioning.

Review effort: Balanced

Comment thread app/actions/organization_delete.rb Resolved
Comment on lines +11 to +12
names = users.select(&:is_oauth_client).map { |user| user.guid.delete_prefix('cf:service-account:').split(':').last }
accounts = ServiceAccountModel.where(name: names).all
def index
message = ServiceAccountsListMessage.from_params(query_params)
invalid_param!(message.errors.full_messages) unless message.valid?
dataset = ServiceAccountModel.dataset
require_writable_org!(account.organization)
space = account.entitled_spaces_dataset.where(spaces__guid: hashed_params[:space_guid]).first
resource_not_found!(:space) unless space
unauthorized! unless org_manager?(account) || developer?(space)
Comment on lines +93 to +95
apps_dataset.any? || ServiceBinding.where(service_account_guid: guid).any? ||
ProcessModel.where(service_account_guid: guid, state: ProcessModel::STARTED).any? ||
TaskModel.where(service_account_guid: guid, state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE, TaskModel::CANCELING_STATE]).any?
end

def before_create
if app
errors.add(:service_account, 'must be entitled to the app space')
raise validation_failed_error
end
self.service_account_guid = account&.guid
else
add_column :service_account_guid, String, **options, size: 255
end
add_foreign_key [:service_account_guid], :service_accounts, key: :guid, name: :fk_apps_service_account_guid

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants