Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Security Model

Ironflow implements Always-on Authentication. Every request to the API, WebSocket, or Dashboard must present valid credentials.

Ironflow uses a unified API key model with RBAC-based authorization:

LayerProtectsCredential
Tenant API Key/api/*, /ws, ConnectRPC (tenant operations).ifkey_... (Bearer Token).
Platform API Key/api/* (platform/cross-tenant operations).ifplatform_... (Bearer Token).
Dashboard AuthWeb UI (/).Email + password → JWT cookie session.

Both ifkey_ and ifplatform_ keys are stored in the same api_keys table and use the same authentication path. The prefix is a cosmetic distinction — authorization is determined by assigned roles. See API Keys for details.

Dashboard login exchanges email + password (bcrypt-verified) for a JWT cookie at /api/v1/auth/login; subsequent dashboard requests authenticate via that cookie. The JWT signing secret is persisted to .ironflow_jwt_secret next to the database so sessions survive restarts (override via IRONFLOW_JWT_SECRET).

The following requests bypass authentication (internal/server/auth.go::isPublicPath):

  • OPTIONS on any path (CORS preflight).
  • /health (liveness probe).
  • /ready (readiness probe: checks the database, NATS and the engine streams).
  • /metrics (Prometheus metrics, when enabled).
  • /api/v1/capabilities (feature discovery).
  • /api/v1/auth/login (dashboard login).
  • /api/v1/auth/logout (expires the dashboard session cookie).
  • /api/v1/auth/validate (JWT validation — stale session detection).
  • /api/v1/platform/auth/login (platform login).

The two login routes and logout set or clear the session cookie, so they refuse a cross-site browser post with 403 (http.CrossOriginProtection). A caller that is not a browser sends no Sec-Fetch-Site or Origin header and is not affected.

  • /api/v1/files/signed (signed file URLs — the ?token= value is the credential).
  • Dashboard SPA shell — any path that is not prefixed by /api/, /ws, or /ironflow.v1. is served by the embedded React app. The HTML/JS bundle is public; the app itself is gated by dashboardAuthMiddleware and the JWT cookie before any data API is reachable.

One further route skips the middleware without being in isPublicPath: POST /api/v1/webhooks/{id} carrying an ifwh_-prefixed token query parameter (ADR 0048). Third-party providers cannot send an Authorization header, so the middleware waives its own check and handleWebhook does the constant-time validation against the source row instead. A request that reaches the handler this way is not yet authenticated. Anything that is not an ifwh_ token — an ifkey_ included — falls through to the normal credential path.


On the first boot only, Ironflow auto-generates a secure environment:

  1. Default Org: org_default is created.
  2. Default Project: proj_default_default is created within the default org.
  3. Default Environment: env_default is created within the default project.
  4. Built-in Roles: admin, developer, and viewer are initialized.
  5. Credentials: A random admin password and a unique API Key are generated.

Ironflow supports multi-tenant isolation via Organizations. Each organization is a fully isolated boundary with its own API keys, roles, projects, environments, and policies.

Resources are namespaced using IRN (Ironflow Resource Names) with the following 7-segment structure:

irn:ironflow:{org_id}:{project_id}:{resource_type}:{env}:{resource_id}

IRN patterns support wildcards (*) for flexible policy matching. For example, irn:ironflow:*:*:function:env_default_prod:* matches all functions in any organization’s and project’s env_default_prod environment. The {env} segment carries the environment ID (env_*), not its display name.

Tenant provisioning creates:

  • An organization record
  • A default project
  • Built-in roles (admin, developer, viewer)
  • A default environment (within the default project)

Ironflow ships a single closed-source build with no tier gating (ADR 0015, 2026-05-07), under the Ironflow License (FSL v1.1 with an Apache-2.0 future grant). There is no Enterprise/Core split. The embedded Rego engine, the policy-set API, the epoch-bump pipeline, and the self-lockout preflight all ship in the binary.

Key capabilities:

  • Built-in roles — admin, developer, viewer are seeded at bootstrap and immutable in normal operation.
  • Custom roles — tenant admins can define additional roles scoped to their organization on top of the three built-ins.
  • Rego policy set with deny-wins semantics (enforcement is opt-in). Each organization has one set of Rego files. The set can only subtract permissions that an L1 role already granted. It runs in an embedded OPA engine (Rego v1) with Envoy ext_authz input, and fails closed on any error. See Authorization policy sets for the two-layer model, the input and the decision contract.
  • Monitor mode. A new set records what it would deny and blocks nothing, until you switch it to enforce.
  • Epoch-based cache invalidation. Every policy-set write increments the organization’s policy_epoch in the same transaction as the set. The node that took the write adopts the new epoch at once. After a ~1 s debounce (DefaultDebounce in internal/auth/policyepoch/manager.go), it writes the epoch to the SYS_policy_epoch NATS KV bucket and broadcasts it over core NATS, so an admin saving repeatedly collapses to one cluster-wide broadcast. The store is the authority. Every node re-reads it for each cached organization every 30 s (DefaultRefetchInterval), so a node that misses a broadcast heals within that interval. A node reads the KV value only on a cold miss whose store read failed. A node that sees a new epoch compiles the new set on the next request. A role change does not increment the epoch. Each API request reads the caller’s roles from the store when it authenticates, so a role change applies to the caller’s next request.
  • No decision cache. Each node compiles a set once for each version and evaluates it on every request, with a 50 ms limit.

See Custom Roles & Policy Sets for the full how-to guide.

Every Ironflow deployment records security-relevant activity in the audit log. That is separate from per-function execution recording, which is opt-in. The AuthAuditEmitter interface is satisfied in production by AuthAuditRecorder, wired unconditionally at server start. NoopEmitter exists as the pre-SetEmitter default and as a test seam; it is not the production runtime path.

Tracked events:

  • Authorization decisions. Denied decisions are always written as authz.decision.denied rows. A decision that a monitor-mode set would have denied is an authz.decision.would_deny row. Both carry the policy-set version and mode. See Audit Logging.
  • CRUD on API keys, roles, and the policy set — captures who changed what and when.
  • Platform events — platform user lifecycle (created/updated/deleted), impersonation events, and platform role changes.
  • Tenant API key lifecycle — creation and revocation.
  • Secret mutations — create, update and delete (secret.created, secret.updated, secret.deleted). The row records the secret name, never its value.
  • Webhook source creation and customer-data exports — webhook.source.created (no secret material in the row) and org.export.
  • Environment lifecycle — environment-scoped create/update/delete events.

Control-plane changes (functions, projections, event schemas, consumer groups, deployments), cluster events and the opt-in KV events (IRONFLOW_AUDIT_KV_EMIT=1) are in the full list in Audit Logging.

Query surfaces:

  • Tenant audit log: GetAuthAuditTrail ConnectRPC endpoint.
  • Platform audit log: GET /api/v1/platform/audit.