From 80e24bb674c4f7cc71a5fc0a276c435695d413e3 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 5 May 2026 13:58:05 -0400 Subject: [PATCH 1/5] docs: add security boundary specification for Ambient platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial draft of the security specification covering: - OpenShift namespace-scoped build agent ServiceAccount - Control Plane SA as the single SRE-owned cluster identity - Per-project Vertex AI credential scoping - User SSO token propagation into runners - Integration credential lifecycle (Credential provider=*) - Dynamic MCP credential watching (sidecar vs pod mode) - Per-session ServiceAccount isolation (closing the shared-SA gap) ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/internal/proposals/security.md | 345 ++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 docs/internal/proposals/security.md diff --git a/docs/internal/proposals/security.md b/docs/internal/proposals/security.md new file mode 100644 index 000000000..6cea3456f --- /dev/null +++ b/docs/internal/proposals/security.md @@ -0,0 +1,345 @@ +# Ambient Code Platform: Security Specification + +**Status:** Draft +**Authors:** Platform Team +**Last Updated:** 2026-05-05 + +## Summary + +The Ambient Code Platform runs agentic AI sessions inside Kubernetes. Each session is a +pod that executes an LLM-powered runner, accesses external services (Vertex AI, GitHub, +Jira), and stores results via the API server. + +This specification defines who can do what. Six identity boundaries govern the platform: +an SRE-managed Control Plane that reconciles state across namespaces, per-session +ServiceAccounts that isolate runner pods from each other, user SSO tokens that scope +runner authorization to the creating user, per-project LLM credentials (Vertex AI), +per-project integration credentials (GitHub/GitLab/Jira/etc.), and a namespace-scoped +build agent SA for OpenShift CI/CD workflows. + +**The critical gap today:** all runner sessions in a namespace share a ServiceAccount with +unscoped Secret access. Any session can read another session's runner tokens. This spec +closes that gap with per-session Roles restricted by `resourceNames`. + +## Accounts and Tokens + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `ambient-control-plane` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Long-lived (token Secret) | Watches API server, reconciles sessions/projects to K8s, writes status back | +| `ambient-control-plane` OIDC token | OAuth2 client_credentials | SRE | API server | Auto-refreshed (30s buffer) | CP authenticates to API server for session/credential CRUD | +| `backend-api` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Backend API: manages CRs, mints session tokens, validates user tokens | +| `frontend` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Frontend: TokenReview and SubjectAccessReview only | +| `ambient-session-` | K8s ServiceAccount | SRE (created by operator) | Namespace (Role) | Session lifetime | Per-session runner identity; scoped to own secrets and session CR | +| Runner bot token | K8s TokenRequest | SRE (minted by operator) | Session-specific | Mounted + refreshed by kubelet | Runner authenticates to K8s API and API server for status/credential ops | +| Runner AGUI token | UUID | SRE (generated per session) | Session-specific | Session lifetime | Authenticates inbound AG-UI requests to runner pod (bearer validation) | +| CP RSA-encrypted session token | RSA + OIDC exchange | SRE | Session-specific | On-demand (per request) | Runner fetches API token from CP `/token` endpoint using encrypted session ID | +| User SSO token | OIDC (Red Hat SSO) | User | User's RBAC scope | SSO session TTL | User authenticates to frontend/backend; propagated as `caller_token` to runner | +| `Credential(provider=vertex)` | GCP service account key | User | Project | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per namespace | +| `Credential(provider=github)` | PAT or GitHub App token | User | Project | Until rotated | Git operations; fetched at runtime, written to ephemeral `/tmp/`, cleared per turn | +| `Credential(provider=gitlab)` | PAT | User | Project | Until rotated | GitLab repository access | +| `Credential(provider=jira)` | API token | User | Project | Until rotated | Jira issue tracking integration | +| `Credential(provider=google)` | OAuth2 token | User | Project | Until rotated | Google Workspace integrations | +| `Credential(provider=kubeconfig)` | Kubeconfig | User | Project | Until rotated | Cross-cluster Kubernetes operations | +| `ambient-agent` (proposed) | K8s ServiceAccount | SRE | Single namespace (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one namespace | + +## Overview + +This document defines the security boundaries for the Ambient Code Platform. It addresses +identity isolation, credential scoping, and the principle of least privilege across the +control plane, runner sessions, and user-facing integrations. + +--- + +## 1. OpenShift Namespace-Scoped Build Agent Service Account + +### Purpose + +A dedicated ServiceAccount for automated build-and-deploy workflows within a single +OpenShift namespace. This SA enables agentic workflows to build container images via +`BuildConfig`, push to the internal registry, and deploy workloads without requiring +cluster-admin privileges. + +### Scope + +- Bound to a single namespace (e.g., `ambient-sandbox`) +- Cannot access other namespaces, nodes, or cluster-scoped resources +- Cannot create or modify CRDs, ClusterRoles, or ClusterRoleBindings + +### Permissions + +| API Group | Resources | Verbs | +|-----------|-----------|-------| +| `build.openshift.io` | `buildconfigs`, `buildconfigs/instantiate`, `builds`, `builds/log` | get, list, watch, create, update, patch, delete | +| `image.openshift.io` | `imagestreams`, `imagestreamtags`, `imagestreamimages` | get, list, watch, create, update, patch, delete | +| `apps` | `deployments`, `statefulsets`, `replicasets` | get, list, watch, create, update, patch, delete | +| `""` (core) | `pods`, `pods/log`, `services`, `configmaps`, `secrets`, `persistentvolumeclaims`, `serviceaccounts`, `events` | get, list, watch, create, update, patch, delete | +| `route.openshift.io` | `routes` | get, list, watch, create, update, patch, delete | +| `batch` | `jobs`, `cronjobs` | get, list, watch, create, update, patch, delete | +| `networking.k8s.io` | `networkpolicies` | get, list, watch, create, update, patch, delete | +| `rbac.authorization.k8s.io` | `roles`, `rolebindings` | get, list, watch, create, update, patch, delete | + +Additionally requires the built-in `system:image-builder` role for internal registry push access. + +### Rationale + +Agentic workflows need to build and deploy without human intervention, but must not +escalate beyond the target namespace. This SA provides the minimal surface area for a +CI/CD agent to operate a full build-test-deploy cycle while remaining invisible to the +rest of the cluster. + +--- + +## 2. Security Boundaries + +### 2.1 SRE Boundary: Control Plane Service Account + +**Identity:** `ambient-control-plane` ServiceAccount (cluster-scoped RBAC) + +**Role:** The single SRE-owned identity that bridges the API server and Kubernetes. The +Control Plane runs as an informer/watcher that: + +- Watches the API server (PostgreSQL-backed) for new/modified session and project resources +- Reconciles desired state to Kubernetes (creates Pods, Services, Secrets, ServiceAccounts + in project namespaces) +- Writes reconciled status back to the API server (phase transitions, runner pod names, + credential resolution results) + +**Current Implementation:** +- SA defined in `components/manifests/base/rbac/control-plane-sa.yaml` +- ClusterRole in `components/manifests/base/rbac/control-plane-clusterrole.yaml` +- Long-lived token via companion Secret (`kubernetes.io/service-account-token`) +- Authenticates to API server via OIDC `client_credentials` flow or static token + +**Security Properties:** +- This SA must never be exposed to user workloads +- Runner containers must not mount or inherit the CP token +- The CP creates per-session SAs with scoped tokens (see 2.5) rather than sharing its own + +### 2.2 User Boundary: Vertex AI Credentials + +**Identity:** Per-project `Credential(provider=vertex)` stored in the API server (PostgreSQL) + +**Role:** Provides Vertex AI / Google Cloud authentication for LLM inference. Each project +can bind its own Vertex credential, allowing different teams to use different GCP projects +or service accounts. + +**Flow:** +1. User creates `Credential(provider=vertex)` in their project via the API +2. On runner pod provisioning, the Control Plane calls `resolveCredentialIDs()` to look up + the project's Vertex credential +3. The CP writes the Vertex service account key into a Kubernetes Secret + (`ambient-vertex`) in the project namespace +4. The runner pod mounts this secret and uses it for `GOOGLE_APPLICATION_CREDENTIALS` + +**Security Properties:** +- Vertex credentials are scoped per project, not shared globally +- Credential tokens are write-only in the API (never returned in GET responses) +- The runner fetches credentials at runtime via authenticated API calls, not baked into + the container image +- Credential rotation requires only updating the `Credential` resource; the CP + re-resolves on next session provisioning + +### 2.3 User Boundary: Red Hat SSO / User Token Propagation + +**Identity:** The authenticated user's own SSO token, propagated into the runner + +**Role:** Ensures the runner operates with the creating user's authorization context, not +an elevated service identity. The runner's API calls (session status updates, credential +fetches, AG-UI event streaming) are scoped to what the user is allowed to access. + +**Flow:** +1. User authenticates via Red Hat SSO (OIDC) +2. The backend mints a per-session K8s ServiceAccount token annotated with the user's + identity (`ambient-code.io/created-by-user-id`) +3. The runner resolves its bot token via the CP token endpoint (OIDC `client_credentials` + exchange, encrypted with CP's RSA public key) +4. When a human interacts via AG-UI, their bearer token is passed through as + `context.caller_token` โ€” the runner uses this token first, falling back to the bot + token only if expired +5. Backend RBAC enforcement (`enforceCredentialRBAC()`) validates the caller is either the + session owner or a bot acting on behalf of the owner + +**Security Properties:** +- Runner cannot access resources the user cannot access +- Cross-user credential access is blocked (403) +- The bot token is scoped to the specific session's namespace and resources +- Token refresh uses RSA-encrypted session ID exchange, not stored credentials + +### 2.4 User Boundary: Integration Credentials + +**Identity:** Per-project `Credential(provider=*)` resources โ€” `github`, `gitlab`, `jira`, +`google`, `kubeconfig`, and future providers + +**Role:** Users bind external integration credentials to their project. The runner fetches +these at runtime to perform git operations, issue tracking, and other integrations. + +**Current Providers (OpenAPI enum):** +- `github` โ€” PAT or GitHub App token for repository access +- `gitlab` โ€” PAT for GitLab repository access +- `jira` โ€” API token for issue tracking +- `google` โ€” OAuth2 token for Google integrations +- `kubeconfig` โ€” Kubernetes config for cross-cluster operations + +**Flow:** +1. User creates `Credential(provider=github, ...)` in their project +2. Runner fetches credential token at runtime: + `GET /api/ambient/v1/projects/{project}/credentials/{id}/token` +3. Token is written to ephemeral files (`/tmp/`) for git credential helper and CLI + wrappers +4. Credentials are cleared from environment after each turn + (`clear_runtime_credentials()`) + +**Security Properties:** +- Credentials are project-scoped โ€” projects cannot access each other's credentials +- RBAC permissions: `credential:token` required for token fetch (separate from + `credential:read`) +- Backend validates caller hostname is cluster-internal (`.svc.cluster.local`, `localhost`) + to prevent token exfiltration +- Credential tokens are write-only in the API (presenter strips `Token` field from GET + responses) + +#### 2.4a Dynamic MCP Credential Watching and Pod Lifecycle + +**Current State:** MCP servers run as sidecars (`ambient-mcp` container) injected by the +Control Plane when provisioning runner pods. Credentials for MCP servers are stored in a +shared Secret (`mcp-server-credentials`) keyed by `serverName:userID`. + +**Target State:** If the Control Plane continuously watches `Credential` resources for +changes, MCP configurations can be dynamically applied without restarting the runner: + +- **Sidecar mode (current):** MCP runs as a sidecar container alongside the runner in the + same Pod. Suitable for lightweight, session-scoped MCP servers. Limited to the pod's + lifecycle โ€” cannot be independently restarted or scaled. +- **Pod mode (proposed):** MCP runs as an independent Pod in the project namespace with its + own lifecycle (readiness probes, independent restarts, resource limits). Required when + MCP servers need: + - Independent scaling or resource allocation + - Persistence across session restarts + - Shared access across multiple sessions in the same project + - Long-running connections (databases, message queues) that survive session recycling + +**Credential Watch Flow:** +1. Control Plane watches `Credential` resources on the API server (via informer or + polling) +2. On credential create/update/delete, CP evaluates which sessions or MCP pods are + affected +3. For sidecar mode: CP triggers a pod rolling restart with updated environment +4. For pod mode: CP creates/updates/deletes MCP Pods with the new credential configuration +5. MCP pods authenticate to external services using the bound credential tokens + +### 2.5 SRE Boundary: Per-Session Service Accounts + +**Problem Statement:** + +Today, all runner sessions within a project namespace share access to each other's +resources. A compromised or misbehaving session can: +- Read other sessions' runner tokens from Kubernetes Secrets +- Access other sessions' mounted credentials +- Interfere with other sessions' pods via the Kubernetes API +- Exfiltrate data from other sessions' PVCs + +This is a significant security gap. The shared access model was an expedient choice during +early development but violates the principle of least privilege. + +**Current State (partially implemented):** + +The operator already creates per-session ServiceAccounts +(`ambient-session-`) with scoped Roles: + +``` +SA: ambient-session- +Role: get/list/watch/create/update/patch AgenticSessions + create SelfSubjectAccessReviews + get Secrets + get/list/update MLflow Experiments +``` + +However, the Role's `get Secrets` permission is not scoped to the session's own secrets. +Any session SA can read any Secret in the namespace, including other sessions' runner +tokens. + +**Target State:** + +Each session must have a ServiceAccount that can only access its own resources: + +| Resource | Allowed Names | Verbs | +|----------|--------------|-------| +| Secrets | `ambient-runner-token-`, `ambient-vertex` (read-only) | get | +| Pods | Labeled `ambient-code/session=` | get, list, watch | +| AgenticSessions | `` | get, update (status only) | +| SelfSubjectAccessReviews | (any) | create | + +**Implementation Requirements:** +1. **Per-session Role:** The operator must generate a Role per session with `resourceNames` + restrictions on Secrets and AgenticSessions +2. **Label-based pod access:** Use label selectors in NetworkPolicies to restrict inter-pod + communication to same-session containers only +3. **Secret naming convention:** All session-scoped secrets must follow the pattern + `ambient-runner-token-` or `-*` to enable `resourceNames` + restrictions +4. **Shared secrets (read-only):** Project-wide secrets (`ambient-vertex`, + `ambient-runner-secrets`) should be mounted as read-only volumes rather than accessed + via the Kubernetes API +5. **NetworkPolicy per session:** Extend the existing `ensureRunnerNetworkPolicy()` to + create per-session policies that restrict ingress/egress to only the session's own pods + and the control plane + +**Migration Path:** +1. Deploy per-session Roles with `resourceNames` restrictions (backward compatible โ€” + existing sessions continue to work with broader access until recreated) +2. Update the operator's `regenerateRunnerToken()` to bind the session SA to the new + scoped Role instead of the namespace-wide Role +3. Audit existing sessions for cross-session secret access (add audit logging for Secret + GET operations with caller identity) +4. Enforce per-session NetworkPolicies in new sessions; backfill existing sessions during + next maintenance window + +--- + +## 3. Security Boundary Summary + +``` ++------------------------------------------------------------------+ +| Cluster | +| | +| +---------------------------+ +-----------------------------+ | +| | ambient-code namespace | | project namespace | | +| | | | | | +| | [Control Plane] | | [Session A Pod] | | +| | SA: ambient-control-plane| | SA: ambient-session-aaa | | +| | - watches API server | | - own secrets only | | +| | - reconciles to K8s | | - own session CR only | | +| | - writes status back | | - user's SSO token | | +| | | | - project vertex cred | | +| | [API Server] | | +------------------+ | | +| | SA: (pod identity) | | | MCP sidecar/pod | | | +| | - PostgreSQL backend | | | - integration | | | +| | - Credential store | | | - creds from API | | | +| | - RBAC enforcement | | +------------------+ | | +| | | | | | +| | [Backend] | | [Session B Pod] | | +| | SA: backend-api | | SA: ambient-session-bbb | | +| | - user token passthrough| | - ISOLATED from A | | +| | - credential RBAC | | - own secrets only | | +| +---------------------------+ +-----------------------------+ | +| | ++------------------------------------------------------------------+ +``` + +**Key Invariants:** +1. No runner session can access another session's secrets or tokens +2. No runner session can operate beyond the user's own authorization scope +3. Integration credentials are project-scoped and fetched at runtime, never baked in +4. The Control Plane SA is the only identity that spans namespaces +5. MCP lifecycle (sidecar vs. pod) is determined by operational requirements, not security + compromise + +--- + +## References + +- [Security Standards](../../security-standards.md) +- [User Token Authentication ADR](../adr/0002-user-token-authentication.md) +- [Credential API OpenAPI Spec](../../../components/ambient-api-server/openapi/openapi.credentials.yaml) +- [Control Plane RBAC](../../../components/manifests/base/rbac/control-plane-clusterrole.yaml) +- [Operator Session Handler](../../../components/operator/internal/handlers/sessions.go) From 798123e8c1cb4497e1424e92d969fef3e13bf5f3 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 5 May 2026 14:44:40 -0400 Subject: [PATCH 2/5] spec: refactor model spec and security spec ownership boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move security.md to specs/security/security.spec.md. Extract HOW content (credential authorization model, RBAC runtime grant semantics, proxy authentication, design decisions) from ambient-model.spec.md into the security spec. Model spec retains WHAT (schemas, endpoints, provider enum, permission matrix, CLI mappings). Cross-references link both documents. Also moves ambient-model.spec.md from specs/sessions/ to specs/api/ and updates all references (BOOKMARKS.md, design README, devflow skill, workflows). ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BOOKMARKS.md | 3 +- docs/internal/design/README.md | 5 +- skills/devflow/SKILL.md | 4 +- specs/{sessions => api}/ambient-model.spec.md | 58 +++++-------- .../security/security.spec.md | 86 +++++++++++++++++-- workflows/integrations/mcp-server.workflow.md | 2 +- workflows/sessions/ambient-model.workflow.md | 4 +- 7 files changed, 112 insertions(+), 50 deletions(-) rename specs/{sessions => api}/ambient-model.spec.md (92%) rename docs/internal/proposals/security.md => specs/security/security.spec.md (76%) diff --git a/BOOKMARKS.md b/BOOKMARKS.md index d2db657e3..cf60a9b9f 100644 --- a/BOOKMARKS.md +++ b/BOOKMARKS.md @@ -25,10 +25,11 @@ Desired state of the system, organized by capability domain. | Spec | Domain | Purpose | |------|--------|---------| -| [Ambient Data Model](specs/sessions/ambient-model.spec.md) | sessions | Platform-wide data model: projects, agents, sessions, credentials, RBAC | +| [Ambient Data Model](specs/api/ambient-model.spec.md) | sessions | Platform-wide data model: projects, agents, sessions, credentials, RBAC | | [Control Plane](specs/control-plane/control-plane.spec.md) | control-plane | CP architecture, runner structure, K8s provisioning | | [Runner](specs/agents/runner.spec.md) | agents | Runner subprocess lifecycle, bridges, gRPC/HTTP endpoints | | [MCP Server](specs/integrations/mcp-server.spec.md) | integrations | MCP tool definitions, sidecar and public endpoint modes | +| [Security](specs/security/security.spec.md) | security | Identity boundaries, credential authorization, per-session isolation, design decisions | Feature specs remain in numbered directories under `specs/` (e.g., `specs/001-*/spec.md`). diff --git a/docs/internal/design/README.md b/docs/internal/design/README.md index 9c6da9c60..8942ffc53 100644 --- a/docs/internal/design/README.md +++ b/docs/internal/design/README.md @@ -52,6 +52,7 @@ A spec is **complete** when it is unambiguous enough that two engineers reading | `ambient-model.spec.md` | All platform Kinds: Session, Agent, Project, Inbox, Role, RoleBinding, User | | `control-plane.spec.md` | Control plane gRPC protocol, session fan-out, runner contract | | `mcp-server.spec.md` | MCP server tools, annotation state, sidecar transport | +| `security.spec.md` | Identity boundaries, credential authorization, per-session SA isolation | ### Guide files (`*.guide.md`) @@ -65,7 +66,7 @@ Guides are **living documents**. Every time the workflow runs and something is d | Guide | Paired spec | |---|---| -| `workflows/sessions/ambient-model.workflow.md` | `specs/sessions/ambient-model.spec.md` | +| `workflows/sessions/ambient-model.workflow.md` | `specs/api/ambient-model.spec.md` | | `workflows/control-plane/control-plane.workflow.md` | `specs/control-plane/control-plane.spec.md` | | `workflows/integrations/mcp-server.workflow.md` | `specs/integrations/mcp-server.spec.md` | @@ -133,6 +134,6 @@ Every time the loop stops because something was wrong, the documents get better. ## Reading Order for a New Contributor 1. This README โ€” the why -2. `specs/sessions/ambient-model.spec.md` โ€” what the platform is +2. `specs/api/ambient-model.spec.md` โ€” what the platform is 3. `workflows/sessions/ambient-model.workflow.md` โ€” how changes are made 4. The context file for the component you are working on diff --git a/skills/devflow/SKILL.md b/skills/devflow/SKILL.md index f2089d1f0..974a7759f 100644 --- a/skills/devflow/SKILL.md +++ b/skills/devflow/SKILL.md @@ -78,7 +78,7 @@ Every component has a spec file. Load the spec for the component being changed: | Component | Spec | Guide | |-----------|------|-------| -| Data Model / API / CLI / RBAC | `specs/sessions/ambient-model.spec.md` | `workflows/sessions/ambient-model.workflow.md` | +| Data Model / API / CLI / RBAC | `specs/api/ambient-model.spec.md` | `workflows/sessions/ambient-model.workflow.md` | ### 2b. Modify the Spec @@ -354,7 +354,7 @@ But prefer fixing the lint/format issue instead. | `specs/standards/backend/conventions.spec.md` | SDK generation | | `specs/standards/control-plane/conventions.spec.md` | CP fan-out, runner contract | | `specs/standards/frontend/conventions.spec.md` | Frontend build, React Query | -| `specs/sessions/ambient-model.spec.md` | Data model spec (source of truth) | +| `specs/api/ambient-model.spec.md` | Data model spec (source of truth) | | `workflows/sessions/ambient-model.workflow.md` | Implementation workflow (wave-based) | --- diff --git a/specs/sessions/ambient-model.spec.md b/specs/api/ambient-model.spec.md similarity index 92% rename from specs/sessions/ambient-model.spec.md rename to specs/api/ambient-model.spec.md index b17f6a003..d8a9ae123 100755 --- a/specs/sessions/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -745,9 +745,10 @@ DELETE /api/ambient/v1/projects/{id}/credentials/{cred_id} soft GET /api/ambient/v1/projects/{id}/credentials/{cred_id}/token fetch raw token โ€” restricted to credential:token-reader ``` -`token` is accepted on `POST` and `PATCH` but **never returned** by the standard read endpoints. It is stored encrypted in the database. The database row is the authoritative store; a future Vault integration can be adopted by pointing the row at a Vault path without changing the API surface. - -`GET /projects/{id}/credentials/{cred_id}/token` is the **only** endpoint that returns the raw token. It is gated by the `credential:token-reader` role โ€” a platform-internal role granted only to runner service accounts at session start. Human users and service accounts do not hold this role by default. Credential CRUD is governed by the caller's project-level role (e.g. `project:owner`, `project:editor`). +`token` is accepted on `POST` and `PATCH` but **never returned** by standard read endpoints. +`GET .../token` is gated by `credential:token-reader`. See +[Security Spec ยง4](../security/security.spec.md#4-credential-authorization-model) for +runtime authorization semantics. #### Provider Enum @@ -788,21 +789,9 @@ Effective permissions = union of all applicable bindings (global โˆช project โˆช #### Credential Access โ€” Project-Scoped by Default -Credentials belong to a project. All agents in the project share the project's credentials automatically โ€” no explicit sharing or per-credential RoleBindings needed. At session start, the resolver lists all credentials in the agent's project and returns the matching credential for each requested provider. - -This follows the Kubernetes resource model: - -| Ambient | Kubernetes Analogy | Relationship | -|---------|-------------------|-------------| -| Project | Namespace | Isolation boundary, owns child resources | -| Agent | Deployment | Mutable definition, runs workloads | -| Session | Pod | Ephemeral execution, created from Agent | -| Credential | Secret | Project-scoped secret, available to all workloads in the namespace | - -Named patterns: -- **Project Robot Account** โ€” credential created in a project; all agents use it automatically. -- **Multi-project credential** โ€” create the same credential (same PAT) in multiple projects. Each project gets its own Credential record. -- **No credential** โ€” projects without credentials simply run sessions without provider integrations. +Credentials belong to a project. All agents in the project share them automatically. +See [Security Spec ยง4](../security/security.spec.md#4-credential-authorization-model) for +sharing model, K8s analogy, and named patterns. ### Built-in Roles @@ -853,7 +842,9 @@ GET /api/ambient/v1/projects/{id}/agents/{agent_id}/role_bindings GET /api/ambient/v1/sessions/{id}/role_bindings ``` -The `credential:token-reader` role is granted to the runner service account by the platform at session start. It is never granted via user-facing `POST /role_bindings`. It is a platform-internal binding managed by the operator. Credential CRUD is governed by the caller's project-level role โ€” `project:owner` and `project:editor` can create/update/delete credentials; `project:viewer` can list/read metadata. +The `credential:token-reader` role is platform-internal. See +[Security Spec ยง4](../security/security.spec.md#credentialtoken-reader-runtime-grant) for +grant semantics and CRUD authorization rules. --- @@ -876,9 +867,14 @@ GET /api/ambient/v1/projects/{id}/scheduled-sessions/{sched_id}/runs ### Generic Proxy -All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded verbatim to the backend service. The API server authenticates the caller, injects service credentials, then proxies the request โ€” preserving method, path, query string, body, and response status. +All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded +verbatim to the backend service. See +[Security Spec ยง4](../security/security.spec.md#api-server-proxy-authentication) for +authentication and credential injection behavior. -This allows SDK and CLI clients to reach the full backend surface through a single authenticated endpoint without requiring every backend route to be natively implemented in the API server. Routes listed here are candidates for future native spec entries. +This allows SDK and CLI clients to reach the full backend surface through a single +authenticated endpoint without requiring every backend route to be natively implemented in +the API server. Routes listed here are candidates for future native spec entries. #### Project Configuration (proxied) @@ -1080,10 +1076,6 @@ This structure means you can define and compose bespoke agent suites โ€” entire | Agent is project-scoped, not global | Simplicity. An agent's identity and prompt are contextual to the project it serves. No indirection via a global registry. | | Agent.prompt is mutable | Prompt editing is a routine operational task. RBAC controls who can change it. No versioning overhead. | | Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | -| Credential is project-scoped, like a Kubernetes Secret | Credentials live inside a project. All agents in the project share them automatically. Duplication across projects is intentional and explicit โ€” each project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | -| Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | -| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via project membership โ€” no dedicated `credential` scope needed. Simpler model with fewer moving parts. | -| Credential CRUD governed by project roles | `project:owner` and `project:editor` can manage credentials. No separate `credential:owner` / `credential:reader` roles โ€” project roles cover it. | | One active Session per Agent | Avoids concurrent conflicting runs; start is idempotent | | Inbox on Agent, not Session | Messages persist across re-ignitions; addressed to the agent, not the run | | Inbox drained at start | Unread messages become part of the start context; session picks up where things left off | @@ -1091,13 +1083,12 @@ This structure means you can define and compose bespoke agent suites โ€” entire | Sessions created only via start | Sessions are run artifacts; direct `POST /sessions` does not exist | | Every layer carries a `prompt` | Project.prompt = workspace context; Agent.prompt = who the agent is; Session.prompt = what this run does; Inbox = prior requests. Pokes roll downhill. | | `SessionMessage` is append-only | Canonical record of the LLM conversation; never edited or deleted | -| `agent:editor` role | Allows prompt updates without full operator access | -| `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox | -| Union-only permissions | No deny rules โ€” simpler mental model for fleet operators | | CLI mirrors API 1-for-1 | Every endpoint has a corresponding command; status tracked explicitly | | This document is the spec | A reconciler will compare the spec (this doc) against code status and surface gaps | | `labels` / `annotations` are JSONB, not strings | Enables GIN-indexed key/value queries (`@>` operator) without joins; every row carries its own metadata without a separate EAV table. `labels` = queryable tags; `annotations` = freeform notes. Applied to first-class Kinds: User, Project, Agent, Session. Not applied to Inbox, SessionMessage, Role/RoleBinding. | +Security and credential design decisions (RBAC scoping, write-only tokens, role catalog rationale) are in [Security Spec ยง5](../security/security.spec.md#5-design-decisions). + --- ## Credential โ€” Usage @@ -1145,15 +1136,8 @@ acpctl apply -f credential.yaml ## Design Decisions โ€” Credential -| Decision | Rationale | -|----------|-----------| -| Credential is project-scoped | Follows the Kubernetes Secret-in-Namespace pattern. All agents in the project share credentials implicitly. No RoleBindings needed for sharing within a project. | -| Token stored in database, encrypted at rest | Single authoritative store. A future Vault integration can be adopted by pointing the DB row at a Vault path without changing the API surface. | -| `google` token serialized as a string | Service Account JSON is serialized into the single `token` field. Keeps the schema uniform across all providers. | -| No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | -| Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | -| No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | -| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each project, not share their personal credentials. Each project gets its own Credential records. | +See [Security Spec ยง5](../security/security.spec.md#5-design-decisions) for credential +design rationale (storage, rotation, provider serialization, migration). --- diff --git a/docs/internal/proposals/security.md b/specs/security/security.spec.md similarity index 76% rename from docs/internal/proposals/security.md rename to specs/security/security.spec.md index 6cea3456f..307ce1418 100644 --- a/docs/internal/proposals/security.md +++ b/specs/security/security.spec.md @@ -336,10 +336,86 @@ Each session must have a ServiceAccount that can only access its own resources: --- +## 4. Credential Authorization Model + +This section defines how credentials are authorized at runtime. For credential Kind schemas, +API endpoints, and provider enum definitions, see +[`specs/api/ambient-model.spec.md`](../api/ambient-model.spec.md). + +### Project-Scoped Sharing + +Credentials belong to a project. All agents in the project share the project's credentials +automatically โ€” no explicit sharing or per-credential RoleBindings needed. At session start, +the resolver lists all credentials in the agent's project and returns the matching credential +for each requested provider. + +This follows the Kubernetes resource model: + +| Ambient | Kubernetes Analogy | Relationship | +|---------|-------------------|--------------| +| Project | Namespace | Isolation boundary, owns child resources | +| Agent | Deployment | Mutable definition, runs workloads | +| Session | Pod | Ephemeral execution, created from Agent | +| Credential | Secret | Project-scoped secret, available to all workloads in the namespace | + +Named patterns: +- **Project Robot Account** โ€” credential created in a project; all agents use it automatically. +- **Multi-project credential** โ€” create the same credential (same PAT) in multiple projects. Each project gets its own Credential record. +- **No credential** โ€” projects without credentials simply run sessions without provider integrations. + +### `credential:token-reader` Runtime Grant + +The `credential:token-reader` role is granted to the runner service account by the platform +at session start. It is never granted via user-facing `POST /role_bindings`. It is a +platform-internal binding managed by the operator. + +Credential CRUD is governed by the caller's project-level role โ€” `project:owner` and +`project:editor` can create/update/delete credentials; `project:viewer` can list/read +metadata. + +### API Server Proxy Authentication + +All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded +verbatim to the backend service. The API server authenticates the caller, injects service +credentials, then proxies the request โ€” preserving method, path, query string, body, and +response status. + +Runner-internal endpoints (called by runner pods at runtime): +- `POST /api/projects/{p}/agentic-sessions/{s}/github/token` โ€” get a GitHub token for a session +- `GET /api/projects/{p}/agentic-sessions/{s}/credentials/{provider}` โ€” fetch credential by provider +- `POST /api/projects/{p}/agentic-sessions/{s}/runner/feedback` โ€” runner feedback + +These are accessible via the API server for SDK/CLI tooling but are not intended for human +interactive use. The API server validates the caller is cluster-internal +(`.svc.cluster.local`, `localhost`) to prevent token exfiltration. + +--- + +## 5. Design Decisions + +| Decision | Rationale | +|----------|-----------|| +| Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | +| Credential is project-scoped, like a Kubernetes Secret | Credentials live inside a project. All agents in the project share them automatically. Duplication across projects is intentional and explicit โ€” each project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | +| Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | +| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via project membership โ€” no dedicated `credential` scope needed. Simpler model with fewer moving parts. | +| Credential CRUD governed by project roles | `project:owner` and `project:editor` can manage credentials. No separate `credential:owner` / `credential:reader` roles โ€” project roles cover it. | +| `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox. | +| Union-only permissions | No deny rules โ€” simpler mental model for fleet operators. | +| Token stored in database, encrypted at rest | Single authoritative store. A future Vault integration can be adopted by pointing the DB row at a Vault path without changing the API surface. | +| `google` token serialized as a string | Service Account JSON is serialized into the single `token` field. Keeps the schema uniform across all providers. | +| No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | +| Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | +| No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | +| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each project, not share their personal credentials. Each project gets its own Credential records. | + +--- + ## References -- [Security Standards](../../security-standards.md) -- [User Token Authentication ADR](../adr/0002-user-token-authentication.md) -- [Credential API OpenAPI Spec](../../../components/ambient-api-server/openapi/openapi.credentials.yaml) -- [Control Plane RBAC](../../../components/manifests/base/rbac/control-plane-clusterrole.yaml) -- [Operator Session Handler](../../../components/operator/internal/handlers/sessions.go) +- [Ambient Data Model Spec](../api/ambient-model.spec.md) โ€” Credential/RBAC schemas, endpoints, provider enum +- [Security Standards](../standards/security/security.spec.md) +- [User Token Authentication ADR](../../docs/internal/adr/0002-user-token-authentication.md) +- [Credential API OpenAPI Spec](../../components/ambient-api-server/openapi/openapi.credentials.yaml) +- [Control Plane RBAC](../../components/manifests/base/rbac/control-plane-clusterrole.yaml) +- [Operator Session Handler](../../components/operator/internal/handlers/sessions.go) diff --git a/workflows/integrations/mcp-server.workflow.md b/workflows/integrations/mcp-server.workflow.md index 2bb747b0c..83fbc9ace 100644 --- a/workflows/integrations/mcp-server.workflow.md +++ b/workflows/integrations/mcp-server.workflow.md @@ -461,5 +461,5 @@ Lessons learned: - Full per-tool schemas, return shapes, and error tables: `specs/integrations/mcp-server.spec.md` - Annotation key conventions and fleet state protocol: `docs/internal/proposals/agent-fleet-state-schema.md` - Agent visual language (how purple SEND/WAIT blocks map to MCP tools): `docs/internal/proposals/agent-script-visual-language.md` -- Platform data model: `specs/sessions/ambient-model.spec.md` +- Platform data model: `specs/api/ambient-model.spec.md` - Component pipeline and wave pattern: `workflows/sessions/ambient-model.workflow.md` diff --git a/workflows/sessions/ambient-model.workflow.md b/workflows/sessions/ambient-model.workflow.md index e50ad437d..4249cd9d9 100755 --- a/workflows/sessions/ambient-model.workflow.md +++ b/workflows/sessions/ambient-model.workflow.md @@ -64,7 +64,7 @@ Checklist: ### Step 2 โ€” Read the Spec -Read `specs/sessions/ambient-model.spec.md` in full. +Read `specs/api/ambient-model.spec.md` in full. Extract and hold in working memory: @@ -453,7 +453,7 @@ The old Gin/K8s backend (`components/backend/`) is covered by `.claude/context/b | Artifact | Location | Owner | | --------------------- | ---------------------------------------------------- | ----------------- | -| Spec | `specs/sessions/ambient-model.spec.md` | Human / consensus | +| Spec | `specs/api/ambient-model.spec.md` | Human / consensus | | This workflow | `workflows/sessions/ambient-model.workflow.md` | Updated each run | | OpenAPI spec | `components/ambient-api-server/openapi/openapi.yaml` | API wave | | Generated SDK | `components/ambient-sdk/go-sdk/` | SDK wave | From 34657acf4da936f6489d839a061ee37eca3fa685 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 5 May 2026 15:14:20 -0400 Subject: [PATCH 3/5] fix(spec): address PR review feedback on security specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite to Requirement:/Scenario: format with RFC 2119 keywords (SHALL/MUST/SHOULD) - Fix broken GFM table (double pipe in Design Decisions header separator) - Remove implementation details (file paths, function names) from spec - Use "Project" consistently instead of "namespace" for Ambient boundary; add terminology note - Register api/ and security/ domains in specs/index.spec.md - Fix BOOKMARKS.md domain label (sessions -> api) - Remove Draft/Authors/Last Updated metadata header to match other specs - Replace fragile ยงN anchors with descriptive anchor links in model spec cross-refs ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- BOOKMARKS.md | 2 +- specs/api/ambient-model.spec.md | 12 +- specs/index.spec.md | 4 +- specs/security/security.spec.md | 584 +++++++++++++++----------------- 4 files changed, 280 insertions(+), 322 deletions(-) diff --git a/BOOKMARKS.md b/BOOKMARKS.md index cf60a9b9f..30586ad1d 100644 --- a/BOOKMARKS.md +++ b/BOOKMARKS.md @@ -25,7 +25,7 @@ Desired state of the system, organized by capability domain. | Spec | Domain | Purpose | |------|--------|---------| -| [Ambient Data Model](specs/api/ambient-model.spec.md) | sessions | Platform-wide data model: projects, agents, sessions, credentials, RBAC | +| [Ambient Data Model](specs/api/ambient-model.spec.md) | api | Platform-wide data model: projects, agents, sessions, credentials, RBAC | | [Control Plane](specs/control-plane/control-plane.spec.md) | control-plane | CP architecture, runner structure, K8s provisioning | | [Runner](specs/agents/runner.spec.md) | agents | Runner subprocess lifecycle, bridges, gRPC/HTTP endpoints | | [MCP Server](specs/integrations/mcp-server.spec.md) | integrations | MCP tool definitions, sidecar and public endpoint modes | diff --git a/specs/api/ambient-model.spec.md b/specs/api/ambient-model.spec.md index d8a9ae123..80e4c1d57 100755 --- a/specs/api/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -747,7 +747,7 @@ GET /api/ambient/v1/projects/{id}/credentials/{cred_id}/token fetch `token` is accepted on `POST` and `PATCH` but **never returned** by standard read endpoints. `GET .../token` is gated by `credential:token-reader`. See -[Security Spec ยง4](../security/security.spec.md#4-credential-authorization-model) for +[Security Spec โ€” Token Reader Role Grant](../security/security.spec.md#requirement-token-reader-role-grant) for runtime authorization semantics. #### Provider Enum @@ -790,7 +790,7 @@ Effective permissions = union of all applicable bindings (global โˆช project โˆช #### Credential Access โ€” Project-Scoped by Default Credentials belong to a project. All agents in the project share them automatically. -See [Security Spec ยง4](../security/security.spec.md#4-credential-authorization-model) for +See [Security Spec โ€” Project-Scoped Credential Sharing](../security/security.spec.md#requirement-project-scoped-credential-sharing) for sharing model, K8s analogy, and named patterns. ### Built-in Roles @@ -843,7 +843,7 @@ GET /api/ambient/v1/sessions/{id}/role_bindings ``` The `credential:token-reader` role is platform-internal. See -[Security Spec ยง4](../security/security.spec.md#credentialtoken-reader-runtime-grant) for +[Security Spec โ€” Token Reader Role Grant](../security/security.spec.md#requirement-token-reader-role-grant) for grant semantics and CRUD authorization rules. --- @@ -869,7 +869,7 @@ GET /api/ambient/v1/projects/{id}/scheduled-sessions/{sched_id}/runs All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded verbatim to the backend service. See -[Security Spec ยง4](../security/security.spec.md#api-server-proxy-authentication) for +[Security Spec โ€” Proxy Authentication](../security/security.spec.md#requirement-proxy-authentication) for authentication and credential injection behavior. This allows SDK and CLI clients to reach the full backend surface through a single @@ -1087,7 +1087,7 @@ This structure means you can define and compose bespoke agent suites โ€” entire | This document is the spec | A reconciler will compare the spec (this doc) against code status and surface gaps | | `labels` / `annotations` are JSONB, not strings | Enables GIN-indexed key/value queries (`@>` operator) without joins; every row carries its own metadata without a separate EAV table. `labels` = queryable tags; `annotations` = freeform notes. Applied to first-class Kinds: User, Project, Agent, Session. Not applied to Inbox, SessionMessage, Role/RoleBinding. | -Security and credential design decisions (RBAC scoping, write-only tokens, role catalog rationale) are in [Security Spec ยง5](../security/security.spec.md#5-design-decisions). +Security and credential design decisions (RBAC scoping, write-only tokens, role catalog rationale) are in the [Security Spec โ€” Design Decisions](../security/security.spec.md#design-decisions). --- @@ -1136,7 +1136,7 @@ acpctl apply -f credential.yaml ## Design Decisions โ€” Credential -See [Security Spec ยง5](../security/security.spec.md#5-design-decisions) for credential +See the [Security Spec โ€” Design Decisions](../security/security.spec.md#design-decisions) for credential design rationale (storage, rotation, provider serialization, migration). --- diff --git a/specs/index.spec.md b/specs/index.spec.md index a20c3acbf..44be1c90e 100644 --- a/specs/index.spec.md +++ b/specs/index.spec.md @@ -79,9 +79,11 @@ specs/ | Domain | Covers | |--------|--------| -| `sessions/` | Lifecycle, initialization, status, messages, events, data model | +| `api/` | Platform data model: projects, agents, sessions, credentials, RBAC, CLI | +| `sessions/` | Lifecycle, initialization, status, messages, events | | `agents/` | Agent model, runtime registry, prompts, runners | | `control-plane/` | Reconciliation, operator, scheduling | | `frontend/` | UI rendering, session views, markdown, navigation | | `integrations/` | MCP, Gerrit, external services | +| `security/` | Identity boundaries, credential authorization, per-session isolation | | `standards/` | Cross-cutting engineering constraints by component | diff --git a/specs/security/security.spec.md b/specs/security/security.spec.md index 307ce1418..63b09eac5 100644 --- a/specs/security/security.spec.md +++ b/specs/security/security.spec.md @@ -1,25 +1,25 @@ -# Ambient Code Platform: Security Specification +# Security Specification -**Status:** Draft -**Authors:** Platform Team -**Last Updated:** 2026-05-05 - -## Summary +## Purpose The Ambient Code Platform runs agentic AI sessions inside Kubernetes. Each session is a pod that executes an LLM-powered runner, accesses external services (Vertex AI, GitHub, Jira), and stores results via the API server. This specification defines who can do what. Six identity boundaries govern the platform: -an SRE-managed Control Plane that reconciles state across namespaces, per-session +an SRE-managed Control Plane that reconciles state across Projects, per-session ServiceAccounts that isolate runner pods from each other, user SSO tokens that scope -runner authorization to the creating user, per-project LLM credentials (Vertex AI), -per-project integration credentials (GitHub/GitLab/Jira/etc.), and a namespace-scoped +runner authorization to the creating user, per-Project LLM credentials (Vertex AI), +per-Project integration credentials (GitHub/GitLab/Jira/etc.), and a Project-scoped build agent SA for OpenShift CI/CD workflows. -**The critical gap today:** all runner sessions in a namespace share a ServiceAccount with -unscoped Secret access. Any session can read another session's runner tokens. This spec -closes that gap with per-session Roles restricted by `resourceNames`. +**Critical gap:** all runner sessions in a Project share a ServiceAccount with unscoped +Secret access. Any session can read another session's runner tokens. This spec closes +that gap with per-session Roles restricted by `resourceNames`. + +**Terminology:** Each Project is realized as a single Kubernetes namespace. This spec +uses "Project" for the Ambient isolation boundary and "namespace" only when referring +to the Kubernetes primitive directly. ## Accounts and Tokens @@ -29,43 +29,197 @@ closes that gap with per-session Roles restricted by `resourceNames`. | `ambient-control-plane` OIDC token | OAuth2 client_credentials | SRE | API server | Auto-refreshed (30s buffer) | CP authenticates to API server for session/credential CRUD | | `backend-api` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Backend API: manages CRs, mints session tokens, validates user tokens | | `frontend` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Frontend: TokenReview and SubjectAccessReview only | -| `ambient-session-` | K8s ServiceAccount | SRE (created by operator) | Namespace (Role) | Session lifetime | Per-session runner identity; scoped to own secrets and session CR | +| `ambient-session-` | K8s ServiceAccount | SRE (created by operator) | Project (Role) | Session lifetime | Per-session runner identity; scoped to own secrets and session CR | | Runner bot token | K8s TokenRequest | SRE (minted by operator) | Session-specific | Mounted + refreshed by kubelet | Runner authenticates to K8s API and API server for status/credential ops | | Runner AGUI token | UUID | SRE (generated per session) | Session-specific | Session lifetime | Authenticates inbound AG-UI requests to runner pod (bearer validation) | | CP RSA-encrypted session token | RSA + OIDC exchange | SRE | Session-specific | On-demand (per request) | Runner fetches API token from CP `/token` endpoint using encrypted session ID | | User SSO token | OIDC (Red Hat SSO) | User | User's RBAC scope | SSO session TTL | User authenticates to frontend/backend; propagated as `caller_token` to runner | -| `Credential(provider=vertex)` | GCP service account key | User | Project | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per namespace | -| `Credential(provider=github)` | PAT or GitHub App token | User | Project | Until rotated | Git operations; fetched at runtime, written to ephemeral `/tmp/`, cleared per turn | +| `Credential(provider=vertex)` | GCP service account key | User | Project | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per Project | +| `Credential(provider=github)` | PAT or GitHub App token | User | Project | Until rotated | Git operations; fetched at runtime, written to ephemeral storage, cleared per turn | | `Credential(provider=gitlab)` | PAT | User | Project | Until rotated | GitLab repository access | | `Credential(provider=jira)` | API token | User | Project | Until rotated | Jira issue tracking integration | | `Credential(provider=google)` | OAuth2 token | User | Project | Until rotated | Google Workspace integrations | | `Credential(provider=kubeconfig)` | Kubeconfig | User | Project | Until rotated | Cross-cluster Kubernetes operations | -| `ambient-agent` (proposed) | K8s ServiceAccount | SRE | Single namespace (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one namespace | +| `ambient-agent` (proposed) | K8s ServiceAccount | SRE | Single Project (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one Project | + +## Requirements + +### Requirement: Control Plane Identity Isolation + +The Control Plane SA SHALL be the only identity that spans Projects. Runner containers +MUST NOT mount or inherit the CP token. The CP SHALL create per-session SAs with scoped +tokens rather than sharing its own. + +#### Scenario: Runner cannot access CP token + +- GIVEN a runner pod in a Project +- WHEN the pod enumerates available ServiceAccount tokens +- THEN no CP token is present in the pod's filesystem or environment + +#### Scenario: CP reconciles across Projects + +- GIVEN the Control Plane is running +- WHEN a new session is created in any Project +- THEN the CP reconciles the session to Kubernetes resources in that Project's namespace +- AND uses its own cluster-scoped SA for cross-Project operations + +### Requirement: Vertex AI Credential Scoping + +Vertex AI credentials SHALL be scoped per Project. The credential token MUST be +write-only in the API (never returned in GET responses). The runner SHALL fetch +credentials at runtime via authenticated API calls. + +#### Scenario: Credential write-only enforcement + +- GIVEN a user creates a `Credential(provider=vertex)` in their Project +- WHEN another user calls `GET /credentials/{id}` +- THEN the response contains metadata but the `token` field is absent + +#### Scenario: Credential materialization + +- GIVEN a Project has a Vertex credential +- WHEN a runner pod is provisioned in that Project +- THEN the CP resolves the credential and writes the service account key into a K8s Secret +- AND the runner pod mounts this secret for `GOOGLE_APPLICATION_CREDENTIALS` + +#### Scenario: Credential rotation + +- GIVEN a Vertex credential is updated via the API +- WHEN the next session is provisioned in that Project +- THEN the CP re-resolves the credential and writes the updated key + +### Requirement: User Token Propagation + +The runner SHALL operate with the creating user's authorization context. The runner +MUST NOT access resources the creating user cannot access. + +#### Scenario: User SSO token passed to runner + +- GIVEN a user authenticates via SSO and creates a session +- WHEN a human interacts via AG-UI +- THEN their bearer token is passed through as `caller_token` +- AND the runner uses this token for API calls, falling back to the bot token only if expired + +#### Scenario: Cross-user credential access blocked + +- GIVEN user A creates a session +- WHEN user B's token is used to access user A's session credentials +- THEN the backend returns 403 Forbidden + +#### Scenario: Bot token scoped to session + +- GIVEN a runner pod with a bot token +- WHEN the bot token is used for API calls +- THEN access is restricted to the specific session's resources within the Project + +### Requirement: Integration Credential Isolation + +Integration credentials SHALL be Project-scoped. Projects MUST NOT access each other's +credentials. Credential tokens SHALL be write-only in the API. + +#### Scenario: Cross-Project credential access blocked + +- GIVEN Project A has a GitHub credential +- WHEN a runner in Project B attempts to fetch that credential +- THEN the request is denied + +#### Scenario: Runner fetches credential at runtime + +- GIVEN a Project has a GitHub credential +- WHEN a runner pod in that Project requests the credential token +- THEN the token is returned via the restricted endpoint +- AND the runner writes it to ephemeral storage +- AND the credential is cleared after each turn + +#### Scenario: Token fetch restricted to cluster-internal callers -## Overview +- GIVEN a valid credential token request +- WHEN the caller is not cluster-internal +- THEN the request is denied to prevent token exfiltration -This document defines the security boundaries for the Ambient Code Platform. It addresses -identity isolation, credential scoping, and the principle of least privilege across the -control plane, runner sessions, and user-facing integrations. +### Requirement: MCP Credential Lifecycle ---- +MCP server credentials SHALL follow the same Project-scoped isolation as other +integration credentials. The Control Plane SHOULD support dynamic credential updates +without requiring full pod restarts. -## 1. OpenShift Namespace-Scoped Build Agent Service Account +#### Scenario: Sidecar mode credential update -### Purpose +- GIVEN an MCP sidecar running alongside a runner +- WHEN the Project's MCP credentials are updated +- THEN the CP triggers a pod rolling restart with updated environment -A dedicated ServiceAccount for automated build-and-deploy workflows within a single -OpenShift namespace. This SA enables agentic workflows to build container images via -`BuildConfig`, push to the internal registry, and deploy workloads without requiring -cluster-admin privileges. +#### Scenario: Pod mode credential update (proposed) -### Scope +- GIVEN an MCP server running as an independent Pod +- WHEN the Project's MCP credentials are updated +- THEN the CP updates the MCP Pod configuration without affecting the runner -- Bound to a single namespace (e.g., `ambient-sandbox`) -- Cannot access other namespaces, nodes, or cluster-scoped resources -- Cannot create or modify CRDs, ClusterRoles, or ClusterRoleBindings +### Requirement: Per-Session Service Account Isolation -### Permissions +Each session MUST have a ServiceAccount that can only access its own resources. +Sessions MUST NOT be able to read other sessions' runner tokens from K8s Secrets. + +#### Scenario: Session cannot read another session's secrets + +- GIVEN Session A and Session B running in the same Project +- WHEN Session A attempts to read Session B's runner token Secret +- THEN the request is denied by RBAC (`resourceNames` restriction) + +#### Scenario: Per-session Role restricts Secret access + +- GIVEN a new session is created +- WHEN the operator provisions the session SA +- THEN the Role restricts Secret access to `ambient-runner-token-` and shared read-only secrets +- AND AgenticSession access is restricted to `` + +#### Scenario: NetworkPolicy isolates session pods + +- GIVEN a session pod is running +- WHEN another session's pod attempts to connect +- THEN the NetworkPolicy blocks the traffic +- AND only the session's own pods and the Control Plane can communicate + +#### Scenario: Shared secrets mounted read-only + +- GIVEN Project-wide secrets exist (e.g., Vertex credentials) +- WHEN a session pod needs access +- THEN the secrets are mounted as read-only volumes +- AND they are not accessible via the K8s API from the session SA + +### Requirement: Per-Session SA Target State + +Each session SA SHALL be restricted to the following resources: + +| Resource | Allowed Names | Verbs | +|----------|--------------|-------| +| Secrets | `ambient-runner-token-`, shared secrets (read-only mount) | get | +| Pods | Labeled `ambient-code/session=` | get, list, watch | +| AgenticSessions | `` | get, update (status only) | +| SelfSubjectAccessReviews | (any) | create | + +### Requirement: Build Agent SA Scoping (OpenShift) + +The build agent SA SHALL be bound to a single Project. It MUST NOT access other +Projects, nodes, or cluster-scoped resources. It MUST NOT create or modify CRDs, +ClusterRoles, or ClusterRoleBindings. + +#### Scenario: Build agent deploys within Project + +- GIVEN a build agent SA bound to a Project +- WHEN the agent triggers a BuildConfig +- THEN the build runs within that Project's namespace +- AND images are pushed to the internal registry via `system:image-builder` + +#### Scenario: Build agent cannot escalate + +- GIVEN a build agent SA bound to a Project +- WHEN the agent attempts to create a ClusterRole +- THEN the request is denied + +### Requirement: Build Agent Permissions + +The build agent SA SHALL have the following permissions within its Project: | API Group | Resources | Verbs | |-----------|-----------|-------| @@ -80,230 +234,95 @@ cluster-admin privileges. Additionally requires the built-in `system:image-builder` role for internal registry push access. -### Rationale +## Credential Authorization Model -Agentic workflows need to build and deploy without human intervention, but must not -escalate beyond the target namespace. This SA provides the minimal surface area for a -CI/CD agent to operate a full build-test-deploy cycle while remaining invisible to the -rest of the cluster. - ---- - -## 2. Security Boundaries - -### 2.1 SRE Boundary: Control Plane Service Account - -**Identity:** `ambient-control-plane` ServiceAccount (cluster-scoped RBAC) - -**Role:** The single SRE-owned identity that bridges the API server and Kubernetes. The -Control Plane runs as an informer/watcher that: - -- Watches the API server (PostgreSQL-backed) for new/modified session and project resources -- Reconciles desired state to Kubernetes (creates Pods, Services, Secrets, ServiceAccounts - in project namespaces) -- Writes reconciled status back to the API server (phase transitions, runner pod names, - credential resolution results) - -**Current Implementation:** -- SA defined in `components/manifests/base/rbac/control-plane-sa.yaml` -- ClusterRole in `components/manifests/base/rbac/control-plane-clusterrole.yaml` -- Long-lived token via companion Secret (`kubernetes.io/service-account-token`) -- Authenticates to API server via OIDC `client_credentials` flow or static token - -**Security Properties:** -- This SA must never be exposed to user workloads -- Runner containers must not mount or inherit the CP token -- The CP creates per-session SAs with scoped tokens (see 2.5) rather than sharing its own - -### 2.2 User Boundary: Vertex AI Credentials - -**Identity:** Per-project `Credential(provider=vertex)` stored in the API server (PostgreSQL) - -**Role:** Provides Vertex AI / Google Cloud authentication for LLM inference. Each project -can bind its own Vertex credential, allowing different teams to use different GCP projects -or service accounts. - -**Flow:** -1. User creates `Credential(provider=vertex)` in their project via the API -2. On runner pod provisioning, the Control Plane calls `resolveCredentialIDs()` to look up - the project's Vertex credential -3. The CP writes the Vertex service account key into a Kubernetes Secret - (`ambient-vertex`) in the project namespace -4. The runner pod mounts this secret and uses it for `GOOGLE_APPLICATION_CREDENTIALS` - -**Security Properties:** -- Vertex credentials are scoped per project, not shared globally -- Credential tokens are write-only in the API (never returned in GET responses) -- The runner fetches credentials at runtime via authenticated API calls, not baked into - the container image -- Credential rotation requires only updating the `Credential` resource; the CP - re-resolves on next session provisioning - -### 2.3 User Boundary: Red Hat SSO / User Token Propagation - -**Identity:** The authenticated user's own SSO token, propagated into the runner - -**Role:** Ensures the runner operates with the creating user's authorization context, not -an elevated service identity. The runner's API calls (session status updates, credential -fetches, AG-UI event streaming) are scoped to what the user is allowed to access. - -**Flow:** -1. User authenticates via Red Hat SSO (OIDC) -2. The backend mints a per-session K8s ServiceAccount token annotated with the user's - identity (`ambient-code.io/created-by-user-id`) -3. The runner resolves its bot token via the CP token endpoint (OIDC `client_credentials` - exchange, encrypted with CP's RSA public key) -4. When a human interacts via AG-UI, their bearer token is passed through as - `context.caller_token` โ€” the runner uses this token first, falling back to the bot - token only if expired -5. Backend RBAC enforcement (`enforceCredentialRBAC()`) validates the caller is either the - session owner or a bot acting on behalf of the owner - -**Security Properties:** -- Runner cannot access resources the user cannot access -- Cross-user credential access is blocked (403) -- The bot token is scoped to the specific session's namespace and resources -- Token refresh uses RSA-encrypted session ID exchange, not stored credentials - -### 2.4 User Boundary: Integration Credentials - -**Identity:** Per-project `Credential(provider=*)` resources โ€” `github`, `gitlab`, `jira`, -`google`, `kubeconfig`, and future providers - -**Role:** Users bind external integration credentials to their project. The runner fetches -these at runtime to perform git operations, issue tracking, and other integrations. - -**Current Providers (OpenAPI enum):** -- `github` โ€” PAT or GitHub App token for repository access -- `gitlab` โ€” PAT for GitLab repository access -- `jira` โ€” API token for issue tracking -- `google` โ€” OAuth2 token for Google integrations -- `kubeconfig` โ€” Kubernetes config for cross-cluster operations - -**Flow:** -1. User creates `Credential(provider=github, ...)` in their project -2. Runner fetches credential token at runtime: - `GET /api/ambient/v1/projects/{project}/credentials/{id}/token` -3. Token is written to ephemeral files (`/tmp/`) for git credential helper and CLI - wrappers -4. Credentials are cleared from environment after each turn - (`clear_runtime_credentials()`) - -**Security Properties:** -- Credentials are project-scoped โ€” projects cannot access each other's credentials -- RBAC permissions: `credential:token` required for token fetch (separate from - `credential:read`) -- Backend validates caller hostname is cluster-internal (`.svc.cluster.local`, `localhost`) - to prevent token exfiltration -- Credential tokens are write-only in the API (presenter strips `Token` field from GET - responses) - -#### 2.4a Dynamic MCP Credential Watching and Pod Lifecycle - -**Current State:** MCP servers run as sidecars (`ambient-mcp` container) injected by the -Control Plane when provisioning runner pods. Credentials for MCP servers are stored in a -shared Secret (`mcp-server-credentials`) keyed by `serverName:userID`. - -**Target State:** If the Control Plane continuously watches `Credential` resources for -changes, MCP configurations can be dynamically applied without restarting the runner: - -- **Sidecar mode (current):** MCP runs as a sidecar container alongside the runner in the - same Pod. Suitable for lightweight, session-scoped MCP servers. Limited to the pod's - lifecycle โ€” cannot be independently restarted or scaled. -- **Pod mode (proposed):** MCP runs as an independent Pod in the project namespace with its - own lifecycle (readiness probes, independent restarts, resource limits). Required when - MCP servers need: - - Independent scaling or resource allocation - - Persistence across session restarts - - Shared access across multiple sessions in the same project - - Long-running connections (databases, message queues) that survive session recycling - -**Credential Watch Flow:** -1. Control Plane watches `Credential` resources on the API server (via informer or - polling) -2. On credential create/update/delete, CP evaluates which sessions or MCP pods are - affected -3. For sidecar mode: CP triggers a pod rolling restart with updated environment -4. For pod mode: CP creates/updates/deletes MCP Pods with the new credential configuration -5. MCP pods authenticate to external services using the bound credential tokens - -### 2.5 SRE Boundary: Per-Session Service Accounts - -**Problem Statement:** - -Today, all runner sessions within a project namespace share access to each other's -resources. A compromised or misbehaving session can: -- Read other sessions' runner tokens from Kubernetes Secrets -- Access other sessions' mounted credentials -- Interfere with other sessions' pods via the Kubernetes API -- Exfiltrate data from other sessions' PVCs - -This is a significant security gap. The shared access model was an expedient choice during -early development but violates the principle of least privilege. - -**Current State (partially implemented):** - -The operator already creates per-session ServiceAccounts -(`ambient-session-`) with scoped Roles: +This section defines how credentials are authorized at runtime. For credential Kind schemas, +API endpoints, and provider enum definitions, see the +[Ambient Data Model Spec](../api/ambient-model.spec.md). -``` -SA: ambient-session- -Role: get/list/watch/create/update/patch AgenticSessions - create SelfSubjectAccessReviews - get Secrets - get/list/update MLflow Experiments -``` +### Requirement: Project-Scoped Credential Sharing -However, the Role's `get Secrets` permission is not scoped to the session's own secrets. -Any session SA can read any Secret in the namespace, including other sessions' runner -tokens. +Credentials SHALL belong to a Project. All agents in the Project SHALL share the Project's +credentials automatically โ€” no explicit sharing or per-credential RoleBindings needed. At +session start, the resolver SHALL list all credentials in the agent's Project and return +the matching credential for each requested provider. -**Target State:** +This follows the Kubernetes resource model: -Each session must have a ServiceAccount that can only access its own resources: +| Ambient | Kubernetes Analogy | Relationship | +|---------|-------------------|--------------| +| Project | Namespace | Isolation boundary, owns child resources | +| Agent | Deployment | Mutable definition, runs workloads | +| Session | Pod | Ephemeral execution, created from Agent | +| Credential | Secret | Project-scoped secret, available to all workloads in the Project | -| Resource | Allowed Names | Verbs | -|----------|--------------|-------| -| Secrets | `ambient-runner-token-`, `ambient-vertex` (read-only) | get | -| Pods | Labeled `ambient-code/session=` | get, list, watch | -| AgenticSessions | `` | get, update (status only) | -| SelfSubjectAccessReviews | (any) | create | +Named patterns: +- **Project Robot Account** โ€” credential created in a Project; all agents use it automatically. +- **Multi-Project credential** โ€” create the same credential (same PAT) in multiple Projects. Each Project gets its own Credential record. +- **No credential** โ€” Projects without credentials run sessions without provider integrations. + +#### Scenario: All agents share Project credentials + +- GIVEN a Project has a GitHub credential +- WHEN any agent in that Project starts a session +- THEN the runner can fetch the GitHub credential token + +#### Scenario: Cross-Project credential isolation + +- GIVEN Project A and Project B each have credentials +- WHEN an agent in Project A requests credentials +- THEN only Project A's credentials are returned + +### Requirement: Token Reader Role Grant + +The `credential:token-reader` role SHALL be granted to the runner service account by the +platform at session start. It MUST NOT be granted via user-facing `POST /role_bindings`. +It is a platform-internal binding managed by the operator. + +Credential CRUD SHALL be governed by the caller's project-level role โ€” `project:owner` and +`project:editor` can create/update/delete credentials; `project:viewer` can list/read +metadata. + +#### Scenario: Runner can fetch token + +- GIVEN a runner SA with `credential:token-reader` bound at session start +- WHEN the runner calls `GET /projects/{id}/credentials/{cred_id}/token` +- THEN the raw token is returned + +#### Scenario: Human user cannot fetch token + +- GIVEN a human user without `credential:token-reader` +- WHEN they call `GET /projects/{id}/credentials/{cred_id}/token` +- THEN the request is denied with 403 + +### Requirement: Proxy Authentication + +All backend paths not mapped to a native `/api/ambient/v1/...` endpoint SHALL be forwarded +verbatim to the backend service. The API server SHALL authenticate the caller, inject +service credentials, then proxy the request โ€” preserving method, path, query string, body, +and response status. + +Runner-internal endpoints (called by runner pods at runtime): +- `POST /api/projects/{p}/agentic-sessions/{s}/github/token` โ€” get a GitHub token for a session +- `GET /api/projects/{p}/agentic-sessions/{s}/credentials/{provider}` โ€” fetch credential by provider +- `POST /api/projects/{p}/agentic-sessions/{s}/runner/feedback` โ€” runner feedback + +These endpoints MUST validate the caller is cluster-internal to prevent token exfiltration. + +#### Scenario: External caller blocked from runner endpoints + +- GIVEN an external client with a valid token +- WHEN they call a runner-internal endpoint +- THEN the request is denied because the caller is not cluster-internal -**Implementation Requirements:** -1. **Per-session Role:** The operator must generate a Role per session with `resourceNames` - restrictions on Secrets and AgenticSessions -2. **Label-based pod access:** Use label selectors in NetworkPolicies to restrict inter-pod - communication to same-session containers only -3. **Secret naming convention:** All session-scoped secrets must follow the pattern - `ambient-runner-token-` or `-*` to enable `resourceNames` - restrictions -4. **Shared secrets (read-only):** Project-wide secrets (`ambient-vertex`, - `ambient-runner-secrets`) should be mounted as read-only volumes rather than accessed - via the Kubernetes API -5. **NetworkPolicy per session:** Extend the existing `ensureRunnerNetworkPolicy()` to - create per-session policies that restrict ingress/egress to only the session's own pods - and the control plane - -**Migration Path:** -1. Deploy per-session Roles with `resourceNames` restrictions (backward compatible โ€” - existing sessions continue to work with broader access until recreated) -2. Update the operator's `regenerateRunnerToken()` to bind the session SA to the new - scoped Role instead of the namespace-wide Role -3. Audit existing sessions for cross-session secret access (add audit logging for Secret - GET operations with caller identity) -4. Enforce per-session NetworkPolicies in new sessions; backfill existing sessions during - next maintenance window - ---- - -## 3. Security Boundary Summary +## Security Boundary Summary ``` +------------------------------------------------------------------+ | Cluster | | | | +---------------------------+ +-----------------------------+ | -| | ambient-code namespace | | project namespace | | +| | ambient-code (platform) | | Project A | | | | | | | | | | [Control Plane] | | [Session A Pod] | | | | SA: ambient-control-plane| | SA: ambient-session-aaa | | @@ -329,76 +348,18 @@ Each session must have a ServiceAccount that can only access its own resources: **Key Invariants:** 1. No runner session can access another session's secrets or tokens 2. No runner session can operate beyond the user's own authorization scope -3. Integration credentials are project-scoped and fetched at runtime, never baked in -4. The Control Plane SA is the only identity that spans namespaces -5. MCP lifecycle (sidecar vs. pod) is determined by operational requirements, not security - compromise +3. Integration credentials are Project-scoped and fetched at runtime, never baked in +4. The Control Plane SA is the only identity that spans Projects +5. MCP lifecycle (sidecar vs. pod) is determined by operational requirements, not security compromise ---- - -## 4. Credential Authorization Model - -This section defines how credentials are authorized at runtime. For credential Kind schemas, -API endpoints, and provider enum definitions, see -[`specs/api/ambient-model.spec.md`](../api/ambient-model.spec.md). - -### Project-Scoped Sharing - -Credentials belong to a project. All agents in the project share the project's credentials -automatically โ€” no explicit sharing or per-credential RoleBindings needed. At session start, -the resolver lists all credentials in the agent's project and returns the matching credential -for each requested provider. - -This follows the Kubernetes resource model: - -| Ambient | Kubernetes Analogy | Relationship | -|---------|-------------------|--------------| -| Project | Namespace | Isolation boundary, owns child resources | -| Agent | Deployment | Mutable definition, runs workloads | -| Session | Pod | Ephemeral execution, created from Agent | -| Credential | Secret | Project-scoped secret, available to all workloads in the namespace | - -Named patterns: -- **Project Robot Account** โ€” credential created in a project; all agents use it automatically. -- **Multi-project credential** โ€” create the same credential (same PAT) in multiple projects. Each project gets its own Credential record. -- **No credential** โ€” projects without credentials simply run sessions without provider integrations. - -### `credential:token-reader` Runtime Grant - -The `credential:token-reader` role is granted to the runner service account by the platform -at session start. It is never granted via user-facing `POST /role_bindings`. It is a -platform-internal binding managed by the operator. - -Credential CRUD is governed by the caller's project-level role โ€” `project:owner` and -`project:editor` can create/update/delete credentials; `project:viewer` can list/read -metadata. - -### API Server Proxy Authentication - -All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded -verbatim to the backend service. The API server authenticates the caller, injects service -credentials, then proxies the request โ€” preserving method, path, query string, body, and -response status. - -Runner-internal endpoints (called by runner pods at runtime): -- `POST /api/projects/{p}/agentic-sessions/{s}/github/token` โ€” get a GitHub token for a session -- `GET /api/projects/{p}/agentic-sessions/{s}/credentials/{provider}` โ€” fetch credential by provider -- `POST /api/projects/{p}/agentic-sessions/{s}/runner/feedback` โ€” runner feedback - -These are accessible via the API server for SDK/CLI tooling but are not intended for human -interactive use. The API server validates the caller is cluster-internal -(`.svc.cluster.local`, `localhost`) to prevent token exfiltration. - ---- - -## 5. Design Decisions +## Design Decisions | Decision | Rationale | -|----------|-----------|| +|----------|-----------| | Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | -| Credential is project-scoped, like a Kubernetes Secret | Credentials live inside a project. All agents in the project share them automatically. Duplication across projects is intentional and explicit โ€” each project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | +| Credential is Project-scoped, like a Kubernetes Secret | Credentials live inside a Project. All agents in the Project share them automatically. Duplication across Projects is intentional and explicit โ€” each Project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | | Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | -| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via project membership โ€” no dedicated `credential` scope needed. Simpler model with fewer moving parts. | +| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via Project membership โ€” no dedicated `credential` scope needed. Simpler model with fewer moving parts. | | Credential CRUD governed by project roles | `project:owner` and `project:editor` can manage credentials. No separate `credential:owner` / `credential:reader` roles โ€” project roles cover it. | | `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox. | | Union-only permissions | No deny rules โ€” simpler mental model for fleet operators. | @@ -407,15 +368,10 @@ interactive use. The API server validates the caller is cluster-internal | No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | | Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | | No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | -| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each project, not share their personal credentials. Each project gets its own Credential records. | - ---- +| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each Project, not share their personal credentials. Each Project gets its own Credential records. | ## References - [Ambient Data Model Spec](../api/ambient-model.spec.md) โ€” Credential/RBAC schemas, endpoints, provider enum - [Security Standards](../standards/security/security.spec.md) - [User Token Authentication ADR](../../docs/internal/adr/0002-user-token-authentication.md) -- [Credential API OpenAPI Spec](../../components/ambient-api-server/openapi/openapi.credentials.yaml) -- [Control Plane RBAC](../../components/manifests/base/rbac/control-plane-clusterrole.yaml) -- [Operator Session Handler](../../components/operator/internal/handlers/sessions.go) From c37f0c90db82ac715d364c6f1658327dff5b468c Mon Sep 17 00:00:00 2001 From: user Date: Thu, 7 May 2026 12:40:43 -0400 Subject: [PATCH 4/5] spec: make credentials global, add vertex/kubeconfig providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credentials are now global resources bound to Projects via RoleBindings instead of project-scoped with a project_id FK. This eliminates duplication when the same PAT is used across multiple Projects. Adds vertex and kubeconfig to the provider enum. Splits the security spec Accounts and Tokens table into scoped subsections for independent implementation. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- specs/api/ambient-model.spec.md | 120 ++++++++++++++++++-------------- specs/security/security.spec.md | 29 +++++++- 2 files changed, 97 insertions(+), 52 deletions(-) diff --git a/specs/api/ambient-model.spec.md b/specs/api/ambient-model.spec.md index 80e4c1d57..afd6c7070 100755 --- a/specs/api/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -17,7 +17,7 @@ The Ambient API server provides a coordination layer for orchestrating fleets of - **Session** โ€” an ephemeral Kubernetes execution run, created exclusively via agent start. Only one active Session per Agent at a time. - **Message** โ€” a single AG-UI event in the LLM conversation. Append-only; the canonical record of what happened in a session. - **Inbox** โ€” a persistent message queue on an Agent. Messages survive across sessions and are drained into the start context at the next run. -- **Credential** โ€” a project-scoped secret. Stores a Personal Access Token or equivalent for an external provider (GitHub, GitLab, Jira, Google). Consumed by runners at session start. All agents in the project share the project's credentials automatically. +- **Credential** โ€” a global secret. Stores a Personal Access Token or equivalent for an external provider (GitHub, GitLab, Jira, Google, Vertex AI, Kubeconfig). Consumed by runners at session start. Bound to Projects via RoleBindings โ€” a single Credential can be shared across multiple Projects without duplication. - **RoleBinding** โ€” binds a Resource to a Role at a given scope (`global`, `project`, `agent`, `session`). Ownership and access for all Kinds is expressed through RoleBindings. The stable address of an agent is `{project_name}/{agent_name}`. It holds the inbox and links to the active session. @@ -162,21 +162,20 @@ erDiagram string ID PK string user_id FK string role_id FK - string scope "global | project | agent | session" + string scope "global | project | agent | session | credential" string scope_id "empty for global" time created_at time updated_at time deleted_at } - %% โ”€โ”€ Credential (project-scoped PAT/token store) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + %% โ”€โ”€ Credential (global PAT/token store, bound via RoleBindings) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Credential { string ID PK "KSUID" - string project_id FK - string name "human-readable; unique within project" + string name "human-readable; globally unique" string description - string provider "github | gitlab | jira | google" + string provider "github | gitlab | jira | google | vertex | kubeconfig" string token "write-only; stored encrypted" string url "nullable; service instance URL" string email "nullable; required for Jira" @@ -210,7 +209,7 @@ erDiagram Project ||--o{ ProjectSettings : "has" Project ||--o{ Agent : "owns" - Project ||--o{ Credential : "owns" + RoleBinding }o--o| Credential : "grants_access" Project ||--o{ ScheduledSession : "owns" User ||--o{ RoleBinding : "bound_to" @@ -417,17 +416,17 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi | `POST /sessions/{id}/tasks/{task_id}/stop` | `acpctl session tasks stop ` | ๐Ÿ”ฒ planned | | `GET /sessions/{id}/tasks/{task_id}/output` | `acpctl session tasks output ` | ๐Ÿ”ฒ planned | -#### Credentials (Project-Scoped) +#### Credentials (Global) | REST API | `acpctl` Command | Status | |---|---|---| -| `GET /projects/{id}/credentials` | `acpctl credential list` | ๐Ÿ”ฒ planned | -| `GET /projects/{id}/credentials?provider={p}` | `acpctl credential list --provider

` | ๐Ÿ”ฒ planned | -| `POST /projects/{id}/credentials` | `acpctl credential create --name --provider

--token [--url ] [--email ] [--description ]` | ๐Ÿ”ฒ planned | -| `GET /projects/{id}/credentials/{cred_id}` | `acpctl credential get ` | ๐Ÿ”ฒ planned | -| `PATCH /projects/{id}/credentials/{cred_id}` | `acpctl credential update [--token ] [--description ]` | ๐Ÿ”ฒ planned | -| `DELETE /projects/{id}/credentials/{cred_id}` | `acpctl credential delete --confirm` | ๐Ÿ”ฒ planned | -| `GET /projects/{id}/credentials/{cred_id}/token` | `acpctl credential token ` | ๐Ÿ”ฒ planned | +| `GET /credentials` | `acpctl credential list [--provider

]` | ๐Ÿ”ฒ planned | +| `POST /credentials` | `acpctl credential create --name --provider

--token [--url ] [--email ] [--description ]` | ๐Ÿ”ฒ planned | +| `GET /credentials/{cred_id}` | `acpctl credential get ` | ๐Ÿ”ฒ planned | +| `PATCH /credentials/{cred_id}` | `acpctl credential update [--token ] [--description ]` | ๐Ÿ”ฒ planned | +| `DELETE /credentials/{cred_id}` | `acpctl credential delete --confirm` | ๐Ÿ”ฒ planned | +| `GET /credentials/{cred_id}/token` | `acpctl credential token ` | ๐Ÿ”ฒ planned | +| `POST /role_bindings` | `acpctl credential bind --scope project --scope-id ` | ๐Ÿ”ฒ planned | #### RBAC @@ -459,7 +458,7 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi |---|---| | `Project` | `name`, `description`, `prompt`, `labels`, `annotations` | | `Agent` | `name`, `prompt`, `labels`, `annotations`, `inbox` (seed messages) | -| `Credential` | `name`, `description`, `provider`, `token` (env var reference), `url`, `email`, `labels`, `annotations` โ€” created in current project context | +| `Credential` | `name`, `description`, `provider`, `token` (env var reference), `url`, `email`, `labels`, `annotations` โ€” global resource; use `credential bind` to grant project access | `Agent` resources in `.ambient/teams/` files also carry an `inbox` list of seed messages. On apply, any message in the list is posted to the agent's inbox if an identical message (same `from_name` + `body`) does not already exist there. @@ -733,16 +732,19 @@ GET /api/ambient/v1/sessions/{id}/tasks/{task_id}/output get POST /api/ambient/v1/sessions/{id}/tasks/{task_id}/stop stop background task ``` -### Credentials (Project-Scoped) +### Credentials (Global) + +Credentials are global resources. Access to credentials is granted via RoleBindings โ€” bind a +credential to a Project, Agent, or Session scope to make it available to runners in that scope. ``` -GET /api/ambient/v1/projects/{id}/credentials list credentials in this project -GET /api/ambient/v1/projects/{id}/credentials?provider={provider} filter by provider -POST /api/ambient/v1/projects/{id}/credentials create a credential -GET /api/ambient/v1/projects/{id}/credentials/{cred_id} read credential (metadata only; token never returned) -PATCH /api/ambient/v1/projects/{id}/credentials/{cred_id} update credential -DELETE /api/ambient/v1/projects/{id}/credentials/{cred_id} soft delete -GET /api/ambient/v1/projects/{id}/credentials/{cred_id}/token fetch raw token โ€” restricted to credential:token-reader +GET /api/ambient/v1/credentials list credentials (filtered by caller's RoleBindings) +GET /api/ambient/v1/credentials?provider={provider} filter by provider +POST /api/ambient/v1/credentials create a credential +GET /api/ambient/v1/credentials/{cred_id} read credential (metadata only; token never returned) +PATCH /api/ambient/v1/credentials/{cred_id} update credential +DELETE /api/ambient/v1/credentials/{cred_id} soft delete +GET /api/ambient/v1/credentials/{cred_id}/token fetch raw token โ€” restricted to credential:token-reader ``` `token` is accepted on `POST` and `PATCH` but **never returned** by standard read endpoints. @@ -758,6 +760,8 @@ runtime authorization semantics. | `gitlab` | GitLab.com or self-hosted | Personal Access Token | optional; required for self-hosted | โ€” | | `jira` | Jira Cloud (Atlassian) | API Token | required (Atlassian instance URL) | required (used in Basic auth) | | `google` | Google Cloud / Workspace | Service Account JSON serialized to string | โ€” | โ€” | +| `vertex` | Vertex AI (GCP) | GCP service account key | โ€” | โ€” | +| `kubeconfig` | Kubernetes clusters | Kubeconfig file serialized to string | โ€” | โ€” | #### Token Response Shape (Runner) @@ -781,17 +785,21 @@ When a runner fetches a credential, the response payload shape is consistent acr | Scope | Meaning | |---|---| | `global` | Applies across the entire platform | -| `project` | Applies to all resources in a project (Agents, Sessions, Credentials) | +| `project` | Applies to all resources in a project (Agents, Sessions) and Credentials bound to the project | | `agent` | Applies to one Agent and all its sessions | | `session` | Applies to one session run only | +| `credential` | Grants access to a specific Credential (used to bind credentials to projects) | Effective permissions = union of all applicable bindings (global โˆช project โˆช agent โˆช session). No deny rules. -#### Credential Access โ€” Project-Scoped by Default +#### Credential Access โ€” Global with RoleBinding Grants -Credentials belong to a project. All agents in the project share them automatically. +Credentials are global resources. Access is granted via RoleBindings โ€” bind a credential to a +Project, Agent, or Session scope. At session start, the resolver lists all credentials the +caller has access to (via RoleBindings) and returns matching credentials for each requested +provider. A single Credential can be shared across multiple Projects without duplication. See [Security Spec โ€” Project-Scoped Credential Sharing](../security/security.spec.md#requirement-project-scoped-credential-sharing) for -sharing model, K8s analogy, and named patterns. +runtime authorization semantics. ### Built-in Roles @@ -806,7 +814,7 @@ sharing model, K8s analogy, and named patterns. | `agent:editor` | Update prompt and metadata on a specific Agent | | `agent:observer` | Read a specific Agent and its sessions | | `agent:runner` | Minimum viable pod credential: read agent, push messages, send inbox | -| `credential:token-reader` | Fetch the raw token via `GET /projects/{id}/credentials/{cred_id}/token`. Granted only to runner service accounts at session start. Human users do not hold this role. | +| `credential:token-reader` | Fetch the raw token via `GET /credentials/{cred_id}/token`. Granted only to runner service accounts at session start. Human users do not hold this role. | ### Permission Matrix @@ -814,9 +822,9 @@ sharing model, K8s analogy, and named patterns. |---|---|---|---|---|---|---|---| | `platform:admin` | full | full | full | full | full | full | full | | `platform:viewer` | read/list | read/list | read/list | โ€” | read/list | read | read/list | -| `project:owner` | full | full | full | full | full | read | project+agent bindings | -| `project:editor` | read | create/update/ignite | read/list | send/read | create/update/delete | read | โ€” | -| `project:viewer` | read | read/list | read/list | โ€” | read/list | read | โ€” | +| `project:owner` | full | full | full | full | manage bindings | read | project+agent bindings | +| `project:editor` | read | create/update/ignite | read/list | send/read | โ€” | read | โ€” | +| `project:viewer` | read | read/list | read/list | โ€” | โ€” | read | โ€” | | `agent:operator` | โ€” | update/ignite | read/list | send/read | โ€” | โ€” | โ€” | | `agent:editor` | โ€” | update | โ€” | โ€” | โ€” | โ€” | โ€” | | `agent:observer` | โ€” | read | read/list | โ€” | โ€” | โ€” | โ€” | @@ -840,11 +848,13 @@ GET /api/ambient/v1/users/{id}/role_bindings GET /api/ambient/v1/projects/{id}/role_bindings GET /api/ambient/v1/projects/{id}/agents/{agent_id}/role_bindings GET /api/ambient/v1/sessions/{id}/role_bindings +GET /api/ambient/v1/credentials/{cred_id}/role_bindings ``` -The `credential:token-reader` role is platform-internal. See +The `credential:token-reader` role is platform-internal. Credential CRUD is governed by +RoleBindings with `credential` scope. See [Security Spec โ€” Token Reader Role Grant](../security/security.spec.md#requirement-token-reader-role-grant) for -grant semantics and CRUD authorization rules. +grant semantics and runtime authorization rules. --- @@ -951,7 +961,7 @@ Every first-class Kind carries two JSONB columns: | `labels` | Queryable key/value tags. Use for filtering, grouping, and selection. | `{"env": "prod", "team": "platform", "tier": "critical"}` | | `annotations` | Freeform key/value metadata. Use for tooling notes, human remarks, external references. | `{"last-reviewed": "2026-03-21", "jira": "PLAT-123", "owner-slack": "@mturansk"}` | -**Kinds with `labels` + `annotations`:** User, Project, Agent, Session, Credential +**Kinds with `labels` + `annotations`:** User, Project, Agent, Session, Credential (global) **Kinds without:** Inbox (ephemeral message queue), SessionMessage (append-only event stream), Role, RoleBinding (RBAC internals โ€” structured by design) @@ -1086,6 +1096,7 @@ This structure means you can define and compose bespoke agent suites โ€” entire | CLI mirrors API 1-for-1 | Every endpoint has a corresponding command; status tracked explicitly | | This document is the spec | A reconciler will compare the spec (this doc) against code status and surface gaps | | `labels` / `annotations` are JSONB, not strings | Enables GIN-indexed key/value queries (`@>` operator) without joins; every row carries its own metadata without a separate EAV table. `labels` = queryable tags; `annotations` = freeform notes. Applied to first-class Kinds: User, Project, Agent, Session. Not applied to Inbox, SessionMessage, Role/RoleBinding. | +| Credential is global, not project-scoped | Eliminates duplication when the same PAT is used across multiple Projects. Access controlled via RoleBindings with `credential` scope. A single Credential can be shared across Projects without creating copies. | Security and credential design decisions (RBAC scoping, write-only tokens, role catalog rationale) are in the [Security Spec โ€” Design Decisions](../security/security.spec.md#design-decisions). @@ -1103,7 +1114,13 @@ acpctl credential create --name my-gitlab-pat --provider gitlab \ echo "$GITLAB_PAT" | acpctl credential create --name my-gitlab-pat --provider gitlab \ --token @- --url https://gitlab.myco.com -# List credentials +# Bind credential to a project (grants access to all agents in the project) +acpctl credential bind my-gitlab-pat --scope project --scope-id my-project + +# Bind the same credential to another project (no duplication) +acpctl credential bind my-gitlab-pat --scope project --scope-id other-project + +# List credentials (filtered by caller's RoleBindings) acpctl credential list # NAME PROVIDER URL CREATED # my-gitlab-pat gitlab https://gitlab.myco.com 2026-03-31 @@ -1127,15 +1144,21 @@ spec: ``` ```sh -acpctl project my-project acpctl apply -f credential.yaml -# credential/platform-gitlab-pat created (in project my-project) +# credential/platform-gitlab-pat created + +# Then bind to the desired project +acpctl credential bind platform-gitlab-pat --scope project --scope-id my-project ``` --- ## Design Decisions โ€” Credential +Credentials are global resources, not project-scoped. This eliminates duplication when the same +PAT is used across multiple Projects. Access is controlled via RoleBindings โ€” bind a credential +to a project scope to grant access to all agents in that project. + See the [Security Spec โ€” Design Decisions](../security/security.spec.md#design-decisions) for credential design rationale (storage, rotation, provider serialization, migration). @@ -1168,8 +1191,8 @@ _Last updated: 2026-04-28. Use this as the authoritative index โ€” click into co | **Projects โ€” labels/annotations** | โœ… PATCH accepts `labels`/`annotations` | โœ… fields on `Project` type; `ProjectAPI.Update(patch map[string]any)` | โš ๏ธ no dedicated subcommand | | | **RBAC โ€” roles** | โœ… | โœ… `RoleAPI` | โœ… `create role` only; list/get not exposed | | | **RBAC โ€” role bindings** | โœ… | โœ… `RoleBindingAPI` | โœ… `create role-binding` only; list/delete not exposed | | -| **Credentials โ€” CRUD** | ๐Ÿ”ฒ | ๐Ÿ”ฒ | ๐Ÿ”ฒ `credential list/get/create/update/delete` | Project-scoped; not yet implemented | -| **Credentials โ€” token fetch (runner)** | ๐Ÿ”ฒ `GET /projects/{id}/credentials/{cred_id}/token` | ๐Ÿ”ฒ | n/a | Gated by `credential:token-reader`; granted to runner SA by operator | +| **Credentials โ€” CRUD** | ๐Ÿ”ฒ | ๐Ÿ”ฒ | ๐Ÿ”ฒ `credential list/get/create/update/delete` | Global; bound to Projects via RoleBindings; not yet implemented | +| **Credentials โ€” token fetch (runner)** | ๐Ÿ”ฒ `GET /credentials/{cred_id}/token` | ๐Ÿ”ฒ | n/a | Gated by `credential:token-reader`; granted to runner SA by operator | | **ScheduledSessions โ€” CRUD** | โœ… scheduledSessions plugin | โœ… `ScheduledSessionAPI.{List,Get,Create,Update,Delete,GetByName}` | โœ… `scheduled-session list/get/create/update/delete` | | | **ScheduledSessions โ€” lifecycle** | โœ… suspend/resume/trigger/runs handlers | โœ… `ScheduledSessionAPI.{Suspend,Resume,Trigger,Runs}` | โœ… `scheduled-session suspend/resume/trigger/runs` | | | **Generic proxy โ€” project config** | โœ… proxy plugin (`plugins/proxy`); forwards non-`/api/ambient/` paths to `BACKEND_URL` | n/a | ๐Ÿ”ฒ raw HTTP fallback | Permissions, keys, MCP servers, secrets, feature flags | @@ -1177,7 +1200,7 @@ _Last updated: 2026-04-28. Use this as the authoritative index โ€” click into co | **Generic proxy โ€” auth integrations** | โœ… proxy plugin | n/a | n/a | GitHub/GitLab/Google/Jira/Gerrit/CodeRabbit/MCP OAuth flows | | **Generic proxy โ€” cluster/platform** | โœ… proxy plugin | n/a | ๐Ÿ”ฒ `acpctl version`, `acpctl cluster-info` | cluster-info, version, health, LDAP, OOTB workflows | | **Declarative apply** | n/a | uses SDK | โœ… `apply -f`, `apply -k` | Upsert semantics; supports inbox seeding | -| **Declarative apply โ€” Credential kind** | n/a | ๐Ÿ”ฒ | ๐Ÿ”ฒ | Planned; token sourced from env var in YAML | +| **Declarative apply โ€” Credential kind** | n/a | ๐Ÿ”ฒ | ๐Ÿ”ฒ | Planned; global resource; token sourced from env var in YAML | | **Declarative apply โ€” ScheduledSession kind** | n/a | ๐Ÿ”ฒ | ๐Ÿ”ฒ | Planned; schedule and agent reference in YAML | ### Labels/Annotations โ€” SDK Ergonomics Gap @@ -1211,21 +1234,16 @@ All Kinds with `labels`/`annotations` store them as JSON strings in the DB (`*st AGENT_ID=$(acpctl agent list --project-id test-cred-1 -o json | python3 -c "import sys,json; print(json.load(sys.stdin)['items'][0]['id'])") echo "AGENT_ID=$AGENT_ID" - # 3. Credential (apply from file โ€” only working path) + # 3. Credential (global resource) printf 'kind: Credential\nname: github-pat-test\nprovider: github\ntoken: %s\ndescription: test\n' \ "$(cat ~/projects/secrets/github.ambient-pat.token)" > /tmp/cred.yaml acpctl apply -f /tmp/cred.yaml && rm /tmp/cred.yaml - CRED_ID=$(acpctl get credentials -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='github-pat-test'))") - echo "CRED_ID=$CRED_ID" - - # 4. Role binding - ROLE_ID=$(acpctl get roles -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='credential:token-reader'))") - MY_USER=$(acpctl whoami | awk '/^User:/{print $2}') - echo "ROLE_ID=$ROLE_ID MY_USER=$MY_USER" + # 4. Bind credential to project + acpctl credential bind github-pat-test --scope project --scope-id test-cred-1 - acpctl create role-binding --user-id "$MY_USER" --role-id "$ROLE_ID" \ - --scope agent --scope-id "$AGENT_ID" + CRED_ID=$(acpctl credential list -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='github-pat-test'))") + echo "CRED_ID=$CRED_ID" # 5. Start session SESSION_ID=$(acpctl start github-agent --project-id test-cred-1 \ diff --git a/specs/security/security.spec.md b/specs/security/security.spec.md index 63b09eac5..16f3e5af6 100644 --- a/specs/security/security.spec.md +++ b/specs/security/security.spec.md @@ -23,24 +23,51 @@ to the Kubernetes primitive directly. ## Accounts and Tokens +### Control Plane Identities + | Identity | Type | Owner | Scope | Lifetime | Purpose | |----------|------|-------|-------|----------|---------| | `ambient-control-plane` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Long-lived (token Secret) | Watches API server, reconciles sessions/projects to K8s, writes status back | | `ambient-control-plane` OIDC token | OAuth2 client_credentials | SRE | API server | Auto-refreshed (30s buffer) | CP authenticates to API server for session/credential CRUD | + +### Platform Service Identities + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| | `backend-api` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Backend API: manages CRs, mints session tokens, validates user tokens | | `frontend` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Frontend: TokenReview and SubjectAccessReview only | + +### Session Runtime Identities + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| | `ambient-session-` | K8s ServiceAccount | SRE (created by operator) | Project (Role) | Session lifetime | Per-session runner identity; scoped to own secrets and session CR | | Runner bot token | K8s TokenRequest | SRE (minted by operator) | Session-specific | Mounted + refreshed by kubelet | Runner authenticates to K8s API and API server for status/credential ops | | Runner AGUI token | UUID | SRE (generated per session) | Session-specific | Session lifetime | Authenticates inbound AG-UI requests to runner pod (bearer validation) | | CP RSA-encrypted session token | RSA + OIDC exchange | SRE | Session-specific | On-demand (per request) | Runner fetches API token from CP `/token` endpoint using encrypted session ID | + +### User Authentication + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| | User SSO token | OIDC (Red Hat SSO) | User | User's RBAC scope | SSO session TTL | User authenticates to frontend/backend; propagated as `caller_token` to runner | + +### Project Credentials + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| | `Credential(provider=vertex)` | GCP service account key | User | Project | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per Project | | `Credential(provider=github)` | PAT or GitHub App token | User | Project | Until rotated | Git operations; fetched at runtime, written to ephemeral storage, cleared per turn | | `Credential(provider=gitlab)` | PAT | User | Project | Until rotated | GitLab repository access | | `Credential(provider=jira)` | API token | User | Project | Until rotated | Jira issue tracking integration | | `Credential(provider=google)` | OAuth2 token | User | Project | Until rotated | Google Workspace integrations | | `Credential(provider=kubeconfig)` | Kubeconfig | User | Project | Until rotated | Cross-cluster Kubernetes operations | -| `ambient-agent` (proposed) | K8s ServiceAccount | SRE | Single Project (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one Project | + +### Build Agent Identity (Proposed) + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `ambient-agent` | K8s ServiceAccount | SRE | Single Project (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one Project | ## Requirements From f81e5c5bfd11c47b968be1e7c76449708f15b9d0 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 7 May 2026 14:21:22 -0400 Subject: [PATCH 5/5] fix(spec): reconcile security spec with global credential model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add credential:owner and credential:viewer roles for self-service CRUD - Update security spec: credentials are global, bound via RoleBindings - Fix all stale references: endpoint paths, K8s analogy, named patterns, design decisions (5-scope RBAC), key invariants, accounts table scopes - Update cross-reference anchor from project-scoped to credential-access Addresses review feedback from jsell-rh on PR #1514. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- specs/api/ambient-model.spec.md | 6 +- specs/security/security.spec.md | 97 +++++++++++++++++---------------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/specs/api/ambient-model.spec.md b/specs/api/ambient-model.spec.md index afd6c7070..cc37268b7 100755 --- a/specs/api/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -798,7 +798,7 @@ Credentials are global resources. Access is granted via RoleBindings โ€” bind a Project, Agent, or Session scope. At session start, the resolver lists all credentials the caller has access to (via RoleBindings) and returns matching credentials for each requested provider. A single Credential can be shared across multiple Projects without duplication. -See [Security Spec โ€” Project-Scoped Credential Sharing](../security/security.spec.md#requirement-project-scoped-credential-sharing) for +See [Security Spec โ€” Credential Access via RoleBindings](../security/security.spec.md#requirement-credential-access-via-rolebindings) for runtime authorization semantics. ### Built-in Roles @@ -814,6 +814,8 @@ runtime authorization semantics. | `agent:editor` | Update prompt and metadata on a specific Agent | | `agent:observer` | Read a specific Agent and its sessions | | `agent:runner` | Minimum viable pod credential: read agent, push messages, send inbox | +| `credential:owner` | Full CRUD on credentials the user created. Bind credentials to projects the user has `project:owner` on. | +| `credential:viewer` | Read metadata (not token) on credentials bound to projects the user has access to. | | `credential:token-reader` | Fetch the raw token via `GET /credentials/{cred_id}/token`. Granted only to runner service accounts at session start. Human users do not hold this role. | ### Permission Matrix @@ -829,6 +831,8 @@ runtime authorization semantics. | `agent:editor` | โ€” | update | โ€” | โ€” | โ€” | โ€” | โ€” | | `agent:observer` | โ€” | read | read/list | โ€” | โ€” | โ€” | โ€” | | `agent:runner` | โ€” | read | read | send | โ€” | โ€” | โ€” | +| `credential:owner` | โ€” | โ€” | โ€” | โ€” | create/update/delete + bind | โ€” | โ€” | +| `credential:viewer` | โ€” | โ€” | โ€” | โ€” | read/list (metadata only) | โ€” | โ€” | | `credential:token-reader` | โ€” | โ€” | โ€” | โ€” | token: read | โ€” | โ€” | ### RBAC Endpoints diff --git a/specs/security/security.spec.md b/specs/security/security.spec.md index 16f3e5af6..2e908a13b 100644 --- a/specs/security/security.spec.md +++ b/specs/security/security.spec.md @@ -9,9 +9,9 @@ Jira), and stores results via the API server. This specification defines who can do what. Six identity boundaries govern the platform: an SRE-managed Control Plane that reconciles state across Projects, per-session ServiceAccounts that isolate runner pods from each other, user SSO tokens that scope -runner authorization to the creating user, per-Project LLM credentials (Vertex AI), -per-Project integration credentials (GitHub/GitLab/Jira/etc.), and a Project-scoped -build agent SA for OpenShift CI/CD workflows. +runner authorization to the creating user, global credentials bound to Projects via +RoleBindings (Vertex AI, GitHub/GitLab/Jira/etc.), and a Project-scoped build agent +SA for OpenShift CI/CD workflows. **Critical gap:** all runner sessions in a Project share a ServiceAccount with unscoped Secret access. Any session can read another session's runner tokens. This spec closes @@ -52,16 +52,16 @@ to the Kubernetes primitive directly. |----------|------|-------|-------|----------|---------| | User SSO token | OIDC (Red Hat SSO) | User | User's RBAC scope | SSO session TTL | User authenticates to frontend/backend; propagated as `caller_token` to runner | -### Project Credentials +### Credentials (Global, Bound via RoleBindings) | Identity | Type | Owner | Scope | Lifetime | Purpose | |----------|------|-------|-------|----------|---------| -| `Credential(provider=vertex)` | GCP service account key | User | Project | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per Project | -| `Credential(provider=github)` | PAT or GitHub App token | User | Project | Until rotated | Git operations; fetched at runtime, written to ephemeral storage, cleared per turn | -| `Credential(provider=gitlab)` | PAT | User | Project | Until rotated | GitLab repository access | -| `Credential(provider=jira)` | API token | User | Project | Until rotated | Jira issue tracking integration | -| `Credential(provider=google)` | OAuth2 token | User | Project | Until rotated | Google Workspace integrations | -| `Credential(provider=kubeconfig)` | Kubeconfig | User | Project | Until rotated | Cross-cluster Kubernetes operations | +| `Credential(provider=vertex)` | GCP service account key | User | Global (bound to Projects via RoleBindings) | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per Project | +| `Credential(provider=github)` | PAT or GitHub App token | User | Global (bound to Projects via RoleBindings) | Until rotated | Git operations; fetched at runtime, written to ephemeral storage, cleared per turn | +| `Credential(provider=gitlab)` | PAT | User | Global (bound to Projects via RoleBindings) | Until rotated | GitLab repository access | +| `Credential(provider=jira)` | API token | User | Global (bound to Projects via RoleBindings) | Until rotated | Jira issue tracking integration | +| `Credential(provider=google)` | OAuth2 token | User | Global (bound to Projects via RoleBindings) | Until rotated | Google Workspace integrations | +| `Credential(provider=kubeconfig)` | Kubeconfig | User | Global (bound to Projects via RoleBindings) | Until rotated | Cross-cluster Kubernetes operations | ### Build Agent Identity (Proposed) @@ -92,13 +92,13 @@ tokens rather than sharing its own. ### Requirement: Vertex AI Credential Scoping -Vertex AI credentials SHALL be scoped per Project. The credential token MUST be -write-only in the API (never returned in GET responses). The runner SHALL fetch -credentials at runtime via authenticated API calls. +Vertex AI credentials SHALL be global resources bound to Projects via RoleBindings. +The credential token MUST be write-only in the API (never returned in GET responses). +The runner SHALL fetch credentials at runtime via authenticated API calls. #### Scenario: Credential write-only enforcement -- GIVEN a user creates a `Credential(provider=vertex)` in their Project +- GIVEN a user creates a `Credential(provider=vertex)` and binds it to a Project - WHEN another user calls `GET /credentials/{id}` - THEN the response contains metadata but the `token` field is absent @@ -141,18 +141,19 @@ MUST NOT access resources the creating user cannot access. ### Requirement: Integration Credential Isolation -Integration credentials SHALL be Project-scoped. Projects MUST NOT access each other's -credentials. Credential tokens SHALL be write-only in the API. +Integration credentials SHALL be global resources. Access SHALL be controlled via +RoleBindings โ€” a credential is only accessible to runners in Projects it has been +bound to. Credential tokens SHALL be write-only in the API. -#### Scenario: Cross-Project credential access blocked +#### Scenario: Unbound credential access blocked -- GIVEN Project A has a GitHub credential +- GIVEN a GitHub credential exists but is not bound to Project B - WHEN a runner in Project B attempts to fetch that credential - THEN the request is denied #### Scenario: Runner fetches credential at runtime -- GIVEN a Project has a GitHub credential +- GIVEN a GitHub credential is bound to a Project - WHEN a runner pod in that Project requests the credential token - THEN the token is returned via the restricted endpoint - AND the runner writes it to ephemeral storage @@ -166,7 +167,7 @@ credentials. Credential tokens SHALL be write-only in the API. ### Requirement: MCP Credential Lifecycle -MCP server credentials SHALL follow the same Project-scoped isolation as other +MCP server credentials SHALL follow the same RoleBinding-scoped access model as other integration credentials. The Control Plane SHOULD support dynamic credential updates without requiring full pod restarts. @@ -267,38 +268,39 @@ This section defines how credentials are authorized at runtime. For credential K API endpoints, and provider enum definitions, see the [Ambient Data Model Spec](../api/ambient-model.spec.md). -### Requirement: Project-Scoped Credential Sharing +### Requirement: Credential Access via RoleBindings -Credentials SHALL belong to a Project. All agents in the Project SHALL share the Project's -credentials automatically โ€” no explicit sharing or per-credential RoleBindings needed. At -session start, the resolver SHALL list all credentials in the agent's Project and return -the matching credential for each requested provider. +Credentials SHALL be global resources. Access SHALL be granted via RoleBindings with +`credential` scope โ€” bind a credential to a Project to make it available to all agents in +that Project. At session start, the resolver SHALL list all credentials the session's +Project has access to (via RoleBindings) and return the matching credential for each +requested provider. This follows the Kubernetes resource model: | Ambient | Kubernetes Analogy | Relationship | |---------|-------------------|--------------| -| Project | Namespace | Isolation boundary, owns child resources | +| Project | Namespace | Isolation boundary | | Agent | Deployment | Mutable definition, runs workloads | | Session | Pod | Ephemeral execution, created from Agent | -| Credential | Secret | Project-scoped secret, available to all workloads in the Project | +| Credential | Secret (cross-namespace) | Global resource, bound to Projects via RoleBindings | Named patterns: -- **Project Robot Account** โ€” credential created in a Project; all agents use it automatically. -- **Multi-Project credential** โ€” create the same credential (same PAT) in multiple Projects. Each Project gets its own Credential record. -- **No credential** โ€” Projects without credentials run sessions without provider integrations. +- **Project Robot Account** โ€” credential created globally and bound to a Project; all agents in the Project use it automatically. +- **Multi-Project credential** โ€” bind the same credential to multiple Projects via separate RoleBindings. No duplication of the Credential record. +- **No credential** โ€” Projects without credential bindings run sessions without provider integrations. -#### Scenario: All agents share Project credentials +#### Scenario: All agents access bound credentials -- GIVEN a Project has a GitHub credential +- GIVEN a GitHub credential is bound to a Project via RoleBinding - WHEN any agent in that Project starts a session - THEN the runner can fetch the GitHub credential token -#### Scenario: Cross-Project credential isolation +#### Scenario: Unbound credential not accessible -- GIVEN Project A and Project B each have credentials -- WHEN an agent in Project A requests credentials -- THEN only Project A's credentials are returned +- GIVEN Project A and Project B exist, and a credential is bound only to Project A +- WHEN an agent in Project B requests credentials +- THEN only credentials bound to Project B are returned ### Requirement: Token Reader Role Grant @@ -306,20 +308,21 @@ The `credential:token-reader` role SHALL be granted to the runner service accoun platform at session start. It MUST NOT be granted via user-facing `POST /role_bindings`. It is a platform-internal binding managed by the operator. -Credential CRUD SHALL be governed by the caller's project-level role โ€” `project:owner` and -`project:editor` can create/update/delete credentials; `project:viewer` can list/read -metadata. +Credential CRUD SHALL be governed by the `credential:owner` role. Users with +`credential:owner` can create, update, and delete credentials they own and bind them +to Projects where they hold `project:owner`. Users with `credential:viewer` can read +metadata (not tokens) on credentials bound to Projects they have access to. #### Scenario: Runner can fetch token - GIVEN a runner SA with `credential:token-reader` bound at session start -- WHEN the runner calls `GET /projects/{id}/credentials/{cred_id}/token` +- WHEN the runner calls `GET /credentials/{cred_id}/token` - THEN the raw token is returned #### Scenario: Human user cannot fetch token - GIVEN a human user without `credential:token-reader` -- WHEN they call `GET /projects/{id}/credentials/{cred_id}/token` +- WHEN they call `GET /credentials/{cred_id}/token` - THEN the request is denied with 403 ### Requirement: Proxy Authentication @@ -356,7 +359,7 @@ These endpoints MUST validate the caller is cluster-internal to prevent token ex | | - watches API server | | - own secrets only | | | | - reconciles to K8s | | - own session CR only | | | | - writes status back | | - user's SSO token | | -| | | | - project vertex cred | | +| | | | - bound vertex cred | | | | [API Server] | | +------------------+ | | | | SA: (pod identity) | | | MCP sidecar/pod | | | | | - PostgreSQL backend | | | - integration | | | @@ -375,7 +378,7 @@ These endpoints MUST validate the caller is cluster-internal to prevent token ex **Key Invariants:** 1. No runner session can access another session's secrets or tokens 2. No runner session can operate beyond the user's own authorization scope -3. Integration credentials are Project-scoped and fetched at runtime, never baked in +3. Integration credentials are global, bound to Projects via RoleBindings, and fetched at runtime, never baked in 4. The Control Plane SA is the only identity that spans Projects 5. MCP lifecycle (sidecar vs. pod) is determined by operational requirements, not security compromise @@ -384,10 +387,10 @@ These endpoints MUST validate the caller is cluster-internal to prevent token ex | Decision | Rationale | |----------|-----------| | Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | -| Credential is Project-scoped, like a Kubernetes Secret | Credentials live inside a Project. All agents in the Project share them automatically. Duplication across Projects is intentional and explicit โ€” each Project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | +| Credential is global, bound via RoleBindings | Credentials are global resources. Access is granted by binding a credential to a Project via a RoleBinding with `credential` scope. A single credential can be shared across multiple Projects without duplication. | | Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | -| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via Project membership โ€” no dedicated `credential` scope needed. Simpler model with fewer moving parts. | -| Credential CRUD governed by project roles | `project:owner` and `project:editor` can manage credentials. No separate `credential:owner` / `credential:reader` roles โ€” project roles cover it. | +| Five-scope RBAC (`global`, `project`, `agent`, `session`, `credential`) | Credential access is explicit via RoleBindings with `credential` scope. Enables cross-project sharing without credential duplication. | +| Credential CRUD governed by credential roles | `credential:owner` manages CRUD and bindings. `credential:viewer` reads metadata. Self-service: users create their own credentials without admin intervention. | | `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox. | | Union-only permissions | No deny rules โ€” simpler mental model for fleet operators. | | Token stored in database, encrypted at rest | Single authoritative store. A future Vault integration can be adopted by pointing the DB row at a Vault path without changing the API surface. | @@ -395,7 +398,7 @@ These endpoints MUST validate the caller is cluster-internal to prevent token ex | No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | | Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | | No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | -| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each Project, not share their personal credentials. Each Project gets its own Credential records. | +| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs, not share their personal credentials. A single credential can be bound to multiple Projects. | ## References