diff --git a/.github/scripts/benchmarks/run_benchmarks_load_tests_flamegraph.sh b/.github/scripts/benchmarks/run_benchmarks_load_tests_flamegraph.sh
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/.github/scripts/ci.sh b/.github/scripts/ci.sh
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/CHANGELOG/develop.md b/CHANGELOG/develop.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/CHANGELOG/feat_perfs_aws.md b/CHANGELOG/feat_perfs_aws.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/CHANGELOG/rbac_design.md b/CHANGELOG/rbac_design.md
new file mode 100644
index 0000000000..db033cc684
--- /dev/null
+++ b/CHANGELOG/rbac_design.md
@@ -0,0 +1,34 @@
+# Changelog — branch `rbac_design`
+
+## Features
+
+- Add `RbacConfig` and `RbacParams` configuration structs for RBAC/OPA authorization
+- Add `tenant_id` column to `objects` table (schema migration for SQLite/PostgreSQL/MySQL)
+- Add startup cross-validation: RBAC mode requires IdP auth, bundle path/URL, and non-empty claim paths
+- Implement Policy Bundle Manager: load, validate (strict Regorus compilation), and hash `.rego` bundles
+- Implement Policy Evaluator: `ArcSwap`-backed Regorus engine with atomic hot-reload and fail-closed semantics
+- Implement Policy Input Builder: `PolicyInput` struct matching OPA input contract
+- Implement RBAC Audit Logger: structured `tracing::info!` events with typed fields
+- Ship default policy bundles: algorithm-only (non-RBAC) and full RBAC (super-admin/admin/operator/auditor)
+- Wire `PolicyEvaluator` into the `KMS` struct with automatic initialization at startup
+- Add `ckms server migrate-tenants` CLI command for tenant_id backfill before RBAC enablement
+- Add `regorus`, `arc-swap`, `notify` workspace dependencies
+- Implement three-tier RBAC enforcement:
+ - Tier 1: `dispatch.rs` pre-dispatch hook for non-object operations
+ - Tier 2: `retrieve_object_utils.rs` object-level authorization via policy
+ - Tier 3: `/access/grant` and `/access/revoke` inline enforcement
+- Extend JWT `UserClaim` with dynamic claim extraction (dot-notation paths for roles/tenant)
+- Bypass legacy `algorithm_policy.rs` when Rego evaluator is active
+- Implement hot-reload file watcher (notify crate, cross-platform)
+- Implement remote bundle polling with JSON manifest support
+- Add `POST /admin/migrate-tenants` server-side REST endpoint
+- Add RBAC step to interactive configuration wizard
+
+## Documentation
+
+- Update `CONTEXT.md` with 16 resolved design decisions from grilling session
+- Add ADR 0003: Always-Rego algorithm enforcement
+- Add ADR 0004: Super-admin role for cross-tenant access
+- Add `documentation/docs/configuration/rbac.md` RBAC documentation page
+- Register RBAC page in `documentation/mkdocs.yml`
+- Add `test_data/vectors/rbac/README.md` documenting planned integration test vectors
diff --git a/CONTEXT.md b/CONTEXT.md
new file mode 100644
index 0000000000..27c4e0b967
--- /dev/null
+++ b/CONTEXT.md
@@ -0,0 +1,500 @@
+
+## Introduction
+
+### Role-Based Access Control (RBAC)
+
+Role-Based Access Control is a security model in which permissions are assigned to
+**roles** rather than to individual users. Users acquire permissions by being assigned to
+roles, and roles can be organised into hierarchies so that senior roles inherit all
+permissions of junior roles.
+
+The NIST RBAC reference model (documented in **NIST IR 7316** and standardised as
+**ANSI INCITS 359-2004**) defines three cumulative feature sets:
+
+| Level | Name | Description |
+|---|---|---|
+| RC-0 | **Core RBAC** | Users, roles, permissions, sessions. Every role is a flat collection of permissions. |
+| RC-1 | **Hierarchical RBAC** | Role inheritance graph. A senior role implicitly holds all permissions of junior roles. |
+| RC-2 | **Constrained RBAC** | Separation of duty (SoD). Mutually exclusive roles prevent privilege escalation. |
+
+This design implements **RC-1 (Hierarchical RBAC)** with the default hierarchy
+`admin ⊇ operator ⊇ auditor`. RC-2 (SoD constraints) is explicitly out of scope for
+this version.
+
+RBAC has two key compliance advantages over discretionary ACL systems:
+
+1. **Least privilege by default** — a user has exactly the permissions of their assigned
+ roles, nothing more. No permission inheritance from object ownership.
+2. **Centralised policy** — adding or removing a permission from a role immediately
+ affects all users in that role, without touching individual user records.
+
+These properties are required by standards such as **NIST SP 800-53 AC-2/AC-3** and the
+**ANSSI RGS** for systems that handle cryptographic key material.
+
+### Key Management Interoperability Protocol (KMIP)
+
+KMIP is an OASIS standard communication protocol between **key management clients** and
+**key management servers**. It defines a rich set of **Managed Object** types —
+Symmetric Keys, Asymmetric Key Pairs, Certificates, Secret Data, Opaque Data — and the
+lifecycle operations that act on them:
+
+| Operation group | Examples |
+|---|---|
+| Provisioning | Create, CreateKeyPair, Register, Import, DeriveKey |
+| Retrieval | Get, Export, Locate, GetAttributes, GetAttributeList |
+| Lifecycle | Activate, Revoke, Destroy, Archive |
+| Cryptographic | Encrypt, Decrypt, Sign, SignatureVerify, MAC, Hash |
+| Administrative | DiscoverVersions, Query, SetAttribute, DeleteAttribute |
+
+KMIP 2.1 (the primary version implemented here) is defined in
+**OASIS Standard kmip-spec-v2.1** and referenced in the local submodule at
+`kmip/v2.1/kmip-spec-v2.1-os.html`. The server also supports KMIP 1.x for
+backward compatibility with legacy clients.
+
+A critical property of KMIP is that **operations are orthogonal**: a client that can
+`Get` a key cannot necessarily `Export` it, `Encrypt` with it, or `Destroy` it. Each
+operation requires an independent authorisation grant. This maps naturally onto RBAC
+role definitions, where roles enumerate exactly which operations are permitted.
+
+### Why RBAC + KMIP?
+
+A KMS without centralised access control presents three operational risks:
+
+1. **Permission sprawl** — individual ACL grants accumulate over time, with no
+ systematic way to audit or revoke them at scale.
+2. **Algorithm drift** — key usage restrictions are hardcoded in the server binary;
+ updating them requires a software deployment rather than a policy change.
+3. **Audit gaps** — per-request authorisation decisions are implicit and not
+ systematically recorded with the policy version that produced them.
+
+RBAC with **Open Policy Agent (OPA) / Rego** policy addresses all three:
+
+- Roles replace ad-hoc grants. Changing a role definition immediately affects every
+ member, with no per-user database surgery.
+- Algorithm allowlists move into Rego policy (`data.kms.config.allowlists`), updatable
+ without redeployment.
+- Every allow *and* deny decision is emitted as an audit log entry carrying the
+ **SHA-256 hash of the active policy bundle**, making decisions attributable to a
+ specific policy version.
+
+The Rego policy engine used is **Regorus** — a pure-Rust, in-process Rego evaluator
+that requires no external sidecar, no WASM compilation step, and adds no network
+round-trip to the authorisation path.
+
+---
+
+## Problem Statement
+
+KMIP users need NIST-compatible RBAC that can be centrally managed and audited without hardwiring permissions into the KMS.
+They want policy to be expressed and updated through Open Policy Agent, support role hierarchies, enforce tenant boundaries, and apply consistently across KMIP and access-management APIs.
+
+## Solution
+
+Add an opt-in RBAC/OPA authorization layer for KMIP and access-management operations.
+Roles come from IdP claims, are expanded by Rego policy evaluated by **Regorus** (pure-Rust Rego engine).
+Decisions are fail-closed, auditable, and based on a well-defined input contract that includes subject, tenant, request context, resource, and operation parameters.
+A default, NIST-aligned policy bundle (separate for FIPS and non-FIPS) ships with the system.
+
+## User Stories
+
+1. As a security admin, I want RBAC to be NIST-compatible with role hierarchies, so that compliance requirements are met.
+2. As a security admin, I want RBAC/OPA to be opt-in, so that upgrades do not change behavior unexpectedly.
+3. As a security admin, I want OPA policy to replace built-in KMIP allowlists in RBAC mode, so that authorization is centralized.
+4. As a security admin, I want in-process Rego evaluation via Regorus, so that there is no external dependency for authorization.
+5. As a security admin, I want policy bundles to load from a local path with hot-reload, so that policy updates are fast.
+6. As a security admin, I want policy bundles to load from a remote bundle URL on a polling interval, so that centralized policy distribution works.
+7. As a security admin, I want KMS to refuse startup when RBAC is enabled but policy is invalid or missing, so that misconfiguration is detected early.
+8. As a security admin, I want policy validation to reject invalid bundles, so that enforcement is reliable.
+9. As a security admin, I want default policy bundles for FIPS and non-FIPS builds, so that algorithms match build capabilities.
+10. As a security admin, I want the default policy to include admin/operator/auditor roles, so that baseline deployments are usable.
+11. As a security admin, I want admin > operator > auditor as the default hierarchy, so that inheritance is predictable.
+12. As an operator, I want to create and import keys, so that I can provision key material.
+13. As an operator, I want to perform cryptographic operations on objects I can access, so that I can run encryption and signing workflows.
+14. As an operator, I want to destroy or revoke objects, so that I can handle lifecycle management.
+15. As an auditor, I want read-only access to locate, list, get, and get attributes, so that I can review inventory.
+16. As an auditor, I do not want export of key material, so that audit access stays read-only.
+17. As an object owner, I want to grant and revoke ACLs on my objects, so that I can delegate access.
+18. As a user, I want to view my own access lists, so that I understand what I can access.
+19. As a user, I want to check my create/privileged permissions, so that I can self-serve basic access status.
+20. As a tenant admin, I want admin permissions scoped to my tenant, so that tenants remain isolated.
+21. As a security admin, I want tenant and role claims to be configurable, so that different IdPs integrate cleanly.
+22. As a security admin, I want to pass request context (IP, TLS subject, user-agent) to policy, so that decisions can use environment signals.
+23. As a security admin, I want policy decisions to be cached for external OPA calls, so that latency stays low.
+24. As a security admin, I want OPA timeouts to fail closed, so that authorization never hangs open.
+25. As a security admin, I want the decision and reason paths to be well-defined, so that policy authors have a stable contract.
+26. As a security admin, I want audit logs for both allow and deny decisions, so that I can trace access.
+27. As a security admin, I want audit entries to include the policy bundle hash, so that decisions are attributable to policy versions.
+28. As a policy author, I want OPA input to include resource attributes and operation parameters, so that I can enforce fine-grained rules.
+29. As a policy author, I want create/import inputs bounded by a configurable allowlist of requested attributes, so that sensitive data is not overexposed.
+30. As a policy author, I want locate/list inputs to include query filters, so that query-level authorization is possible.
+31. As a security admin, I want wildcard ACL grants deprecated under RBAC, so that global grants do not bypass policy intent.
+32. As a security admin, I want ACL grants to be additive allows, so that explicit grants can permit access without roles.
+33. As a developer, I want object ownership modeled as a role that policy can still deny, so that policy remains authoritative.
+34. As a developer, I want multi-object operations authorized against each involved object, so that composite operations are safe.
+35. As a developer, I want server-wide operations authorized as global resources, so that non-object operations are governed consistently.
+36. As an integration admin, I want enterprise integration routes to keep existing auth, so that RBAC rollout does not break integrations.
+
+## Module Architecture
+
+```mermaid
+graph LR
+ subgraph Config["Config & Claim Mapping"]
+ CC[RbacConfig\nrole_claim · tenant_claim\nbundle_path / bundle_url]
+ end
+ subgraph Bundle["Bundle Manager"]
+ BM[Load · validate · hash\nhot-reload · remote poll\ndisk cache]
+ end
+ subgraph Evaluator["Policy Evaluator"]
+ PE[Regorus engine\ndata.kms.config.allowlists\nevaluate rule]
+ end
+ subgraph InputBuilder["Policy Input Builder"]
+ PIB[build_input\nsubject · request · operation\nresource · acl]
+ end
+ subgraph Enforcement["RBAC Enforcement Layer"]
+ EL[dispatch hook\nretrieve_and_authorize\nfail-closed]
+ end
+ subgraph Audit["Audit Logger"]
+ AL[allow/deny · reason\nbundle hash · user · op\nstructured log]
+ end
+
+ Config --> Bundle
+ Config --> InputBuilder
+ Bundle --> Evaluator
+ InputBuilder --> Evaluator
+ Evaluator --> Enforcement
+ Enforcement --> Audit
+```
+
+## Role Hierarchy
+
+```mermaid
+graph TD
+ SA["🌐 **super-admin** All KMIP operations · Cross-tenant scope Granted via server config, not IdP claims"]
+ A["🛡️ **admin** All KMIP operations · Grant Create Tenant-scoped"]
+ O["⚙️ **operator** Create · Import · Register Encrypt · Decrypt · Sign Destroy · Revoke · Activate"]
+ Au["🔍 **auditor** Locate · GetAttributes GetAttributeList · DiscoverVersions _(no key material)_"]
+
+ SA -- inherits --> A
+ A -- inherits --> O
+ O -- inherits --> Au
+```
+
+## Request Authorization Flow
+
+```mermaid
+sequenceDiagram
+ participant C as KMIP Client
+ participant R as Routes (Actix)
+ participant D as dispatch.rs
+ participant E as RBAC Enforcement
+ participant Reg as Regorus
+ participant DB as Database
+ participant H as Op Handler
+
+ C->>R: HTTP POST /kmip/2_1
+ R->>D: deserialised TTLV
+
+ alt Non-object op (Create, DiscoverVersions…)
+ D->>E: build_input(op, subject, null resource)
+ E->>Reg: evaluate(input)
+ Reg-->>E: allow / deny + reason
+ E-->>D: KResult<()>
+ D->>H: handle(request)
+ H->>DB: insert / query
+ H-->>D: response
+ else Object-targeting op (Get, Decrypt, Sign…)
+ D->>H: handle(request)
+ H->>DB: fetch object metadata
+ DB-->>H: ObjectWithMetadata
+ H->>E: retrieve_and_authorize(object, op, subject)
+ E->>Reg: evaluate(input with resource + acl)
+ Reg-->>E: allow / deny + reason
+ E-->>H: KResult
+ H->>DB: perform operation
+ H-->>D: response
+ end
+
+ D-->>R: TTLV response
+ R-->>C: HTTP 200 / 4xx
+```
+
+## Bundle Loading & Startup
+
+```mermaid
+flowchart TD
+ Start([KMS Startup]) --> RBACEnabled{RBAC enabled?}
+ RBACEnabled -- No --> Legacy[Legacy ACL mode]
+ RBACEnabled -- Yes --> BundleSource{Bundle source}
+
+ BundleSource -- local path --> LoadLocal[Load .rego files\nfrom directory]
+ BundleSource -- remote URL --> CheckCache{Disk cache\nexists?}
+
+ CheckCache -- No --> RefuseStart([❌ Refuse startup\nno policy available])
+ CheckCache -- Yes --> FetchRemote[Fetch from remote URL]
+
+ FetchRemote -- reachable --> ValidateRemote[Validate + SHA-256 hash]
+ FetchRemote -- unreachable --> UseCache[Use cached bundle\n+ log warning]
+
+ LoadLocal --> ValidateLocal[Validate + SHA-256 hash]
+ ValidateLocal -- invalid --> RefuseStart2([❌ Refuse startup\ninvalid policy])
+ ValidateLocal -- valid --> LoadData[Load KmipAllowlistsConfig\nas OPA data]
+ ValidateRemote -- invalid --> RefuseStart2
+ ValidateRemote -- valid --> PersistCache[Persist to disk cache]
+ PersistCache --> LoadData
+ UseCache --> LoadData
+
+ LoadData --> StartEngine[Initialise Regorus engine]
+ StartEngine --> WatchReload[Watch hot-reload / remote poll]
+ WatchReload --> Ready([✅ Server ready])
+ Legacy --> Ready
+```
+
+## Implementation Decisions
+
+- Build/modify six modules: Policy Input Builder, Policy Evaluator, Policy Bundle Manager, RBAC Enforcement Layer, Audit Logger, and Config & Claim Mapping.
+- RBAC/OPA is opt-in; legacy ACL behavior remains default when disabled.
+- **Rego Engine**: Regorus (pure-Rust Rego evaluator, `regorus` crate) is the sole policy evaluation engine; no OPA WASM, no wasmtime, no external OPA service dependency.
+- **Enforcement insertion points**: three-tier — (1) `dispatch.rs` for KMIP non-object operations (Create, DiscoverVersions, etc.); (2) a `retrieve_and_authorize` wrapper around `retrieve_object_utils.rs` for KMIP object-targeting operations; (3) an Actix middleware (or extractor) for REST access-management endpoints (`/access/grant`, `/access/revoke`, `/access/list`) that builds the RBAC input from route context and evaluates Regorus before the handler executes. No extra DB round-trip for object metadata in tier (2).
+- **ACL semantics in RBAC mode**: OPA/Regorus is the single gatekeeper. DB ACL state (owner, per-object grants) is passed as input fields to the policy; existing DB-level ACL enforcement code is bypassed when RBAC mode is active. Legacy DB ACL enforcement is preserved unchanged when RBAC is disabled.
+- **Tenant storage**: A `tenant_id` column is added to the `objects` table via schema migration. It is populated at object creation time from the creator's JWT tenant claim. For Locate queries, `WHERE tenant_id = ?` is applied at the SQL level. The server refuses to start in RBAC mode if any objects have `NULL` tenant_id. A CLI migration tool (`ckms server migrate-tenants`) assigns tenant_id to existing objects based on an admin-provided owner-to-tenant mapping file.
+- External OPA calls support configurable mTLS, bearer token, or no auth, with a short configurable timeout and fail-closed behavior. (**Not in scope — Regorus only.**)
+- Authorization Surface includes KMIP operations and access-management endpoints; enterprise integration routes (AWS XKS, Azure EKM, Google CSE, MS DKE) are fully excluded. Their internal KMIP calls bypass Regorus entirely — they are trusted paths that rely solely on their own authentication (XKS signatures, mTLS, OAuth). No synthetic subject is injected. Additionally, `DiscoverVersions` and `Query` are exempt from RBAC enforcement — they are protocol-level informational operations that always succeed regardless of policy, as they expose no key material and are needed for KMIP handshake.
+- Authorization decisions use the OPA Decision Path `data.kms.authz.allow` and Reason Path `data.kms.authz.reason`.
+- OPA Decision Output is allow/deny with optional reason; reasons are logged/audited only and not returned to clients. On deny, the server returns KMIP `ErrorReason::Permission_Denied` with a generic message ("authorization denied") — no policy details leak to the client.
+- Policy Bundle Loading supports local bundles with hot-reload and remote bundles via polling; invalid bundles are rejected and prevent startup when RBAC is enabled. On sustained remote unavailability after startup, the server continues with the last-known-good cached bundle and logs a warning on each failed poll. No automatic expiry or deny-all — the operator is responsible for monitoring staleness.
+- Default Policy Bundle ships with baseline roles (super-admin/admin/operator/auditor), default hierarchy (super-admin > admin > operator > auditor), and conservative NIST/ANSSI allowlists; separate bundles for FIPS and non-FIPS.
+- **Algorithm enforcement always via Rego**: The legacy Rust-level `enforce_kmip_algorithm_policy_for_operation` is removed. Algorithm allowlist enforcement is always delegated to the Regorus engine, regardless of whether full RBAC (roles, tenants, ACL bypass) is enabled. When RBAC is disabled, an embedded default algorithm-only policy (`include_str!` compiled into the binary) is loaded — no external bundle path required. Full RBAC mode still requires an explicit external bundle path. This ensures a single source of truth for algorithm policy and eliminates the dual-enforcement path.
+- Role Assignment Source is IdP JWT role/group claims; Role Claim Mapping and Tenant Claim Mapping are configurable claim paths; Role Expansion is performed in policy.
+- Tenant Boundary is enforced in all decisions; admin is tenant-scoped by default. A distinct `super-admin` role (above admin in the hierarchy) skips the tenant filter — Locate queries for super-admin have no `WHERE tenant_id` clause. Super-admin is granted via server config (`privileged_users` or a dedicated `super_admins` config field), not IdP claims.
+- Operation Authorization Rule requires explicit authorization per KMIP operation; Get does not imply other operations.
+- **Owner Role**: Ownership is passed as `input.acl.is_owner = true` but confers no server-side guarantee. In the default policy, ownership grants access only *within* the context of a role (e.g., operator + is_owner → accessible). A user with no roles has no access, even to objects they own. This is an intentional departure from legacy behavior where ownership always implied full access.
+- **ACL Semantics**: ACL grants (`input.acl.granted_ops`) are advisory input to the Rego policy — they carry no server-side guarantee. The default bundle honors them as additive allows, but custom policies may ignore or override them. Wildcard ACL grants are deprecated under RBAC.
+- Global Operation Resource model is used for Create and server-wide operations, with requested attributes supplied via a configurable allowlist.
+- OPA Resource Input for object operations includes object id, owner, type, state, and tags; Operation Parameter Input includes algorithm/mode/padding for crypto ops; Attribute Operation Input includes attribute names; Locate Query Input includes query filters.
+- Access-Management Input includes target user, object id, and operation list; Endpoint Input includes route identifiers.
+- **Locate/List in RBAC mode**: DB-level user filter (`find()`) is parameterized; when OPA grants a Locate query for a role with global read scope (admin/auditor), the caller passes a wildcard user to `find()` to bypass the per-user DB filter. User-scoped Locate (operators, object owners) retains the normal user filter. Tenant boundary is enforced at the DB query level (`WHERE tenant_id = ?`), not via per-object post-filtering through Regorus. The policy evaluation for Locate determines *scope parameters* (user, tenant) that are baked into the SQL query. No per-object Regorus evaluation occurs for Locate results.
+- **Authorization Audit**: Allow and deny decisions are emitted as structured `tracing::info!` events with typed fields (`user`, `operation`, `decision`, `reason`, `bundle_hash`, `resource_id`, `tenant_id`). These are exported via the existing OTEL tracing pipeline. No separate audit log file or database table — compliance queries are handled by the log aggregation backend (e.g., Grafana Loki).
+- **Privileged Users Mapping**: `privileged_users` from server config is passed as `input.subject.is_privileged` (boolean) to the Rego policy. The policy is authoritative — the default bundle grants admin-equivalent access to privileged users, but custom policies can override or restrict this. No server-side role injection.
+- Self Access Views and Self Permission Checks are permitted for users.
+- **Missing tenant claim**: when the configured JWT tenant-claim path is absent, `input.subject.tenant_id` is `null`. Policy is authoritative — the default bundle denies on null tenant; custom bundles can override (e.g. for service accounts). No server-level fallback.
+- **Auditor role operations**: Locate, GetAttributes, GetAttributeList, DiscoverVersions only — no KMIP `Get` (returns key material), no Export. User story 15's "get" refers to GetAttributes, not the KMIP Get operation.
+- **Allowlists as OPA data**: `KmipAllowlistsConfig` from `kms.toml` is always serialized to JSON and loaded into Regorus as engine-level static data (`data.kms.config.allowlists.*`), regardless of RBAC mode. This is the single source for algorithm/hash/curve/key-size restrictions. In full RBAC mode with an external bundle, the bundle may include its own data file that overrides the server config allowlists.
+- **Create-grant privilege invariant**: The default policy bundle must explicitly encode that only admins can grant the `Create` operation to others, replicating the `privileged_users` guard that is bypassed in RBAC mode. A test vector confirms a non-admin cannot delegate `Create` grants.
+- **Policy bundle format**: a directory of `.rego` files; the entry point must be `authz.rego` defining `data.kms.authz.allow`. Hot-reload watches the directory using the `notify` crate (cross-platform: inotify on Linux, kqueue on macOS, ReadDirectoryChanges on Windows); remote polling downloads an archive and unpacks to a local temp directory. Bundle hash for audit is SHA-256 over sorted per-file content hashes (filenames excluded) — renaming files without changing logic preserves the hash for audit trail continuity.
+- **Strict bundle validation**: On load/reload, all `.rego` files are compiled with Regorus. If any file references an unsupported built-in function, the bundle is rejected. This catches OPA-only built-ins (`http.send`, `opa.runtime`, `io.jwt.decode_verify`, etc.) at load time rather than failing-closed at runtime. The supported built-in surface should be documented for policy authors.
+- **Hot-reload atomicity**: The Regorus engine is held behind `ArcSwap`. On reload, a new engine is built and validated in the background; if valid, the pointer is atomically swapped. In-flight evaluations hold a clone of the old `Arc` and finish cleanly against the previous policy version. No request ever sees a partially-loaded engine.
+- **Multi-object operations**: one Regorus evaluation call per involved object; input schema is consistent across all operations. All involved objects must be individually authorized (all must allow). A per-request evaluation cache keyed by `(subject.user_id, resource.id, operation.kmip_op)` prevents redundant evaluations for the same object within recursive/composite operations (e.g., Certify touching issuer chain).
+- **Composite operation authorization**: `CreateKeyPair` is authorized as a Create-class operation via tier 1 (non-object). `DeriveKey` is authorized twice: first as Create-class (tier 1 — can the user create?), then against the parent key via tier 2 (`retrieve_and_authorize` — can the user use this parent key for derivation?). Both checks must pass.
+- **RBAC code is always compiled in** — no feature flag; runtime opt-in via config (`--rbac-enabled` / `kms.toml`).
+- **Migration is an intentional break**: Enabling RBAC mode removes the legacy `Get`-implies-all-operations shortcut. Admins must reconfigure ACL grants before enabling RBAC. A migration guide documents the required steps; no automatic ACL expansion is performed.
+- **Startup cross-validation**: All RBAC config consistency checks happen during `ServerParams` construction. Invalid combinations produce a clear error message and prevent startup. Checks include: RBAC requires IdP auth configured, RBAC requires `bundle_path` or `bundle_url`, `role_claim`/`tenant_claim` must be non-empty strings, no `NULL` tenant_id objects in DB when RBAC enabled, policy bundle must load and validate successfully.
+
+## OPA Input Contract (stable API)
+
+The input document passed to `data.kms.authz.allow` on every authorization call:
+
+```json
+{
+ "subject": {
+ "user_id": "alice@example.com",
+ "roles": ["operator"],
+ "tenant_id": "acme-corp",
+ "is_privileged": false
+ },
+ "request": {
+ "ip": "192.168.1.1",
+ "tls_subject": "CN=alice,O=Acme",
+ "user_agent": "ckms/1.0"
+ },
+ "operation": {
+ "kmip_op": "Get",
+ "algorithm": "AES",
+ "mode": "GCM",
+ "padding": null
+ },
+ "resource": {
+ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "owner": "bob@example.com",
+ "type": "SymmetricKey",
+ "state": "Active",
+ "tags": ["env:prod", "project:alpha"],
+ "tenant_id": "acme-corp"
+ },
+ "acl": {
+ "is_owner": false,
+ "granted_ops": ["Get", "Encrypt"]
+ }
+}
+```
+
+- For non-object operations (Create, DiscoverVersions, server-wide): `resource` and `acl` are `null`.
+- For Locate/List: `resource` is `null`; `operation.query_filters` carries the Locate attributes.
+- For access-management endpoints: `operation.target_user` and `operation.grant_ops` are present.
+
+**Decision paths** (must be defined in every policy bundle):
+
+- `data.kms.authz.allow` → `boolean` — true = allow, false or undefined = deny (fail-closed)
+- `data.kms.authz.reason` → `string` (optional) — logged/audited only, never returned to the client
+
+**Static data** (loaded at engine init, not per-request):
+
+- `data.kms.config.allowlists` — the `KmipAllowlistsConfig` for the build (algorithms, hashes, curves, etc.)
+
+## Example Default Policy Skeleton (Rego)
+
+```rego
+package kms.authz
+
+import rego.v1
+
+# Role hierarchy: super-admin > admin > operator > auditor
+role_inherits := {
+ "super-admin": {"admin", "operator", "auditor"},
+ "admin": {"operator", "auditor"},
+ "operator": {"auditor"},
+}
+
+# Expand roles transitively
+effective_roles(user_roles) := roles if {
+ roles := {r | some base in user_roles; some r in ({base} | role_inherits[base])}
+}
+
+roles := effective_roles(input.subject.roles)
+
+# Auditor: metadata-only (no Get/key material, no Export)
+auditor_ops := {"Locate", "GetAttributes", "GetAttributeList", "DiscoverVersions"}
+
+# Operator: create, import, crypto, destroy, revoke (on accessible objects)
+operator_ops := auditor_ops | {"Create", "CreateKeyPair", "Import", "Register",
+ "Encrypt", "Decrypt", "Sign", "SignatureVerify",
+ "Destroy", "Revoke", "Activate"}
+
+# Admin: all operations
+admin_ops := operator_ops | {"Grant", "Revoke", "DiscoverVersions", "Query"}
+
+# Algorithm allowlist check (uses static data loaded at engine init)
+algorithm_allowed if {
+ input.operation.algorithm == null # non-crypto op
+}
+algorithm_allowed if {
+ input.operation.algorithm in data.kms.config.allowlists.algorithms
+}
+
+# Tenant boundary: resource and subject must share tenant
+same_tenant if { input.resource == null }
+same_tenant if { input.resource.tenant_id == input.subject.tenant_id }
+
+# Super-admin: all operations, no tenant boundary
+allow if {
+ "super-admin" in roles
+ algorithm_allowed
+}
+
+# Main allow rule
+allow if {
+ "admin" in roles
+ same_tenant
+ algorithm_allowed
+}
+allow if {
+ "operator" in roles
+ input.operation.kmip_op in operator_ops
+ same_tenant
+ algorithm_allowed
+ # For object ops: user must own object or have an explicit ACL grant
+ object_accessible
+}
+allow if {
+ "auditor" in roles
+ input.operation.kmip_op in auditor_ops
+ same_tenant
+}
+
+# Privileged users: treated as admin by default policy
+allow if {
+ input.subject.is_privileged
+ same_tenant
+ algorithm_allowed
+}
+
+object_accessible if { input.resource == null }
+object_accessible if { input.acl.is_owner }
+object_accessible if { input.operation.kmip_op in input.acl.granted_ops }
+
+# Only admins can delegate Create grants
+allow if {
+ input.operation.kmip_op == "Grant"
+ "Create" in input.operation.grant_ops
+ "admin" in roles
+}
+
+reason := "allowed by role policy"
+```
+
+
+
+- Good tests assert observable authorization behavior (allow/deny outcomes, audit entries, and error paths) without relying on internal policy evaluation details.
+- **Two-layer testing strategy**: (1) **Rego unit tests** — load the default policy bundle into Regorus with synthetic JSON inputs, assert allow/deny outcomes directly. Fast feedback for policy TDD. (2) **Server integration tests** — boot with the default bundle, make real KMIP requests, verify end-to-end enforcement (correct HTTP error codes, audit entries emitted, tenant isolation).
+- All modules above should have tests, with emphasis on input construction, policy evaluation modes, bundle validation/reload, enforcement across KMIP and access endpoints, and audit logging.
+- Prior art includes existing KMIP policy and access control test suites, plus database permission tests; new tests should mirror their style and focus on end-to-end authorization behavior.
+
+## Out of Scope
+
+- Constrained RBAC (e.g., separation of duty) beyond core + hierarchical RBAC.
+- UI changes for policy management.
+- Reworking enterprise integration authentication flows.
+- Per-object filtering of Locate/List results.
+
+## Further Notes
+
+- ADRs: OPA replaces KMIP allowlists when RBAC is enabled; hybrid OPA evaluation mode is supported.
+- Default policies should be documented as baseline examples, not hard constraints for deployments.
+
+---
+
+## References
+
+### RBAC & Access Control
+
+| Reference | Title | URL |
+|-----------|-------|-----|
+| NIST IR 7316 | Assessment of Access Control Systems — covers Core RBAC, Hierarchical RBAC, and Constrained RBAC models | https://csrc.nist.gov/pubs/ir/7316/final |
+| NIST SP 800-162 Upd2 | Guide to Attribute Based Access Control (ABAC) Definition and Considerations (Jan 2014, updated Aug 2019) | https://csrc.nist.gov/pubs/sp/800/162/upd2/final |
+| NIST SP 800-207 | Zero Trust Architecture (Aug 2020) | https://csrc.nist.gov/pubs/sp/800/207/final |
+
+### Key Management
+
+| Reference | Title | URL |
+|-----------|-------|-----|
+| NIST SP 800-57 Pt1 Rev 5 | Recommendation for Key Management — Part 1: General (May 2020) | https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final |
+| NIST SP 800-131A Rev 2 | Transitioning the Use of Cryptographic Algorithms and Key Lengths (Mar 2019) — Rev 3 IPD posted Oct 2024 | https://csrc.nist.gov/pubs/sp/800/131/a/r2/final |
+
+### Cryptographic Algorithm Standards
+
+| Reference | Title | URL |
+|-----------|-------|-----|
+| FIPS 140-3 | Security Requirements for Cryptographic Modules (Mar 2019) | https://csrc.nist.gov/pubs/fips/140-3/final |
+| FIPS 197 | Advanced Encryption Standard (AES) — updated May 2023, no algorithm changes | https://csrc.nist.gov/pubs/fips/197/final |
+| FIPS 203 | Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM / Kyber) (Aug 2024) | https://csrc.nist.gov/pubs/fips/203/final |
+| FIPS 204 | Module-Lattice-Based Digital Signature Standard (ML-DSA / Dilithium) (Aug 2024) | https://csrc.nist.gov/pubs/fips/204/final |
+| FIPS 205 | Stateless Hash-Based Digital Signature Standard (SLH-DSA / SPHINCS+) (Aug 2024) | https://csrc.nist.gov/pubs/fips/205/final |
+| ANSSI | Recommandations de sécurité relatives aux mécanismes cryptographiques (RGS B1 / DAT-NT-028) — defines conservative algorithm and key-size baselines used in `KmipAllowlistsConfig::conservative()` | https://www.ssi.gouv.fr/uploads/2021/03/anssi-guide-mecanismes_crypto-2.04.pdf |
+
+### KMIP Specifications (OASIS)
+
+The KMS implements KMIP 1.x and 2.x. Local copies of all specs are in `kmip/` (git submodule).
+
+| Version | Status | Local path | Online |
+|---------|--------|-----------|--------|
+| KMIP 1.0 | OASIS Standard | `kmip/v1.0/kmip-spec-1.0-os.html` | https://docs.oasis-open.org/kmip/spec/v1.0/os/kmip-spec-1.0-os.html |
+| KMIP 1.1 | OASIS Standard | `kmip/v1.1/kmip-spec-v1.1-os.html` | https://docs.oasis-open.org/kmip/spec/v1.1/os/kmip-spec-v1.1-os.html |
+| KMIP 1.2 | OASIS Standard | `kmip/v1.2/kmip-spec-v1.2-os.html` | https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html |
+| KMIP 1.3 | OASIS Standard | `kmip/v1.3/kmip-spec-v1.3-os.html` | https://docs.oasis-open.org/kmip/spec/v1.3/os/kmip-spec-v1.3-os.html |
+| KMIP 1.4 | OASIS Standard + Errata 01 | `kmip/v1.4/kmip-spec-v1.4-os.html` | https://docs.oasis-open.org/kmip/spec/v1.4/errata01/os/ |
+| KMIP 2.0 | OASIS Standard | `kmip/v2.0/kmip-spec-v2.0-os.html` | https://docs.oasis-open.org/kmip/kmip-spec/v2.0/os/kmip-spec-v2.0-os.html |
+| KMIP 2.1 | OASIS Standard (**primary**) | `kmip/v2.1/kmip-spec-v2.1-os.html` | https://docs.oasis-open.org/kmip/kmip-spec/v2.1/os/kmip-spec-v2.1-os.html |
+| KMIP 3.0 | CSD 01 (draft) | `kmip/v3.0/kmip-spec-v3.0-csd01.html` | — |
+
+> **AI agent rule**: always verify section numbers, operation names, and tag values against
+> the local spec files (or the online canonical version above) before writing KMIP-related code.
+> Never rely on recalled knowledge of a specification.
diff --git a/Cargo.lock b/Cargo.lock
index b70efc3d22..c237606cd4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,7 +8,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"bytes",
"futures-core",
"futures-sink",
@@ -44,7 +44,7 @@ dependencies = [
"actix-service",
"actix-utils",
"actix-web",
- "bitflags",
+ "bitflags 2.11.0",
"bytes",
"derive_more 2.1.0",
"futures-core",
@@ -68,7 +68,7 @@ dependencies = [
"actix-service",
"actix-tls",
"actix-utils",
- "bitflags",
+ "bitflags 2.11.0",
"bytes",
"bytestring",
"derive_more 2.1.0",
@@ -631,6 +631,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -1018,7 +1024,7 @@ name = "cosmian_cng"
version = "5.23.0"
dependencies = [
"base64 0.22.1",
- "bitflags",
+ "bitflags 2.11.0",
"ckms",
"cosmian_logger",
"serial_test",
@@ -1102,7 +1108,7 @@ name = "cosmian_kmip"
version = "5.23.0"
dependencies = [
"base64 0.22.1",
- "bitflags",
+ "bitflags 2.11.0",
"cosmian_crypto_core",
"cosmian_logger",
"hex",
@@ -1313,6 +1319,7 @@ dependencies = [
"actix-session",
"actix-tls",
"actix-web",
+ "arc-swap",
"async-recursion",
"base64 0.22.1",
"chrono",
@@ -1332,6 +1339,7 @@ dependencies = [
"http 1.4.0",
"jsonwebtoken",
"native-tls",
+ "notify",
"num-bigint-dig",
"openssl",
"openssl-sys",
@@ -1340,6 +1348,7 @@ dependencies = [
"opentelemetry_sdk 0.27.1",
"pem",
"proteccio_pkcs11_loader",
+ "regorus",
"reqwest",
"scratchstack-aws-signature",
"serde",
@@ -2096,6 +2105,16 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
@@ -2170,6 +2189,15 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "futures"
version = "0.3.32"
@@ -2864,6 +2892,26 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "inotify"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "inout"
version = "0.1.4"
@@ -2882,6 +2930,15 @@ dependencies = [
"hybrid-array 0.4.5",
]
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -3005,6 +3062,26 @@ dependencies = [
"syn",
]
+[[package]]
+name = "kqueue"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
+dependencies = [
+ "bitflags 2.11.0",
+ "libc",
+]
+
[[package]]
name = "language-tags"
version = "0.3.2"
@@ -3060,7 +3137,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"libc",
"redox_syscall",
]
@@ -3288,7 +3365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052"
dependencies = [
"base64 0.22.1",
- "bitflags",
+ "bitflags 2.11.0",
"btoi",
"byteorder",
"bytes",
@@ -3347,6 +3424,34 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+[[package]]
+name = "notify"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
+dependencies = [
+ "bitflags 2.11.0",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio",
+ "notify-types",
+ "walkdir",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "notify-types"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
+dependencies = [
+ "instant",
+]
+
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -3491,7 +3596,7 @@ version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"cfg-if",
"foreign-types",
"libc",
@@ -4061,7 +4166,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec",
- "bitflags",
+ "bitflags 2.11.0",
"num-traits",
"rand 0.9.4",
"rand_chacha 0.9.0",
@@ -4320,7 +4425,7 @@ version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
]
[[package]]
@@ -4361,7 +4466,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
]
[[package]]
@@ -4399,6 +4504,19 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+[[package]]
+name = "regorus"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843c3d97f07e3b5ac0955d53ad0af4c91fe4a4f8525843ece5bf014f27829b73"
+dependencies = [
+ "anyhow",
+ "lazy_static",
+ "scientific",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "reqwest"
version = "0.12.24"
@@ -4508,7 +4626,7 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"fallible-iterator 0.3.0",
"fallible-streaming-iterator",
"hashlink",
@@ -4557,7 +4675,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
@@ -4665,6 +4783,26 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "scientific"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8"
+dependencies = [
+ "scientific-macro",
+]
+
+[[package]]
+name = "scientific-macro"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -4725,7 +4863,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -5600,7 +5738,7 @@ version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"bytes",
"futures-util",
"http 1.4.0",
@@ -6063,7 +6201,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"hashbrown 0.15.5",
"indexmap 2.12.1",
"semver",
@@ -6202,7 +6340,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"widestring",
"windows-sys 0.61.2",
]
@@ -6520,7 +6658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
- "bitflags",
+ "bitflags 2.11.0",
"indexmap 2.12.1",
"log",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 2dcbc9d9c4..9a63d49c0b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -146,6 +146,7 @@ actix-tls = "3.4"
actix-web = { version = "4.12", default-features = false }
async-recursion = "1.1"
async-trait = "0.1"
+arc-swap = "1.7"
base64 = "0.22"
bitflags = "2.9"
chrono = "0.4"
@@ -178,6 +179,7 @@ libloading = "0.8"
log = { version = "0.4", default-features = false }
lru = "0.16"
native-tls = { version = "0.2", default-features = false }
+notify = { version = "7.0", default-features = false, features = ["macos_fsevent"] }
num_cpus = "1.16"
num-format = "0.4"
itertools = "0.10"
@@ -192,6 +194,7 @@ pem = "3.0"
pkcs11-sys = "0.2"
rand = "0.10"
regex = { version = "1.11", default-features = false }
+regorus = { version = "0.2", default-features = false }
reqwest = { version = "0.12", default-features = false }
scratchstack-aws-signature = "=0.10" # Must stay 0.10 for now (Feb 2026)
serde = "1.0"
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/crate/clients/clap/src/actions/kms_actions.rs b/crate/clients/clap/src/actions/kms_actions.rs
index a8e6b50635..d25aa0332a 100644
--- a/crate/clients/clap/src/actions/kms_actions.rs
+++ b/crate/clients/clap/src/actions/kms_actions.rs
@@ -19,9 +19,10 @@ use crate::{
azure::AzureCommands, bench::BenchAction, certificates::CertificatesCommands,
cng::CngCommands, console::Stdout, derive_key::DeriveKeyAction,
elliptic_curves::EllipticCurveCommands, google::GoogleCommands, hash::HashAction,
- login::LoginAction, mac::MacCommands, opaque_object::OpaqueObjectCommands,
- pkcs11::Pkcs11Commands, rng::RngAction, rsa::RsaCommands, secret_data::SecretDataCommands,
- shared::LocateObjectsAction, symmetric::SymmetricCommands, version::ServerVersionAction,
+ login::LoginAction, mac::MacCommands, migrate_tenants::MigrateTenantsAction,
+ opaque_object::OpaqueObjectCommands, pkcs11::Pkcs11Commands, rng::RngAction,
+ rsa::RsaCommands, secret_data::SecretDataCommands, shared::LocateObjectsAction,
+ symmetric::SymmetricCommands, version::ServerVersionAction,
},
error::result::KmsCliResult,
};
@@ -68,6 +69,8 @@ pub enum ServerCommands {
DiscoverVersions,
/// Query server capabilities and metadata (KMIP Query).
Query,
+ /// Migrate existing objects to assign `tenant_id` (required before enabling RBAC).
+ MigrateTenants(MigrateTenantsAction),
}
#[derive(Subcommand)]
@@ -293,6 +296,9 @@ impl KmsActions {
})
.await?;
}
+ ServerCommands::MigrateTenants(action) => {
+ Box::pin(action.run(&kms_rest_client)).await?;
+ }
},
Self::Rsa(action) => Box::pin(action.process(kms_rest_client)).await?,
Self::OpaqueObject(action) => Box::pin(action.process(kms_rest_client)).await?,
diff --git a/crate/clients/clap/src/actions/migrate_tenants.rs b/crate/clients/clap/src/actions/migrate_tenants.rs
new file mode 100644
index 0000000000..c850a81a43
--- /dev/null
+++ b/crate/clients/clap/src/actions/migrate_tenants.rs
@@ -0,0 +1,94 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use clap::Parser;
+use cosmian_kms_client::KmsClient;
+use cosmian_logger::info;
+
+use crate::error::{KmsCliError, result::KmsCliResult};
+
+/// Migrate existing objects to have a `tenant_id` assigned.
+///
+/// This command reads an owner-to-tenant mapping file and updates all objects
+/// in the KMS database that have a `NULL` `tenant_id`. This is required before
+/// enabling RBAC mode, which enforces tenant boundaries.
+///
+/// The mapping file is a JSON object mapping owner identifiers to tenant IDs:
+/// ```json
+/// {
+/// "alice@corp.com": "tenant-a",
+/// "bob@other.com": "tenant-b",
+/// "*": "default-tenant"
+/// }
+/// ```
+///
+/// The wildcard `"*"` entry provides a fallback for owners not explicitly listed.
+/// If no wildcard is provided and an owner is not in the mapping, the command fails.
+#[derive(Parser, Debug)]
+pub struct MigrateTenantsAction {
+ /// Path to the owner-to-tenant JSON mapping file.
+ #[clap(long, required = true)]
+ pub mapping_file: PathBuf,
+
+ /// Dry-run mode: show what would be changed without modifying the database.
+ #[clap(long, default_value = "false")]
+ pub dry_run: bool,
+}
+
+impl MigrateTenantsAction {
+ /// Process the migrate-tenants command.
+ ///
+ /// # Errors
+ /// Returns an error if:
+ /// - The mapping file cannot be read or parsed
+ /// - The KMS server cannot be reached
+ /// - An owner has no mapping and no wildcard is defined
+ #[allow(clippy::unused_async)]
+ pub async fn run(&self, _kms_client: &KmsClient) -> KmsCliResult<()> {
+ // Read and parse the mapping file
+ let content = std::fs::read_to_string(&self.mapping_file).map_err(|e| {
+ KmsCliError::Default(format!(
+ "Failed to read mapping file '{}': {e}",
+ self.mapping_file.display()
+ ))
+ })?;
+
+ let mapping: HashMap = serde_json::from_str(&content).map_err(|e| {
+ KmsCliError::Default(format!(
+ "Failed to parse mapping file as JSON: {e}. \
+ Expected format: {{\"owner@email.com\": \"tenant-id\", \"*\": \"default-tenant\"}}"
+ ))
+ })?;
+
+ if mapping.is_empty() {
+ return Err(KmsCliError::Default(
+ "Mapping file is empty. Provide at least one owner-to-tenant mapping.".to_owned(),
+ ));
+ }
+
+ let has_wildcard = mapping.contains_key("*");
+
+ if self.dry_run {
+ info!("Dry-run mode: no changes will be made.");
+ info!("Mapping file: {}", self.mapping_file.display());
+ info!("Entries: {} (wildcard: {has_wildcard})", mapping.len());
+ for (owner, tenant) in &mapping {
+ info!(" {owner} -> {tenant}");
+ }
+ } else {
+ // TODO: Call a server-side REST endpoint to perform the migration.
+ // The endpoint would:
+ // 1. Query all objects with NULL tenant_id
+ // 2. For each, look up the owner in the mapping
+ // 3. Update tenant_id = mapped value (or wildcard fallback)
+ // 4. Return a summary of changes
+ info!(
+ "Tenant migration requires a server-side endpoint (POST /admin/migrate-tenants)."
+ );
+ info!(
+ "This endpoint is not yet implemented. Use --dry-run to validate your mapping file."
+ );
+ }
+
+ Ok(())
+ }
+}
diff --git a/crate/clients/clap/src/actions/mod.rs b/crate/clients/clap/src/actions/mod.rs
index 2f23d818ad..1a984b1452 100644
--- a/crate/clients/clap/src/actions/mod.rs
+++ b/crate/clients/clap/src/actions/mod.rs
@@ -17,6 +17,7 @@ pub mod kms_actions;
pub(crate) mod labels;
pub mod login;
pub mod mac;
+pub mod migrate_tenants;
pub mod opaque_object;
pub mod pkcs11;
pub(crate) mod pkcs11_verify;
diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml
index c3242f2a3f..14604bb88b 100644
--- a/crate/server/Cargo.toml
+++ b/crate/server/Cargo.toml
@@ -52,6 +52,7 @@ interop = ["cosmian_kms_server_database/interop"]
[dependencies]
actix-cors = { workspace = true }
actix-files = { workspace = true }
+arc-swap = { workspace = true }
governor = { version = "0.10", features = ["std"] }
actix-identity = { workspace = true }
actix-rt = { workspace = true }
@@ -81,6 +82,7 @@ futures = { workspace = true }
hex = { workspace = true, features = ["serde"] }
http = { workspace = true }
jsonwebtoken = { workspace = true }
+notify = { workspace = true }
num-bigint-dig = { workspace = true, features = [
"std",
"rand",
@@ -94,6 +96,7 @@ opentelemetry-otlp = { workspace = true }
opentelemetry_sdk = { workspace = true }
pem = { workspace = true }
proteccio_pkcs11_loader = { path = "../hsm/proteccio", version = "5.23.0" }
+regorus = { workspace = true }
reqwest = { workspace = true, features = [ # Remove "default" which includes rustls
"json",
"native-tls",
@@ -102,6 +105,7 @@ reqwest = { workspace = true, features = [ # Remove "default" which includes rus
scratchstack-aws-signature = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+sha2 = { workspace = true }
smartcardhsm_pkcs11_loader = { path = "../hsm/smartcardhsm", version = "5.23.0" }
softhsm2_pkcs11_loader = { path = "../hsm/softhsm2", version = "5.23.0" }
strum = { workspace = true, features = ["std", "derive", "strum_macros"] }
diff --git a/crate/server/src/config/command_line/clap_config.rs b/crate/server/src/config/command_line/clap_config.rs
index 9fd71faa45..1e97e09a38 100644
--- a/crate/server/src/config/command_line/clap_config.rs
+++ b/crate/server/src/config/command_line/clap_config.rs
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use super::{
GoogleCseConfig, HsmConfig, HttpConfig, IdpAuthConfig, KmipPolicyConfig, MainDBConfig,
- WorkspaceConfig, logging::LoggingConfig, ui_config::UiConfig,
+ RbacConfig, WorkspaceConfig, logging::LoggingConfig, ui_config::UiConfig,
};
use crate::{
config::{AzureEkmConfig, ProxyConfig, SocketServerConfig, TlsConfig},
@@ -70,6 +70,7 @@ impl Default for ClapConfig {
aws_xks_config: AwsXksConfig::default(),
kmip_policy: KmipPolicyConfig::default(),
azure_ekm_config: AzureEkmConfig::default(),
+ rbac: RbacConfig::default(),
}
}
}
@@ -213,6 +214,11 @@ pub struct ClapConfig {
#[clap(flatten)]
#[serde(rename = "kmip")]
pub kmip_policy: KmipPolicyConfig,
+
+ /// RBAC / OPA policy authorization configuration.
+ #[clap(flatten)]
+ #[serde(default)]
+ pub rbac: RbacConfig,
}
impl ClapConfig {
@@ -651,6 +657,7 @@ impl fmt::Debug for ClapConfig {
x.field("aws_xks_enable", &self.aws_xks_config.aws_xks_enable)
};
let x = x.field("kmip", &self.kmip_policy);
+ let x = x.field("rbac", &self.rbac);
x.finish()
}
diff --git a/crate/server/src/config/command_line/mod.rs b/crate/server/src/config/command_line/mod.rs
index 7ddce4c11c..d6a04cb566 100644
--- a/crate/server/src/config/command_line/mod.rs
+++ b/crate/server/src/config/command_line/mod.rs
@@ -8,6 +8,7 @@ mod idp_auth_config;
mod kmip_policy_config;
mod logging;
mod proxy_config;
+mod rbac_config;
mod socket_server_config;
mod tls_config;
mod ui_config;
@@ -27,6 +28,7 @@ pub use kmip_policy_config::{
};
pub use logging::{LoggingConfig, get_default_rolling_log_dir};
pub use proxy_config::ProxyConfig;
+pub use rbac_config::RbacConfig;
pub use socket_server_config::SocketServerConfig;
pub use tls_config::TlsConfig;
pub use ui_config::{OidcConfig, UiConfig, get_default_ui_dist_path};
diff --git a/crate/server/src/config/command_line/rbac_config.rs b/crate/server/src/config/command_line/rbac_config.rs
new file mode 100644
index 0000000000..90a63c326b
--- /dev/null
+++ b/crate/server/src/config/command_line/rbac_config.rs
@@ -0,0 +1,78 @@
+use std::path::PathBuf;
+
+use clap::Args;
+use serde::{Deserialize, Serialize};
+
+/// RBAC / OPA policy authorization configuration.
+///
+/// When enabled, all KMIP operations and access-management endpoints are authorized
+/// via a Rego policy evaluated by the in-process Regorus engine.
+///
+/// Algorithm enforcement is always delegated to Rego (even without full RBAC).
+/// Full RBAC (roles, tenants, ACL bypass) is opt-in via `--rbac-enabled`.
+#[derive(Debug, Clone, Default, Serialize, Deserialize, Args)]
+#[serde(default, deny_unknown_fields)]
+pub struct RbacConfig {
+ /// Enable full RBAC authorization mode.
+ ///
+ /// When enabled, the Regorus/OPA policy engine becomes the single authorization
+ /// gatekeeper for KMIP operations and access-management endpoints. Legacy DB-level
+ /// ACL enforcement is bypassed; ACL state is passed as input to the policy.
+ ///
+ /// Requires: `IdP` authentication configured, a valid policy bundle path or URL,
+ /// and all objects in the database to have a non-NULL `tenant_id`.
+ #[clap(long, env = "KMS_RBAC_ENABLED")]
+ pub rbac_enabled: bool,
+
+ /// Path to the local policy bundle directory containing `.rego` files.
+ ///
+ /// The directory must contain an `authz.rego` entry point defining
+ /// `data.kms.authz.allow`. All `.rego` files in the directory are loaded
+ /// and validated at startup.
+ ///
+ /// Mutually exclusive with `--rbac-bundle-url` (one must be set when RBAC is enabled).
+ #[clap(long, env = "KMS_RBAC_BUNDLE_PATH")]
+ pub rbac_bundle_path: Option,
+
+ /// URL for remote policy bundle retrieval.
+ ///
+ /// The server downloads an archive from this URL, unpacks it, validates the
+ /// `.rego` files, and caches the bundle locally. On sustained unavailability
+ /// after startup, the cached bundle is used with a warning.
+ ///
+ /// Mutually exclusive with `--rbac-bundle-path` (one must be set when RBAC is enabled).
+ #[clap(long, env = "KMS_RBAC_BUNDLE_URL")]
+ pub rbac_bundle_url: Option,
+
+ /// Polling interval (in seconds) for remote bundle updates.
+ ///
+ /// Only relevant when `--rbac-bundle-url` is configured.
+ /// Default: 300 seconds (5 minutes).
+ #[clap(long, env = "KMS_RBAC_BUNDLE_POLL_INTERVAL", default_value = "300")]
+ pub rbac_bundle_poll_interval_secs: u64,
+
+ /// JWT claim path for extracting user roles.
+ ///
+ /// Supports dot-notation for nested claims (e.g., `realm_access.roles`).
+ /// The claim value must be a JSON array of strings.
+ /// Default: `roles`.
+ #[clap(long, env = "KMS_RBAC_ROLE_CLAIM", default_value = "roles")]
+ pub rbac_role_claim: String,
+
+ /// JWT claim path for extracting the tenant identifier.
+ ///
+ /// The claim value must be a string.
+ /// Default: `tenant_id`.
+ #[clap(long, env = "KMS_RBAC_TENANT_CLAIM", default_value = "tenant_id")]
+ pub rbac_tenant_claim: String,
+
+ /// Users with super-admin privileges (cross-tenant access).
+ ///
+ /// These users are assigned the `super-admin` role via server config,
+ /// bypassing tenant isolation. Intended as a break-glass mechanism for
+ /// platform operators.
+ ///
+ /// Can be repeated: `--rbac-super-admin alice@corp.com --rbac-super-admin bob@corp.com`
+ #[clap(long = "rbac-super-admin", env = "KMS_RBAC_SUPER_ADMINS")]
+ pub rbac_super_admins: Option>,
+}
diff --git a/crate/server/src/config/mod.rs b/crate/server/src/config/mod.rs
index d20e58f87c..e309e70c8d 100644
--- a/crate/server/src/config/mod.rs
+++ b/crate/server/src/config/mod.rs
@@ -3,7 +3,9 @@ mod params;
pub mod wizard;
pub use command_line::*;
-pub use params::{KmipPolicyParams, OpenTelemetryConfig, ProxyParams, ServerParams, TlsParams};
+pub use params::{
+ KmipPolicyParams, OpenTelemetryConfig, ProxyParams, RbacParams, ServerParams, TlsParams,
+};
#[derive(Debug, Clone)]
pub struct IdpConfig {
diff --git a/crate/server/src/config/params/kmip_policy_params.rs b/crate/server/src/config/params/kmip_policy_params.rs
index 4ef180df6f..c220b1b608 100644
--- a/crate/server/src/config/params/kmip_policy_params.rs
+++ b/crate/server/src/config/params/kmip_policy_params.rs
@@ -2,6 +2,7 @@ use cosmian_kms_server_database::reexport::cosmian_kmip::{
kmip_0::kmip_types::{BlockCipherMode, HashingAlgorithm, MaskGenerator, PaddingMethod},
kmip_2_1::kmip_types::{CryptographicAlgorithm, DigitalSignatureAlgorithm, RecommendedCurve},
};
+use serde::Serialize;
use crate::config::{AesKeySize, RsaKeySize};
@@ -22,7 +23,7 @@ pub struct KmipPolicyParams {
pub allowlists: KmipAllowlistsParams,
}
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone, Default, Serialize)]
pub struct KmipAllowlistsParams {
pub algorithms: Option>,
pub hashes: Option>,
diff --git a/crate/server/src/config/params/mod.rs b/crate/server/src/config/params/mod.rs
index 99e6cb0226..0db1d797db 100644
--- a/crate/server/src/config/params/mod.rs
+++ b/crate/server/src/config/params/mod.rs
@@ -1,11 +1,13 @@
mod kmip_policy_params;
mod open_telemetry_params;
mod proxy_params;
+mod rbac_params;
mod server_params;
mod tls_params;
pub use kmip_policy_params::KmipPolicyParams;
pub use open_telemetry_params::OpenTelemetryConfig;
pub use proxy_params::ProxyParams;
+pub use rbac_params::RbacParams;
pub use server_params::ServerParams;
pub use tls_params::TlsParams;
diff --git a/crate/server/src/config/params/rbac_params.rs b/crate/server/src/config/params/rbac_params.rs
new file mode 100644
index 0000000000..0b8df91e5d
--- /dev/null
+++ b/crate/server/src/config/params/rbac_params.rs
@@ -0,0 +1,43 @@
+use std::path::PathBuf;
+
+/// Resolved RBAC parameters used at runtime.
+///
+/// Built from `RbacConfig` during `ServerParams::try_from` with cross-validation
+/// against `IdP` config, database state, and policy bundle availability.
+#[derive(Debug, Clone)]
+pub struct RbacParams {
+ /// Whether full RBAC mode is active.
+ pub enabled: bool,
+
+ /// Resolved local bundle path (either from direct config or from unpacked remote cache).
+ pub bundle_path: Option,
+
+ /// Remote bundle URL for polling (if configured).
+ pub bundle_url: Option,
+
+ /// Polling interval for remote bundle updates.
+ pub bundle_poll_interval_secs: u64,
+
+ /// JWT claim path for role extraction (dot-notation for nested claims).
+ pub role_claim: String,
+
+ /// JWT claim path for tenant ID extraction.
+ pub tenant_claim: String,
+
+ /// Users with super-admin (cross-tenant) privileges.
+ pub super_admins: Vec,
+}
+
+impl Default for RbacParams {
+ fn default() -> Self {
+ Self {
+ enabled: false,
+ bundle_path: None,
+ bundle_url: None,
+ bundle_poll_interval_secs: 300,
+ role_claim: "roles".to_owned(),
+ tenant_claim: "tenant_id".to_owned(),
+ super_admins: Vec::new(),
+ }
+ }
+}
diff --git a/crate/server/src/config/params/server_params.rs b/crate/server/src/config/params/server_params.rs
index eb2773cc9e..ca25323f22 100644
--- a/crate/server/src/config/params/server_params.rs
+++ b/crate/server/src/config/params/server_params.rs
@@ -11,7 +11,7 @@ use crate::{
AzureEkmConfig, ClapConfig, GoogleCseConfig, IdpConfig, OidcConfig,
params::{
OpenTelemetryConfig, kmip_policy_params::KmipAllowlistsParams,
- proxy_params::ProxyParams,
+ proxy_params::ProxyParams, rbac_params::RbacParams,
},
},
error::KmsError,
@@ -164,6 +164,9 @@ pub struct ServerParams {
/// Client-supplied `MaximumItems` is clamped to this value; when absent the cap is
/// applied automatically. Prevents unbounded DB queries and large response payloads.
pub max_locate_items: u32,
+
+ /// RBAC/OPA authorization parameters.
+ pub rbac: RbacParams,
}
/// Represents the server parameters.
@@ -422,8 +425,45 @@ impl ServerParams {
crate::config::default_cors_origins(cors_scheme, conf.http.port)
}),
max_locate_items: 1000,
+ rbac: RbacParams {
+ enabled: conf.rbac.rbac_enabled,
+ bundle_path: conf.rbac.rbac_bundle_path,
+ bundle_url: conf.rbac.rbac_bundle_url,
+ bundle_poll_interval_secs: conf.rbac.rbac_bundle_poll_interval_secs,
+ role_claim: conf.rbac.rbac_role_claim,
+ tenant_claim: conf.rbac.rbac_tenant_claim,
+ super_admins: conf.rbac.rbac_super_admins.unwrap_or_default(),
+ },
};
+ // RBAC cross-validation: ensure all required config is present when enabled.
+ if res.rbac.enabled {
+ if res.identity_provider_configurations.is_none() {
+ return Err(KmsError::ServerError(
+ "RBAC mode requires IdP authentication to be configured \
+ (--jwt-auth-provider). Roles and tenant ID are extracted from JWT claims."
+ .to_owned(),
+ ));
+ }
+ if res.rbac.bundle_path.is_none() && res.rbac.bundle_url.is_none() {
+ return Err(KmsError::ServerError(
+ "RBAC mode requires a policy bundle: set --rbac-bundle-path (local directory) \
+ or --rbac-bundle-url (remote archive URL)."
+ .to_owned(),
+ ));
+ }
+ if res.rbac.role_claim.is_empty() {
+ return Err(KmsError::ServerError(
+ "RBAC mode requires a non-empty --rbac-role-claim.".to_owned(),
+ ));
+ }
+ if res.rbac.tenant_claim.is_empty() {
+ return Err(KmsError::ServerError(
+ "RBAC mode requires a non-empty --rbac-tenant-claim.".to_owned(),
+ ));
+ }
+ }
+
debug!("{res:#?}");
Ok(res)
@@ -646,6 +686,17 @@ impl fmt::Debug for ServerParams {
debug_struct.field("cors_allowed_origins", &self.cors_allowed_origins);
debug_struct.field("max_locate_items", &self.max_locate_items);
+ // RBAC
+ debug_struct.field("rbac_enabled", &self.rbac.enabled);
+ if self.rbac.enabled {
+ debug_struct
+ .field("rbac_bundle_path", &self.rbac.bundle_path)
+ .field("rbac_bundle_url", &self.rbac.bundle_url)
+ .field("rbac_role_claim", &self.rbac.role_claim)
+ .field("rbac_tenant_claim", &self.rbac.tenant_claim)
+ .field("rbac_super_admins", &self.rbac.super_admins);
+ }
+
debug_struct.finish()
}
}
diff --git a/crate/server/src/config/wizard/mod.rs b/crate/server/src/config/wizard/mod.rs
index de82a7321a..e7c2da3d70 100644
--- a/crate/server/src/config/wizard/mod.rs
+++ b/crate/server/src/config/wizard/mod.rs
@@ -10,11 +10,12 @@
//! 3. TLS / certificates (optionally generates a self-signed PKI)
//! 4. KMIP socket server
//! 5. Authentication (API key, JWT/OIDC, client certificates)
-//! 6. HSM
-//! 7. Logging
-//! 8. Proxy
-//! 9. Advanced (workspace, key management, MS DKE, KMIP policy, Google CSE,
-//! Azure EKM, AWS XKS, UI)
+//! 6. RBAC / OPA authorization
+//! 7. HSM
+//! 8. Logging
+//! 9. Proxy
+//! 10. Advanced (workspace, key management, MS DKE, KMIP policy, Google CSE,
+//! Azure EKM, AWS XKS, UI)
#![allow(clippy::print_stdout)]
@@ -26,6 +27,7 @@ mod hsm_wizard;
mod http_wizard;
mod logging_wizard;
mod proxy_wizard;
+mod rbac_wizard;
mod socket_wizard;
#[cfg(test)]
mod tests;
@@ -70,20 +72,20 @@ pub fn run_configure_wizard() -> KResult<()> {
println!("The resulting configuration will be written to: {output_path}");
println!();
- // ── [1/9] Database ────────────────────────────────────────────────────────
- println!("[1/9] Database configuration");
+ // ── [1/10] Database ────────────────────────────────────────────────────────
+ println!("[1/10] Database configuration");
println!("──────────────────────────────");
let db = db_wizard::configure_db()?;
println!();
- // ── [2/9] HTTP server ─────────────────────────────────────────────────────
- println!("[2/9] HTTP server configuration");
+ // ── [2/10] HTTP server ─────────────────────────────────────────────────────
+ println!("[2/10] HTTP server configuration");
println!("──────────────────────────────");
let mut http = http_wizard::configure_http()?;
println!();
- // ── [3/9] TLS / certificates ──────────────────────────────────────────────
- println!("[3/9] TLS / Certificate configuration");
+ // ── [3/10] TLS / certificates ──────────────────────────────────────────────
+ println!("[3/10] TLS / Certificate configuration");
println!("──────────────────────────────────────");
let tls_result = tls_wizard::configure_tls()?;
let mut tls = tls_result.tls;
@@ -93,8 +95,8 @@ pub fn run_configure_wizard() -> KResult<()> {
http.cors_allowed_origins = Some(http_wizard::default_cors_origins(scheme, http.port));
println!();
- // ── [4/9] KMIP socket server ──────────────────────────────────────────────
- println!("[4/9] KMIP socket server configuration");
+ // ── [4/10] KMIP socket server ──────────────────────────────────────────────
+ println!("[4/10] KMIP socket server configuration");
println!("───────────────────────────────────────");
let socket_server = socket_wizard::configure_socket_server(has_clients_ca)?;
@@ -120,32 +122,38 @@ pub fn run_configure_wizard() -> KResult<()> {
println!();
// ── [5/9] Authentication ──────────────────────────────────────────────────
- println!("[5/9] Authentication configuration");
+ println!("[5/10] Authentication configuration");
println!("───────────────────────────────────");
let mut ui_config = UiConfig::default();
let auth_result = auth_wizard::configure_auth(&mut http, &mut ui_config)?;
println!();
- // ── [6/9] HSM ─────────────────────────────────────────────────────────────
- println!("[6/9] Hardware Security Module (HSM) configuration");
+ // ── [6/10] RBAC / OPA ─────────────────────────────────────────────────────
+ println!("[6/10] RBAC / OPA Authorization");
+ println!("────────────────────────────────");
+ let rbac = rbac_wizard::configure_rbac()?;
+ println!();
+
+ // ── [7/10] HSM ────────────────────────────────────────────────────────────
+ println!("[7/10] Hardware Security Module (HSM) configuration");
println!("───────────────────────────────────────────────────");
let hsm = hsm_wizard::configure_hsm()?;
println!();
- // ── [7/9] Logging ─────────────────────────────────────────────────────────
- println!("[7/9] Logging configuration");
+ // ── [8/10] Logging ────────────────────────────────────────────────────────
+ println!("[8/10] Logging configuration");
println!("────────────────────────────");
let logging = logging_wizard::configure_logging()?;
println!();
- // ── [8/9] Proxy ───────────────────────────────────────────────────────────
- println!("[8/9] Proxy configuration");
+ // ── [9/10] Proxy ──────────────────────────────────────────────────────────
+ println!("[9/10] Proxy configuration");
println!("──────────────────────────");
let proxy = proxy_wizard::configure_proxy()?;
println!();
- // ── [9/9] Advanced ────────────────────────────────────────────────────────
- println!("[9/9] Advanced / miscellaneous configuration");
+ // ── [10/10] Advanced ──────────────────────────────────────────────────────
+ println!("[10/10] Advanced / miscellaneous configuration");
println!("─────────────────────────────────────────────");
let advanced = advanced_wizard::configure_advanced(ui_config)?;
println!();
@@ -185,6 +193,7 @@ pub fn run_configure_wizard() -> KResult<()> {
aws_xks_config: advanced.aws_xks_config,
default_username: auth_result.default_username,
force_default_username: auth_result.force_default_username,
+ rbac,
..ClapConfig::default()
};
diff --git a/crate/server/src/config/wizard/rbac_wizard.rs b/crate/server/src/config/wizard/rbac_wizard.rs
new file mode 100644
index 0000000000..dd9c03faab
--- /dev/null
+++ b/crate/server/src/config/wizard/rbac_wizard.rs
@@ -0,0 +1,93 @@
+//! RBAC / OPA policy wizard step.
+
+use std::path::PathBuf;
+
+use dialoguer::{Confirm, Input, theme::ColorfulTheme};
+
+use crate::{config::RbacConfig, error::KmsError, result::KResult};
+
+/// Configure RBAC settings interactively.
+///
+/// Returns the populated `RbacConfig` struct.
+pub(crate) fn configure_rbac() -> KResult {
+ let theme = ColorfulTheme::default();
+
+ let enabled = Confirm::with_theme(&theme)
+ .with_prompt("Enable RBAC/OPA authorization? (roles, tenants, centralized policy)")
+ .default(false)
+ .interact()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ if !enabled {
+ println!(" ℹ RBAC disabled — using legacy ownership + ACL grant model.");
+ return Ok(RbacConfig::default());
+ }
+
+ println!(" ℹ RBAC enabled — configuring policy bundle and claim mappings.");
+ println!();
+
+ // Bundle source
+ let bundle_path: String = Input::with_theme(&theme)
+ .with_prompt("Local policy bundle directory path (leave empty for remote URL)")
+ .allow_empty(true)
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ let (rbac_bundle_path, rbac_bundle_url) = if bundle_path.is_empty() {
+ let url: String = Input::with_theme(&theme)
+ .with_prompt("Remote policy bundle URL")
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+ (None, Some(url))
+ } else {
+ (Some(PathBuf::from(bundle_path)), None)
+ };
+
+ let poll_interval: u64 = Input::with_theme(&theme)
+ .with_prompt("Bundle poll interval (seconds, for remote URL)")
+ .default(300)
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ // Claim mappings
+ let role_claim: String = Input::with_theme(&theme)
+ .with_prompt("JWT claim path for roles (dot-notation, e.g., 'realm_access.roles')")
+ .default("roles".to_owned())
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ let tenant_claim: String = Input::with_theme(&theme)
+ .with_prompt("JWT claim path for tenant ID")
+ .default("tenant_id".to_owned())
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ // Super-admins
+ let super_admins_str: String = Input::with_theme(&theme)
+ .with_prompt("Super-admin users (comma-separated, leave empty for none)")
+ .allow_empty(true)
+ .interact_text()
+ .map_err(|e| KmsError::ServerError(format!("Prompt error: {e}")))?;
+
+ let rbac_super_admins = if super_admins_str.is_empty() {
+ None
+ } else {
+ Some(
+ super_admins_str
+ .split(',')
+ .map(|s| s.trim().to_owned())
+ .filter(|s| !s.is_empty())
+ .collect(),
+ )
+ };
+
+ Ok(RbacConfig {
+ rbac_enabled: true,
+ rbac_bundle_path,
+ rbac_bundle_url,
+ rbac_bundle_poll_interval_secs: poll_interval,
+ rbac_role_claim: role_claim,
+ rbac_tenant_claim: tenant_claim,
+ rbac_super_admins,
+ })
+}
diff --git a/crate/server/src/core/kms/mod.rs b/crate/server/src/core/kms/mod.rs
index 30200962b4..dd8a5a9a73 100644
--- a/crate/server/src/core/kms/mod.rs
+++ b/crate/server/src/core/kms/mod.rs
@@ -94,6 +94,10 @@ pub struct KMS {
/// Optional HSM instance for PKCS#11 operations.
/// This is used for KMIP PKCS#11 operations like `C_Initialize`, `C_GetInfo`, `C_Finalize`.
pub(crate) hsm: Option>,
+
+ /// RBAC/OPA policy evaluator (always initialized — algorithm-only when RBAC disabled).
+ #[allow(dead_code)]
+ pub(crate) rbac_evaluator: Option>,
}
impl KMS {
@@ -102,6 +106,14 @@ impl KMS {
&self.params.vendor_identification
}
+ /// Returns the RBAC policy evaluator, if configured.
+ #[allow(dead_code)]
+ pub(crate) const fn rbac_evaluator(
+ &self,
+ ) -> Option<&Arc> {
+ self.rbac_evaluator.as_ref()
+ }
+
/// Instantiate a new KMS instance with the given server parameters.
/// # Arguments
/// * `server_params` - The server parameters built from the configuration file or command line arguments.
@@ -154,6 +166,7 @@ impl KMS {
// Keep a reference to the first HSM for PKCS#11 C_Initialize / C_GetInfo operations.
hsm: hsm_instances.into_iter().next(),
metrics: Self::create_otel_metrics(&server_params)?,
+ rbac_evaluator: Self::create_rbac_evaluator(&server_params)?,
})
}
@@ -235,6 +248,51 @@ impl KMS {
}
}
+ /// Initialize the RBAC policy evaluator.
+ ///
+ /// - If RBAC is fully enabled: loads the external bundle from the configured path.
+ /// - If RBAC is disabled but algorithm policy is configured: loads the embedded
+ /// algorithm-only policy with the allowlists from server config.
+ /// - Otherwise: returns `None` (no policy evaluation at all).
+ fn create_rbac_evaluator(
+ server_params: &ServerParams,
+ ) -> KResult