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_key is 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:

TierReach
TierInstanceEverything, unfiltered. Cross-tenant reach preserved.
TierOrgOnly resources carrying the principal’s own organization id.
TierTeamOnly 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

StatusUsual cause
401Missing, malformed, expired, or unverifiable identity context.
403Authenticated, but the endpoint requires a higher tier.
404Either genuinely absent, or outside your scope. Check your tier.
422Well-formed request that fails a domain rule.
429Rate or quota limit. Honour Retry-After — it reflects the upstream’s own backoff, not a fixed value.
503A dependency is unavailable. temporal not configured means the workflow engine is not wired; pipeline, eval and agent runs cannot dispatch.