v9.13.0 — authoritative principals, scoped pre-auth lookups, deployment-mode validation
Read the Migration section before upgrading. This release carries two migrations (
core/155,core/156) and several deliberate behaviour changes, including one that can stop a stack at boot ifDEPLOYMENT_MODEis not a recognised value, and one that makes rows with a blank tenancy key permanently unreachable. Full detail is inCHANGELOG.md— this page is a summary of a long entry.
Who was affected: the pre-authentication row-level-security fixes apply to deployments running the hardened least-privilege DB posture (
AXONFLOW_DB_USE_APP_ROLE=true). The authoritative-principal, tenancy-binding, egress and authentication fixes benefit every deployment.DEPLOYMENT_MODEchanges affect every self-hosted stack.
This release is the output of a cross-tenant remediation programme. The recurring shape it closes: the platform trusted values the caller supplied, in places that decided authorization, tenancy or policy outcomes.
Security
The request body decided things it should never have decided
user.roleanduser.emailwere taken from the request body on the governed request plane — a policy-evasion primitive, not merely an attribution one.user.roleis resolvable as a policy-condition field, is offered in the portal's policy builder, and appears in the shipped HIPAA PHI-access template, so a tenant policy of the form{query contains …} AND {user.role not_equals "admin"} → blockwas defeated by putting"user":{"role":"admin"}in the body. Precise severity: no enabled policy keyed onuser.roleis seeded by default, so this was latent-by-default rather than live on a stock install. Principals are now bound from the validated credential.- The governed request plane fell back to a body-supplied tenancy.
POST /api/v1/process,/api/v1/workflows/execute,/api/v1/planand/api/v1/plan/executenow return 401 without bothX-Org-IDandX-Tenant-ID. A body naming a different tenancy is now 403 — refused, not silently corrected. Callers going through the AxonFlow Agent or the customer portal are unaffected; both stamp these from the validated credential. - MAP HITL approve/reject took the approver's name from the request body, and RBI compliance actors were read the same way. Both now come from the authenticated identity.
- Workflow-control and RBI compliance routes took tenant scope from client-supplied headers and query parameters.
- MCP dynamic-policy evaluation took its tenant scope from the request body.
POST /api/v1/mcp/evaluate-policiesnow derives tenancy from the authenticated caller; naming another tenant is 403, and omitting the field — previously400— evaluates your own policies.
Authentication and exposure
- The orchestrator API required no authentication at all, and was internet-reachable on affected topologies. It now requires an internal-service credential.
- The customer portal's orchestrator catch-all forwarded any path to any authenticated session.
- The portal admin API served anonymously outside
DEPLOYMENT_MODE=saas, and in-VPC deployments could not enable admin authentication at all. - OPTIONS requests ran auth-gated handlers anonymously.
- The orchestrator's embedded Execution Viewer UI is no longer reachable by addressing the orchestrator directly, and the orchestrator is no longer published host-wide by any Compose file in this repository.
AXONFLOW_CORS_ALLOWED_ORIGINS— the portal's allowed origins are now operator-configured rather than carrying a hardcoded third-party origin with credentials.
Tenant isolation
- Org binding failed open on eight-plus governance call sites. A fail-closed choke point now makes an unscoped call unrepresentable rather than merely discouraged.
- The dynamic-policy verdict cache was tenant-blind.
- The dynamic-policy list endpoint disclosed every tenant's policies, and policy simulation disclosed the deployment-wide policy count. List and enforcement scoping now share one decision point.
- The MAS FEAT compliance module never scoped a single statement to its organization.
- MCP tool-governance plane: deployment-wide dynamic policies were silently not evaluated —
contentpolicies (the default type) were dropped before evaluation, and the evaluator understood neither thequeryfield nor theregexoperator that the policy-creation tool writes.
Least-privilege DB posture (AXONFLOW_DB_USE_APP_ROLE=true)
- The portal's pre-authentication API-key lookup ran inside the row-level-security scope it exists to discover — so it matched nothing and reported a valid key as an authentication failure. Operators rotated credentials that were never wrong.
- SCIM bearer-token authentication rejected every valid token, and the SCIM directory returned an empty directory instead of an error — identity providers acted on that as "no users" . If you run SCIM on an app-role deployment, read the SCIM section of the CHANGELOG.
- The app-role boot guard passed when the administrative pool was configured but unusable — it checked that a string was set, never that a pool was obtained.
Egress (SSRF)
- Nine SSRF egress classifiers with five distinct behaviours, unified onto one range table.
2001:db8::/32, NAT64 and 6to4 were treated as public by all nine. The HTTP connector had no socket-level egress guard at all. Four callback dialers validated the DNS answer and then dialled the hostname, which resolved again (a DNS-rebinding window). - Operator action: circuit-breaker notifications and orchestrator webhooks now refuse ranges they previously accepted. If either targets an internal address, re-check it before upgrading.
Migration
HAS MIGRATION: core/155 and core/156.
core/156makes tenancy keysNOT NULLwith a blank-stringCHECKonplans,workflows,workflow_checkpoints,execution_summariesandwebhook_subscriptions, after stamping orphaned rows with the inert sentinel__axonflow_unowned__. These are scans, not table rewrites — but all five ALTERs run in one transaction, so ACCESS EXCLUSIVE locks are held until COMMIT. Size the window for the sum of the five scans, not the largest table.- Behaviour change: a blank-tenancy row was previously readable by every tenant — that is the vulnerability. Such rows are now unreachable and unwritable. The migration does not attempt to determine an owner, so no such row is recovered to a real tenant. An execution that started before the upgrade and finishes after it will fail its final update rather than be marked completed. That window is the upgrade itself.
budgetsis deliberately not constrained — its scope lookup treats an empty org as apply-to-all, so the constraint would silently disable deployment-global spend caps.
DEPLOYMENT_MODE — do this before upgrading
- A stack whose
DEPLOYMENT_MODEis not a recognised value now REFUSES TO BOOT instead of silently applying the SaaS schema. Recognised:community,community-saas,enterprise,evaluation,in-vpc-banking,in-vpc-enterprise,in-vpc-healthcare,in-vpc-travel,invpc,saas. A leading space or a capitalised spelling now stops the agent. Check withdocker compose exec axonflow-agent printenv DEPLOYMENT_MODE. - An UNSET
DEPLOYMENT_MODEis unchanged and still selectscore/only. It is still not fatal. DEPLOYMENT_MODE=enterprisewas not a recognised mode, so every self-hosted enterprise stack applied the SaaS schema. It is now an alias forin-vpc-enterprise. Nothing is dropped — the industry tables your stack acquired through the old fallback stay where they are and simply stop being re-selected. No corrective migration drops them.- On a FRESH install,
DEPLOYMENT_MODE=enterpriseno longer creates the industry tables, and the compliance routes that read them will answer 500. Existing stacks keep everything. If you run a compliance module, name its vertical (in-vpc-banking,in-vpc-healthcare,in-vpc-travel) orsaas. Note the RBI kill switch is on the Decision Mode hot path and fails open on a database without its tables — the same statein-vpc-enterprisehas always been in. - AxonFlow's own E2E fixtures and demo tenants were seeded into every customer deployment and are no longer applied. Rows already present are NOT removed — removing them is an operator decision, on your schedule. See the CHANGELOG for the exact predicates.
scripts/marketplace/deploy-with-metering.shnow refuses to run without an explicitDEPLOYMENT_MODE.
Also in this release
- MCP tool-governance:
contentpolicies now govern MCP tool calls where the dynamic plane is enabled (MCP_DYNAMIC_POLICIES_ENABLED=true). Deployments on the default posture are unaffected until they opt in. Review tenantcontentpolicies before enabling — a known field with a negated operator can match on an empty value, e.g.{user.role not_equals "admin"}is true when the role is unset. axonflow_create_tenant_policynow reports real enforcement state instead of an unconditional success promise that was false on every default install.
Full detail, including the reasoning and the precise blast radius of each item, is in CHANGELOG.md.