Authentication
Auth is required by default. There is no step where the server is briefly open while you configure it.
First boot
On the first start with an empty database, the server creates an administrator and generates a one-time password. It appears in two places:
<dataDir>/initial-admin-password, mode0600- the server log, at
warnlevel
Read it, log in, and complete setup. The file is deleted when setup succeeds.
$ docker compose exec flowwright cat /data/.flowwright/initial-admin-password
The file is removed only on successful setup — there is no expiry. If you never finish onboarding it stays on disk indefinitely. And because the password is also in the log, anywhere you ship logs has a copy: complete setup promptly, or delete the file and re-bootstrap from an empty database.
This happens only when there are zero users and auth is not disabled. It is not a per-boot event.
The two modes
FW_AUTH (or security.authMode) takes required or disabled. There is no middle
setting.
disabled is not "relaxed" — it removes the identity boundary. Every caller resolves
to an implicit admin, and stored tokens are ignored entirely. The server says so at
startup:
auth is disabled — anyone with network access can run commands and change projects; all users are implicit admins and stored tokens are ignored
Reasonable on a laptop. Never on anything reachable.
Sessions and tokens
Sessions are for people. Logging in sets an HttpOnly fw_session cookie with a
7-day lifetime, SameSite=Lax, and Secure when the request arrived over TLS. Passwords
are stored as scrypt hashes and compared in constant time; the login path hashes a dummy
value on a missing user so timing doesn't reveal who exists.
Bearer tokens are for automation. Authorization: Bearer <token>. Only the SHA-256
hash is stored — a token is shown once at creation and is unrecoverable afterward, so
rotation means issuing a new one. Bearer principals never participate in interactive MFA.
Service accounts are password-less token identities (kind: service). A
workspace-scoped one collapses to exactly workspace:read regardless of its stored role —
scope beats role, so a scoped token can't be escalated by changing the role.
Roles
Three, each a superset of the last.
| Role | Can |
|---|---|
viewer | Read runs, projects, workers, settings, workspace |
member | …plus trigger, cancel and retry runs, and read credentials |
admin | Everything — 21 permissions including settings:admin and users:admin |
Authorization is per-permission, not per-role; the roles are presets over the same permission set. Every one of them is listed on Permissions.
This role is not the whole picture. It decides which routes an account may reach; what it may then do to a particular workspace's projects, runs and credentials comes from a second, per-workspace role — see Members and roles.
Enforcement is one guard in front of every route. Any /api/* route is authenticated
by default — a route is anonymous only if it explicitly opts out, which /healthz and
/api/health do. Routes that declare a permission additionally check it, and a principal
flagged for a forced password change is refused everywhere until it changes.
Creating users
Three ways, no CLI command:
- The web UI, as an admin — see Administration
- The admin API
resources.usersin the config file — declarative, and how you'd manage a fleet
A user created declaratively gets one-time credentials logged the same way the bootstrap admin does. Passwords and tokens cannot be declared in the file.
FW_REGISTRATION_MODE=open lets any visitor self-register as a member. It's closed
by default and should stay that way on anything public.
MFA
FW_MFA_POLICY takes optional (default), required_privileged — admins must enrol —
or required_all. Enforced on interactive sessions; bearer tokens are unaffected, since
there's no one there to prompt.