15 KiB
Relicario Enterprise Org Vault — Design Spec
Scope: Multi-user organizational vault for security-conscious self-hosting shops. Covers the git-native org model, cryptographic key-wrapping scheme, access control, admin CLI surface, audit trail, and testing strategy. Does not cover SSO/SAML, live SIEM streaming, or the HTTP management plane (deferred to a later server-tier spec).
Next: docs/superpowers/specs/2026-05-02-relay-server-design.md (relay server — future phase 2 management plane)
Target Audience
Security-focused organizations that self-host their entire stack: infosec shops, security consultancies, law firms handling privileged client data, small financial firms. Key requirements:
- Full air-gap capability — no mandatory internet connectivity
- Cryptographically provenance-linked audit trail
- Personal vaults remain isolated from org vault (separate cryptographic domains)
- Member offboarding with clean key revocation
- Deployable without an IdP, SSO provider, or cloud dependency
Architecture Overview
An org vault is a second git repository alongside each member's personal vault. Personal and org vaults are cryptographically isolated — the personal vault's two-factor KDF (passphrase + image → Argon2id → master key) is completely untouched by org operations.
Personal: ~/.config/relicario/personal/ → personal git repo (passphrase + image → Argon2id → master key)
Org: ~/.config/relicario/acme-org/ → org git repo (org master key, wrapped per-member)
Two cryptographic domains, one CLI and extension.
The org vault uses a random 256-bit org master key to encrypt all org items. Each authorized member receives a copy of the org master key wrapped (XChaCha20-Poly1305) to their existing ed25519 device public key — converted to X25519 for the Diffie-Hellman step. To open the org vault, a member uses their device private key to unwrap their copy of the org master key, then decrypts items exactly as today.
Key security properties:
- Member departure = delete their
keys/<member-id>.enc+ rotate the org master key. No passphrase change, no re-imaging. - Every write to the org repo is a signed git commit — the audit log is the repo history.
- The pre-receive hook in
relicario-serveris the policy enforcement point. - Fully air-gapped: the org repo is just git, push/pull over SSH.
- A compromised org master key does not expose personal vault items.
Phase 2 (not in this spec): live SIEM streaming, SSO/SAML, HTTP management plane via the relicario-server relay skeleton.
Data Model
The org repo has a defined on-disk schema. The pre-receive hook rejects pushes that violate it.
acme-org/
├── org.json # org identity: name, org_id, created_at, schema_version
├── members.json # user directory (unencrypted — roles are not secrets)
├── collections.json # collection definitions
├── keys/
│ └── <member-id>.enc # org master key wrapped to each member's X25519 public key
├── manifest.enc # encrypted org manifest (item index + collection membership)
└── items/
└── <item-id>.enc # encrypted items (identical format to personal vault items)
org.json
{
"schema_version": 1,
"org_id": "<16-char hex>",
"display_name": "Acme Security",
"created_at": 1748000000
}
members.json
Public and unencrypted — readable without the org master key. Roles are not secrets; the key material is in keys/.
{
"schema_version": 1,
"members": [
{
"member_id": "<16-char hex>",
"display_name": "Alice",
"role": "owner",
"ed25519_pubkey": "<base64>",
"collections": ["prod-infra", "shared-tools"],
"added_at": 1748000000,
"added_by": "<member-id>"
}
]
}
role is one of owner, admin, member. collections is the list of collection slugs this member is granted access to. member_id is a 16-char lowercase hex string generated from 64 bits of OsRng entropy — the same convention as ItemId and FieldId in relicario-core/src/ids.rs.
collections.json
{
"schema_version": 1,
"collections": [
{
"slug": "prod-infra",
"display_name": "Production Infrastructure",
"created_by": "<member-id>",
"created_at": 1748000000
}
]
}
keys/<member-id>.enc
The org master key (32 bytes) encrypted with XChaCha20-Poly1305 using a key derived via X25519 from the member's ed25519 public key (converted to X25519 using the standard Ristretto/curve25519 mapping). One file per member. Deleting this file + rotating the org master key constitutes clean revocation.
manifest.enc
Encrypted with the org master key. Same schema as the personal vault manifest but each item entry includes a collection field (the slug). The manifest is the only place collection membership is stored for items — item blobs carry no collection metadata.
items/<item-id>.enc
Identical .enc format to personal vault items (XChaCha20-Poly1305, random 24-byte nonce, no Argon2id — the org master key is used directly). Item IDs follow the same 16-char hex convention.
Access Control
Roles
| Role | Permissions |
|---|---|
| Owner | All operations. Add/remove admins. Rotate org master key. Delete org. Transfer ownership. |
| Admin | Add/remove members. Create/delete collections. Grant/revoke collection access. Read all collections. Cannot remove owners or other admins. |
| Member | Read/write items in granted collections only. Cannot see items outside their grant list. |
Collection Access
Access grants are stored in the member's collections array in members.json. No separate ACL file. An admin edits the member record and commits; the pre-receive hook validates the commit author has admin or owner role.
Enforcement Layers
Access control is enforced at two layers:
-
Manifest filtering — the CLI and extension filter the decrypted manifest to items whose
collectionslug appears in the authenticated member's grant list. Members never receive item blobs for collections they are not granted. -
Pre-receive hook — rejects pushes where the committing device's member record does not have write access to the collections whose item files are modified.
Known Limitation
Every member holds the same org master key (wrapped to their device key). A member with a local copy of the org repo and their device key can technically decrypt any item blob — the org master key is not scoped per-collection. Access control is enforced by the CLI, extension, and pre-receive hook, not by cryptographic isolation between collections.
For items requiring true cryptographic separation within a security shop, place them in a separate org vault. Per-collection subkeys are an explicit non-goal for this phase.
"Hide value" (phase 2)
Bitwarden and LastPass support "autofill without revealing plaintext." This requires per-item subkeys or a relay that mediates autofill. Out of scope for phase 1 — documented as a known gap.
Admin Operations
All org management uses relicario org <subcommand>.
relicario org init --name "Acme Security"
Create org repo, generate org master key, add caller as owner.
relicario org add-member --key <base64-pubkey> --name Alice --role member
Wrap org master key to Alice's X25519 key. Write keys/<id>.enc + update members.json.
relicario org remove-member <member-id>
Delete keys/<id>.enc, update members.json. Prompts to run org rotate-key immediately.
relicario org set-role <member-id> admin|member
Update role in members.json. Owners only for admin promotion.
relicario org create-collection <slug> --name "Display Name"
Append to collections.json.
relicario org grant <member-id> <collection-slug>
Add slug to member's collections array in members.json.
relicario org revoke <member-id> <collection-slug>
Remove slug from member's collections array.
relicario org rotate-key
Generate new org master key. Re-wrap for all current members. Re-encrypt manifest.
Item blobs do not need re-encryption (only the manifest references them).
Does git pull --rebase before generating to detect concurrent rotations.
relicario org status
Print members, roles, and collection grants. No decryption required.
relicario org audit [--since <iso-date>] [--member <id>] [--collection <slug>] [--action <type>] [--format json]
Query git log for Relicario-* trailers. Output structured table or JSON array.
Onboarding Flow
- Alice runs
relicario device add, exports her ed25519 public key. - Alice sends her public key to an admin out-of-band (Signal, email, printed QR — Relicario does not mediate key exchange).
- Admin runs
org add-member --key <pubkey> --name Alice. - Alice pulls the org repo. She can now open the org vault.
Offboarding Flow
- Admin runs
org remove-member <id>. - Admin immediately runs
org rotate-key. - Former member's wrapped key is gone; even with the old key material they cannot decrypt items written after rotation.
Extension — Org Context
The vault tab grows a top-level org switcher (Personal + each configured org). Switching context loads the selected org's manifest through the service worker. The SW holds the unwrapped org master key in a Zeroizing<[u8; 32]> session handle — identical pattern to the personal master key. The org master key is never written to localStorage, IndexedDB, or any persistent browser storage.
Audit Trail
Git Log as Audit Record
Every write to the org repo is a git commit. Commits use structured trailers for machine-readable audit:
add item to prod-infra collection
Relicario-Actor: alice <a1b2c3d4e5f6a1b2>
Relicario-Action: item-create
Relicario-Collection: prod-infra
Relicario-Item: 9f8e7d6c5b4a3f2e
Relicario-Device: d1c2b3a4e5f6d1c2
The trailer schema is versioned via org.json's schema_version.
Action Vocabulary
Relicario-Action |
Trigger |
|---|---|
item-create |
New item added to org vault |
item-update |
Existing org item edited |
item-delete |
Item moved to trash |
item-purge |
Trashed item permanently deleted |
member-add |
org add-member |
member-remove |
org remove-member |
member-role-change |
org set-role |
collection-create |
org create-collection |
collection-grant |
org grant |
collection-revoke |
org revoke |
key-rotate |
org rotate-key |
relicario org audit
A query layer over git log --format. Parses trailer lines, applies filters, emits structured output.
--format json emits a JSON array of event objects — pipe to any log shipper without a live webhook:
[
{
"timestamp": 1748000000,
"actor_name": "alice",
"actor_id": "a1b2c3d4e5f6a1b2",
"action": "item-create",
"collection": "prod-infra",
"item_id": "9f8e7d6c5b4a3f2e",
"device_id": "d1c2b3a4e5f6d1c2",
"commit": "abc123def456"
}
]
A cron job running relicario org audit --since <last-run> --format json | <siem-ingest> is sufficient for Splunk/Elastic/Loki ingestion without any live server.
Read Audit Gap
Git commits only record writes. Read access (a member decrypting an item without modifying it) is invisible to the git log. This is the same limitation as Bitwarden self-hosted's event log. Read audit requires a server that mediates item fetch — deferred to phase 2.
Pre-Receive Hook Policy Enforcement
relicario-server validates on every push to the org repo:
- Only owners/admins may push changes to
members.json,collections.json,org.json. - Members may only push item writes (
items/*.enc) to collections in their grant list. org.jsonschema_versionmust not decrease.members.jsonandcollections.jsonmust pass schema validation.keys/path ACL (read-own-only) is delegated to git hosting ACLs (Gitea/Forgejo team permissions) — documented as a deployment note, not enforced by the hook.- After
member-remove, the hook emits a warning iforg rotate-keyhas not been run since the removal but does not block further pushes. This allows an admin to batch multiple removals before a single rotation.
Error Handling
Key Rotation Race
If two admins simultaneously remove different members and rotate the key, the second rotation's git pull --rebase detects a concurrent rotation commit and aborts with: "Concurrent key rotation detected — pull and re-run org rotate-key." Admin re-runs after pulling.
Org Repo Schema Invalid
If members.json or collections.json fails schema validation on pull, the CLI refuses to open the org vault: "Org repo schema invalid at <path> — contact your org owner." No silent degradation.
Member Device Key Lost
If a member loses their device key before a backup device was added, an owner generates a new keys/<member-id>.enc for a replacement device key the member generates. No master key escrow is required — owners can always re-grant access because they hold the org master key.
Extension Offline
If the git remote is unreachable, the extension operates read-only from the last-pulled org repo state. Writes are blocked and the org context shows an "org offline — writes disabled" indicator. Identical behavior to personal vault offline mode.
Testing Strategy
Unit Tests (relicario-core)
- Org master key wrap/unwrap round-trip: ed25519 → X25519 conversion + XChaCha20-Poly1305 encrypt/decrypt.
- Manifest filtering by collection grant list: member with grants
["prod-infra"]receives only items taggedprod-infra. members.jsonandcollections.jsonschema validation: valid and invalid inputs.
Fast Argon2id params are irrelevant for org tests — the org crypto path bypasses Argon2id entirely (key wrapping is X25519-based). Standard test params apply to any personal vault operations in shared test fixtures.
Integration Tests (relicario-cli)
- Full
org init→add-member→grant→ item write → audit output against a local bare git repo. org remove-member→rotate-key→ former member cannot decrypt newly written items.org audit --format jsonoutput is valid JSON matching the action vocabulary.- Pre-receive hook rejects a push where the committing member writes to an ungranated collection.
- Concurrent rotation race: two concurrent
rotate-keyoperations, second one detects and aborts.
Extension Tests (vitest)
- SW org context switching: loading org manifest replaces personal manifest in session state without cross-contamination.
- Org master key is stored only in the Zeroizing session handle, never in
localStorageorIndexedDB. - Offline read-only mode: SW surfaces read-only flag when git remote returns a network error.
Phase Boundary
This spec covers phase 1 (git-native org). Phase 2 adds:
- HTTP management plane via
relicario-server(relay skeleton → org API) - Live audit event streaming to SIEM (webhooks, not cron-poll)
- SSO/SAML assertion validation + LDAP/IdP member sync
- Read audit (server-mediated item fetch with event recording)
- "Hide value" autofill (per-item subkeys or server-mediated relay)
- Per-collection cryptographic isolation (subkeys — explicit non-goal for phase 1)