Authentication
operate supports HTTP Basic authentication, OAuth 2.0 with the authorization code flow and PKCE, for engines behind a gateway that checks tokens of Keycloak, Microsoft Entra ID, Auth0 or another OpenID Connect provider, bearer tokens you get elsewhere (SSO tooling, a CI secret, az, gcloud) and token commands that operate runs to fetch one. Other schemes go into headers.
Basic auth
Basic auth works e.g. with Camunda 7 Run with camunda.bpm.run.auth.enabled=true, CIB seven Run additionally with camunda.bpm.run.auth.authentication=basic (its default pseudo lets every request in), Operaton Run with operaton.bpm.run.auth.enabled=true, or a ProcessEngineAuthenticationFilter or proxy in front of /engine-rest.
| Value | Flag | Environment | Profile (auth object) |
|---|---|---|---|
| type | --auth <type> | OPERATE_AUTH | type: none or basic |
| username | --auth-user <name> | OPERATE_USERNAME | username |
| password | --auth-password-stdin | OPERATE_PASSWORD | passwordEnv (variable name) or password |
- Each value is resolved on its own: flag > environment variable > profile. Without a type anywhere, a username or
--auth-password-stdinselectsbasic;--auth noneorOPERATE_AUTH=noneswitch Basic auth off even when the profile sets it. - Credentials follow URL overrides: with
--urlorOPERATE_URLset, the credentials of the profile (andOPERATE_USERNAME/OPERATE_PASSWORD) go to that URL. Checkoperate config showwhen a shell has a leftoverOPERATE_URL, and usehttps://beyondlocalhost: Basic auth sends the password readable to anyone on the path. - The password is never a plain flag (shell history, process list).
--auth-password-stdinreads the first line of stdin, so it cannot be combined with--body -. In a profile, preferpasswordEnv, the name of an environment variable that holds the password; a literalpasswordis stored in plain text (the config file has mode0600, andconfig setwarns).config setalso warns, without repeating it, when the variable given to--auth-password-envis not set in its environment: pass the name, never the password. An unset name that is not in the usual upper-case form (--auth-password-env s3cret_pwinstead ofCAMUNDA_PASSWORD) is refused without being stored or repeated. Errors repeat thepasswordEnvof a profile only in that form, and the profile thatconfig set|unset|useprint shows***for an unset name in another form (the same holds for--oauth-client-secret-env/clientSecretEnvand--auth-token-env/tokenEnv). - Basic auth with a missing username or password fails with exit code 3 before any request is sent, naming where each value was looked up. Usernames must not contain
:(RFC 7617), and neither value may contain control characters. operate pingreports"auth": "basic"and"user".config showlistsauth,usernameandpasswordwith their sources. The password and theAuthorizationheader are masked (***,Basic ***) inconfig show,--verboseand--dry-runoutput, including the curl line, unless--show-secretsis given.- An HTTP 401 (exit code 4) says whether operate sent no credentials and why (for example
OPERATE_AUTH=none, or a password without a username), or which user the engine rejected and where the credentials came from. - A mistyped password flag (
--auth-password=..., a value after--auth-password-stdin) is a usage error that never repeats the value.
# per call: the password comes from stdin
printf '%s\n' "$CAMUNDA_PASSWORD" | operate ping --auth basic --auth-user demo --auth-password-stdin
# per shell or CI job: take the password from a secret, never type it into the command line
export OPERATE_USERNAME=demo OPERATE_PASSWORD="$CAMUNDA_PASSWORD"
# interactive shell: read it without echo and without shell history
read -rs OPERATE_PASSWORD && export OPERATE_PASSWORD
operate task list --assignee demo
# per profile: only the name of the variable is stored
operate config set staging --url https://staging.example.com/engine-rest --auth basic --auth-user demo --auth-password-env CAMUNDA_PASSWORDOAuth
A person logs in once with operate auth login in a terminal; every other command reads the cached tokens, sends Authorization: Bearer <token> and refreshes the token on its own. No command except auth login ever starts a login: without a usable login a command fails with LOGIN_REQUIRED (exit code 4) and a hint naming the command a person runs, so agents never end up waiting for a browser. Client credentials and the device flow are not supported.
Configure the profile (or the OPERATE_OAUTH_* variables), then log in:
operate config set sso --url https://camunda.example.com/engine-rest --auth oauth --oauth-issuer https://login.example.com/realms/camunda --oauth-client-id operate-cli --oauth-audience engine-rest$ operate auth login --profile sso
Logging in to https://login.example.com/realms/camunda as client operate-cli (profile "sso").
Open this URL in a browser to log in:
https://login.example.com/realms/camunda/protocol/openid-connect/auth?response_type=code&client_id=operate-cli&redirect_uri=...
Opened the system browser.
Waiting up to 300 s for the login at http://127.0.0.1:53682/callback (Ctrl+C cancels).
Logged in as alice (profile "sso").
{"profile":"sso","profiles":["sso"],"issuer":"https://login.example.com/realms/camunda","clientId":"operate-cli","user":"alice","subject":"27c6dbd7-...","scopes":["openid","offline_access","profile","email"],"accessTokenExpiresAt":"2026-10-09T12:05:00.000Z","accessTokenValid":true,"refreshTokenExpiresAt":"2026-11-08T12:00:00.000Z","canRefresh":true,"loggedInAt":"2026-10-09T12:00:00.000Z","refreshedAt":null,"tokenCache":"/home/me/.config/operate/tokens/login-5d41402abc4b2a76b9719d911017c592.json"}
$ operate process-instance list --profile sso| Value | Environment | Profile (auth object) / config set option |
|---|---|---|
| type | OPERATE_AUTH=oauth (or --auth oauth) | type: "oauth" / --auth oauth; OAuth is never implied |
| issuer | OPERATE_OAUTH_ISSUER | issuer / --oauth-issuer <url>; the endpoints come from OpenID Connect discovery |
| endpoints | OPERATE_OAUTH_AUTHORIZATION_ENDPOINT, OPERATE_OAUTH_TOKEN_ENDPOINT | authorizationEndpoint, tokenEndpoint (both or neither) / --oauth-*-endpoint <url> |
| client id | OPERATE_OAUTH_CLIENT_ID | clientId / --oauth-client-id <id> |
| client secret | OPERATE_OAUTH_CLIENT_SECRET | clientSecretEnv (variable name) or clientSecret / --oauth-client-secret-env <VAR> |
| scopes | OPERATE_OAUTH_SCOPES (spaces or commas) | scopes / --oauth-scopes <scopes>; default openid offline_access |
| audience | OPERATE_OAUTH_AUDIENCE | audience / --oauth-audience <audience> (sent as audience parameter, Auth0 style) |
| redirect port | OPERATE_OAUTH_REDIRECT_PORT | redirectPort / --oauth-redirect-port <port>; default 0 = any free port |
- The issuer, or both endpoints, from the environment replace those of the profile as a group; every other value resolves on its own (environment > profile > default). URLs must use
https://(http://only forlocalhost,127.0.0.1and[::1]). A public client (no secret) with PKCE is the normal case;--oauth-client-secret-stdinstores a literal secret (with a warning); a client secret given to--oauth-client-secret-envin place of the name of its variable is refused like a password (see Basic auth). OAuth together with anAuthorizationheader, or with--auth-password-stdin, is a configuration error, and so is a missing issuer or client id. operate auth login [--no-browser] [--login-timeout <ms>]uses the authorization code flow with PKCE (S256) and a randomstate, and listens on127.0.0.1only while it waits for the redirect (RFC 8252). It prints the URL before it starts the system browser (BROWSER, elseopen,xdg-openwith a display, or the Windows URL handler), so it also works over SSH: set a fixed--oauth-redirect-port 8765, forward it withssh -L 8765:127.0.0.1:8765 hostand open the URL in your local browser. A login always replaces the cached one.operate auth statusshows user, scopes and the expiry of the access and refresh token without network access and exits with 4 (LOGIN_REQUIRED) when no command could run without a new login. Tokens are never printed. It reads only the cache: a session revoked at the server is noticed by the next command that refreshes, which records the refusal (invalid_grant) in the cache, soauth statusand later commands then fail withLOGIN_REQUIREDat once.operate auth logoutrevokes the refresh token (when the server supports RFC 7009) and removes the cached login. An access token issued before stays valid until it expires at gateways that check tokens offline (JWT gateways such as Envoy);logoutsays until when. Without a revocation endpoint (explicit endpoints, or a provider whose discovery document lists none, such as Microsoft Entra ID) nothing is revoked andlogoutwarns: end the session at the provider. A confidential client's login is revoked only with the client secret of the same issuer and client.- Tokens are cached in
$XDG_CONFIG_HOME/operate/tokens(default~/.config/operate/tokens, Windows%APPDATA%\operate\tokens), one file per login (login-<hash>.json, see shared logins; without a profileenv-<hash>.json), written atomically with mode0600in a0700directory, never next to a config file named by--config. A login is kept for the issuer, client, audience and scopes it was made for; changing them needs a new login (LOGIN_REQUIREDnames what differs and whichOPERATE_OAUTH_*variable of the environment causes it). Theprofile-<name>.jsonlogins of operate 0.7.0 and earlier are no longer used: log in once more.auth logout --profile <name>of an OAuth profile revokes and removes the old file of the profile, too (oldTokenCaches),config deleteremoves that of the deleted profile; delete others by hand. - Refreshes happen 60 s before the access token expires (at most half its lifetime) and once after a 401; rotated refresh tokens are stored. Commands running in parallel share one refresh through a lock file, so a refresh token is never sent twice (Keycloak ends the session when one is reused). With strict rotation a refresh answer lost on the way (timeout, dropped connection) ends the login, too: run
auth loginagain. - The gateway must expect exactly the issuer operate logs in with:
http://localhost:8180/realms/xandhttp://127.0.0.1:8180/realms/xare different issuers to a JWT gateway, and Keycloak issues tokens for the host name the browser and operate used.UNAUTHORIZEDorFORBIDDENafter a successful login are gateway (issuer, audience, clock) or permission problems, not login problems; the hints say what to check, and a 403 never triggers a refresh. --dry-runshows the cached token asBearer ***and notes on stderr when there is no usable login;--verbosetraces token requests too (client credentials always masked) and says when a failed refresh falls back to the still valid token;--show-secretsreveals only the Bearer token.operate pingreports"auth": "oauth"and the user.config showlists the OAuth values with their sources (the client secret masked).
Shared logins
One operate auth login serves every OAuth profile of the same origin that stores the same issuer (or explicit token endpoint), client id, audience and scopes, so a repository with an engine per service needs one login, not one per profile:
$ operate auth login --profile orders
...
Logged in as alice (profile "orders").
The login also serves 2 other profiles with the same issuer, client, audience and scopes:
billing http://localhost:8113/engine-rest
invoices http://localhost:8114/engine-rest- The origin is your config file, a config file named by
--configorOPERATE_CONFIG, or the repository of a project file: every project file below the repository root, tracked or not, and every worktree of the repository (a worktree counts when its git directory links back to it, asgit worktree addwrites it). Separate clones, a submodule, a copied.gitfile and a project file outside a repository are origins of their own. Like git'ssafe.directory, a.gitof another user or in a file system root makes no repository (Windows has no owners operate can compare: there only the drive root is refused). A profile of your config file that overrides a project profile of the same name uses the logins of your config file. - Only the stored settings decide which login a profile uses; values from
OPERATE_OAUTH_*variables must still fit the login. A profile with another audience or other scopes has a login of its own, and so has a profile that gets OAuth only fromOPERATE_AUTH=oauth(no storedtype: "oauth"). The profiles refresh through one cache file and one lock, so they never send a refresh token twice. auth loginlists the other profiles the login serves, with their URLs, andauth statusandauth logoutname them (profiles): those of the files in effect. In a repository, project files elsewhere and other worktrees with the same settings use the login, too, andlogoutends it for all of them.config setsays which other profiles share the profile's login.LOGIN_REQUIRED, the notes of--dry-runand the hint of a 401 name where to log in: the directory of a project file, or--configwith the config file it names.config set|unset|deleteremove the login of a profile's previous OAuth settings (without revoking it) when no other OAuth profile of the file uses it any more. The login of a repository stays, with a note: other worktrees and project files of the repository may still use it.- A login belongs to the file or repository at its path, so run
auth logoutbefore you delete a repository whose login is still valid. A login whose settings change outsideoperate config(a pull of the project file, an edit by hand) is no longer found and stays in the token directory, its refresh token valid until it expires: log out before, or delete the file (itsidentitynames issuer, client, audience and scopes).
Security notes:
- Use
https://for the engine URL: operate warns on stderr when it sends the access token over plainhttp://to a host other thanlocalhost,127.0.0.1or[::1](RFC 6750 §5.3). - A login only serves the profiles of the origin it was made for: another repository's project file or a config file named by
--confignever gets it, also not with a profile of the same name, issuer and client. Within its origin it goes wherever the profiles' URLs point, so treat a project file like code, and--urlorOPERATE_URLsend the login of the selected profile to that URL. Each value resolves on its own, soOPERATE_OAUTH_CLIENT_SECRETalso goes to the token endpoint of whatever issuer the profile orOPERATE_OAUTH_ISSUERnames. Point operate (and agents) only at config files and variables you trust. - operate rejects a login response whose
iss(RFC 9207) names another issuer, and requiresisswhen the provider advertises it. A provider that sends noiss(Microsoft Entra ID) gives no such mix-up protection: log in only to issuers you trust.
Client registration:
- Keycloak: a client with Client authentication off (public), Standard flow on, Direct access grants off, Valid redirect URIs
http://127.0.0.1/callback(Keycloak accepts any port for loopback redirects) and, under Advanced, the PKCE methodS256. Users need theoffline_accessrole (part of the default roles) for refresh tokens that outlive the browser session. Keycloak ignores theaudienceparameter: add an Audience mapper to the client's dedicated scope so the access token carries the audience the gateway expects. - Microsoft Entra ID: register a "Mobile and desktop applications" redirect URI
http://127.0.0.1/callback(Entra ignores the port of loopback redirect URIs; thehttploopback URI currently has to be added in the application manifest), issuerhttps://login.microsoftonline.com/<tenant-id>/v2.0, and request the API by scope, e.g.--oauth-scopes 'openid offline_access api://<app-id>/.default'instead of an audience. - Auth0: the issuer is
https://<tenant>.auth0.com/with the trailing slash (operate compares it with the discovery document exactly), the API identifier goes into--oauth-audience; registerhttp://127.0.0.1:8765/callbackwith a fixed--oauth-redirect-port 8765.
Bearer tokens
When you already get an access token elsewhere (company SSO tooling, a CI secret, az account get-access-token, gcloud auth print-access-token, a token copied from another tool) because the OAuth login of operate auth login is not possible or not wanted, --auth bearer sends it as Authorization: Bearer <token> and does nothing else: no login, no refresh, no cache.
| Value | Flag | Environment | Profile (auth object) / config set option |
|---|---|---|---|
| type | --auth bearer | OPERATE_AUTH | type: "bearer" / --auth bearer |
| token | --auth-token-stdin | OPERATE_TOKEN | tokenEnv (variable name) / --auth-token-env <VAR>, or token / --auth-token-stdin |
# Microsoft Entra ID: a token for the app registration of the API the engine's gateway expects
# (without --resource, az returns a token for Azure Resource Manager, whose audience a gateway rejects)
export OPERATE_TOKEN=$(az account get-access-token --resource api://<engine-api-app-id> --query accessToken -o tsv)
operate process-instance list
# Google Cloud
export OPERATE_TOKEN=$(gcloud auth print-access-token)
# CI job: the token is the secret CI_ENGINE_TOKEN; the profile stores only the variable name
operate config set ci --url https://camunda.example.com/engine-rest --auth bearer --auth-token-env CI_ENGINE_TOKEN- The token comes from
--auth-token-stdin(the first line of stdin, e.g.gcloud auth print-access-token | operate task list --auth-token-stdin; not together with--body -or--auth-password-stdin), elseOPERATE_TOKEN, else the variable named by the profile'stokenEnv(it must be set), else the profile'stoken. It is never a plain flag. - Without an auth type anywhere,
OPERATE_TOKENor--auth-token-stdinselectsbearer, as a username selectsbasic; both at once is a configuration error asking for an explicit--auth. An explicit type always wins:--auth noneswitches the token off, and withbasicoroauth(also the type a profile made byconfig setalways stores)OPERATE_TOKENis ignored:config showlists it as unused, and the hints ofLOGIN_REQUIRED, 401/403 and configuration errors say to add--auth bearerorOPERATE_AUTH=bearer. A token never selects OAuth. - A pasted
Bearerprefix and surrounding blanks are dropped; the rest must be an RFC 6750 token (letters, digits,-._~+/, trailing=), otherwise exit 3 without repeating it. In a profile prefer--auth-token-env <VAR>; a token stored withconfig set --auth-token-stdinis plain text in the0600config file (config setwarns), and an unset variable gets a warning that does not repeat its "name" (it may be the token itself). An unset name that is not in the usual upper-case form (--auth-token-env "$CI_TOKEN"instead ofCI_TOKEN) is refused without being stored or repeated. - JWTs: operate reads
exp,sub,preferred_username,issandaudwithout checking the signature (the engine or its gateway does). A JWT whoseexplies more than 30 s in the past fails before any request withTOKEN_EXPIRED(exit 4): fetch a new token (a token command runs again instead). Opaque tokens are sent as they are. operate auth statusshows where the token comes from and, for a JWT, subject, user, issuer, audience and expiry (exit 4 when it expired), also for a piped token (--auth-token-stdin; with a Basic or OAuth profile addOPERATE_AUTH=bearer,authcommands have no--auth); it never prints the token.auth loginandauth logoutdo not apply: operate neither fetches nor stores such tokens (a token command lets operate fetch them).- A 401 names the token's source and, for a JWT, the expiry, issuer and audience to check; a 403 points to missing roles or scopes. operate never retries a rejected token.
- The token is masked as
Bearer ***in--dry-run, curl,--verboseandconfig showunless--show-secrets; usage errors never repeat a word typed after--auth bearer,--auth-token-stdinoroperate auth(--auth bearer $TOKENis a mistake:--authtakes only the type).operate pingreports"auth": "bearer"and, for a JWT,userandsubject. A bearer token together with anAuthorizationheader is a configuration error, and operate warns when it sends the token over plainhttp://beyondlocalhost.
Token commands
With short-lived tokens (a local Keycloak dev realm, corporate SSO tooling) someone has to fetch a new token before nearly every call. OPERATE_TOKEN_COMMAND does it for operate: a command line that prints a bearer token on the first line of its stdout. operate runs it when it needs a token. A repository sets it for agents in the env of its .claude/settings.json (Claude Code applies it once you trusted the folder), people in their shell or in direnv's .envrc.
# a local Keycloak dev realm: the password grant of a test user (only for local test realms)
export OPERATE_TOKEN_COMMAND="$PWD/scripts/dev-token.sh"
operate task list- The token sources in order:
--auth-token-stdin,OPERATE_TOKEN,OPERATE_TOKEN_COMMAND, then the profile'stokenEnvortoken. LikeOPERATE_TOKEN,OPERATE_TOKEN_COMMANDselects bearer auth without an auth type; an explicit other type leaves it unused (config showsays so). - The shell runs the command (
sh -c,cmd.exeon Windows) in the working directory with stdin closed, so it must not wait for input; the request timeout (--timeout) bounds it. The first line of its stdout is the token (a leadingBeareris dropped, the rest must be a token, not a JSON answer: use e.g.jq -r .access_token). Name a script by its absolute path, or use a command without a path (curl ... | jq -r .access_token): a relative one such as./scripts/dev-token.shruns the script of whatever directory operate runs in, also of a repository you just cloned. - A JWT with
expis cached in the token directory (command-<hash>.json, mode0600) and reused, also by later and parallel commands, until 60 s before it expires (at most half its lifetime), so a longoperate waitkeeps a valid token. There is one cached token per command line and project (the directory of the project file in effect, else the working directory). An opaque token is fetched once per call. A JWT the command prints is sent even when it expired: after a 401 operate runs the command once more and retries the request with the new token before it reports the error. - A command that cannot start, fails, times out or prints no token fails with
LOGIN_FAILED(exit 4); the message quotes the end of its stderr. Error messages never show the token, nor the command line (operate config showandoperate auth statusprint it): keep secrets out of the command line and let the command read them from a file or a variable. operate auth statusruns the command (or uses its cached token) and shows the command and the claims of the token;operate auth logoutremoves the cached token.--dry-runnever runs it: it shows a cached token, else a note.
Only the environment sets a token command, never a profile or a project file: a project file cannot make operate run code. The files that set the environment for you, .claude/settings.json and .envrc, have trust steps of their own.
Other headers
Other schemes, such as API keys or gateway specific headers, are passed as headers: per call with -H, in the environment with OPERATE_HEADERS (keeps the credential out of the command line and shell history), or stored in a profile. A bearer token can still go into an Authorization header, but --auth bearer also checks its syntax and expiry and explains a 401. Headers merge per name: profile, then OPERATE_HEADERS, then -H. An Authorization header together with Basic auth, OAuth or a bearer token is a configuration error; in a profile, config set with auth options replaces a stored Authorization header (with a notice on stderr), and --auth none keeps the header and switches the auth off. Authorization and other secret headers (cookies, names containing token, secret, password, api-key) are masked as *** in dry-run, verbose and config output unless --show-secrets is given.
export OPERATE_HEADERS='X-API-Key: k-7f3a...'
operate config set prod --header 'X-Tenant-Id: acme'