feat: OPA server + Authentication server - #998
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an OPA (Open Policy Agent) sidecar integration to add JWT role/domain-based RBAC to the KMS authorization path, alongside a documentation restructuring and new integration test vectors to validate the three authorization modes (native-only, OPA-exclusive, OPA+native enforcing).
Changes:
- Add OPA client/config/input/context plumbing and wire it into
user_has_permission()withexclusiveandenforcingmodes. - Add domain stamping to objects (new
domaincolumn across DB backends +ObjectWithMetadata.domain) to support domain-scoped RBAC decisions. - Add documentation + mkdocs navigation restructure for authorization modes, plus OPA integration test vectors and auth-server provisioning helpers.
Reviewed changes
Copilot reviewed 55 out of 56 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| OPA-middleware.md | Design/implementation plan and rationale for OPA RBAC integration and domain model. |
| documentation/mkdocs.yml | Updates nav to new authorization section structure (overview + mode pages). |
| documentation/docs/configuration/authorization/index.md | New authorization overview page (modes, role model, domain model, JWT claims, OPA input). |
| documentation/docs/configuration/authorization/mode1.md | New Mode 1 documentation for native KMS permission system. |
| documentation/docs/configuration/authorization/mode2.md | New Mode 2 documentation for exclusive OPA RBAC behavior and operations. |
| documentation/docs/configuration/authorization/mode3.md | New Mode 3 documentation for dual-gate (OPA then native KMS) behavior. |
| documentation/docs/configuration/authorization.md | Removes old single-page authorization doc in favor of the new multi-page structure. |
| docker-compose.yml | Adds an opa service to compose for local/integration usage with kms.rego. |
| crate/test_kms_server/src/vector_runner.rs | Adds JWT identity support and substantial OPA/auth-server provisioning + new OPA test vectors. |
| crate/test_kms_server/README.md | Documents OPA vectors and auth-server provisioning flow for local runs. |
| crate/test_kms_server/Cargo.toml | Adds dev-deps and reqwest features needed for auth-server provisioning in tests. |
| crate/server/src/tests/test_set_attribute.rs | Updates object creation calls to include the new domain parameter. |
| crate/server/src/tests/test_modify_attribute.rs | Updates object creation calls to include the new domain parameter. |
| crate/server/src/middlewares/mod.rs | Extends AuthenticatedUser to carry roles and domain. |
| crate/server/src/middlewares/tls_auth.rs | Populates AuthenticatedUser with empty roles/domain for mTLS auth. |
| crate/server/src/middlewares/jwt/jwt_config.rs | Adds JWT roles and as_domain (alias) parsing into UserClaim. |
| crate/server/src/middlewares/jwt/jwt_token_auth.rs | Uses email or sub as username; propagates roles/domain into AuthenticatedUser. |
| crate/server/src/middlewares/ensure_auth.rs | Ensures default user injection also sets empty roles/domain. |
| crate/server/src/middlewares/api_token/api_token_middleware.rs | Ensures API-token auth sets empty roles/domain. |
| crate/server/src/main.rs | Updates test config initialization to include default OpaConfig. |
| crate/server/src/core/mod.rs | Exposes new core::opa module. |
| crate/server/src/core/opa/mod.rs | Introduces OPA integration module exports (client/config/context/input). |
| crate/server/src/core/opa/client.rs | Adds reqwest-based OPA client with fail-closed behavior. |
| crate/server/src/core/opa/config.rs | Adds OpaMode and OpaParams configuration types. |
| crate/server/src/core/opa/context.rs | Adds per-request OPA user context storage (roles/domain). |
| crate/server/src/core/opa/input.rs | Adds OpaInput struct used as the OPA decision input document. |
| crate/server/src/core/kms/mod.rs | Stores optional opa_client on the KMS struct and initializes it when configured. |
| crate/server/src/core/kms/permissions.rs | Sets per-request OPA context as a side-effect of KMS::get_user(). |
| crate/server/src/core/retrieve_object_utils.rs | Wires OPA decision into user_has_permission() with mode-dependent behavior. |
| crate/server/src/core/operations/create.rs | Stamps created objects with the creator’s domain (from OPA context). |
| crate/server/src/core/operations/derive_key.rs | Updates DB create call to include domain parameter. |
| crate/server/src/core/operations/key_ops/mod.rs | Updates ObjectWithMetadata::new test helpers for new domain field. |
| crate/server/src/config/command_line/mod.rs | Registers new CLI OPA config module. |
| crate/server/src/config/command_line/opa_config.rs | Adds --opa-url / --opa-mode CLI + env var config. |
| crate/server/src/config/command_line/clap_config.rs | Plumbs OpaConfig into the top-level clap config. |
| crate/server/src/config/params/server_params.rs | Converts CLI config into runtime opa_params and exposes it via debug output. |
| crate/interfaces/src/stores/objects_store.rs | Extends ObjectsStore::create() with a domain parameter. |
| crate/interfaces/src/stores/object_with_metadata.rs | Adds domain field + getter, and updates Display. |
| crate/interfaces/src/hsm/hsm_store.rs | Updates HSM object creation paths for new domain parameter (ignored). |
| crate/server_database/src/core/database_objects.rs | Plumbs domain through Database::create() to underlying stores. |
| crate/server_database/src/core/unwrapped_cache.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/tagging_tests.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/owner_test.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/list_uids_for_tags_test.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/json_access_test.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/find_attributes_test.rs | Updates tests for new domain parameter. |
| crate/server_database/src/tests/database_tests.rs | Updates tests for new domain parameter. |
| crate/server_database/src/stores/sql/query.sql | Adds domain column and updates insert/select queries (non-MySQL SQL). |
| crate/server_database/src/stores/sql/query_mysql.sql | Adds domain column and updates insert/select queries (MySQL SQL). |
| crate/server_database/src/stores/sql/sqlite.rs | Adds sqlite migration and CRUD updates for domain column. |
| crate/server_database/src/stores/sql/pgsql.rs | Updates Postgres insert/select to include domain. |
| crate/server_database/src/stores/sql/mysql.rs | Updates MySQL insert/select to include domain. |
| crate/server_database/src/stores/redis/redis_with_findex.rs | Updates Redis-findex store signatures and object conversion for domain (currently empty). |
| CHANGELOG/rbac_rego.md | Adds a detailed changelog entry for the OPA RBAC feature branch. |
| Cargo.lock | Updates lockfile for new deps/features used by tests and OPA integration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
serene-kitfisto-8899
left a comment
There was a problem hiding this comment.
--opa-url and --opa-mode is confusing :
I can start the KMS with opa-mode as exclusive without opa-url :
$ cargo r --bin cosmian_kms -- --opa-mode exclusive
...
opa: OpaConfig {
opa_url: None,
opa_mode: "exclusive",
},
I think that opa_url should be mandatory when opa_mode is exclusive.
The KMS should not start, inmho.
🔍 OPA Integration — Testing session notes (2026-06-23)OPA server setupStart via docker-compose: docker compose up -d opaThe
Verify OPA is serving the policy: curl -s http://localhost:8181/v1/policies # should show kms packageTest OPA policy (use lowercase operation names — matches # CryptoOfficer create → true
curl -s http://localhost:8181/v1/data/kms/allow -H "Content-Type: application/json" \
-d '{"input":{"user":"alice","user_domain":"acme.com","roles":["CryptoOfficer"],"operation":"create","object_uid":"*","object_domain":"acme.com","is_owner":false}}'
# → {"result":true}
# User create → false (create not in user_ops)
curl -s http://localhost:8181/v1/data/kms/allow -H "Content-Type: application/json" \
-d '{"input":{"user":"bob","user_domain":"acme.com","roles":["User"],"operation":"create","object_uid":"*","object_domain":"acme.com","is_owner":false}}'
# → {"result":false}
# Debug: why was it denied?
curl -s http://localhost:8181/v1/data/kms/reasons -H "Content-Type: application/json" -d '{...}'
Running KMS with OPAcargo run --bin cosmian_kms -- \
--database-type sqlite --sqlite-path /tmp/kms-data \
--opa-url http://localhost:8181 \
--opa-mode enforcing
# For local dev without a JWT auth server, bypass OPA for admin:
cargo run --bin cosmian_kms -- \
--database-type sqlite --sqlite-path /tmp/kms-data \
--opa-url http://localhost:8181 \
--opa-mode enforcing \
--privileged-users adminckms CLI ( cargo run --bin ckms -- server version
# → 5.23.0 (OpenSSL 3.6.2 7 Apr 2026-FIPS)
cargo run --bin ckms -- sym keys create # uses KMS_DEFAULT_URL or ~/.cosmian/ckms.toml
export KMS_DEFAULT_URL=http://localhost:9998🐛 Bug fixed: OPA fail-closed not enforced on create/import/register/create_key_pairRoot cause: In Fix: OPA is now checked whenever let opa_active = kms.opa_client.is_some();
if opa_active || privileged_users.is_some() {
let has_permission = user_has_permission(owner, None, &KmipOperation::Create, kms).await?;
let is_privileged = privileged_users.as_deref().is_some_and(|users| users.iter().any(|u| u == owner));
if !has_permission && !is_privileged {
kms_bail!(KmsError::Unauthorized("User does not have create access-right."))
}
}Files changed: Rights Matrix — OPA Modes × Roles × OperationsLegend: ✅ Allowed · ❌ Denied · ⊕ Same-domain only · Owner override always ✅ Mode 1 —
|
| Operation | SuperAdmin |
DomainAdmin |
CryptoOfficer |
Auditor |
User |
[] no role |
|---|---|---|---|---|---|---|
create / create_key_pair / import / register |
✅ | ⊕ | ⊕ | ❌ | ❌ | ❌ |
get / export |
✅ | ⊕ | ⊕ | ⊕ get only |
❌ | ❌ |
locate / get_attributes |
✅ | ⊕ | ⊕ | ⊕ | ⊕ | ❌ |
set/modify/delete/add_attribute |
✅ | ⊕ | ⊕ | ❌ | ❌ | ❌ |
activate / revoke / archive / recover / destroy |
✅ | ⊕ | ⊕ | ❌ | ❌ | ❌ |
rekey / rekey_key_pair |
✅ | ⊕ | ⊕ | ❌ | ❌ | ❌ |
encrypt / decrypt / sign / verify |
✅ | ⊕ | ❌ | ❌ | ⊕ | ❌ |
mac / derive_key |
✅ | ⊕ | ❌ | ❌ | ⊕ | ❌ |
mac_verify |
✅ | ⊕ | ❌ | ⊕ | ⊕ | ❌ |
list_access / query_access |
✅ | ⊕ | ❌ | ⊕ | ❌ | ❌ |
OPA down → fail-closed: all requests denied in Modes 2 & 3 (unwrap_or(false)).
Mode 3 extra gate: after OPA allows, the native KMS grant system also runs — OPA allows the operation class, KMS checks per-object access rights.
Notes generated from a Copilot CLI debugging session.
|
| Client type | Identity | roles |
domain |
OPA Modes 2/3 |
|---|---|---|---|---|
| HTTPS + JWT Bearer | JWT sub |
JWT roles |
JWT as_domain |
✅ Full RBAC |
| HTTPS + mTLS cert | Cert CN | [] |
"" |
❌ Fail-closed |
| HTTPS + API token | Token id | [] |
"" |
❌ Fail-closed |
| TCP socket (PyKMIP, Synology DSM) | Cert CN | [] |
"" |
❌ Fail-closed |
KMIP Authentication.Credential |
Ignored | Ignored | Ignored | — |
Socket-mode clients (PyKMIP, Synology DSM, PKCS#11) are denied all operations in Modes 2 and 3 unless they own the object.
Workarounds
-
Migrate to HTTPS
/kmipendpoint — send binary TTLV asContent-Type: application/octet-streamwithAuthorization: Bearer <JWT>. Full spec-compliant RBAC, no code change needed server-side. -
Use Mode 1 (Disabled) for socket clients — rely on network-level access control (firewall, VPN) and native KMS grants for object-level access.
Future improvement
Implement Credential.Ticket extraction from the KMIP Authentication header in the message body. This would allow socket clients to embed a JWT inside the KMIP protocol itself, closing the gap without requiring HTTP transport. The Ticket credential type (KMIP 2.1 Table 442) is designed for exactly this use case (Kerberos tokens, opaque security tokens).
Also noted: operation names are lowercase
Operation names sent to OPA in input.operation are lowercase snake_case ("create", "get_attributes") — not PascalCase as in the KMIP spec text. OPA policies must use lowercase or evaluation silently returns false.
Documented in documentation/docs/configuration/authorization/index.md § Known limitations.
📋 KMIP Profiles v2.1 — Compliance Analysis vs. OPA RBACChecked against KMIP Profiles v2.1 OS ( Authentication Suites defined (§3)KMIP Profiles v2.1 defines exactly two authentication suites:
Neither suite mentions JWT, Bearer tokens, or any HTTP-level authorization header. These are implementation extensions. 🔴 Compliance gap found — §3.1.3 Basic Authentication Client AuthenticityThe spec says (verbatim):
Current behavior: Cosmian KMS ignores This is a SHALL violation. When a KMIP client includes an No RBAC in KMIP Profiles either§3.1.3 explicitly punts: "The exact mechanisms determining the client identity are outside the scope of this specification." No profile in KMIP Profiles v2.1 defines:
RBAC is entirely implementation-defined at all levels of the KMIP standard stack (core spec + profiles). The OPA RBAC system in this PR is a valid and non-conflicting extension. Full compliance matrix
Recommended fix (closes both gaps)Implement
This would:
Analysis based on KMIP Profiles v2.1 OS, section 3 (Authentication Suites). The full profiles spec is at |
📋 KMIP Profiles v2.1 Compliance Audit — Authentication & AuthorizationChecked against KMIP Profiles v2.1 OS (https://docs.oasis-open.org/kmip/kmip-profiles/v2.1/os/kmip-profiles-v2.1-os.html), specifically Section 3 (Authentication Suites). Section 3 — Two Authentication Suites defined3.1 Basic Authentication Suite (TCP/TLS — used by socket server):
3.2 HTTPS Authentication Suite (used by HTTP endpoint):
🔴 Compliance gap found — §3.1.3 Client IdentityThe spec normatively states:
The Cosmian KMS currently ignores the KMIP Impact: A client conformant to the Basic Authentication Suite that embeds its identity in No RBAC in profiles either§3.1.3 explicitly defers: "The exact mechanisms determining the client identity are outside the scope of this specification." No KMIP profile defines authorization roles, RBAC, or what the server does after determining client identity. The OPA RBAC layer in this PR is a fully spec-compliant implementation-defined extension — KMIP does not constrain it. Full compliance matrix
Recommended fixImplement
Priority order for implementation:
Spec reference: KMIP Profiles v2.1 OS §3.1.3 — https://docs.oasis-open.org/kmip/kmip-profiles/v2.1/os/kmip-profiles-v2.1-os.html |
| steps: | ||
| - name: Free disk space | ||
| run: | | ||
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc |
There was a problem hiding this comment.
why killing dotnet / android and haskell ? ;-)
There was a problem hiding this comment.
how the domain is stored in pkcs#11 ?
384589b to
021310f
Compare
55f21aa to
a3d6ae5
Compare
The test relied on test_data/certificates/openssl/prime256v1.crl which is not available in all CI environments (submodule not checked out for some jobs). Replace with a self-contained tempfile::NamedTempFile write so the test is hermetic on every runner. Rephrase inline comment to avoid lychee false-positive on file:// placeholder text.
SSRF via attacker-controlled CRL Distribution Points in KMIP Validate/Import (COSMIAN-2026-020). - Add validate_crl_url() in crate/server/src/core/certificate/mod.rs: blocks private/loopback/link-local IPs and internal hostnames before any network I/O; allows HTTP and HTTPS (RFC 5280 CDPs are typically HTTP). - Rewrite get_crl_bytes() in crate/server/src/core/operations/validate.rs: - Call validate_crl_url() before every HTTP(S) fetch - Exempt kms_public_url prefix (server's own CRL endpoint is trusted) - Add reqwest::redirect::Policy::none() (no redirect following) - Add 30-second request timeout - Cap response body at 10 MiB (CRL_MAX_RESPONSE_BYTES) - Reject bare filesystem paths and file:// URIs in production - Allow file:// in #[cfg(any(test, feature = "insecure"))] for test fixtures - Use ? on .send() so network failures stay ClientConnectionError (soft-fail) - Add kms_public_url param to verify_crls() and get_crl_bytes(); pass from both validate_operation() and import_operation() via kms.params - Add 10 regression tests SR-CRL-01 through SR-CRL-10 covering all vectors - Add COSMIAN-2026-020 entry to SECURITY.md - Exclude RFC-1918/link-local test URLs from lychee link checker
SSRF via attacker-controlled CRL Distribution Points in KMIP Validate/Import (COSMIAN-2026-020). - Add validate_crl_url() in crate/server/src/core/certificate/mod.rs: blocks private/loopback/link-local IPs and internal hostnames before any network I/O; allows HTTP and HTTPS (RFC 5280 CDPs are typically HTTP). - Rewrite get_crl_bytes() in crate/server/src/core/operations/validate.rs: - Call validate_crl_url() before every HTTP(S) fetch - Exempt kms_public_url prefix (server's own CRL endpoint is trusted) - Add reqwest::redirect::Policy::none() (no redirect following) - Add 30-second request timeout - Cap response body at 10 MiB (CRL_MAX_RESPONSE_BYTES) - Reject bare filesystem paths and file:// URIs in production - Allow file:// in #[cfg(any(test, feature = "insecure"))] for test fixtures - Use ? on .send() so network failures stay ClientConnectionError (soft-fail) - Add kms_public_url param to verify_crls() and get_crl_bytes(); pass from both validate_operation() and import_operation() via kms.params - Add 10 regression tests SR-CRL-01 through SR-CRL-10 covering all vectors - Add COSMIAN-2026-020 entry to SECURITY.md - Exclude RFC-1918/link-local test URLs from lychee link checker
- ui: JoinSplitKey initialValues now uses DEFAULT_SHARE_COUNT (3) instead of a hardcoded array of 2 entries, making the initial render consistent with the constant and fixing the 'join-share-id-2' unit test failure - windows: set RUST_MIN_STACK=8388608 in cargo_test.ps1 to give test threads an 8 MB stack (matching Linux/macOS defaults); prevents the STATUS_STACK_OVERFLOW crash in integration_tests_use_ids_no_tags on debug builds where async state machines have larger stack frames
…and multi-tenancy coverage - Remove inline auth-server provisioning from vector_runner.rs (250+ lines); provisioning is now delegated to provision_opa_integration_users.sh - Mark all test_vec_opa_* tests #[ignore] (run via `mise test:opa_rbac`) - Replace silent Ok(()) skip with hard error when required env vars are absent - Add test_vec_opa_mode_exclusive_other_domain_allowed: multi-tenancy positive - Add auth_verifier multi-realm support; UI shows realm selector for multiple realms - Add GET /ui/auth_method response field auth_verifier_realms - Extend test_opa_rbac.sh Phase 3: start auth verifier, provision users, run Rust OPA tests - Add login-page-auth-method-matrix.spec.ts: 10 Playwright tests for all auth method combos - fix(clippy): use ? propagation in tests (no unwrap/expect)
…e not available on CI)
…ction The create_crls SQL query was fetched but never executed in the SQLite bootstrap transaction, causing the crls table to never be created on SQLite databases. Also regenerate log-reference.md to include new OPA and JWT log entries added in this branch.
…ain extraction Add unit tests covering every OPA module and the middleware domain/roles extraction pipeline. 407 tests pass (0 failures, 0 Clippy warnings). - from_str: all three valid values (disabled/exclusive/enforcing) - from_str: case-insensitive parsing (EXCLUSIVE, Enforcing) - from_str: unknown value returns Err with helpful message - Display: each variant formats to expected lowercase string - Default: OpaMode::default() == Disabled (backward-compat invariant) - Round-trip: Display -> from_str is identity for every variant - All 7 field names serialize with correct snake_case JSON keys - is_owner: true/false serialize as JSON booleans (not strings) - roles serializes as JSON array - Object-less op has object_uid='*' and is_owner=false - Default context is zero-privilege (empty roles, None domain) - Outside task scope returns default (no panic) - Inside OPA_USER_CONTEXT.scope() returns the scoped value - Scope does not leak after future completes - Nested scopes: inner value visible inside, outer visible outside - OpaClient::new constructs the correct /v1/data/kms/allow URL - Trailing slash on base URL is stripped before path append - Custom path prefix (non-default OPA mount) is preserved - OpaResponse: result=true, result=false, missing key, null value - Object-less op: uid='*', object_domain=user_domain, is_owner=false - Object-less op with no user domain: both domain fields default to empty string - Owner: is_owner=true, uid and domain taken from ObjectWithMetadata - Non-owner: is_owner=false - Cross-domain: object_domain comes from stored object, not user_domain - Operation names are lowercase snake_case (create, get, get_attributes...) - Roles are passed through unchanged - User identity is preserved verbatim - Default has no URL and mode='disabled' - Default mode string parses as OpaMode::Disabled - handle_auth_verifier: domain, roles, sub-as-username, missing header, absent domain - handle_jwt: domain, roles, sub fallback, email preferred over sub, absent domain - jwt_token_auth.rs: remove panicking actix_identity::Identity::extract call; handle_jwt reads directly from Authorization: Bearer header - server_params.rs: add opa_params to manual Debug impl (was missing, triggered Clippy manual_debug lint) - mode_exclusive_auditor_wrong_domain - mode_exclusive_user_wrong_domain - mode_enforcing_wrong_domain - mode_exclusive_super_admin_cross_domain (positive: SA can cross domains) Total OPA vectors: 15 Rename 0003-rbac-opa-authorization.md -> 2026-06-24-rbac-opa-authorization.md with YAML frontmatter and SUMMARY.md nav entry.
## Problem (SPIRE / kmip-go limitation) spiffe/spire#7235 (comment) documented that the Eviden KMS did not expose a way for SPIRE's UpstreamAuthority plugin to specify the TTL (validity period) when asking the KMS to sign an intermediate CA CSR. The workaround was to rely on the KMS server's global certificate_expiry_days setting and acknowledge the PreferredTtl hint as lost. ## Root cause The KMS already honours the 'requested_validity_days' Cosmian vendor attribute on CSR-based Certify requests (build_and_sign_certificate reads it at line 75 before any subject-type branch). The gap was: 1. No test proved end-to-end that the vendor attribute was applied correctly for CSR-based requests (only subject-DN-based requests were tested). 2. github.com/Cosmian/kmip-go's Certify() had no ttlDays parameter, so callers had no way to pass requested_validity_days. ## Fix ### crate/server/src/tests/crl_tests.rs — 2 new regression tests - test_certify_from_csr_with_requested_validity_days - test_certify_from_csr_default_validity ### github.com/Cosmian/kmip-go (commit e01d034) - Certify(ctx, csrPEM, caKeyUID, caCertUID, x509Ext, ttlDays int)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve the rebased Crypto Officer and CRL implementation while synchronizing the PR base branch.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the ceremony sealing-key load log and make the updater directly executable.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…by_default Merge the base branch into rbac_rego, resolving conflicts in: - permissions_store.rs: keep HEAD (rbac_rego) trait definition - clap_config.rs: keep OpaConfig import from rbac_rego - crl_tests.rs: keep rbac_rego version (more complete) - database_permissions.rs: keep HEAD ceremony verification, remove duplicate CRL methods - redis_with_findex.rs: keep HEAD find-filter logic, remove duplicate CRL impl - sqlite.rs: auto-resolved by rerere - kms/mod.rs, generate_crl.rs, revoke.rs: auto-resolved by rerere - SUMMARY.md, key_ceremony.md, log-reference.md: keep HEAD docs - lychee.toml: keep HEAD, deduplicate entries - documentation/theme: keep rbac_rego submodule ref (ahead of base) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What changed
The KMS gains a second, independent authorization layer powered by
Open Policy Agent (OPA) and the Eviden
Authentication Server. It sits on top of — and is fully decoupled from — the
existing native KMS permission system.
Three authorization modes
--opa-url--opa-modeexclusiveenforcingCustom OPA policies
Roles are stored per-user per-realm in the Authentication Server, embedded in
the JWT as a
rolesclaim (RFC 9068 §2.2.3.1), and evaluated by the Regopolicy at
test_data/opa/kms.rego(that is only an example of Rego policy):SuperAdminDomainAdminCryptoOfficerAuditorUserObject owners always have full access regardless of role. Domain isolation
is enforced via the
as_domainJWT claim stamped at object creation time.OPA input (sent on every KMIP operation)
{ "input": { "user": "alice@acme.com", "user_domain": "acme.com", "roles": ["CryptoOfficer"], "operation": "create", "object_uid": "*", "object_domain": "acme.com", "is_owner": false } }Any error or non-
trueresponse from OPA → deny (fail-closed).Ceremony super-admin interaction
The native split-key ceremony CO activation (from
feat/split_key) operatesexclusively inside the native KMS gate and is invisible to OPA. In Mode 2
(exclusive OPA) the ceremony super-admin has no effect; in Mode 3 it takes
effect only after OPA has allowed.
Closes #651