ADR-0032: Pluggable secret store — keyring default, encrypted file vault optional¶
Status: Accepted (2026-06-13) Context: Finance build phase F2. Amends ADR-0003 (Keychain credentials over a custom vault). Companion: ADR-0011 (keychain module shape), ADR-0031 (finance data layer).
Context¶
ADR-0003 stored OAuth tokens in the OS keychain via the keyring
library and explicitly rejected a custom vault. That call was right for
tokens, and keyring is already cross-platform (macOS Keychain, Windows
Credential Manager, Linux Secret Service) — so "OS keychain" was never
macOS-only.
F2 needs to store a few more secrets — a PAN, a date of birth, and per-institution statement-PDF passwords — to unlock CAS/bank PDFs during extraction. Two pressures surfaced:
- Headless / server / Docker / WSL / CI contexts have no desktop
secret service;
keyringerrors or silently falls back to an insecure backend there. IRIS is meant to be self-hostable and open-sourceable, so a keychain-only story is too narrow. - The user asked specifically whether secrets could live in an encrypted local vault for portability across Windows and macOS.
credentials.py already anticipated this: its docstring says connector
skills never touch keyring directly "so the storage backend stays
swappable." This ADR makes that swap real.
Decision¶
A small SecretStore protocol (get/set/delete, namespaced) with
two backends behind one factory (get_secret_store), selected by
IRIS_SECRET_BACKEND (default keyring):
KeyringSecretStore(default). Native OS keychain via the existingidentity.credentialshelpers — zero-config on macOS/Windows/Linux desktops. Finance secrets sit alongside OAuth tokens under the sameiris-<namespace>service scheme.FernetVaultStore(opt-in). An AES-128 (Fernet) encrypted SQLite file atdata/secrets.db(gitignored). Portable — one file — and works headless. Values are encrypted before touching disk; namespace/key are stored in clear (lookup coordinates, not secrets).
Master-key derivation for the vault (the chicken-and-egg of "where
does the key live"):
- IRIS_VAULT_PASSPHRASE → scrypt-derived key (n=2¹⁴). Fully portable, no
OS dependency — unlock with a passphrase like a password manager.
- else → a random Fernet key auto-created and kept in the OS keychain, so
only one small key depends on keyring and all the actual secrets
stay in the portable file.
cryptography (Fernet/scrypt) was already a dependency; no new deps.
Amendment to ADR-0003: "no custom vault" stands as the default for tokens, but is narrowed to "keyring is the default backend; an encrypted-file vault is an available backend for finance secrets and headless deployments." Same spirit (don't roll bespoke crypto for the common desktop case), broader reach.
Alternatives considered¶
- Keychain only. Simplest, but excludes headless/server/open-source self-hosters — the exact users IRIS courts.
- Vault only (drop keyring). Portable, but loses the zero-config native-keychain experience and forces a passphrase prompt on every desktop user.
- Per-vault random salt for the passphrase KDF. Marginally stronger, but the salt must be readable before the vault opens (chicken-and-egg); a constant app salt + strong passphrase + scrypt work factor is the standard pragmatic choice. Documented in-code so it isn't mistaken for an oversight.
- Plaintext on disk with chmod 700. Rejected — statement passwords and a PAN are exactly the secrets that must be encrypted at rest.
Consequences¶
Positive:
- One call site (get_secret_store); finance never imports a backend.
- Desktop users: nothing to configure. Headless/portable/open-source
users: IRIS_SECRET_BACKEND=vault + a passphrase.
- Encrypted-at-rest finance secrets; verified by a test asserting the
plaintext never appears in the vault file.
Negative / deferred:
- The vault's passphrase UX (prompt, cache for a session) isn't built yet;
F2 reads IRIS_VAULT_PASSPHRASE from env. A CLI unlock flow can come
later.
- No secret rotation tooling yet.
Reversibility: high — the protocol means new backends (age, libsodium, cloud KMS) are additive.
Related ADRs¶
- ADR-0003 / ADR-0011 — keychain credentials (amended here).
- ADR-0031 (finance data layer), ADR-0033 (statement extraction, the first consumer of finance secrets).