Files
relicario/docs/SECURITY.md
adlee-was-taken ed50735e91 docs(org): pre-stage A5 living-docs for merged core+server+CLI-admin (item-CRUD/extension TODO)
Pre-stages the A5 living-docs sweep for the already-merged A (relicario-core org
module) + C (relicario-server pre-receive hook) + CLI admin/rotate/status-audit
work, so the final A5 sweep (after Dev-B B9-B14 merges) is fast.

Adds org sections to docs/FORMATS.md (org repo wire formats + wrapped-key blob
layout), docs/CRYPTO.md (ECIES X25519 wrap/unwrap, no-Argon2id contrast, rotate
re-encryption), docs/SECURITY.md (signature-verifying hook, owner-only elevation,
audit vocabulary, honest limitations), DESIGN.md (org-master-key secrets row +
server org mode + deps), core/cli ARCHITECTURE.md (org module + org_session), and
an Unreleased CHANGELOG entry.

B item-CRUD (org add/get/list/edit/rm/restore/purge + main.rs wiring) and extension
parity are left as explicit TODO. STATUS/ROADMAP mark-shipped and
extension/ARCHITECTURE are deferred to the full A5 (track not yet landed; Dev-D
deferred). All cited code constants pinned with file:line per living-docs discipline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy
2026-06-20 14:39:08 -04:00

222 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](CRYPTO.md)), wire formats (see [FORMATS.md](FORMATS.md)), or implementation (see [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md) and [../crates/relicario-cli/ARCHITECTURE.md](../crates/relicario-cli/ARCHITECTURE.md)).
## Cryptographic Protection
Relicario uses two-factor vault decryption:
1. **Passphrase** — user-memorized, zxcvbn score ≥3 required
2. **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 `.enc` files
or git-revert commits. The manifest decrypts successfully but won't
contain the deleted items.
- **Rollback attacks**: An attacker can replace `manifest.enc` with an
older valid version. AEAD accepts any ciphertext created with the key.
### Mitigation
Item deletion and rollback are detectable via **git history**:
```bash
git log --oneline items/
```
For environments where git history could be rewritten (force-push):
1. Enable device authentication (commit signing + pre-receive hook)
2. Use a git server that rejects non-fast-forward pushes
3. 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-receive`
- `relicario-server verify-commit <sha>` — checks one commit's signature
against `.relicario/devices.json` and `.relicario/revoked.json` as 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:
1. **Signature verification** — a temporary `allowed_signers` file is
constructed from the current `members.json`; `git verify-commit --raw`
is run and the resulting SHA-256 fingerprint is matched back to a
`members.json` entry. A commit not signed by a *current* member is
rejected outright.
2. **Path-level write authorisation** — each modified path is classified by
`classify_path` (`crates/relicario-server/src/lib.rs:19`) into
`ProtectedJson` (owner/admin write only), `CollectionItem` (the
`items/<slug>/…` prefix; write allowed only if the slug appears in the
signer's `collections` grant array), or `Unrestricted`. 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.
3. **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 escalated
`members.json`; there is no epoch in which the transition is
self-authorised.
4. **Schema monotonicity** (`enforce_schema_monotonicity`, `main.rs:521`)
`schema_version` values 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:
- **Live (merged A + C streams):** `member-add`, `member-remove`,
`member-role-change`, `collection-create`, `collection-grant`,
`collection-revoke`, `key-rotate`, `org-init`, `ownership-transfer`,
`org-delete`.
- **TODO (pending Dev-B B9B13):** `item-create`, `item-update`,
`item-delete`, `item-restore`, `item-purge` — the emitter code lands with
the item-CRUD command stream.
### 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-org` is a local tombstone in phase 1.** The schema-monotonicity
check causes the hook to reject protected-file deletion, so an
`org-delete` action 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](../crates/relicario-core/ARCHITECTURE.md) — implementation, starting with the platform-agnostic core.