Security principles
Nine rules that keep authentication, authorization and traffic one framework as tenants, users and apps multiply. They are normative: a change that breaks one needs a written reason, not a workaround. The models behind them (MAC / ReBAC layering, the principal chain) are in security.md §2.
The goal they serve: safe by default, easy to control, easy to audit, without limiting what can be done. Every rule below is a way of getting the fourth without giving up the first three.
1. Identity is a verified claim set from a known issuer
Never a header, never a name in a request body. Humans hold Keycloak OIDC
tokens; workloads hold audience-bound service-account tokens. A component
that accepts X-Whoever: alice has no identity, it has an opinion.
Pattern: the custodian — it is handed the caller's token, verifies it against the issuer's keys, and takes the caller's name from nothing else.
2. Three questions, three answerers, no overlap
Who → Keycloak. May → OpenFGA. Is this shape allowed at all → MAC: admission policy, NetworkPolicy, CRD validation. A component that answers two of these is where the framework forks.
Who includes which groups the person belongs to: membership is an identity fact, and Keycloak is the only place it is changed. OpenFGA holds a projection of it, written only from Keycloak's events, to compute may; it never alters it, and Keycloak never decides a permission. What the rule separates is authority, not copies.
3. Few, named policy-enforcement points; everything else consumes a verdict
The gateway with its bouncer (browser and API traffic behind a session), the
publishing proxy (everything published without sign-in, in a DMZ
namespace), the director (configuration writes), the custodian (secret
writes), and the registrar (people and groups). The proxy is one of these
because it decides what is published: only an approved entry's declared paths pass, and every
perimeter entry passes through it. It verifies no caller — authMode: none
forwards for anyone, authMode: app hands the caller's own credential to the
app to check, and basic, bearer, jwt and signature are refused on a
perimeter entry because nothing verifies them. Being a PEP obliges it: it
strips every inbound identity header before forwarding and sets none,
which is the condition rule 1 attaches to trusting a header at all.
A request that reaches a workload without passing a named PEP is a bug.
Apps never decide platform questions; they receive the decision as identity
headers or an exchanged token.
A header is identity only where the path to the app cannot be bypassed: the PEP is the single route to the workload, and it strips any inbound copy of the header it sets. Where that cannot be shown, principle 1 applies and the app needs a token. The clause is the whole difference between the two rules — without it, principle 1 reads as a ban on the proxied-app pattern and principle 3 as a license for a bypassable one.
4. The authorization vocabulary grows with the API, not with use cases
One OpenFGA type per kind a PEP decides about (cluster, tenant, app, contract, …), one relation per verb a PEP exposes. A new kind ships with its
type and a case in authz/model/*/tests.fga.yaml, or it does not ship. Group
membership is in the graph only as a projection of Keycloak, written by the
operator from Keycloak's events, never edited in place. RBAC is only the assignment of
a role to a group — never a second decision path.
5. Authority is derived, never granted sideways
agent ≤ human ≤ tenant ≤ cluster, each expressed through the one above
(from cluster). Revocation is one tuple delete; the ceiling is an invariant of the model, not a setting.
An agent is a component that acts for a person who is away. It speaks only for people who linked themselves to it with their own token, receives a token of that person for one app, and the person's own right to that app is asked on every call (agentic-ai.md). Taking the right away, or the person's link, ends what the agent may do. This is the single rule that survives heterogeneity — anything new is placed in the chain, not beside it.
6. Traffic is identity-aware and closed by default
L3/4: a tenant namespace is closed by default and opens only to the traffic
its apps declare, through NetworkPolicy derived from those requirements. The
kernel, system and shared namespaces run the platform's own services: their
ingress is closed by policy, on by default, and their egress is left to those
services. L7: every route declares its authMode; none is a word someone
wrote and a reviewer can find, never an absence.
7. Audit is three logs joined by one request id
Issuer events (Keycloak), decision log (every OpenFGA check with its request id), change log (git, written only by the director). A fact in none of these did not happen. Everything else is telemetry.
The change log is signed, and the registrar keeps a record of identity changes. Keycloak's events stay in its database for thirty days and go to its stdout as JSON. The bouncer writes the decision log to stdout. Records are kept by the destination the Cluster claim names (audit-events.md).
8. Safe defaults by construction: every permissive setting is a diff
Schemas default to the strictest value (auth required, egress none, no
cross-app contracts, no waivers). A relaxation is a declared field — one that
admission can refuse and audit can grep. "What is open on this cluster?"
must be answerable by listing fields, never by reading code.
9. Control means declaring, not configuring
Who may do what — PlatformSecurityPolicy, AppGrant, egress intent —
is data in git, written through the director, read by the
PEPs. Nobody logs into anything to change a permission.
Applying them to a change. Ask, in order: which issuer signed the caller (1)? which single component decides (2, 3)? which FGA type and relation does this add (4)? where in the chain does the new principal sit (5)? which layer denies by default and what field opens it (6, 8)? which of the three logs records it (7)? is the setting a commit (9)? A change with a blank answer is not finished.