OpenClaw EnterpriseDOCSGitHub

IAM overview

Identity and access management (IAM) controls who can read, create, change, or operate OpenClaw Enterprise resources. Signing in identifies a caller; it does not grant access. OpenClaw Control Plane (OCC) checks each action against the caller’s current permissions and the exact Installation, Namespace, or resource. Without a matching grant, OCC denies the request. A matching Restriction also overrides a grant.

Choose an identity

Service accounts are a separate feature: Agents can use them for upstream credentials, such as a Backend-issued model credential. A service account is not an IAM ServicePrincipal.

Grant and check access

Permissions are assigned through Roles and AccessBindings. Installation administrators create human accounts through the HTTP API only, and manage Namespace Roles and AccessBindings through the HTTP API or CLI. Apart from the Groups that the Installation configuration declares, Groups and Restrictions remain managed by the selected IAM authority. See Authorization for available actions, scope, and how denials work.

If a request returns 401, check the session or service key. A 403 means the current identity, scope, grant, or a Restriction did not permit the operation. Have an administrator check the exact resource and action; access to a Namespace does not give access to every resource in it.

Add a person

New accounts join configured default Groups. As a human Installation administrator:

  1. Sign in so OCC_SESSION_COOKIE_JAR holds your session, and set OCC_ORIGIN to the console origin. Under recovery-only password sign-in, only the recovery account signs in this way.

  2. Create the account from a private file with a generated password, and keep the returned id and principalId:

    bash
    umask 077printf '{"email":"%s","password":"%s"}' 'person@example.com' \  "$(openssl rand -base64 24)" > account.jsoncurl --fail-with-body --silent --show-error \  --cookie "$OCC_SESSION_COOKIE_JAR" -H "Origin: $OCC_ORIGIN" \  -H 'Content-Type: application/json' --data-binary @account.json \  "$OCC_URL/api/auth/accounts" > created.jsonUSER_ID="$(jq -r .data.id created.json)"PRINCIPAL_ID="$(jq -r .data.principalId created.json)"

    If creation returns 503 with an unknown outcome, confirm whether the original request committed before sending another request or changing grants. See account provisioning.

    No API lists accounts. With Google or OIDC sign-in enabled, GET /api/auth/accounts/$USER_ID returns principalId again; a signed-in person finds their USER_ID as user.id in GET /api/auth/session.

  3. If the person signs in only through Google or OIDC (always, under recovery-only), explicitly attach their identity to USER_ID: see Google or OIDC. Then delete account.json; never hand the password over.

  4. Grant access in each Namespace the person needs. With OCC_NAMESPACE set, create a Role, then bind it to the Namespace for discovery and to each exact resource, such as an Agent:

    bash
    echo '{"name":"Read Namespace and Agent","permissions":[{"action":"read","resourceKind":"namespace"},{"action":"read","resourceKind":"agent"}]}' > role.jsonROLE_ID="$(occ iam role create --file role.json -o json | jq -r .id)"for target in "namespace:$OCC_NAMESPACE" "agent:<agent-id>"; do  jq -n --arg p "$PRINCIPAL_ID" --arg r "$ROLE_ID" \    --arg k "${target%%:*}" --arg i "${target#*:}" \    '{subjectKind:"identity",subjectId:$p,roleId:$r,resourceKind:$k,resourceId:$i}' > binding.json  occ iam access-binding create --file binding.jsondone

    occ has no human sign-in: it uses the service key in OCC_SERVICE_KEY_FILE (see service API keys), so these grants run as, and are audited to, that key's ServicePrincipal. To record them under your own account, POST the same JSON to $OCC_URL/namespaces/$OCC_NAMESPACE/iam/roles and .../iam/access-bindings with your session cookie and Origin: $OCC_ORIGIN; read the Role ID from .data.id.

  5. For a person who signs in with a password, give them the password from account.json through your own secure channel, then delete the file.

Manual account creation requires a password, so a manually created SSO-only account keeps one that nobody uses. It is still a credential:

Add actions such as update or deploy to the Role for more access; see Authorization for actions and scope. Role creation refuses create Permissions, and a binding refuses a Role with none for its target's kind, because those grants could never apply.

Use Namespace bindings for everyone who is not an Installation administrator.

Appoint an administrator

Pass the Installation administrator roleId in the account creation body (step 2 of Add a person). The binding is scoped to the exact Installation, while the bootstrap administrator's binding is unscoped, so the appointed account holds only installation:administer and installation:read:

Appoint one only for account and service-key administration. The draft RFC in PR #889 proposes a change to this behavior.

Let people create Agents

Declare the Default users Roles, Group, and bindings in the Installation configuration to let people create Agents in the shared default Namespace without other grants. Each account that an administrator creates afterwards joins the Group. The person owns each Agent, Configuration, Secret, and credential source they create, and deploys their Agents themselves. Other members cannot see or change those resources. Default users and configured Roles defines each field and its limits.

  1. Add the iam block from the reference example to the Installation startup configuration of both the API and the worker. Omit the namespace_creator binding unless members can also create and own Namespaces. OCC sets no limit on how many Namespaces or Agents a member creates.
  2. Restart the API and the worker. At startup, OCC writes the Roles, the Group, and the bindings. A configuration error stops startup and names the field.
  3. Create each person's account (steps 1–3 of Add a person). The account joins default-user as it is created. Accounts that already exist do not join; there is no API, CLI, or console support to add them.

The person can now create Agents in default. To stop all members from creating more, remove the binding from the configuration and restart. To stop one person, disable their account. What they created stays, and their Agents keep running. To end their access to one resource, delete it, or delete its Owner binding binding_owner_<resource-id> with occ iam access-binding delete in that Namespace.

Remove a person

Accounts cannot be deleted; disable the account instead. As a human Installation administrator with your session in OCC_SESSION_COOKIE_JAR (step 1 of Add a person), set USER_ID to the account's id, kept from its creation. Without it, read it from the database with your approved access: SELECT id FROM occ."user" WHERE email = 'person@example.com'. Read the account version, then disable:

bash
VERSION="$(curl --fail-with-body --silent --show-error \  --cookie "$OCC_SESSION_COOKIE_JAR" -H "Origin: $OCC_ORIGIN" \  "$OCC_URL/api/auth/accounts/$USER_ID" | jq -er .data.version)" &&jq -n --argjson version "$VERSION" '{expectedVersion: $version}' |  curl --fail-with-body --silent --show-error \    --cookie "$OCC_SESSION_COOKIE_JAR" -H "Origin: $OCC_ORIGIN" \    -H 'Content-Type: application/json' --data-binary @- \    "$OCC_URL/api/auth/accounts/$USER_ID/disable"

The response acknowledges data.userId. Repeat the GET above to verify data.disabled: true and read the new data.version. Every session of the account ends at once, and password and Google or OIDC sign-in are refused; an external sign-in tells the person the account is disabled. IAM grants stay, unused. The recovery account cannot be disabled, and account controls need external sign-in (on a password-only Installation they return 409). POST .../enable reverses disablement; use the freshly read version as expectedVersion in its body.

Disabling does not end service keys. Keys belong to ServicePrincipals, and OCE does not record whom a key was issued for, so find a person's keys by the name given at issuance (name them after the person, as in CLI access). Issuance and revocation are audited with the key's ID and name; this query lists keys issued with a name prefix and not yet revoked:

sql
SELECT issued.occurred_at, issued.details ->> 'serviceKeyId' AS key_id,       issued.details ->> 'serviceKeyName' AS name,       issued.details ->> 'servicePrincipalId' AS service_principal_idFROM occ.audit_events issuedWHERE issued.action = 'openclaw.auth.service-keys.create'  AND issued.outcome = 'success'  AND issued.details ->> 'serviceKeyName' LIKE 'nora-%'  AND NOT EXISTS (    SELECT 1 FROM occ.audit_events revoked    WHERE revoked.action = 'openclaw.auth.service-keys.revoke'      AND revoked.outcome = 'success'      AND revoked.details ->> 'serviceKeyId' = issued.details ->> 'serviceKeyId')ORDER BY issued.occurred_at;

Revoke each with occ service-key revoke <key-id> or the HTTP request. Keys past their expiry are already refused. If a ServicePrincipal served only this person, also delete its AccessBindings in each Namespace: find them with occ iam access-binding list and remove each with occ iam access-binding delete <id>. Finally, disable the person at their identity provider as well.

Let a person run an existing Agent

People cannot receive Namespace-wide create, so an Installation administrator creates each Agent with its Configuration and Secrets. To let a team member edit, deploy, stop and inspect it, keep the Namespace read binding from Add a person, create one Role with all of these permissions, and bind it to each target:

Target Permissions
The Agent read, update, deploy, operate, read_logs, delete
Its Configuration read, update
Each Secret or credential source it binds read, operate
Each Preset the Configuration uses read
Each AgentRevision read
Its SandboxPolicy read

A binding applies only the Role's permissions for its target's kind. Deploying needs operate on each Secret and credential source the Agent binds, also for a person without update. Omit delete to keep the Agent's removal with administrators. Bind each new Agent revision after it is deployed, or the person cannot see its status; see Agent revisions. Secret reads return metadata, never values. The SandboxPolicy ID is sbp_ followed by the UUID part of the Agent ID. occ agent policy get and occ agent policy recommendations need its read.

The console has no form for these grants. Share Agent on the Agent's Access tab grants full native administration of the Agent, not this set, so use the CLI or HTTP API as in Add a person.

The person still cannot create Agents, Secrets or Configurations. Codex device login on the Agent needs Agent update and read, plus Secret and credential-source create in the Namespace, which exact-resource grants cannot give; see Use a personal Codex login. To message the Agent, the person can use a configured channel or native chat with exact Agent operate. Native administration requires administer. Password and OIDC sessions support native access; Google sessions do not.

Let an Agent use a Secret

Besides the person who deploys it, the Agent's own ServicePrincipal needs operate on each Secret and credential source the Agent binds; without it, OCC refuses the deployment. When a person who can operate those Secrets deploys, OCC writes these grants. Otherwise, only an Installation administrator can give the Agent this grant: bind a Role that holds only Secret operate to the Agent's ServicePrincipal, once for each exact Secret.

When the console creates an Agent, it binds the Role role_<namespace-id>_agent_secret_operate, named Agent Secret operate, and creates that Role on first use. Later console grants reuse any Namespace Role that holds exactly Secret operate. Do the same with an administrator key file and OCC_NAMESPACE set:

bash
AGENT_SP="$(occ agent get '<agent-id>' -o json | jq -r .servicePrincipalId)"ROLE_ID="$(occ iam role list -o json | jq -r 'map(select(.permissions == [{"action":"operate","resourceKind":"secret"}]))[0].id // empty')"if [ -z "$ROLE_ID" ]; then  echo '{"name":"Agent Secret operate","permissions":[{"action":"operate","resourceKind":"secret"}]}' > role.json  ROLE_ID="$(occ iam role create --file role.json -o json | jq -r .id)"fijq -n --arg p "$AGENT_SP" --arg r "$ROLE_ID" --arg s '<secret-id>' \  '{subjectKind:"identity",subjectId:$p,roleId:$r,resourceKind:"secret",resourceId:$s}' > binding.jsonocc iam access-binding create --file binding.json

Repeat the last two commands for each Secret. Creating the binding also needs read on that exact Secret. For a credential source, use credential_source in the Role and the binding. occ iam access-binding list shows the Agent's grants: their SUBJECT is $AGENT_SP.

Search documentation