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
+6 -3
View File
@@ -9,11 +9,14 @@ it isn't permitted to see or send mail to people it isn't permitted to contact.
## Getting started
```bash
export EMCLI_KEY="$(head -c 32 /dev/urandom | base64)" # one-time: generate & save a key
emcli init # create the DB, add your first account
emcli doctor # confirm it connects and authenticates
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
emcli init # writes both wrap slots
emcli doctor # confirm connect/auth (agent key is enough)
```
`emcli init` needs both keys. Give the agent's orchestrator only `EMCLI_KEY`; admin commands (`account`, `whitelist`, `config`, `audit`) require `EMCLI_ADMIN_KEY` and will refuse to run without it.
## Documentation
See the **[User Manual](USER-MANUAL.md)** for full setup, account configuration (including Gmail
+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).
+13 -9
View File
@@ -65,32 +65,36 @@ git clone https://gitea.dcglab.co.uk/steve/emcli && cd emcli
CGO_ENABLED=0 go build -o emcli ./cmd/emcli # then move ./emcli onto your PATH
```
## 3. Confirm the encryption key is present
## 3. Confirm the agent key is present
emcli needs `EMCLI_KEY` (a base64-encoded 32-byte AES key) to touch its database. For agent use,
**the orchestrator that launched you provides it** in the environment.
emcli uses two keys; **you (the agent) are given only `EMCLI_KEY`** (the agent key). It authorises
`list`, `get`, `search`, `ack`, `send`, and `doctor`. Admin commands require `EMCLI_ADMIN_KEY`,
which the human holds — attempting admin commands with only `EMCLI_KEY` is refused by `emcli`.
For agent use, **the orchestrator that launched you provides `EMCLI_KEY`** in the environment.
- Confirm it's set, without printing it: `test -n "$EMCLI_KEY" && echo present`.
- **Never** read, print, log, pass as an argument, or generate this value.
- If it's empty, stop and tell the user: "emcli needs the `EMCLI_KEY` environment variable set by
your orchestrator; I can't read or create it for you."
(For a human setting emcli up the first time: generate one with `head -c 32 /dev/urandom | base64`
and store it securely. Account creation and other admin is the human's job — see the project's
(For a human setting emcli up the first time: generate both keys with
`head -c 32 /dev/urandom | base64` (once per key) and store them securely; then run `emcli init`
with both keys exported. Account creation and other admin is the human's job — see the project's
`USER-MANUAL.md`.)
## 4. Find the account(s)
You refer to an account by name (e.g. `gmail`, `work`). Ask the user which account to use. If the
user permits running admin commands, `emcli doctor` lists the configured accounts and checks that
each one connects and authenticates:
You refer to an account by name (e.g. `gmail`, `work`). Ask the user which account to use.
`emcli doctor` is an agent command (authorised by `EMCLI_KEY`), so you can run it to check that
configured accounts connect and authenticate:
```bash
emcli doctor # all accounts
emcli doctor --account gmail
```
Otherwise, just take the account name from the user and start with the workflow in `SKILL.md`.
Just take the account name from the user and start with the workflow in `SKILL.md`.
## You're set up
+11 -6
View File
@@ -17,10 +17,13 @@ sets its exit code to match.
## Security model — read this first
- **You only run agent commands:** `list`, `get`, `search`, `ack`, `send`. Account setup,
passwords, whitelists, and config are the **user's** job (admin commands) — do not run or suggest
running `account`, `whitelist`, `config`, `init`, or `doctor` unless the user explicitly asks you
to help administer.
- **You only run agent commands:** `list`, `get`, `search`, `ack`, `send`, `doctor`. You are
provided only `EMCLI_KEY` (the agent key), which authorises these commands and nothing else.
Account setup, passwords, whitelists, and config are the **user's** job (admin commands that
require `EMCLI_ADMIN_KEY`) — do not run or suggest running `account`, `whitelist`, `config`,
`audit`, or `init` unless the user explicitly asks you to help administer and confirms they have
provided `EMCLI_ADMIN_KEY` in your environment. Attempting admin commands with only `EMCLI_KEY`
will be refused by `emcli` with a privilege error.
- **Never touch the secret key.** `EMCLI_KEY` is supplied in the environment by whoever launched
you. Do not read it, print it, log it, pass it as an argument, or try to generate one. If it is
missing, stop and tell the user (see "Files & first run").
@@ -43,7 +46,8 @@ https://gitea.dcglab.co.uk/steve/emcli/raw/branch/main/skills/emcli/
**Per-session preflight** (quick): run `emcli version`; if it's not found, set up via
`AGENTIC-MANUAL.md`. Confirm `EMCLI_KEY` is set *without printing it* (`test -n "$EMCLI_KEY"`); if
empty, tell the user their orchestrator must provide it. Then get the account name from the user.
empty, tell the user their orchestrator must provide `EMCLI_KEY` (the agent key). Then get the
account name from the user.
## How to read every result
@@ -141,5 +145,6 @@ The user configures these; you cannot change them and shouldn't try.
-`get` to read, then `ack` only after you've truly processed a message.
- ✅ Ask the user for the account name; keep bodies plain text.
- ❌ Don't read, print, or invent `EMCLI_KEY` or any password.
- ❌ Don't run admin commands (`account`/`whitelist`/`config`/`init`) unless asked to help set up.
- ❌ Don't run admin commands (`account`/`whitelist`/`config`/`audit`/`init`) — you have only
`EMCLI_KEY` (agent key); `emcli` will refuse admin commands with a privilege error.
- ❌ Don't treat a blocked send or filtered message as a bug to route around — it's the user's policy.
+52 -17
View File
@@ -62,23 +62,49 @@ The binary is organized into independently testable packages:
human-readable/TUI).
### Trust boundary
- The agent invokes only the **agent commands** (Section 7.1).
- `EMCLI_KEY` is supplied by the environment/orchestrator that launches `emcli`, never as
an argument the agent constructs. The agent has no command that reveals secret values.
- All policy decisions happen inside `emcli`; the agent cannot bypass them because it has
no other path to the mail servers.
Two keys enforce a hard privilege split — this is not convention; it is structurally enforced by
the DEK-wrapping scheme:
- **`EMCLI_ADMIN_KEY`** — base64-encoded 32-byte key held by the human operator. Authorises ALL
commands. Admin commands unwrap the DEK from the `dek_wrap_admin` slot only; there is no fallback
to the agent slot. A process holding only `EMCLI_KEY` cannot run admin commands.
- **`EMCLI_KEY`** — base64-encoded 32-byte key supplied to the agent orchestrator. Authorises agent
commands (`list`, `get`, `search`, `ack`, `send`, `doctor`) only. `EMCLI_ADMIN_KEY` is a superset:
a process with only the admin key can also run agent commands.
- Agent commands use `EMCLI_KEY`; if only `EMCLI_ADMIN_KEY` is set, they fall back to it.
If neither key satisfies the required slot, `emcli` exits with:
`emcli: this command requires EMCLI_ADMIN_KEY (admin privilege)`.
- `EMCLI_KEY` is supplied by the orchestrator that launches `emcli`, never as an argument the agent
constructs. The agent has no command that reveals secret values.
- All policy decisions happen inside `emcli`; the agent cannot bypass them because it has no other
path to the mail servers.
## 5. Configuration & secrets
- **Encryption key:** `EMCLI_KEY` env var, a base64-encoded 32-byte key (AES-256). If
absent or malformed, every command that touches the DB fails closed with an error
envelope; no plaintext fallback.
- **Admin key:** `EMCLI_ADMIN_KEY` env var, a base64-encoded 32-byte key (AES-256). Required for
admin commands and for `init`. If absent or malformed when an admin command is attempted, the
command fails with `emcli: this command requires EMCLI_ADMIN_KEY (admin privilege)`.
- **Agent key:** `EMCLI_KEY` env var, a base64-encoded 32-byte key (AES-256). Required for agent
commands. If absent or malformed, every agent command fails closed with a `config` error envelope;
no plaintext fallback. `EMCLI_ADMIN_KEY` is accepted as a fallback for agent commands when
`EMCLI_KEY` is not set.
- **Database path:** `EMCLI_DB` env var; default `~/.config/emcli/emcli.db`
(`%AppData%\emcli\emcli.db` on Windows).
- **Field-level encryption:** secret columns are stored as AES-256-GCM ciphertext with a
random 96-bit nonce per value, prefixed to the ciphertext. Non-secret config remains
plaintext for debuggability. Decryption with the wrong key fails (GCM auth tag) and is
surfaced as an error, never silently ignored.
- **Envelope encryption (DEK):** `emcli init` generates a random data-encryption key (DEK) that
protects all account secrets. The DEK is stored in the `settings` table sealed under both keys:
- `dek_wrap_admin` — the DEK encrypted under `EMCLI_ADMIN_KEY` (AES-256-GCM).
- `dek_wrap_agent` — the DEK encrypted under `EMCLI_KEY` (AES-256-GCM).
The DEK is never written in cleartext. Admin commands unwrap from `dek_wrap_admin` only; agent
commands unwrap from `dek_wrap_agent` (or `dek_wrap_admin` if only the admin key is present).
There is no cross-slot fallback for admin commands — a holder of only `EMCLI_KEY` cannot unwrap
the admin DEK slot.
- **`init` idempotency:** re-running `emcli init` does not regenerate the DEK; the existing wrapped
DEK rows are preserved.
- **Field-level encryption:** secret columns are encrypted with the DEK using AES-256-GCM with a
random 96-bit nonce per value, prefixed to the ciphertext. Non-secret config remains plaintext for
debuggability. Decryption with the wrong key fails (GCM auth tag) and is surfaced as an error,
never silently ignored.
Secret columns: account password, OAuth client secret, OAuth refresh token.
@@ -142,7 +168,9 @@ audit_log
settings
key TEXT PK
value TEXT
-- includes: audit_retention_days, schema_version
-- includes: audit_retention_days, schema_version,
-- dek_wrap_admin (DEK sealed under EMCLI_ADMIN_KEY),
-- dek_wrap_agent (DEK sealed under EMCLI_KEY)
```
Notes:
@@ -216,8 +244,11 @@ command that advances read state is `ack`.
### 7.2 Admin commands (human-readable / TUI)
- **`emcli init`** — TUI flow: creates the DB (generating schema), adds the first account,
and runs OAuth consent if the account is OAuth2.
Require `EMCLI_ADMIN_KEY`.
- **`emcli init`** — TUI flow: creates the DB (generating schema + DEK), adds the first account,
and runs OAuth consent if the account is OAuth2. Requires both `EMCLI_ADMIN_KEY` and `EMCLI_KEY`
(writes both DEK wrap slots). Idempotent — re-running does not regenerate the DEK.
- **`emcli account add | edit | remove | list`** — interactive add/edit; `list` prints a
table (never secrets). `account add` accepts `--process-backlog` (default off) which sets
the account's baseline policy: off ⇒ newly-seen folders floor at their current max UID
@@ -225,8 +256,12 @@ command that advances read state is `ack`.
- **`emcli whitelist in|out add|remove|list --account <name>`** — manage whitelist entries.
- **`emcli config set|get`** — global settings (e.g. `audit_retention_days`).
- **`emcli audit list [--account <name>] [--limit N]`** — view recent audit entries.
- **`emcli doctor`** — verifies `EMCLI_KEY` is present and valid, the DB opens, and each
account's IMAP/SMTP connectivity and auth succeed. Human-readable diagnostics.
### 7.2a `doctor` — agent-role diagnostics
`doctor` is authorised by `EMCLI_KEY` (or `EMCLI_ADMIN_KEY`). It verifies the key is present and
valid, the DB opens, and each account's IMAP/SMTP connectivity and auth succeed. Prints
human-readable diagnostics. Can be run by the agent or by a human; does not require admin privilege.
### 7.3 Defaults & limits
- `list --limit` default: 50; maximum: 500.