Identity and policy plane: operator guide
This guide covers Keycloak (A-03), SPIFFE/SPIRE (A-04), OpenFGA (A-05), OPA and signed bundles (A-06, A-07), KMS and HSM key management (A-08). The evidence is tests that start the real components from the official releases fetched by tools/iam/fetch.sh (pinned versions, sha256 verified) into /tmp/toolchains.
| Component | Version | Licence | Test |
|---|---|---|---|
| Keycloak | 26.8.1 | Apache-2.0 | tests/test_iam_keycloak_real.py |
| SPIRE server and agent | 1.15.5 | Apache-2.0 | tests/test_iam_spire_real.py |
| OpenFGA | 1.22.1 | Apache-2.0 | tests/test_iam_openfga_real.py |
| OPA | 1.21.4 | Apache-2.0 | tests/test_iam_opa_bundles_real.py, tests/test_iam_fail_closed.py |
| SoftHSM2 (built from source) | 2.7.0 | BSD-2-Clause | tests/test_iam_kms.py |
Keycloak (OIDC, SAML, MFA)
- The realm
deploy/keycloak/realm-enterprise-ai.jsonis imported as is (the LDAP federation is removed in the test, no directory server). The authorization code flow with PKCE S256 is driven over HTTP, the token is accepted by the Control Plane (EAI_OIDC_ISSUER, discovered JWKS), refreshed, then logout revokes the refresh token (invalid_grant) and the session (userinfo401). - MFA: the platform administrator carries the required action
CONFIGURE_TOTP; on the next login the password alone does not finish the login, a wrong code is refused, a valid TOTP code (HmacSHA1, 6 digits, 30 s, codes not reusable) passes. - SAML: the client
https://<apps-host>/saml/metadata(RSA-SHA256 signed assertions, POST) is declared in the realm; the IdP descriptor/realms/enterprise-ai/protocol/saml/descriptoris validated (SSO POST, SLO, RSA certificate of 2048 bits minimum). A full SAML browser login is not driven by the tests. - Resolved identity:
GET /v1/identity/whoami.
SPIFFE and SPIRE
- Join token node attestation,
unixworkload attestation (binary path), one registration entry per workload, X.509 SVIDs fetched through the Workload API. eai.domains.identity.spiffe: a TLS 1.3 context that requires the peer certificate chained to the bundle, then authorizes the SPIFFE ID against an allow list (deny by default). A valid SVID outside the list, a missing client certificate or an unexpected server are refused.- The verified ID is resolved by the Control Plane (
X-SPIFFE-IDbehind the mesh proxy,EAI_AUTH_SPIFFE) to the registered service identity; an unknown SVID gets 401unknown_spiffe_id.
OpenFGA
- Model
deploy/policies/openfga/model.fga, tuples written by/v1/authz/tuplesand by resource creation, checks per document and per action (can_read,editor,owner,can_use), contextual tuples (contextual_tuplesofPOST /v1/authz/check, never stored), tenant isolation by object prefix. - Outage:
require_relationand/v1/authz/checkanswer 502authz_backend_errorand the audit records a deny with the reasonbackend_unavailable.
OPA and versioned bundles
EAI_OPA_DELIVERY=bundle:POST /v1/policies/bundlespacks the engine=opa policies (active ones, and draft ones for libraries such aseai.lib) with a.manifestcarrying the revision and roots, signed RS256 likeopa build --signing-key. OPA pollsGET /v1/policies/bundles/current/bundle.tar.gzwith an auditor API key and verifies the signature with the public key declared underkeys.- Typical OPA configuration:
services.cp.url = https://<control-plane>/v1/policies/bundles,bundles.eai.resource = current/bundle.tar.gz,bundles.eai.signing.keyid = <EAI_POLICY_BUNDLE_KEY_ID>. GET /v1/policies/enginesreads the revision really loaded in OPA (/v1/data/system/bundles) and reportsin_sync. A bundle signed with another key is refused by OPA, which keeps the previous revision:in_syncturns false.- Decisions: deny, allow, obligations (
dlpmasking,approval) returned by the PEP/v1/policies/evaluate.
Deny by default and fail closed
- OPA stopped: the sensitive action (tool call) is refused with 403
policy_deniedand the reasonpolicy engine unavailable: <policy>, the decision is recorded and audited. - OpenFGA stopped: 502
authz_backend_error, audited deny. - Audit store unavailable: the audit write is part of the mutation transaction, so delegation of rights and policy publication fail and leave nothing behind; reads that are not audited (policy list) keep answering.
Simulator and history
- Every create, update and delete appends an immutable revision (
GET /v1/policies/{id}/revisions,.../revisions/diff). POST /v1/policies/simulatecompares, input by input and on the last N recorded decisions, the active policies and the same set with the candidate; nothing changes, the run is audited.
Role separation
Tested matrix (tests/test_iam_role_matrix.py): only platform_admin creates a tenant; tenant_admin and security_officer write policies, relations and service identities; auditor reads audit, decisions and engine status without changing anything; developer simulates and reads history without publishing; viewer reaches none of these. The requester of an approval can never decide it (separation_of_duties), nor can the auditor.
Roles of the reference architecture
Fifteen roles (eai.auth.ROLES, Keycloak realm roles, migration V161). The seven business roles added:
| Role | Capabilities | Never |
|---|---|---|
soc_analyst | alerts, detections, incidents, investigations, responses allowed by the proportionality guard | policy writes, keys, tenant administration |
ai_governance_officer | models, agents, risks, evaluations, validation | infrastructure, secrets |
data_steward | sources, classification, RAG quarantine, lineage, retention (metadata) | content of documents it is not a reader of |
application_owner | agents and applications it owns (owner relation) | other people's resources |
ai_platform_engineer | providers, routes, deployments, gateway, runtime | IAM, audit export |
business_owner | approval and validation of use cases | administration |
support_engineer | diagnostics, health, SLO, read only operations | writes |
Technical administrators (platform_admin, tenant_admin) have no implicit access to business content: documents,
RAG search and memories depend on the resource ACL, and the refusal is audited.
Proof: services/control-plane/tests/test_iam_role_matrix.py.
KMS and HSM: what the client must provide
EAI_KMS_PROVIDER=openbao uses OpenBao Transit. EAI_KMS_PROVIDER=pkcs11 uses the client HSM through python-pkcs11 (MIT). To negotiate with the client:
- The vendor PKCS#11 library, mounted in the container (
EAI_KMS_PKCS11_MODULE). - The token or partition label (
EAI_KMS_PKCS11_TOKEN_LABEL). - A reference to the user PIN: a file mounted from a secret (
EAI_KMS_PKCS11_PIN_FILE), never the PIN in the environment. - The keys, created by the client, sensitive and non extractable: an AES-256 key
<label>(CKM_AES_CBC_PAD), a generic secret<label>.mac(CKM_SHA256_HMAC, encrypt then MAC), an EC P-256 signing pair (CKM_ECDSA_SHA256). - A check that these mechanisms are allowed on the partition (some HSMs restrict CBC or require GCM: validate before go-live).
Any HSM error raises KmsError (code kms_unavailable): the caller fails closed. The PKCS#11 path is proven against a real SoftHSM2 token.