- Security & Policies
- API Keys
API Keys
API keys are the primary authentication mechanism for Ironflow’s API. Most /api/*, /ws, and ConnectRPC (/ironflow.v1.*) endpoints require a valid API key, with the exception of public endpoints: /health, /ready, /metrics, /api/v1/capabilities, /api/v1/auth/validate, /api/v1/auth/login, and /api/v1/platform/auth/login. Any OPTIONS request
also bypasses auth, so CORS preflight works without a key.
One further route authenticates without an API key: POST /api/v1/webhooks/{id} accepts a per-source webhook ingest token (?token=ifwh_…, ADR 0048) so third-party providers that cannot send an Authorization header can deliver. An ifwh_ token is not an API key — it authenticates exactly one webhook source and carries no roles or scopes.
Unified Key Model
Ironflow uses a single API key type with RBAC-based authorization. All keys are stored in the same api_keys table and follow the same authentication path. What a key can do is determined entirely by its assigned roles, not by its type.
Keys are differentiated only by a cosmetic prefix:
| Prefix | Purpose | Typical Roles |
|---|---|---|
ifkey_ | Tenant operations (functions, events, runs, etc.) | admin, developer, viewer |
ifplatform_ | Platform/cross-tenant operations | platform_admin, platform_operator, platform_viewer |
Both prefixes go through the same auth middleware and are validated identically. The prefix is a visual hint for operators — it has no effect on authorization.
All key database IDs use the ak_ prefix (e.g., ak_550e8400-e29b-41d4-a716-446655440000). This is distinct from the display prefix shown in key listings, which uses the original ifkey_ or ifplatform_ prefix plus the first 8 hex characters (e.g., ifkey_a1b2c3d4).
Key Format
API keys use a prefix followed by 32 random hex characters (16 cryptographically random bytes):
ifkey_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 (38 chars, tenant)ifplatform_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 (43 chars, platform)Keys are stored as SHA-256 hashes in the database — the raw key is never persisted. Once generated, a key can only be verified, not retrieved.
Creating Keys
Tenant Keys
# Key inherits the creating caller's environment when --env is omittedironflow apikey create my-key
# With specific rolesironflow apikey create my-key --role role_abc123 --role role_def456
# Env-scoped key with expirationironflow apikey create my-key --env env_abc123 --expires 720h# Org-scoped keycurl -X POST http://localhost:9123/api/v1/apikeys \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-key", "role_ids": ["role_abc123"]}'
# Env-scoped keycurl -X POST http://localhost:9123/api/v1/apikeys \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-key", "env_id": "env_abc123", "expires_in": "720h"}'- Navigate to API Keys in the sidebar
- Click Create API Key
- Enter a name
- Choose scope:
- Select a specific environment from the dropdown to create an env-scoped key
- Select “All environments” to create an org-scoped key
- Optionally assign roles and set an expiration
- Click Create and copy the key immediately
Platform Keys
Platform keys authenticate cross-tenant operations (user management, tenant provisioning, impersonation).
ironflow apikey create my-platform-key --platformCreated Platform Key: my-platform-key (id: ak_h8i9j0k1)Key: ifplatform_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Save this key — it will not be shown again.curl -X POST http://localhost:9123/api/v1/apikeys \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-platform-key", "platform": true}'Platform keys are assigned platform roles (platform_admin, platform_operator, platform_viewer) which control access to platform management endpoints.
The full key value is only returned once at creation time. Store it in a secrets manager.
Environment Scoping
Tenant API keys can optionally be scoped to a single environment. When an environment-scoped key authenticates, the server resolves the full hierarchy (environment → project → organization) and injects it into the request context for downstream authorization.
ironflow apikey create staging-key --env env_staging_abc123Scoping rules:
- One environment per key — A key can be scoped to at most one environment. It cannot be scoped to multiple environments.
- Every tenant key is environment-scoped — When
--envis omitted,handleCreateAPIKeystores the creating caller’s environment on the new key (internal/server/apikey_handlers.go). A tenant key that somehow carries no environment is rejected with403 request has no environment scopeon every request, and supplyingX-Ironflow-Environment/?envthat differs from the key’s environment returns403 environment does not match API key scope. The header/query fallback applies to JWT dashboard auth only. - Platform keys — Never environment-scoped. They use impersonation (
X-Ironflow-Orgheader) to act on behalf of specific tenants. - Immutable scope — A key’s environment and organization cannot be changed after creation. To change scope, create a new key and delete the old one.
Authentication
HTTP Requests
Use the Authorization header with a Bearer prefix:
curl -H "Authorization: Bearer ifkey_a1b2c3d4..." http://localhost:9123/ironflow.v1.IronflowService/ListFunctions -H "Content-Type: application/json" -d '{}'WebSocket Connections
WebSocket connections cannot always set custom headers. The Browser SDK sends
credentials as base64url-encoded WebSocket subprotocol metadata and negotiates
only the stable ironflow.v1 protocol. The credential is not placed in the URL
or reflected in the upgrade response.
ironflow.configure({ serverUrl: "https://ironflow.example.com", transport: "websocket", auth: { token: session.accessToken },});The server still accepts ?token= from older and non-browser clients for
compatibility, but new clients should use the Authorization header or the
subprotocol mechanism. Query credentials can appear in server and proxy access
logs.
SDK Authentication
SDKs pass credentials as a Bearer token in the Authorization header on HTTP
requests. The Browser SDK uses the subprotocol mechanism above for WebSocket
upgrades.
client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123", APIKey: "ifkey_a1b2c3d4...",})import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123", apiKey: "ifkey_a1b2c3d4...",});import { ironflow } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123", auth: { token: session.accessToken },});Use a short-lived session token issued by a trusted backend. Development builds
warn if an ifkey_ environment key is configured in a browser context.
from ironflow import IronflowClient
client = IronflowClient( server_url="http://localhost:9123", api_key="ifkey_a1b2c3d4...",)
# Key lifecycle, from the same clientclient.api_keys_create(body={"name": "ci", "expires_in": "720h"})client.api_keys_list()client.api_keys_get("ak_123")client.api_keys_rotate("ak_123")client.api_keys_delete("ak_123")api_keys_create and api_keys_rotate return the plaintext key in ["key"]
— the only time the server ever sends it. Every other call returns
["key_prefix"] only.
Dashboard Authentication
The web dashboard uses JWT cookies for authentication, not API keys. Users log in via /api/v1/auth/login and receive an ironflow_dashboard_token cookie (24-hour TTL). The auth middleware tries API key auth first; if no key is present, it falls back to the JWT cookie.
Validation Flow
The auth middleware runs on every non-public request. Here is the full validation sequence:
sequenceDiagram
participant Client
participant Middleware as Auth Middleware
participant Store as Database
participant RBAC as RBAC Evaluator
participant Audit as Audit Emitter
participant Handler as Route Handler
Client->>Middleware: HTTP request
Note over Middleware: Check if public path<br/>(/health, /ready, /metrics, etc.)
alt Public path
Middleware->>Handler: Pass through (no auth)
else Protected path
Middleware->>Middleware: Extract API key from<br/>Authorization header or ?token param
alt API key present
Middleware->>Store: Lookup key by SHA-256 hash
alt Key not found
Middleware-->>Client: 401 Invalid API key
else Key found
Middleware->>Middleware: Check expiration
alt Key expired
Middleware-->>Client: 401 API key expired
else Key valid
Middleware->>Store: Load roles via api_key_roles
Middleware->>Middleware: Resolve project from environment
Middleware->>RBAC: Evaluate policy (roles, action, resource)
Middleware->>Audit: Emit authz decision (async)
alt Denied
Middleware-->>Client: 403 Insufficient permissions
else Allowed
Middleware->>Store: Update last_used_at (async)
Middleware->>Middleware: Build RequestContext<br/>(org, project, env, roles)
alt Platform key with X-Ironflow-Org header
Middleware->>Middleware: Handle impersonation flow
end
Middleware->>Handler: Forward with RequestContext
Handler-->>Client: Response
end
end
end
else No API key
Middleware->>Middleware: Check for JWT dashboard cookie
alt Valid JWT
Middleware->>Handler: Forward with JWT context
Handler-->>Client: Response
else No valid auth
Middleware-->>Client: 401 Authentication required
end
end
end
Key Lifecycle
Rotation
Rotation creates a new key (new ID, new raw value) while preserving the name, environment, expiration, and role assignments from the old key. The old key is deleted atomically.
ironflow apikey rotate ak_abc123curl -X POST http://localhost:9123/api/v1/apikeys/ak_abc123/rotate \ -H "Authorization: Bearer $API_KEY"Rotation produces a new key ID (ak_*) and a new raw key value (ifkey_* / ifplatform_*). The raw key is only printed once — save it immediately. If your systems reference the old key ID, update them along with the raw key value.
Expiration
Keys can be created with an optional expiration duration:
ironflow apikey create short-lived-key --expires 720h # 30 daysironflow apikey create ci-key --expires 24h # 1 dayThe expiration is enforced at authentication time — any request with an expired key receives a 401 API key expired response. Expired keys cannot be rotated; delete them and create a new key instead.
Deletion
ironflow apikey delete ak_abc123curl -X DELETE http://localhost:9123/api/v1/apikeys/ak_abc123 \ -H "Authorization: Bearer $API_KEY"The key is immediately revoked. Any requests using this key will start receiving 401 errors.
Listing Keys
# List all tenant keysironflow apikey list
# List platform keysironflow apikey list --platform
# JSON outputironflow apikey list --jsonID NAME PREFIX ROLES CREATED LAST USED EXPIRESak_abc123 my-key ifkey_a1b2c3d4 role_abc123 2026-01-01T00:00:00Z 2026-03-01T12:00:00Z -# List tenant keyscurl -H "Authorization: Bearer $API_KEY" http://localhost:9123/api/v1/apikeys
# List platform keyscurl -H "Authorization: Bearer $API_KEY" http://localhost:9123/api/v1/apikeys?platform=trueThe full key value is never shown in list output — only the display prefix (ifkey_ or ifplatform_ + first 8 hex chars) for identification.
Bootstrap Key
On first boot, Ironflow generates a default admin API key (ID: ak_admin_bootstrap) with the admin role. The plaintext key is written to a file (mode 0400) and the server prints the file path; it is never written to stdout or journalctl. On subsequent restarts, only the key prefix is shown.
The file path is controlled by --bootstrap-key-file:
| Deployment | Default | Notes |
|---|---|---|
| Single binary / dev | <db-dir>/.ironflow_bootstrap_key.json | Same dir as the SQLite database. |
PostgreSQL (no --bootstrap-key-file) | <os.TempDir>/.ironflow_bootstrap_key.json | Fallback whenever the storage driver is Postgres or no database path is set. Host-global and a fixed name, so two serve processes on one box share it — pass --bootstrap-key-file explicitly in multi-node deployments. |
| Meta-cluster systemd | /run/ironflow/bootstrap-key.json | Set explicitly by the unit; tmpfs (auto-clears on reboot, never persisted to disk). |
Read the key with:
cat /path/to/bootstrap-key.json | jq -r .keyThe file contains JSON: {"key":"…","key_id":"ak_admin_bootstrap","org_id":"org_default","role":"admin","dashboard_password":"…","timestamp":"…"}.
dashboard_password is the one-time dashboard admin password for admin@ironflow.local (ADR 0072). It rides the same file so it is not printed to stdout — on a container that stdout is the json-file log, where a working credential would outlive the deploy. Read it with:
cat /path/to/bootstrap-key.json | jq -r .dashboard_passwordThe field is omitted when that boot issued a key but reused an existing dashboard user, and by any server released before ADR 0072 landed. Treat absence as “not here”, not as an error. When the file carries no password, the server prints the plaintext in its startup banner instead — stdout is then the only channel.
Read it once and delete the file, the same as the key. A reader that keeps the file must strip the field instead: Ironflow Desktop re-reads this file on every session, so it removes dashboard_password on first read rather than leaving a second live admin credential in the app-data directory.
Read, delete, rotate
Read the file once, delete it immediately (rm -f /path/to/bootstrap-key.json), then rotate via ironflow apikey rotate ak_admin_bootstrap and change the dashboard password. Do not leave the file on disk — anything that can read the file as the ironflow user can take over the meta-cluster. The structured log event BOOTSTRAP_ADMIN_KEY_ISSUED carries the file path (file_path field) but never the key or the password, so central log shipping is safe to enable.
If the file write fails (read-only fs, ENOSPC, etc.), no admin key is issued at all. Since the key file is written before the ak_admin_bootstrap row is inserted, a plaintext that cannot be made durable never gets a row that would accept it. The server logs BOOTSTRAP_ADMIN_KEY_ISSUED with a file_path_error field, prints the same in its startup banner, and issues nothing. There is nothing to reset: fix the --bootstrap-key-file path and restart, and the next boot issues a key normally.
One caveat on that boot: the dashboard admin user is created before the key, but its admin role is assigned after — deliberately, so an unissued key leaves bootstrap incomplete and the next boot retries it. The password printed in that boot’s banner is live, but the account carries no role until the restart succeeds.
Roles
Keys derive all their permissions from assigned roles. See Roles & Permissions for the full permissions matrix.
Tenant roles (for ifkey_ keys):
| Role | Description |
|---|---|
admin | Full access — all read, write, and delete operations |
developer | Read and write access — can create and modify resources |
viewer | Read-only access — can list and read resources |
Platform roles (for ifplatform_ keys):
| Role | Description |
|---|---|
platform_admin | Full access to all platform operations (wildcard) |
platform_operator | Tenant management, impersonation, read access to users/keys/roles |
platform_viewer | Read-only access to all platform resources |
Custom roles can be created for fine-grained access control.
FAQ
Can a key be scoped to multiple environments?
No. A key can be scoped to at most one environment. Create separate keys per environment — no key can span environments.
Can I change a key’s environment or organization after creation?
No. A key’s scope (org, environment) and name are immutable. There is no update endpoint for keys. To change scope, create a new key and delete the old one. This prevents a distributed key from silently gaining or losing access.
What can I change on an existing key?
Only role assignments can be modified (PUT /api/v1/apikeys/{id}/roles). Everything else — name, environment, organization — is fixed for the lifetime of the key.
What’s the difference between omitting --env and using --env?
- Omit
--env: the key inherits the environment of the caller that created it. SendingX-Ironflow-Environment(or?env) for a different environment returns403 environment does not match API key scope. - Use
--env env_id: env-scoped key. The key is locked to that environment permanently.
How do I move a key from one environment to another?
You can’t. Delete the old key and create a new one scoped to the new environment.
What happens to expired keys?
Expired keys return 401 API key expired on every request. They cannot be rotated — delete and recreate. Expired keys remain in listings until explicitly deleted.