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
- People sign in with an administrator-provisioned email and password. See Authentication for sign-in and account provisioning, and external sign-in for Google or OIDC sign-in. Optional OIDC enrollment creates admitted accounts with configured initial access. Public signup is not supported.
- Non-Agent automation authenticates as an existing ServicePrincipal with a service API key. Issuing a key does not give that identity new permissions.
- Agents have their own Namespace-scoped ServicePrincipal. That identity is separate from the user who creates or deploys the Agent. Agent-owned principals cannot use service API keys. See Agent permissions.
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:
-
Sign in so
OCC_SESSION_COOKIE_JARholds your session, and setOCC_ORIGINto the console origin. Underrecovery-onlypassword sign-in, only the recovery account signs in this way. -
Create the account from a private file with a generated password, and keep the returned
idandprincipalId: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
503with 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_IDreturnsprincipalIdagain; a signed-in person finds theirUSER_IDasuser.idinGET /api/auth/session. -
If the person signs in only through Google or OIDC (always, under
recovery-only), explicitly attach their identity toUSER_ID: see Google or OIDC. Then deleteaccount.json; never hand the password over. -
Grant access in each Namespace the person needs. With
OCC_NAMESPACEset, 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.jsondoneocchas no human sign-in: it uses the service key inOCC_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,POSTthe same JSON to$OCC_URL/namespaces/$OCC_NAMESPACE/iam/rolesand.../iam/access-bindingswith your session cookie andOrigin: $OCC_ORIGIN; read the Role ID from.data.id. -
For a person who signs in with a password, give them the password from
account.jsonthrough 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:
- Under
auth.passwordSignIn: all, the default even with a provider enabled, it signs in. Switch torecovery-onlyonce everyone has an identity. - Under
recovery-onlyit is refused, but it works again if the setting returns toallor deactivation restores password-only sign-in. - No API resets or removes it, and detach refuses password methods with
409. Of the account controls, only disabling stops it, and that also stops the person's SSO sign-in. - Accounts cannot be deleted. If an attach fails, for example with
409because another account already holds the subject, correct the subject and attach again. An account you abandon keeps its email, which creation will not reuse, so disable it. Account controls need Google or OIDC sign-in. On a password-only Installation they return409, so an abandoned account there cannot be disabled.
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:
- It can create accounts, read the Installation, Backends and the observability link, and issue service keys for principals whose grants it already holds.
- Its Namespace list is empty. It cannot create or read Namespaces, create
Secrets, credential sources or Agents, start a device login, read the
deployment inventory, or change Namespace IAM. Each refusal is a
403audited as an authorization denial, and its message still says to ask an Installation administrator. - To let it manage IAM in one Namespace, the bootstrap administrator binds it
there to a Role with
namespace:read, as in step 4 of Add a person. Namespace IAM writes then work in that Namespace; Namespace creation stays refused. - Like every account created through the API, it joins the configured
default Groups. With them, it can create Agents
in
default.
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.
- Add the
iamblock from the reference example to the Installation startup configuration of both the API and the worker. Omit thenamespace_creatorbinding unless members can also create and own Namespaces. OCC sets no limit on how many Namespaces or Agents a member creates. - 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.
- Create each person's account (steps 1–3 of Add a person).
The account joins
default-useras 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:
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:
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:
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.jsonRepeat 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.
