- Security & Policies
- Security Model
Security Model
Ironflow implements Always-on Authentication. Every request to the API, WebSocket, or Dashboard must present valid credentials.
Authentication Layers
Section titled “Authentication Layers”Ironflow uses a unified API key model with RBAC-based authorization:
| Layer | Protects | Credential |
|---|---|---|
| Tenant API Key | /api/*, /ws, ConnectRPC (tenant operations). | ifkey_... (Bearer Token). |
| Platform API Key | /api/* (platform/cross-tenant operations). | ifplatform_... (Bearer Token). |
| Dashboard Auth | Web 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).
Public Endpoints
Section titled “Public Endpoints”The following requests bypass authentication (internal/server/auth.go::isPublicPath):
OPTIONSon 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 bydashboardAuthMiddlewareand 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.
Zero-Config Bootstrap
Section titled “Zero-Config Bootstrap”On the first boot only, Ironflow auto-generates a secure environment:
- Default Org:
org_defaultis created. - Default Project:
proj_default_defaultis created within the default org. - Default Environment:
env_defaultis created within the default project. - Built-in Roles:
admin,developer, andviewerare initialized. - Credentials: A random admin password and a unique API Key are generated.
Organizations & Multi-Tenancy
Section titled “Organizations & Multi-Tenancy”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)
Authorization
Section titled “Authorization”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,viewerare 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_authzinput, 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_epochin the same transaction as the set. The node that took the write adopts the new epoch at once. After a ~1 s debounce (DefaultDebounceininternal/auth/policyepoch/manager.go), it writes the epoch to theSYS_policy_epochNATS 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.
Audit Trail
Section titled “Audit Trail”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.deniedrows. A decision that a monitor-mode set would have denied is anauthz.decision.would_denyrow. 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) andorg.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:
GetAuthAuditTrailConnectRPC endpoint. - Platform audit log:
GET /api/v1/platform/audit.