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.
Auth server no longer emits as_domain; the realm ID is now only in as_rid. Add as_rid as an additional alias so both old tokens (as_domain) and new tokens (as_rid only) are accepted during the transition window.
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 |
Two-phase test design: - Phase 1: Query OPA /v1/data/kms/allow directly (no KMS needed) to validate all roles × operations × domain combinations from kms.rego. - Phase 2: Start KMS with --features insecure + --opa-mode enforcing and verify the full HTTP → KMS → OPA stack: - Missing JWT → HTTP 401 (auth middleware) - OPA-allowed ops → HTTP 200 + KMIP ResultStatus "Success" - OPA-denied ops → HTTP 200 + KMIP ResultStatus "OperationFailed" - Cross-domain destroy → denied (tested on an existing object) Key design decisions: - --features insecure: KMS accepts unsigned JWTs so no auth server is needed in CI; JWTs are crafted inline with base64url(). - OPA runs as a standalone Docker container on port 8182 (avoids conflict with docker-compose OPA on 8181). - TOML config uses correct [idp_auth] / jwt_auth_provider = ["..."] format (flat Vec<String>, not a table array). - HTTP assertions are separated by layer: 401 only for auth failures; KMIP body checked for OPA decisions. Wired into: - .github/scripts/nix.sh (opa_rbac test type + WITH_CURL=1) - .github/workflows/test_all.yml (matrix entry for both fips/non-fips) - .github/scripts/test/test_all.sh (after OTEL step) - .github/workflows/test_opa_rbac.yml (standalone workflow) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cing mode In OpaMode::Enforcing, user_has_permission() previously queried OPA (allowing the op) and then fell through to the legacy DB-grant check. For object-less operations (Create, CreateKeyPair, Import, Register), owm=None so id is set to "*" and list_user_operations_on_object returns an empty list — causing a false denial even when OPA allows. Fix: after OPA allows, return Ok(true) immediately when owm=None. No DB grant can exist for an object that hasn't been created yet, so OPA's decision is authoritative for these creation operations. Belt-and-suspenders (legacy grant also required) is preserved for operations on existing objects (owm=Some). Verified: all 29 OPA RBAC tests now pass (21 Phase 1 policy + 8 Phase 2 integration), including SuperAdmin/DomainAdmin/CryptoOfficer can create keys, and Auditor/User/no-role are correctly denied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
workflow_dispatch requires the workflow file on the default branch. Add push: branches: [rbac_rego] so the standalone workflow runs on every push to the feature branch without waiting for a merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three bugs preventing OPA vector tests from passing with a real OPA server
and auth server:
1. Argon2 param mismatch (HTTP 401 on user provisioning)
The auth server uses argon2 v0.4.1 (m=4096, t=3, p=1); the KMS workspace
uses argon2 v0.5.3 with different defaults (m=19456, t=2, p=1).
Hardcode v0.4.1 params in setup_auth_server_for_opa() so provisioned
password hashes match what the auth server expects.
2. DELETE-then-POST idempotency for user provisioning
Re-running tests after a partial failure left stale users in the auth
server DB with wrong-params hashes. Change provisioning to DELETE the
user first, then POST, making the setup truly idempotent.
3. OPA denied server config (port conflict + Google CSE + privileged owner)
- cert_auth.toml uses port 9999; ONCE_VECTOR_CERT_AUTH already binds it.
Added cfg.http.port = 13001 for the OPA denied server.
- cert_auth.toml has google_cse_enable = true; on startup KMS tries to
create the CSE RSA key as the default user (no OPA roles) → denied in
enforcing/exclusive mode. Disabled Google CSE in the denied server patch.
- Cert-auth users carry no JWT roles, so OPA denies their Create operation.
The denied test vector owner (owner.client\@acme.com) needs to create a
key as setup. Added that cert CN to privileged_users so Create bypasses
OPA for the owner while OPA still correctly denies the non-owner cert user
for all subsequent Get/Destroy operations.
All 11 OPA vector tests now pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three interrelated issues prevented `bash .github/scripts/nix.sh --variant fips test opa_rbac`
from completing inside the pure Nix FIPS shell:
1. **docker not in PATH**: The pure nix-shell strips system PATH. Resolve the docker
binary path *before* entering the shell and append only the docker directory (not
prepend it) to PATH so the Nix cargo/rustc take precedence over older system versions.
2. **mold linker CXXABI mismatch**: User `~/.cargo/config.toml` settings are not
overridable via env vars (they are additive); cargo always merges them. The
system `/usr/bin/ld.mold` links against a newer libstdc++ (CXXABI_1.3.15) that
the Nix gcc-13.3.0-lib does not provide. Fix: add `pkgs.mold` to shell.nix
buildInputs so the Nix-native mold (which IS compatible with the Nix libstdc++) is
found before `/usr/bin/ld.mold`. Also add `CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=cc`
and `RUSTC_WRAPPER=""` to handle clang/sccache user overrides.
3. **libz.so.1 / libcurl FIPS ABI mismatch**: Two separate issues:
- The KMS binary built in the FIPS nix-shell has a stale RUNPATH pointing to
the session-specific nix-shell temp dir. Fix: add `${pkgs.zlib}/lib` to
`LD_LIBRARY_PATH` in the shell.nix shellHook (both FIPS and non-FIPS variants).
- The system libcurl (needed by curl for HTTP calls) was linked against OpenSSL 3.3+
but LD_LIBRARY_PATH exposed FIPS 3.1.2 → missing OPENSSL_3.2.0 symbols.
Fix: use `env -u LD_LIBRARY_PATH -u LD_PRELOAD curl` for all plain-HTTP calls
(OPA health check + KMIP integration test helpers).
Result: Phase 1 (21 OPA policy assertions) and Phase 2 (8 KMS integration tests) all
pass inside the pure FIPS nix-shell.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cargo requires the boolean env var to be the string 'true' or 'false'; the value '1' caused: error in environment variable `CARGO_NET_OFFLINE`: provided string was not `true` or `false` Broke Phase 2 of the opa_rbac CI job on GitHub Actions runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
--opa-url/--opa-modeCLI flags (env varsKMS_OPA_URL,KMS_OPA_MODE) to point the KMS at an OPA sidecar.rolesandas_domainclaims from incoming requests into a per-requestOpaUserContext(now backed bytokio::task_local!for correct async isolation) and propagates them to OPA without threading through all call signatures.domaincolumn to the objects table (SQLite, PostgreSQL, MySQL, Redis-findex) with automatic schema migration; objects are stamped with the creator's domain at creation time.kms.regoRego policy with role-based, domain-scoped rules forCryptoOfficer,Auditor,DomainAdmin, andUserroles;super_adminis intentionally decoupled from OPA (native KMS gate only).setup_auth_server_for_opa()helper that provisions a live Cosmian auth server with five test users via REST.audclaim and to fall back tosubwhenemailis absent, enabling compatibility with the Cosmian auth server.argon2dev-dependency intest_kms_serverto the workspace version (0.5) and adds thepassword-hash+allocfeatures needed for password hashing in test helpers.kms.regoto matchKmipOperation::Displaylowercase snake_case output (e.g."get_attributes"not"GetAttributes").