Management API authentication
How the management API authenticates and scopes callers.
How AOCore decides who you are and what you may see. Read this before the endpoint reference — most “why am I getting a 404” questions are answered here.
AOCore does not authenticate you
This is the part that surprises people. AOCore issues no credentials, validates no passwords, runs no sessions, and has no master key. Its auth layer is a consumer of identity, not a producer of it.
Every administrative request arrives carrying an X-Aoedge-Identity-Context
header: a compact JWS signed by AOEdge. AOCore verifies that signature against
AOEdge’s published JWKS and resolves the payload into an internal Principal.
That is the whole authentication path.
client → AOEdge (authenticates, signs an identity context)
→ AOCore (verifies the signature, resolves a Principal)
Consequences worth knowing:
- There is no local login endpoint to call. If you are integrating, you obtain an identity context from AOEdge and forward it.
master_keyis dead. It survives only in historical audit rows. Anything documenting it as a live credential is out of date.- The failure mode is fail-secure. An absent, unsigned, expired, or JWKS-unverifiable context is denied. There is no bypass, no degraded mode, and no “allow if the verifier is unreachable” path.
For local development the dev-edge binary mints these contexts and serves a
matching JWKS. It is triple-gated — build tag, runtime flag, separate binary —
specifically so it cannot be reached in production.
Three tiers decide what you can reach
The resolved principal carries a tier, and the tier determines the breadth of every read and write:
| Tier | Reach |
|---|---|
TierInstance | Everything, unfiltered. Cross-tenant reach preserved. |
TierOrg | Only resources carrying the principal’s own organization id. |
TierTeam | Only resources carrying the principal’s own team id. |
This is not applied as a post-fetch ownership check. The tier is compiled into a
parameterised SQL fragment (ACLReadSQL / ACLWriteSQL) that is baked into
the WHERE clause of the query itself. Rows you may not see are never
selected in the first place.
Two practical consequences:
Cross-tenant access returns 404, not 403. Deliberately. A 403 confirms
the resource exists, which is itself a disclosure. Because the ACL lives in the
WHERE clause, a resource outside your scope is indistinguishable from one that
does not exist — and that is the intended behaviour, not a bug. If you are
getting an unexpected 404 on a resource you know exists, check your tier
before you check the id.
Write scope is stricter than read scope. ACLWriteSQL requires explicit
ownership; team or organization membership alone is not sufficient to mutate a
resource. A principal that can list something may still be unable to modify it.
Endpoints marked instance-only enforce it directly via RequireInstance, which
admits TierInstance and denies everything else.
Tenant isolation is a separate axis
Scoping answers which rows; tenancy answers which schema.
AOCore supports schema-per-tenant. Each provisioned tenant has its own Postgres
schema holding a fixed set of tables (db.TenantResidentTables is the single
source of truth for which). Tenant-resident access routes through
db.WithTenantSchema, which issues SET LOCAL search_path inside the
transaction.
The rule that matters operationally: a tenant-routing failure drops the
operation and returns an error. It never falls back to public. Postgres
silently ignores a missing schema in search_path, so a misrouted write would
land in the shared schema and contaminate it rather than failing visibly — which
is precisely the outcome the rule exists to prevent.
The audit chain follows the same split. public.audit_logs carries control-plane
events; each tenant schema has its own chain starting from its own genesis. The
chains never interleave.
Every mutation is chained
Administrative mutations write an entry to an append-only, SHA256 hash-chained audit log. Each entry digests the previous entry’s hash together with the resolved principal’s actor string, so removing or editing a historical entry breaks verification for every entry after it.
This is why the API returns a before/after snapshot on updates — the pre-mutation state is captured inside the same transaction as the write, so the audit record and the data cannot disagree.
Reading an error
| Status | Usual cause |
|---|---|
401 | Missing, malformed, expired, or unverifiable identity context. |
403 | Authenticated, but the endpoint requires a higher tier. |
404 | Either genuinely absent, or outside your scope. Check your tier. |
422 | Well-formed request that fails a domain rule. |
429 | Rate or quota limit. Honour Retry-After — it reflects the upstream’s own backoff, not a fixed value. |
503 | A dependency is unavailable. temporal not configured means the workflow engine is not wired; pipeline, eval and agent runs cannot dispatch. |