Self Hosting / Machine tokens
Verified against Flow 5.0.0-beta.39
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.
openssl, and sha256sum (on macOS, shasum -a 256).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:
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:
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.
Generate it. The value must be bfg_ followed by at least 32 characters:
BOOTSTRAP="bfg_$(openssl rand -hex 32)"Store it in a secret and set FLOW_SECURITY_BOOTSTRAP_TOKEN from it on service-core.
Restart service-core. The log shows Registered the bootstrap token (new).
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.
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.
Generate the raw token:
RAW="bfg_$(openssl rand -hex 32)"The raw token must start with bfg_.
Hash it:
printf %s "$RAW" | sha256sum | cut -d' ' -f1Use printf %s, not echo: a trailing newline changes the hash.
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 jobRestart service-core. The log shows Registered the declared token 'reporting' (new).
Give the client $RAW. It sends Authorization: Bearer <token>.
The token works when reporting appears in Admin › Tokens and the client's calls succeed.
flow.security.tokens.reporting.permissions must each be <resource>/<action>, for example workspace/read. Fix the entry and restart.Revoked the declared token 'reporting': it is no longer configured.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.401, and service-core logs Refused a token whose value more than one stored token holds.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.
A permission is <resource>/<action>, in lowercase. ** is a wildcard on either side.
system, workflow, workflowrun, workflowtemplate, taskrun, task, action, user, workspace, token, parameter, schedule, insights, integration, webhook.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>.
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.
Declare the new token beside the old one, move the client, then remove the old one. This example rotates the dispatcher's token.
Generate a new raw token and its hash, as in Declare a token.
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=SERVICERestart every service-core instance. The log shows Registered the declared token 'dispatcher2' (new). Both tokens now work.
Set FLOW_ENGINE_DISPATCHER_TOKEN to the new raw token and restart the dispatcher. Its log shows registered as <id>.
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.