OpenClaw EnterpriseDOCSGitHub

Service API Keys Flow

Overview

An Installation administrator, authenticated by human session or service API key, issues a Better Auth key for an existing non-Agent IAM ServicePrincipal. Automation uses that key to request an exact resource, and an authorized administrator can later revoke it. This flow follows a Namespace reader from issuance through GET /namespaces/:namespaceId to revocation. It stops at the OCC resource response or the credential's deletion and audit result; provisioning IAM identities and Agent workload credentials remain separate lifecycles. Fresh native-IAM bootstrap provisions the initial service administrator and calls the same key helper, delivering its response to protected storage rather than an HTTP issuance response. That separate entry and commit boundary is traced in the bootstrap flow.

Better Auth owns key material and persistence. The selected IAM Driver owns identity lookup and authorization. A key fixes the identity's Installation and optional Namespace at issuance, but it does not snapshot or grant permissions.

Entry Points

The controller already has configured Better Auth storage and its selected IAM Driver. Native IAM requires an explicitly provisioned ServicePrincipal, Role, and AccessBinding; issuance creates none of them. Request fields and lifetime limits are defined in the authentication reference and API reference.

Flow

graph TD
  subgraph Issue["Administrator issues a key"]
    A["POST service-keys with session or Installation key"] --> B["requireInstallationAdmin and IAM lookup"]
    B -->|Unauthorized or invalid principal| C["Reject issuance"]
    B -->|Exact non-Agent ServicePrincipal| D["createServiceKey persists hashed key and scope"]
    D --> E["Append issuance audit"]
    E -->|Success| F["Return plaintext key once"]
    E -->|Failure| G["Attempt deletion and return 503"]
  end
  subgraph Request["Automation requests an exact Namespace"]
    F --> H["GET Namespace with x-api-key"]
    H --> I["ControllerAdmissionVerifier verifies stored key"]
    I -->|Invalid, expired, or deleted| J["Return 401 without cookie fallback"]
    I -->|Valid| K["resolveIdentity checks current IAM identity and fixed scope"]
    K -->|Scope matches| L["getNamespace asks IAM to authorize exact read"]
    K -->|Missing identity or scope mismatch| M["Audit denial and return 403"]
    L -->|Denied by current policy| M
    L -->|Allowed| N["Return Namespace resource"]
  end
  subgraph Revoke["Administrator revokes the key"]
    N --> O["DELETE service-keys with session or Installation key"]
    O --> P["revokeServiceKey deletes the Better Auth record"]
    P --> Q["Append revocation audit and return revoked ID"]
    P -->|Subsequent key use| J
  end

Execution Trace

1. Authorize the issuer and resolve the target principal

apps/controller/src/index.ts:requireInstallationAdmin, called after admit and resolveIdentity in createFastifyApp.

The issuance route admits a human session or Installation-scoped service key, resolves its current IAM Principal or non-Agent ServicePrincipal, then requires administer on the server-owned Installation. Invalid credentials return 401 without falling back to an accompanying cookie. A valid caller without the exact authority, or a Namespace-scoped key, receives 403. Account creation and bootstrap retain their human-session boundary.

The handler asks the selected IAM Driver for the requested ServicePrincipal and checks that its ID and optional Namespace match exactly. Unknown identities, human Principals, Agent-owned ServicePrincipals, and mismatched scope return 400. Successful lookup grants no new permission and passes the existing principal to Better Auth.

2. Persist the key, freeze its scope, and audit before disclosure

apps/controller/src/auth/index.ts:createServiceKey, configured by createControllerAuth; packages/occ/src/state/postgres-schema.ts:apikey.

The server calls Better Auth's supported createApiKey API. The plugin hashes the key and stores the principal reference with the Installation and optional Namespace metadata. These metadata values are the credential's fixed scope; changing IAM scope later does not widen an existing credential. PostgreSQL uses the official Drizzle adapter and the occ.apikey table. Key-based sessions and public plugin management routes are not enabled.

The controller appends an issuance audit containing the resolved issuer and non-secret key/principal IDs, then returns 201 with the plaintext key once. There is no plaintext retrieval endpoint. If creation or audit persistence fails, the response is 503 with no credential. If creation already succeeded, the controller attempts deletion of the unreturned key; this cleanup is best effort, not an atomic transaction with the audit sink.

3. Verify the credential and enforce its fixed identity scope

apps/controller/src/auth/index.ts:ControllerAdmissionVerifier.verify; apps/controller/src/index.ts:resolveIdentity.

For the resource request, an explicitly supplied x-api-key selects Better Auth's verifyApiKey before cookie handling. Blank, forged, expired, or revoked keys return 401 without falling back to a session. A mismatched Installation or malformed stored key also fails authentication. Without the key header, the existing session path remains unchanged; bearer authentication remains unsupported.

Successful verification supplies the service-principal ID and stored scope to resolveIdentity. The selected IAM Driver looks up the current identity by ID. The controller rejects a missing identity, Agent ownership, changed scope, or another requested Namespace with 403. A Namespace key cannot enter an Installation-level resource operation. Better Auth neither creates a human session nor authorizes this request.

4. Authorize the exact read with current IAM policy

apps/controller/src/index.ts:perform; packages/occ/src/index.ts:OpenClawController.getNamespace; packages/iam/src/index.ts:NativeIAMDriver.authorize.

perform passes the resolved service-principal ID and requested Namespace to OCC. Before reading the resource, getNamespace requires read on that exact Namespace through the selected IAM Driver. Native IAM loads current policy separately for identity lookup and authorization. Removed bindings or Roles, and matching Restrictions, affect subsequent decisions without key reissuance. Installation-scoped keys still need their own explicit grants; the issuer's permissions are never inherited.

A denied decision records authorization evidence and returns 403. Unavailable required dependencies fail closed with 503. An allowed read returns the Namespace response through the existing OCC API envelope. Other resource operations retain their own OCC authorization and mutation-audit behavior; key admission does not bypass them.

5. Delete the credential and record revocation

apps/controller/src/index.ts:requireInstallationAdmin, used by the service-key DELETE handler; apps/controller/src/auth/index.ts:revokeServiceKey, called after getServiceKey.

The administrator repeats credential admission and current IAM Installation-authority checks. Either a human session or an Installation-scoped service key can authorize this request. The handler looks up the non-secret key ID in this Installation; a missing or already removed key returns 404. It deletes the Better Auth record through the adapter, appends the revocation audit, and returns the ID with revoked: true. The IAM principal and its policy remain unchanged.

Deletion prevents ordinary concurrent verification updates from recreating the row. Subsequent verification fails across controller instances, although a request already authorized may finish. If audit persistence fails after deletion, the response is 503 and the key stays deleted. Rotation therefore uses the same lifecycle: issue a replacement, switch the client, then revoke the old key. An authorized service administrator can perform these calls for itself or another eligible principal in the same Installation; OCC does not schedule rotation.

Debugging and Verification

These commands describe the proof hooks, not a new runtime execution record.

Search documentation