- Security & Policies
- Dashboard Authentication
Dashboard Authentication
The Ironflow dashboard protects the Web UI using email and password authentication. This is managed independently of the API Keys used for SDK and worker communication.
Default Credentials
Section titled “Default Credentials”During the Bootstrap Process, Ironflow creates a default administrator:
| Field | Value |
|---|---|
admin@ironflow.local | |
| Password | Randomly generated, written to the bootstrap key file (ADR 0072). To choose it yourself, see Seed the key from a secret store. |
The first boot writes the password to the same file as the bootstrap admin API
key, as the dashboard_password field — not to stdout, where it would outlive
the deploy in a container’s log:
cat /path/to/bootstrap-key.json | jq -r .dashboard_passwordThe server’s Dashboard Admin: startup banner names the email and points at
that file on its Password: line. The banner carries the plaintext instead only
when the file holds no password — the boot reused an existing dashboard user, or
the server predates ADR 0072. See API Keys
for the file’s path per deployment.
Seed the key from a secret store
Section titled “Seed the key from a secret store”Use this when you cannot read a file inside the container. Examples are a container platform with a secret store (Fly secrets, Railway variables) or a Docker Compose env_file. You set the credentials before the first boot, and the engine does not write them to disk.
| Variable | Rule |
|---|---|
IRONFLOW_BOOTSTRAP_ADMIN_KEY | ifkey_ followed by 32 or more lowercase hex characters, with at least 8 distinct characters. No spaces, no line breaks. |
IRONFLOW_BOOTSTRAP_ADMIN_PASSWORD | The dashboard password for admin@ironflow.local. Needs the key variable. 72 bytes or less, with no line break and no space at the start or end. |
To make a key in fish, then in bash or zsh:
echo ifkey_(openssl rand -hex 16)echo "ifkey_$(openssl rand -hex 16)"The command prints the key on your screen. Store it in your secret store, and keep it out of chat and of committed files.
On first boot the engine stores the key as ak_admin_bootstrap. It writes no key file and emits no BOOTSTRAP_ADMIN_KEY_ISSUED event, and it prints no key. The startup banner says Seeded from IRONFLOW_BOOTSTRAP_ADMIN_KEY (not written to disk, not printed). A malformed key stops the start with an error that names the variable. It does not show the value.
To rotate the key, change the secret and restart. At the next start the engine compares the secret with the stored key. If they differ, the engine replaces the stored key and logs a warning, and the old key stops working. If they match, or the variable is unset, the engine writes nothing. Removing the variable does not revoke the key.
The dashboard password is first-boot only. The engine hashes it at the first boot and never reads the variable again. Changing IRONFLOW_BOOTSTRAP_ADMIN_PASSWORD later has no effect. To change the password later, call PATCH /api/v1/users/{id}/password with current_password and new_password, or call client.users.changePassword(id, { currentPassword, newPassword }) in the Node SDK. The route changes only the signed-in user’s own password, and it needs the current password. The dashboard has no change-password page. If you seed the key but no password, the engine makes a random password that nobody knows, so you cannot use this route. Set the password variable before the first boot.
These variables apply to the flags-only path (ironflow serve without -f). With -f file.yaml, serve ignores both and logs a warning for each. The guidance above to read the file once, delete it, and rotate applies to the unseeded path. It is unchanged.
How It Works
Section titled “How It Works”- Login: The user submits credentials to
POST /api/v1/auth/login. The route refuses a cross-site browser post with403, so another site cannot sign a user in or out. A client that is not a browser sends noSec-Fetch-SiteorOriginheader and is not affected. - Session: The server returns a JWT (JSON Web Token) signed with an HMAC-SHA256 secret.
- Storage: The login response sets the token in the HttpOnly
ironflow_dashboard_tokencookie. The dashboard also keeps it in localStorage. Platform login sets the same cookie, so the last login decides the session. Sign out callsPOST /api/v1/auth/logout, which expires the cookie. - Expiry: Sessions are valid for 24 hours, unless the account is deleted first.
- Validation: On load, if a token exists in localStorage, the dashboard calls
GET /api/v1/auth/validateto verify the JWT is still accepted by the server. If the token is stale (e.g., the signing secret changed), the token is cleared and the user is redirected to the login page. If no token exists, the login page is shown directly. The response also carriesactions, a map offiles:read,files:writeandfiles:manageto booleans. The dashboard uses it to hide controls the user cannot use.
Authentication Flow
Section titled “Authentication Flow”sequenceDiagram
participant Browser
participant Dashboard as Dashboard (React)
participant Server as Ironflow Server
Note over Server: Startup: load JWT secret from<br/>.ironflow_jwt_secret (or generate + persist)
rect rgb(40, 40, 55)
Note right of Browser: First visit (no token in localStorage)
Dashboard->>Server: GET /api/v1/capabilities
Server-->>Dashboard: { auth_required: true }
Note right of Dashboard: hasTenantSession() = false, skip validate
Dashboard->>Browser: Show login page
Browser->>Server: POST /api/v1/auth/login { email, password }
Server-->>Browser: { token: "eyJ..." }<br/>Set-Cookie: ironflow_dashboard_token (HttpOnly)
Browser->>Browser: Keep the cookie, store JWT in localStorage
end
rect rgb(40, 55, 40)
Note right of Browser: Normal usage (JWT valid)
Dashboard->>Server: POST /ironflow.v1.IronflowService/ListFunctions<br/>Cookie: ironflow_dashboard_token=eyJ...<br/>X-Ironflow-Environment-ID: env_default
Server->>Server: Verify JWT signature + expiry
Server-->>Dashboard: 200 { functions: [...] }
Note right of Browser: User switches to new project/environment
Dashboard->>Server: POST /ironflow.v1.IronflowService/ListFunctions<br/>Cookie: ironflow_dashboard_token=eyJ...<br/>X-Ironflow-Environment-ID: env_myproject_staging
Server->>Server: Same JWT, different env header
Server-->>Dashboard: 200 { functions: [...] }
end
rect rgb(55, 50, 40)
Note right of Browser: Returning after server restart
Dashboard->>Server: GET /api/v1/capabilities
Server-->>Dashboard: { auth_required: true }
Note right of Dashboard: hasTenantSession() = true (localStorage token exists)
Dashboard->>Server: GET /api/v1/auth/validate<br/>Cookie: ironflow_dashboard_token=eyJ...
Server->>Server: Same secret from file, JWT still valid
Server-->>Dashboard: 200 { valid: true }
Note right of Dashboard: Session continues, no re-login needed
end
rect rgb(55, 40, 40)
Note right of Browser: Stale token (secret was reset)
Dashboard->>Server: GET /api/v1/capabilities
Server-->>Dashboard: { auth_required: true }
Note right of Dashboard: hasTenantSession() = true (localStorage token exists)
Dashboard->>Server: GET /api/v1/auth/validate<br/>Cookie: ironflow_dashboard_token=eyJ...
Server-->>Dashboard: 401 (signature mismatch)
Dashboard->>Dashboard: Clear stale token (logout)
Dashboard->>Browser: Redirect to login page
end
JWT Signing Secret Persistence
Section titled “JWT Signing Secret Persistence”The JWT signing secret is resolved at startup with the following precedence:
-
Explicit config: one of two, never both (
jwtSecret and jwtSecretFile are mutually exclusive).spec.auth.jwtSecretFile(--jwt-secret-file, orIRONFLOW_JWT_SECRET_FILE): an existing private file that holds the hex key. Rotation persists to this file.spec.auth.jwtSecretin YAML, or theIRONFLOW_JWT_SECRETenv var: the key as hex, minimum 16 bytes.
With
-f file.yaml,serveignores both env vars and warns. Set the YAML field, or reference the variable as${IRONFLOW_JWT_SECRET}. -
Persisted file (SQLite only):
.ironflow_jwt_secretnext to the SQLite database, or in the working directory when no database path is set. -
Auto-generate (SQLite only): If neither exists, a 32-byte random secret is generated and written to the file for next time.
PostgreSQL has no persisted file or auto-generate step. serve refuses to start unless you set jwtSecretFile or jwtSecret.
File-backed startup requires a private regular file: no symlink/reparse point, and on Unix, ownership by the effective user with no group/other permissions. Fix the owner and set mode 0600 on the intended file; remove unsafe links without deleting their targets. Read or persistence errors prevent startup. Read-only deployments, or filesystems without hard-link support for first-time key creation, must supply an explicit key. A corrupt file is regenerated only after these trust checks pass.
With more than one node, set the same secret on every node, or a login made on one node fails on the others. The Starter, Cluster and HA Compose files pass IRONFLOW_JWT_SECRET from .env to every node. Rotating the secret requires restarting every node and signs every user out.
# Via environment variable (hex-encoded, minimum 16 bytes)IRONFLOW_JWT_SECRET="$(openssl rand -hex 32)" ironflow serve# Via ironflow.yamlspec: auth: jwtSecret: ${IRONFLOW_JWT_SECRET}User Management
Section titled “User Management”Once logged in as an admin, you can manage users through the Users page in the dashboard sidebar. You can:
- Add team members by email.
- Assign roles (
viewer,developer,admin, or a custom role by its name). The organization’s last admin cannot lose theadminrole. - Revoke access by deleting user accounts.
Every request checks the session against the current account. Deleting a user ends their sessions at once, and a role change applies on their next request.
Dev Mode
Section titled “Dev Mode”For local development, you can bypass dashboard authentication entirely:
ironflow serve --devThis skips the login screen and grants full admin access to the dashboard. See the CLI Reference for details.