Extends the A5 pre-stage now that dev-b's full B-stream (item CRUD + all 19 org subcommands) merged to main (7392795). Living docs: - FORMATS/CRYPTO/SECURITY/DESIGN: flip the item-CRUD "pending Dev-B" markers to shipped; SECURITY audit vocabulary moves item-* actions to live. - crates/relicario-cli/ARCHITECTURE.md: full 19-subcommand surface (12 admin + 7 item CRUD), accurate OrgAddKind scope (Login/SecureNote/Identity). - STATUS.md: enterprise-org-vault landed section (merged7392795) + tracked follow-ups + honest known-limitations; correct spec citation. - ROADMAP.md: backend-complete row + phase-2 follow-ups. - CHANGELOG.md: finalize the enterprise-org-vault Unreleased section (item CRUD into Added; Card/Key/Document/Totp + extension + phase-2 into Deferred). Code (PM-directed dead_code fixes): wire device::current_device_seed by removing the identical duplicate private fn in org_session.rs (de-dup); #[allow(dead_code)] + justification on org_session org_meta_path/load_meta (API completeness, no command consumes org.json yet). Also silence a 3rd pre-existing test-only warning (unused relicario() helper in tests/org_init_signing.rs). Honest deferrals kept explicit throughout: Card/Key/Document/Totp org add/edit parity, extension org switch/read (Dev-D) + writes, phase-2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane). Full workspace cargo test green, zero warnings. All cited code constants pinned file:line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy
10 KiB
Relicario Security Model
Audience: auditors and curious users. This doc owns the threat model, attacker-scenarios table, device-authentication model, env-var trust surface, and known limitations. Does NOT own: crypto primitive details (see CRYPTO.md), wire formats (see FORMATS.md), or implementation (see ../crates/relicario-core/ARCHITECTURE.md and ../crates/relicario-cli/ARCHITECTURE.md).
Cryptographic Protection
Relicario uses two-factor vault decryption:
- Passphrase — user-memorized, zxcvbn score ≥3 required
- Reference image — JPEG carrying 256-bit secret via DCT steganography
Key derivation: Argon2id (64 MiB memory, 3 iterations, 4 parallelism) Encryption: XChaCha20-Poly1305 (192-bit nonce, 256-bit key)
Manifest Integrity
The manifest (manifest.enc) is encrypted with AEAD, which provides:
- Confidentiality: Contents unreadable without master key
- Integrity: Any modification detected and rejected on decrypt
- Authenticity: Only master key holders can create valid ciphertexts
What AEAD Does NOT Protect
-
Item deletion: An attacker with write access can delete
.encfiles or git-revert commits. The manifest decrypts successfully but won't contain the deleted items. -
Rollback attacks: An attacker can replace
manifest.encwith an older valid version. AEAD accepts any ciphertext created with the key.
Mitigation
Item deletion and rollback are detectable via git history:
git log --oneline items/
For environments where git history could be rewritten (force-push):
- Enable device authentication (commit signing + pre-receive hook)
- Use a git server that rejects non-fast-forward pushes
- Regular backups with
relicario backup export
Device Authentication
When enabled, device authentication provides:
- Commit authorship: All commits signed by registered device keys
- Push access control: Deploy keys managed via Gitea API
- Instant revocation: One command cuts off both signing and push
Enforcement requires deploying the relicario-server pre-receive hook
on the vault remote. The crate provides two subcommands:
relicario-server generate-hook— emits the hook script to install at<repo>/hooks/pre-receiverelicario-server verify-commit <sha>— checks one commit's signature against.relicario/devices.jsonand.relicario/revoked.jsonas of that commit; the hook calls this for every pushed ref
Without the server hook, signed commits provide authorship metadata only — any process with push access can land an unsigned commit, since verification is otherwise advisory.
See docs/superpowers/specs/2026-05-02-device-authentication-design.md.
Access Control
Without device authentication, access control is transport-layer only:
- CLI: SSH key authentication to git remote
- Extension: Git credentials in browser storage
Device registration is optional but recommended for shared vaults.
Org vault security
An org vault is a separate git repository alongside the personal vault. It uses ed25519 commit-signing and a server-side pre-receive hook to make least-privilege access control server-enforced, not advisory.
Org device-key authentication
Every org member registers an ed25519 device key. The key appears in
members.json as an OpenSSH public-key string alongside the member's role
and collection grants. Fingerprint matching is done via
relicario_core::fingerprint, which normalises the OpenSSH format so that
whitespace and comment differences do not create phantom mismatches.
Org access requires two things at once: a wrapped key blob (keys/<member-id>.enc)
and the device private key that can unwrap it. There is no org passphrase —
removing a member's blob and rotating the org master key is sufficient to
revoke access (see Key rotation below). Device keys are completely
separate from the personal vault's KDF inputs; revoking org access does not
affect the member's personal vault.
Pre-receive hook enforcement
relicario-server generate-org-hook (crates/relicario-server/src/main.rs:511)
emits a hook script that calls relicario-server verify-org-commit for
every pushed commit. Unsigned or structurally invalid commits are rejected
before they land.
verify_org_commit (main.rs:286) performs four checks in order:
-
Signature verification — a temporary
allowed_signersfile is constructed from the currentmembers.json;git verify-commit --rawis run and the resulting SHA-256 fingerprint is matched back to amembers.jsonentry. A commit not signed by a current member is rejected outright. -
Path-level write authorisation — each modified path is classified by
classify_path(crates/relicario-server/src/lib.rs:19) intoProtectedJson(owner/admin write only),CollectionItem(theitems/<slug>/…prefix; write allowed only if the slug appears in the signer'scollectionsgrant array), orUnrestricted. The write is authorised if and only if the signer's role and grants satisfy the classification. Item blobs are authorised by the leading path segment alone — the ciphertext is never decrypted by the hook. -
Owner-only elevation guard (
enforce_owner_only_elevation,main.rs:438) — only a member whose pre-commit (parent) role is Owner may introduce a new member at Owner or Admin level, or promote an existing member to either. Checking the pre-commit role means an Admin cannot self-promote in the same commit that writes the escalatedmembers.json; there is no epoch in which the transition is self-authorised. -
Schema monotonicity (
enforce_schema_monotonicity,main.rs:521) —schema_versionvalues in org JSON containers may not decrease. Merge commits are rejected. A genesis commit (no parents) is allowed only when it is signed by the sole Owner it introduces.
Key rotation
relicario org rotate-key generates a fresh 256-bit org master key,
re-wraps it for every current member, and re-encrypts every
items/<slug>/<id>.enc blob and the manifest under the new key in a single
signed commit tagged Relicario-Action: key-rotate. A revoked member's
wrapped blob is simply not written during rotation, so they hold a blob that
decrypts to a stale key — they cannot read items encrypted under the new
key.
Audit action vocabulary
The relicario org audit command attributes actions to their verified
signer (not to the commit author or trailer value). Each event records two
actors: the verified actor resolved from the signing key (authoritative)
and the actor claimed by the Relicario-Actor trailer (advisory). When the
claimed actor disagrees with the verified signer, the event is flagged
TAMPERED. Trailers are advisory metadata; the trustworthy actor is always
the cryptographically verified signer.
Actions live in two groups:
- Membership / collections / lifecycle:
member-add,member-remove,member-role-change,collection-create,collection-grant,collection-revoke,key-rotate,org-init,ownership-transfer,org-delete. - Item CRUD:
item-create,item-update,item-delete(soft-delete / trash),item-restore,item-purge— emitted by theorg add/edit/rm/restore/purgecommands.
Honest limitations
The following are deliberate design boundaries, not oversights:
-
Shared org master key — reads are not cryptographically scoped per collection. The pre-receive hook scopes writes by collection path and the CLI filters the manifest to each member's grants, but a single org key opens all collection blobs. A member with any grant can, outside the CLI, decrypt items from collections they are not granted. For true cryptographic separation, use a separate org vault per access boundary. Per-collection subkeys are a phase-2 non-goal.
-
No read audit. Git records writes only. A member who reads blobs directly leaves no server-visible trace.
-
No "hide value." There is no mechanism to show a member that an item exists without revealing its field values on decrypt.
-
delete-orgis a local tombstone in phase 1. The schema-monotonicity check causes the hook to reject protected-file deletion, so anorg-deleteaction cannot be pushed to a hook-protected remote. The deletion is recorded locally only until a future phase addresses it.
Configuration env vars
Relicario reads the following environment variables. Each is a trust boundary: an attacker who can set them in the user's environment can influence Relicario's behavior. They are listed here for security reviewers to audit the surface in one place.
User-facing (active in all builds)
| Variable | Purpose | Trust |
|---|---|---|
RELICARIO_IMAGE |
Override the reference-image JPEG path used during vault unlock. | Trusted: filesystem path under the user's control. Read-only; its bytes feed imgsecret::extract_secret. |
RELICARIO_GITEA_URL |
Gitea API base URL for relicario device add. Equivalent to --gitea-url. |
Trusted: HTTPS URL. Used only in the device-add code path. |
RELICARIO_GITEA_TOKEN |
Gitea personal-access token. Equivalent to --gitea-token. |
Secret: anyone who can read this env var can manage the user's deploy keys via the Gitea API. The CLI never logs it. |
RELICARIO_GITEA_OWNER |
Gitea repository owner (e.g. alee). Equivalent to --owner. |
Trusted: opaque string. |
RELICARIO_GITEA_REPO |
Gitea repository name (e.g. vault). Equivalent to --repo. |
Trusted: opaque string. |
Debug-only (compiled out of cargo build --release)
The following variables are gated behind cfg(debug_assertions) and
are no-ops in release builds. The env-var lookup is removed by the
optimiser from any binary built without debug assertions (i.e. the
standard --release profile).
| Variable | Purpose |
|---|---|
RELICARIO_NO_GROUPS_CACHE |
Suppress the plaintext groups.cache write. Developer debugging tool for the cache logic. |
RELICARIO_TEST_PASSPHRASE |
Bypass the rpassword prompt during integration tests. |
RELICARIO_TEST_ITEM_SECRET |
Bypass the rpassword prompt for item-secret fields during integration tests. |
RELICARIO_TEST_BACKUP_PASSPHRASE |
Bypass the rpassword prompt for backup export/restore passphrases during integration tests. |
Next: ../crates/relicario-core/ARCHITECTURE.md — implementation, starting with the platform-agnostic core.