| Boundary |
Enforced by |
| Internet → cluster |
Nothing inbound. cloudflared dials out; every tunnel hostname is behind Access (service token only). The public hostnames are Workers. |
| Worker → origin |
Access service token (CF-Access-Client-Id / CF-Access-Client-Secret); one Access application per origin hostname, never a wildcard. |
| gateway → services |
Private network + NetworkPolicy (only backend-tier pods reach :50051). Services trust the gateway’s authenticated subject_id / requester_id and re-check ownership in SQL. |
| services → data |
One database per service; no shared schema; no cross-database foreign keys. |
Two caller types, never the same principal:
|
Subject |
Requester |
| Proves identity with |
GitHub OAuth |
client credentials (secret stored as a SHA-256 hash, compared in constant time) |
| Carries |
a sealed cookie; the edge turns it into a 15-minute bearer |
its own 1-hour bearer |
| Is introspected by |
the gateway, via auth.IntrospectToken — one indexed lookup by token hash |
the same |
| Served on |
console listener only |
machine listener only |
A token on the wrong listener is 403 wrong_principal.
- Deny by default. No rule → deny. Missing or foreign persona → deny. Unknown
subject → deny, and the gateway answers a uniform
403 so handles cannot be
enumerated. Rules only grant; there is no deny rule.
- The service that owns the data re-checks ownership. Every mutating RPC carries
the acting
subject_id, and the owning service puts it in the SQL predicate. The
gateway authenticating the caller is not a substitute.
- Tokens and secrets are stored hashed, compared in constant time, never logged.
Refresh tokens rotate on every use; reuse revokes the whole session family. Cookie
values, tokens and persona field values never appear in logs.
- No unlogged disclosure.
disclosure awaits audit.Record before answering; if
audit is down the request fails closed (503). Record is idempotent on
event_id. The audit write is never asynchronous or best-effort.
- Cross-service references are plain UUIDs, validated by RPC at write time (rule →
persona via
identity, rule → requester via auth), so an invalid rule is never
stored. At read time a dangling reference is a deny, never an error.
- Nothing in the deployment exposes the cluster: no LoadBalancer or NodePort
service, no Ingress, no hostPort.
| Secret |
Where |
Never |
SESSION_KEY, Access service token |
afixo-api Worker secrets |
in afixo-web, which holds no secrets at all |
| Database URLs, GitHub OAuth app credentials |
Kubernetes Secrets per namespace |
in files that are tracked |
| Requester secrets, all tokens |
Postgres, as SHA-256 hashes |
in plaintext anywhere server-side |