Skip to main content

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.

ComponentVersionLicenceTest
Keycloak26.8.1Apache-2.0tests/test_iam_keycloak_real.py
SPIRE server and agent1.15.5Apache-2.0tests/test_iam_spire_real.py
OpenFGA1.22.1Apache-2.0tests/test_iam_openfga_real.py
OPA1.21.4Apache-2.0tests/test_iam_opa_bundles_real.py, tests/test_iam_fail_closed.py
SoftHSM2 (built from source)2.7.0BSD-2-Clausetests/test_iam_kms.py

Keycloak (OIDC, SAML, MFA)​

  • The realm deploy/keycloak/realm-enterprise-ai.json is 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 (userinfo 401).
  • 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/descriptor is 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, unix workload 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-ID behind the mesh proxy, EAI_AUTH_SPIFFE) to the registered service identity; an unknown SVID gets 401 unknown_spiffe_id.

OpenFGA​

  • Model deploy/policies/openfga/model.fga, tuples written by /v1/authz/tuples and by resource creation, checks per document and per action (can_read, editor, owner, can_use), contextual tuples (contextual_tuples of POST /v1/authz/check, never stored), tenant isolation by object prefix.
  • Outage: require_relation and /v1/authz/check answer 502 authz_backend_error and the audit records a deny with the reason backend_unavailable.

OPA and versioned bundles​

  • EAI_OPA_DELIVERY=bundle: POST /v1/policies/bundles packs the engine=opa policies (active ones, and draft ones for libraries such as eai.lib) with a .manifest carrying the revision and roots, signed RS256 like opa build --signing-key. OPA polls GET /v1/policies/bundles/current/bundle.tar.gz with an auditor API key and verifies the signature with the public key declared under keys.
  • 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/engines reads the revision really loaded in OPA (/v1/data/system/bundles) and reports in_sync. A bundle signed with another key is refused by OPA, which keeps the previous revision: in_sync turns false.
  • Decisions: deny, allow, obligations (dlp masking, 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_denied and the reason policy 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/simulate compares, 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:

RoleCapabilitiesNever
soc_analystalerts, detections, incidents, investigations, responses allowed by the proportionality guardpolicy writes, keys, tenant administration
ai_governance_officermodels, agents, risks, evaluations, validationinfrastructure, secrets
data_stewardsources, classification, RAG quarantine, lineage, retention (metadata)content of documents it is not a reader of
application_owneragents and applications it owns (owner relation)other people's resources
ai_platform_engineerproviders, routes, deployments, gateway, runtimeIAM, audit export
business_ownerapproval and validation of use casesadministration
support_engineerdiagnostics, health, SLO, read only operationswrites

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:

  1. The vendor PKCS#11 library, mounted in the container (EAI_KMS_PKCS11_MODULE).
  2. The token or partition label (EAI_KMS_PKCS11_TOKEN_LABEL).
  3. A reference to the user PIN: a file mounted from a secret (EAI_KMS_PKCS11_PIN_FILE), never the PIN in the environment.
  4. 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).
  5. 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.