Skip to main content

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.