Machine tokens

Verified against Flow 5.0.0-beta.39

View as Markdown
On this page

A machine token lets a service, a script or a dispatcher call Flow without a person signing in. This page is for operators: the bootstrap token, tokens declared in configuration, the dispatcher's token, and rotation.

Before you start

  • You can set environment variables on service-core and its dispatchers, and restart them.
  • You have somewhere to keep raw tokens, such as a Kubernetes Secret.
  • You have openssl, and sha256sum (on macOS, shasum -a 256).

Which token kinds are there?

Flow tells token kinds apart by their prefix. It stores only each token's SHA-256 hash, and shows the raw value once, when the token is created.

Kind Prefix For Permissions come from
session bfs_ A signed-in person The person's user type and workspace memberships, when the session starts. Only sign-in makes one.
user bfu_ A person's long-lived personal token The person's workspace roles, copied when the token is created
key bfk_ A service, an AI agent or a workflow, on one workspace The permissions chosen when the token is created, granted on its workspace
global bfg_ A platform admin, an automation or a dispatcher One global grant: the permissions chosen when the token is created

A machine token can also carry an actor kind: SERVICE, AGENT or WORKFLOW. The dispatcher protocol accepts only a global token that has one. POST /api/v2/token refuses an actor kind that doesn't fit the token kind with 400 TOKEN_INVALID_ACTOR: a global token takes SERVICE, AGENT or none, a key token any of the three or none, and a user token none.

Flow refuses a bearer that doesn't start with bfg_, bfk_, bfu_ or bfs_ before it looks anything up, so v3 bft_ and bfw_ tokens no longer work. Each instance caches validated tokens for 60 seconds, so a revoked token can keep working on another instance for up to 60 seconds.

Admin › Tokens lists global tokens, including the bootstrap token and declared tokens. Its Actor column names each token's actor kind, or shows --- for none. To create a global token there, select Create token. Its Actor choice offers:

  • Service, the default: "A system, script or dispatcher";
  • Agent: "An AI agent";
  • None: "A person's token", with no actor kind.

Use the bootstrap token

The bootstrap token, flow.security.bootstrap-token (FLOW_SECURITY_BOOTSTRAP_TOKEN), is a global **/** token with actor kind SERVICE, registered at start under the id bootstrap-token. It is full admin. Use it for:

  • a secured install where no person signs in;
  • first setup through the API before anyone signs in: creating workspaces, minting narrower tokens with POST /api/v2/token, or naming the first admin.

It isn't for people: the webapp can't sign in with it. It never counts as a user, so it doesn't activate the install.

  1. Generate it. The value must be bfg_ followed by at least 32 characters:

    BOOTSTRAP="bfg_$(openssl rand -hex 32)"
  2. Store it in a secret and set FLOW_SECURITY_BOOTSTRAP_TOKEN from it on service-core.

  3. Restart service-core. The log shows Registered the bootstrap token (new).

  4. Check it. FLOW_API is the address your front door routes /api/v2 to:

    curl -H "Authorization: Bearer $BOOTSTRAP" "$FLOW_API/api/v2/workspace/query"

The token works when the call answers 200. If service-core stops at start with flow.security.bootstrap-token must be bfg_ followed by at least 32 characters., fix the value and restart. Flow registers the token whether or not security is on.

Rotating it: change the value and restart. The old value stops working at once on the instance that restarted, and on the others within 60 seconds. There's only one bootstrap token, so a client that uses it is refused until it has the new value. Give a client that needs a gapless rotation a declared token instead.

Retiring it: clear FLOW_SECURITY_BOOTSTRAP_TOKEN on every service-core instance and restart them. At its next start, an instance with the property blank deletes the stored token and logs Revoked the bootstrap token: flow.security.bootstrap-token is no longer set. The old value then answers 401 AUTH_REQUIRED on that instance at once, and on the others within 60 seconds. An instance that starts with the value still set registers it again.

Declare machine tokens in configuration

flow.security.tokens.<name>.* declares narrower global machine tokens, which Flow registers at start. A hosted install then needs no minting step. In environment variables, the name is part of the variable:

Variable Required Meaning
FLOW_SECURITY_TOKENS_<NAME>_HASH One of _HASH or _VALUE The lowercase hex SHA-256 of the raw token. The environment never holds the token itself.
FLOW_SECURITY_TOKENS_<NAME>_VALUE One of _HASH or _VALUE The raw token: bfg_ followed by at least 32 characters
FLOW_SECURITY_TOKENS_<NAME>_PERMISSIONS No Comma-separated permissions, such as workflow/read,workflowrun/action. Together they form the token's global grant. Without any, the token holds no grant and reaches only the dispatcher protocol.
FLOW_SECURITY_TOKENS_<NAME>_ACTORKIND No SERVICE (the default), AGENT or WORKFLOW
FLOW_SECURITY_TOKENS_<NAME>_DESCRIPTION No Shown in Admin › Tokens
FLOW_SECURITY_TOKENS_<NAME>_EXPIRY No An ISO-8601 instant, such as 2027-06-30T00:00:00Z, or a date, such as 2027-06-30, meaning the end of that day in UTC

A name is 1 to 63 lowercase letters and digits. It can't hold a hyphen or an underscore, because an underscore in a variable name becomes a dot. bootstrap is reserved. Flow lowercases a name it reads from a variable; in a properties file, write the name in lowercase.

Declare a token

  1. Generate the raw token:

    RAW="bfg_$(openssl rand -hex 32)"

    The raw token must start with bfg_.

  2. Hash it:

    printf %s "$RAW" | sha256sum | cut -d' ' -f1

    Use printf %s, not echo: a trailing newline changes the hash.

  3. Set the entry on service-core:

    FLOW_SECURITY_TOKENS_REPORTING_HASH=<the 64-character hash>
    FLOW_SECURITY_TOKENS_REPORTING_PERMISSIONS=workflow/read,workflowrun/read
    FLOW_SECURITY_TOKENS_REPORTING_DESCRIPTION=Nightly report job
  4. Restart service-core. The log shows Registered the declared token 'reporting' (new).

  5. Give the client $RAW. It sends Authorization: Bearer <token>.

The token works when reporting appears in Admin › Tokens and the client's calls succeed.

What service-core does with entries

  • A bad entry stops the start, with a message naming the entry, such as flow.security.tokens.reporting.permissions must each be <resource>/<action>, for example workspace/read. Fix the entry and restart.
  • An unchanged entry writes nothing on restart. A changed hash or grant replaces the stored token, and the old hash stops working.
  • Removing an entry revokes its token at the next start. The log shows Revoked the declared token 'reporting': it is no longer configured.
  • Two entries can be valid at once, which is how a token rotates without a gap. Two configured tokens holding the same value stop the start, whether both are entries or one is the bootstrap token.
  • A configured token takes over a minted one. If a token created in Admin › Tokens or with POST /api/v2/token already holds an entry's value, service-core deletes the minted token at start and logs a warning, such as flow.security.tokens.reporting took over the value of the token 'nightly' (<id>), which is deleted. The client keeps working, with the entry's permissions and actor kind. The bootstrap token takes over a value the same way. This is how to move a minted token into configuration without handing out a new one.
  • A value two stored tokens hold authenticates as neither. A request with it answers 401, and service-core logs Refused a token whose value more than one stored token holds.
  • A past expiry doesn't stop the start. The token is stored and refused like any expired token.
  • Every instance must carry the same entries. Each instance revokes, at its start, the declared tokens its own configuration doesn't name.
  • The token API can't delete a declared token. DELETE /api/v2/token/declared-token:<name> answers 409 with TOKEN_DECLARED: "This token is declared in flow.security.tokens and cannot be changed through the API. Remove its entry from the configuration to revoke it."

Declared tokens are registered whether or not security is on.

Write permissions

A permission is <resource>/<action>, in lowercase. ** is a wildcard on either side.

  • Resources: system, workflow, workflowrun, workflowtemplate, taskrun, task, action, user, workspace, token, parameter, schedule, insights, integration, webhook.
  • Actions: read, write, action, delete.

For example: workflow/read, workflowrun/action, workflow/** and **/read.

Declared tokens and POST /api/v2/token run the same check. When it fails, the API answers 400 TOKEN_INVALID_PERMISSION. That error's message describes a three-part format with uppercase actions, which Flow doesn't accept. Write <resource>/<action>.

Give the dispatcher its token

Flow's Kubernetes dispatcher sends flow.engine.dispatcher.token (FLOW_ENGINE_DISPATCHER_TOKEN) as a bearer token on every call to service-core. service-core accepts it only if it is a stored, unexpired global token with an actor kind. The token needs no permissions: the dispatcher protocol doesn't check them.

Declare the dispatcher's token with no permissions. Its actor kind defaults to SERVICE:

# On service-core
FLOW_SECURITY_TOKENS_DISPATCHER_HASH=<the hash of the dispatcher's raw token>
FLOW_SECURITY_TOKENS_DISPATCHER_ACTORKIND=SERVICE
 
# On the dispatcher
FLOW_ENGINE_DISPATCHER_TOKEN=<the raw token>

Without permissions, the token reaches only the dispatcher protocol: a REST API route that needs a permission answers it with 403.

To create the token in Admin › Tokens instead, keep Actor at Service. The form also asks for permissions; the dispatcher doesn't use them, so choose a narrow one, such as workspace/read. Through the API, send POST /api/v2/token with "type": "global", "actorKind": "SERVICE", a name and permissions, using a credential that holds a global grant, such as the bootstrap token. Either way, the raw token is shown once, when it's created.

Symptom Cause Fix
The dispatcher logs Dispatcher registration attempt 1 failed, retrying in … with a 401, and keeps retrying. No task runs start. The token is missing, wrong or expired, or has no actor kind, such as a token created with Actor None Check FLOW_ENGINE_DISPATCHER_TOKEN against the token, then restart the dispatcher

The dispatcher has its token when its log shows Dispatcher <name> (<host>) registered as <id> for task types […].

flow.dispatcher.auth.enabled=false on service-core turns the check off. Use it for local development only.

Rotate a token without a gap

Declare the new token beside the old one, move the client, then remove the old one. This example rotates the dispatcher's token.

  1. Generate a new raw token and its hash, as in Declare a token.

  2. Add a second entry with a new name and the same actor kind, and the same permissions if the old entry has any:

    FLOW_SECURITY_TOKENS_DISPATCHER2_HASH=<the new hash>
    FLOW_SECURITY_TOKENS_DISPATCHER2_ACTORKIND=SERVICE
  3. Restart every service-core instance. The log shows Registered the declared token 'dispatcher2' (new). Both tokens now work.

  4. Set FLOW_ENGINE_DISPATCHER_TOKEN to the new raw token and restart the dispatcher. Its log shows registered as <id>.

  5. Remove the FLOW_SECURITY_TOKENS_DISPATCHER_* variables and restart every service-core instance. The log shows Revoked the declared token 'dispatcher': it is no longer configured.

The rotation is done when the dispatcher keeps claiming work and the old token is refused with 401. Next time, rotate back to the name dispatcher.

Changing an entry's hash in place also rotates the token, but the old token stops working at the restart, before the client has the new one. For a token created in Admin › Tokens or with POST /api/v2/token, create the new token, move the client, then delete the old one.

Next steps