Skip to content

Service Account Model (Pre-Implementation Baseline)

Purpose

Define machine identity and authorization shape before service-account coding begins. This document is contract-first guidance for the upcoming implementation phase.

Scope

  • Service account ownership and lifecycle.
  • Auth token model.
  • Authorization boundaries.
  • Audit and security controls.
  • Minimal schema contract.

Core Principles

  1. Service accounts are non-human identities scoped to a single project.
  2. Service accounts inherit tenant boundary through project ownership.
  3. Tokens minted for service accounts are short-lived and audience-scoped.
  4. Service-account actions are fully auditable with identity attribution.
  5. No static long-lived bearer secrets in application code or UI.

Important current limitation: - this baseline is sufficient for project-scoped app controllers, - it is not yet the final model for tenant-owned shared app runtimes, - those require a tenant-bounded machine-identity model that does not widen into platform-global authority, - see: - doc/architecture/App_Tenant_Shared_Attachment_Model_v1.md - doc/architecture/IAM_Token_Issuer_v1.md

Ownership Model

Tenant (org)
  -> Project
    -> Service Account

Rules: 1. service_accounts.project_id is required. 2. service_accounts.org_id is required (denormalized to avoid project-join fanout on every policy/billing/audit query; consistency enforced by project/org FK invariant). 3. Cross-project use is not allowed; access is project-scoped. 4. Cross-tenant use is forbidden.

Minimal Schema Shape (Planned)

service_accounts

  • id uuid pk
  • org_id uuid not null references organizations(id)
  • project_id uuid not null references projects(id)
  • name text not null
  • slug text not null
  • description text null
  • state text not null check (state in ('active','disabled','deleted'))
  • created_by_user_id uuid not null references users(id)
  • created_at timestamptz not null default now()
  • disabled_at timestamptz null
  • deleted_at timestamptz null

Constraints: - unique (project_id, slug) - invariant: project.org_id == service_accounts.org_id

service_account_credentials

  • id uuid pk
  • service_account_id uuid not null references service_accounts(id)
  • key_id text not null
  • public_jwk jsonb not null
  • private_key_enc jsonb not null
  • algorithm text not null (MVP: RS256 or EdDSA, choose one in implementation ADR)
  • state text not null check (state in ('active','rotated','revoked'))
  • created_at timestamptz not null default now()
  • expires_at timestamptz null
  • revoked_at timestamptz null

Constraints: - unique (service_account_id, key_id) - at most one active primary credential per service account in MVP

Auth Model (MVP)

  1. Human users authenticate with OIDC as today.
  2. Service accounts use a dedicated machine-token endpoint (client-credentials style).
  3. Machine access token claims include:
  4. sub: service account id
  5. actor_type: service_account
  6. org_id
  7. project_id
  8. scope / permissions
  9. exp, iat, iss, aud, jti
  10. Token TTL is short (default 15m; policy-controlled).
  11. Refresh tokens are not issued for service accounts in MVP.

Follow-on consolidation: - doc/architecture/IAM_Token_Issuer_v1.md defines the shared issuer model for service-account tokens, delegated shared-runtime operator tokens, future workload-bound tokens, and provider credential brokering. - Service-account endpoint contracts remain stable; the issuer is an internal issuer/policy/audit consolidation, not a new broad public token endpoint.

Authorization Model

  1. Requests from service-account tokens are evaluated against project-scoped permissions.
  2. Service accounts cannot call admin-only endpoints.
  3. Service accounts cannot manage IAM/billing/tenant settings.
  4. Endpoint allowlist for service accounts must be explicit in contract docs.
  5. ownership_required checks resolve by project scope in token claims.

MVP allowlist (implemented): - GET /api/v1/skus - GET /api/v1/nodes - GET /api/v1/projects/{project_id}/app-instances - POST /api/v1/projects/{project_id}/app-instances - GET /api/v1/projects/{project_id}/app-instances/{app_instance_id} - DELETE /api/v1/projects/{project_id}/app-instances/{app_instance_id} - POST /api/v1/projects/{project_id}/app-instances/{app_instance_id}/upgrade - POST /api/v1/projects/{project_id}/app-instances/{app_instance_id}/rollback - POST /api/v1/projects/{project_id}/app-instances/{app_instance_id}/decommission - POST /api/v1/projects/{project_id}/access-credentials/{credential_id}/deliver - GET /api/v1/storage/list - GET /api/v1/storage/download - PUT /api/v1/storage/upload - POST /api/v1/storage/mkdir - POST /api/v1/storage/rename - DELETE /api/v1/storage/delete

For project-scoped app-instance endpoints, {project_id} must equal token claim project_id. For storage endpoints, X-Project-ID is required and must equal token claim project_id. Any non-allowlisted endpoint returns 403 insufficient_permissions.

API Surface (Planned)

Tenant/project admin managed endpoints: 1. POST /api/v1/projects/{project_id}/service-accounts 2. GET /api/v1/projects/{project_id}/service-accounts 3. POST /api/v1/projects/{project_id}/service-accounts/{id}/disable 4. POST /api/v1/projects/{project_id}/service-accounts/{id}/rotate-key 5. DELETE /api/v1/projects/{project_id}/service-accounts/{id} (soft delete)

Token issuance endpoint: 1. POST /api/v1/auth/service-account/token

Audit Requirements

Every service-account lifecycle mutation must write audit_logs with: - actor (human admin user), - action (service_account.create|disable|rotate|delete), - target (service_account id), - result, - correlation_id.

Every machine action should log actor context: - actor_type=service_account - actor_id=<service_account_id> - org_id, project_id - correlation_id.

Security Controls

  1. Private keys for service-account credentials are always stored encrypted (*_enc envelope format).
  2. Rotation is mandatory and policy-driven.
  3. Revocation must be immediate (deny-list by jti or credential state check).
  4. Tokens never via query parameters.
  5. Scope minimization: per-service-account endpoint permissions only.

MVP Key Custody Assumption

  1. MVP allows encrypted private-key material to be stored in Postgres (private_key_enc).
  2. Production hardening target is external key custody (KMS/Vault reference model) with rotation and revocation parity.
  3. This assumption is tracked in doc/governance/Assumptions_Register.md (A-019).
  4. Vault-first custody baseline is defined in doc/architecture/Platform_Vault_Secrets_Baseline_v1.md.

Policy Keys (Planned)

  1. auth.service_account_token_ttl_seconds
  2. auth.service_account_max_active_keys
  3. auth.service_account_rotation_days
  4. auth.service_account_allowed_scopes (json)

Non-Goals (Initial Rollout)

  1. Cross-project or cross-tenant service accounts.
  2. Service-account UI in MVP if API-first rollout is sufficient.
  3. External IdP-managed machine identities.

Follow-on note: - tenant-owned shared runtimes may require tenant-scoped machine identity or an equivalent delegated model, but that should be added as an explicit next contract rather than weakening the project-scoped baseline in place.

Acceptance Gate Before Coding

  1. OpenAPI endpoints and token schema added.
  2. Async/event contracts added if lifecycle events are emitted.
  3. ERD and db schema include service account tables/constraints.
  4. Authorization matrix updated for actor_type = service_account.
  5. Security controls mapped in Security_Control_Verification.md.

References

  • doc/architecture/Tenant_Project_Ownership_Baseline.md
  • doc/architecture/adrs/ADR-004-identity-authz-model.md
  • doc/governance/Security_Control_Verification.md
  • doc/api/openapi.draft.yaml
  • doc/architecture/App_Tenant_Shared_Attachment_Model_v1.md
  • doc/architecture/IAM_Token_Issuer_v1.md