docs: document two-key privilege model

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:11:18 +01:00
parent 456e15a2f8
commit add9515b5c
5 changed files with 163 additions and 66 deletions
+81 -31
View File
@@ -13,7 +13,8 @@ This manual is for **using and administering** `emcli`. It assumes you have the
## Contents
1. [Key concepts](#1-key-concepts)
2. [Setup: the encryption key and database](#2-setup-the-encryption-key-and-database)
2. [Setup: encryption keys and database](#2-setup-encryption-keys-and-database)
- [Privilege model](#2a-privilege-model)
3. [Quick start](#3-quick-start)
4. [Adding accounts](#4-adding-accounts)
- [Gmail (app password)](#gmail-app-password)
@@ -32,10 +33,12 @@ This manual is for **using and administering** `emcli`. It assumes you have the
## 1. Key concepts
**Two kinds of commands.**
- **Admin commands** (`init`, `account`, `whitelist`, `config`, `audit`, `doctor`) are for *you*,
the human. They print human-readable text or open an interactive form.
- **Agent commands** (`list`, `get`, `search`, `ack`, `send`) are for the *agent*. They print one
line of JSON and nothing else, so a program can consume them reliably.
- **Admin commands** (`init`, `account`, `whitelist`, `config`, `audit`) require `EMCLI_ADMIN_KEY`
and are for *you*, the human. They print human-readable text or open an interactive form.
- **Agent commands** (`list`, `get`, `search`, `ack`, `send`, `doctor`) require `EMCLI_KEY` (or
`EMCLI_ADMIN_KEY` as a superset) and are for the *agent*. They print one line of JSON and
nothing else, so a program can consume them reliably. (`doctor` prints human-readable text but
is authorised by the agent key — the agent or a human with either key can run it.)
**Accounts** are named (e.g. `gmail`, `work`). The agent refers to an account by name and never
sees its password.
@@ -61,50 +64,91 @@ acking is a deliberate, separate step.
---
## 2. Setup: the encryption key and database
## 2. Setup: encryption keys and database
`emcli` reads two environment variables:
`emcli` reads three environment variables:
| Variable | Purpose | Default |
|---|---|---|
| `EMCLI_KEY` | **Required.** Base64-encoded 32-byte key (AES-256) used to encrypt passwords at rest. | none — commands fail without it |
| `EMCLI_ADMIN_KEY` | **Required for admin.** Base64-encoded 32-byte key (AES-256). Authorises ALL commands. | none — admin commands fail without it |
| `EMCLI_KEY` | **Required for agents.** Base64-encoded 32-byte key (AES-256). Authorises agent commands only. | none — agent commands fail without it |
| `EMCLI_DB` | Path to the database file. | `~/.config/emcli/emcli.db` (Linux/macOS), `%AppData%\emcli\emcli.db` (Windows) |
**Generate a key once** and keep it safe (store it the way the program/orchestrator that launches
`emcli` expects — e.g. a secrets manager or your shell profile):
**Generate both keys once** and keep them safe:
```bash
head -c 32 /dev/urandom | base64
export EMCLI_ADMIN_KEY="$(head -c 32 /dev/urandom | base64)" # you (human) keep this
export EMCLI_KEY="$(head -c 32 /dev/urandom | base64)" # the agent launcher gets ONLY this
```
Set it in your environment before running any command:
```bash
export EMCLI_KEY='paste-the-base64-key-here'
```
> **Important:** the key encrypts your account passwords. If you lose it, the stored passwords
> can't be decrypted and you'll have to re-add accounts. If you change it, the same applies.
> `emcli` never falls back to plaintext — a missing or wrong key makes every command fail safely.
> **Important:** the keys protect your account passwords via envelope encryption (see "Privilege
> model" below). If you lose `EMCLI_ADMIN_KEY`, account secrets can't be decrypted and you'll have
> to re-add accounts. `emcli` never falls back to plaintext — a missing or wrong key makes every
> command fail safely.
Account passwords are stored **encrypted**; they never appear in command output, error messages,
or the audit log.
---
## 2a. Privilege model
`emcli` enforces a two-role privilege split so a process holding only the agent key cannot
reconfigure accounts, whitelists, or audit settings.
### The two keys
| Key | Holder | Authorises |
|---|---|---|
| `EMCLI_ADMIN_KEY` | Human / secrets manager | ALL commands (`account`, `whitelist`, `config`, `audit`, `init`, plus all agent commands) |
| `EMCLI_KEY` | Agent orchestrator | Agent commands only (`list`, `get`, `search`, `ack`, `send`, `doctor`) |
`EMCLI_ADMIN_KEY` is a strict superset: a process with only the admin key can run agent commands
too. A process with only `EMCLI_KEY` is refused with `emcli: this command requires EMCLI_ADMIN_KEY
(admin privilege)` if it attempts an admin command.
### Envelope encryption (DEK)
`emcli init` generates a random data-encryption key (DEK) that seals all account secrets. The DEK
is stored in the `settings` table in two sealed copies:
- `dek_wrap_admin` — the DEK encrypted under `EMCLI_ADMIN_KEY`.
- `dek_wrap_agent` — the DEK encrypted under `EMCLI_KEY`.
The DEK is never written in cleartext. Admin commands unwrap the DEK from the admin slot only; they
have no fallback to the agent slot. This means a process holding only `EMCLI_KEY` cannot unlock the
DEK for an admin command, even if it somehow knows the agent key.
### Command → role table
| Command | Role required |
|---|---|
| `list`, `get`, `search`, `ack`, `send`, `doctor` | Agent (`EMCLI_KEY` or `EMCLI_ADMIN_KEY`) |
| `account`, `whitelist`, `config`, `audit` | Admin (`EMCLI_ADMIN_KEY` required) |
| `init` | Both keys required (writes both wrap slots) |
### Agent launcher guidance
Configure your agent's orchestrator with **only `EMCLI_KEY`**. Never give the orchestrator
`EMCLI_ADMIN_KEY`. If the agent tries to run an admin command — even by mistake — `emcli` will
refuse it at the key level, not just by convention.
---
## 3. Quick start
```bash
# 1. Set your key (see section 2)
export EMCLI_KEY='…'
# 1. Generate and export both keys (see section 2)
export EMCLI_ADMIN_KEY="$(head -c 32 /dev/urandom | base64)" # keep this yourself
export EMCLI_KEY="$(head -c 32 /dev/urandom | base64)" # give only this to the agent
# 2. Create the database and add your first account (interactive form)
emcli init
# 3. Check it connects and authenticates
# 3. Check it connects and authenticates (agent key is enough for doctor)
emcli doctor
# 4. The agent can now read
# 4. The agent can now read (needs only EMCLI_KEY)
emcli list --account gmail --folder INBOX --limit 10
```
@@ -491,10 +535,16 @@ emcli config get audit_retention_days
## 11. Troubleshooting
**"EMCLI_KEY is not set" / "must be base64 of exactly 32 bytes".** Set `EMCLI_KEY` to a valid
base64-encoded 32-byte key (section 2). Every command that touches the database needs it.
base64-encoded 32-byte key (section 2). Agent commands (`list`, `get`, `search`, `ack`, `send`,
`doctor`) need this key.
**A command fails to decrypt / "wrong EMCLI_KEY?".** The key doesn't match the one used when the
account was added. Restore the original key, or re-add the account with the current key.
**"this command requires EMCLI_ADMIN_KEY (admin privilege)".** Set `EMCLI_ADMIN_KEY` (section 2).
Admin commands (`account`, `whitelist`, `config`, `audit`, `init`) need this key; `EMCLI_KEY`
alone is not enough for them.
**A command fails to decrypt / wrong key.** The key doesn't match the one used when the database
was initialised. Restore the original key, or re-run `emcli init` (idempotent — it won't regenerate
the DEK if one already exists) with both correct keys, then re-add any accounts if needed.
**`doctor` shows `IMAP FAIL` / `SMTP FAIL`.**
- *Invalid credentials / authentication failed* — wrong username or password. For Gmail, make sure
@@ -528,7 +578,7 @@ running non-interactively.
emcli # or: emcli help / emcli --help — list all commands
emcli <command> --help # usage and flags for one command
# Admin
# Admin (requires EMCLI_ADMIN_KEY)
emcli init # create DB + add first account (form)
emcli account add [flags | none for form] # add an account
emcli account list # list accounts (no secrets)
@@ -537,10 +587,10 @@ emcli account remove --name N --yes # delete an account
emcli whitelist in|out add|remove|list --account N [--address A]
emcli config set|get <key> [value] # e.g. audit_retention_days
emcli audit list [--account N] [--limit K]
emcli doctor [--account N] # connectivity/auth check
emcli version
# Agent (one line of JSON each)
# Agent (requires EMCLI_KEY or EMCLI_ADMIN_KEY; one line of JSON each)
emcli doctor [--account N] # connectivity/auth check
emcli list --account N [--folder F] [--new] [--limit K] [--before U] [--since U]
emcli get --account N [--folder F] --uid U
emcli search --account N [--folder F] [--from A] [--subject-contains S] [--text S] [--since-date D] [--before-date D] [--limit K]
@@ -548,4 +598,4 @@ emcli ack --account N [--folder F] --uid-list U1,U2,U3
emcli send --account N --to A [--cc A] [--bcc A] --subject S --body B [--attach P]… [--reply-to U [--folder F]]
```
Environment: `EMCLI_KEY` (required, base64 32-byte key), `EMCLI_DB` (optional DB path).
Environment: `EMCLI_ADMIN_KEY` (required for admin commands, base64 32-byte key), `EMCLI_KEY` (required for agent commands, base64 32-byte key), `EMCLI_DB` (optional DB path).