Skip to content

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 DB + NATS).
  • /metrics (Prometheus metrics, when enabled).
  • /api/v1/capabilities (feature discovery).
  • /api/v1/auth/login (dashboard login).
  • /api/v1/auth/validate (JWT validation — stale session detection).
  • /api/v1/platform/auth/login (platform login).
  • 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.

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. Built-in Roles: admin, developer, and viewer are initialized.
  4. 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:prod:* matches all functions in any organization’s and project’s production environment.

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 (ADR 0015, 2026-05-07). There is no Enterprise/Core split — the CEL policy engine, custom roles, and the full audit recorder are all wired unconditionally in cmd/ironflow/serve.go.

Key capabilities:

  • Built-in rolesadmin, 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.
  • CEL policy engine with deny-wins semantics — a tenant-authored CEL policy can only subtract permissions an L1 role already granted. See Authorization Policies (CEL) for the two-layer model, the canonical request/subject shape, and the audit hash chain.
  • Epoch-based cache invalidation — every role or policy mutation increments the organization’s policy_epoch. The bump is debounced ~1 s (DefaultDebounce in internal/auth/policyepoch/manager.go) so an admin saving repeatedly collapses to one cluster-wide broadcast over the SYS_policy_epoch NATS KV bucket, with PG as the durable backup.
  • Decision caching — a per-node 16,384-entry LRU of CEL decisions (DefaultDecisionCacheSize in internal/auth/rbac/cel/evaluator.go), plus a 4,096-entry compiled-program cache. Caches self-purge on the next lookup after an epoch bump.

See Custom Roles & CEL Policies for the full how-to guide.

Every Ironflow deployment records a full audit trail of security-relevant activity. 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 — every Layer 2 DENY (and audit-write failures on the DENY path) writes a row to policy_decisions with a SHA-256 hash chain link — see policies.md for the chain construction.
  • CRUD on API keys, roles, and policies — 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.
  • Environment lifecycle — environment-scoped create/update/delete events.

Query surfaces:

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