Skip to content

Wire the encrypted secrets vault (ahead of a trade key) - #115

Closed
eaitbrahim wants to merge 1 commit into
mainfrom
feat/secrets-vault
Closed

Wire the encrypted secrets vault (ahead of a trade key)#115
eaitbrahim wants to merge 1 commit into
mainfrom
feat/secrets-vault

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Wires the encrypted vault ahead of a trade-enabled key. The next thing before real money.

Why

The vault (security/secrets.py — scrypt → AES-GCM) was built in Phase 3 and sat unused: the CLI read credentials from a plaintext .env. Fine for the read-only key in use today; the wrong risk class for a trade-enabled key, which the supervised live test needs. This wires the vault the design already built.

The resolution layer — precedence, not fallback

New keel/security/secret_source.py centralises where credentials come from:

  1. The vault (secrets.enc) when it exists — the home for a trade key.
  2. .env only when no vault exists — the pre-vault path, unchanged.

A vault present but unlockable — no passphrase, wrong passphrase, tampered file — RAISES rather than silently reading .env. Falling through would let a stale or lower-privilege .env key stand in for the vault the operator deliberately created — the exact downgrade the vault exists to prevent. Pinned by a test.

The passphrase comes from KEEL_VAULT_PASSPHRASE (headless) or a TTY prompt — never from config or the DB.

CLI

  • keel vault init — import from .env, prompts twice, refuses to clobber without --force.
  • keel vault status — reports present / unlocks without printing values.
  • keel vault rekey — re-encrypt under a new passphrase; secrets unchanged.

_build_broker now resolves through this layer.

Verified end to end against the real CLI

init from .envstatus locked without a passphrase → unlocks via KEEL_VAULT_PASSPHRASEresolve_secrets prefers the vault → the .enc file contains no plaintext key. Errors never contain the passphrase or a secret (tested).

What the vault does NOT do

It decides only where the credential comes from. Confirm-mode, the 15 rails, and the dangerous-action gate still sit in front of every live order. docs/operator-secrets.md documents the trade-key setup: seal → verify → then delete .env.

Verification

1273 tests pass (up from 1258), ruff clean.

🤖 Generated with Claude Code

The vault (security/secrets.py: scrypt -> AES-GCM) was built in Phase 3
and sat UNUSED -- the CLI read credentials from a plaintext .env via
config.load_secrets. Fine for the read-only key in use today; the wrong
risk class for a TRADE-enabled key, which is what the supervised live
test needs. This wires the vault the design already built.

New keel/security/secret_source.py centralises WHERE credentials come
from, with a deliberate precedence that is NOT a fallback chain:
  1. the vault (secrets.enc) when it exists -- the home for a trade key;
  2. .env only when no vault exists -- the pre-vault path, unchanged.
⛔ A vault present but UNLOCKABLE (no/wrong passphrase, tampered file)
RAISES rather than silently reading .env. Falling through would let a
stale or lower-privilege .env key stand in for the vault the operator
deliberately created -- the exact downgrade the vault exists to prevent.
Pinned by test_a_vault_that_cannot_be_unlocked_RAISES_never_reads_env.

The passphrase comes from KEEL_VAULT_PASSPHRASE (headless) or a TTY
prompt -- never from config or the DB. A vault present with no passphrase
available fails closed with guidance.

_build_broker now resolves through this layer. New CLI: vault init
(import from .env, prompts twice, refuses to clobber without --force),
vault status (reports present/unlocks WITHOUT printing values), vault
rekey (re-encrypt under a new passphrase, secrets unchanged).

Verified end to end against the real CLI: init from .env -> status locked
without a passphrase -> unlocks via KEEL_VAULT_PASSPHRASE -> resolve_
secrets prefers the vault -> the .enc file contains NO plaintext key.
Errors never contain the passphrase or a secret (tested).

docs/operator-secrets.md documents the trade-key setup: seal, verify,
THEN delete .env. The vault decides only where the credential comes from
-- confirm-mode, the 15 rails and the dangerous-action gate still sit in
front of every live order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@eaitbrahim

Copy link
Copy Markdown
Contributor Author

Closing: superseded by a change of direction, not by a problem with the work.

The local encrypted secrets vault is being removed rather than wired in. Credentials come from a git-ignored .env, and real authentication arrives with the future server-hosted deployment — a local single-user vault is ceremony that buys nothing in between.

Worth recording: keel/security/secrets.py was already effectively dead code on main. data/cb_client.py and cli.py both load credentials via config.load_secrets() from .env; the vault was only reachable through migrate_from_env. So this PR would have wired up a second, competing credential path rather than replacing the existing one.

Reopen if we ever go back to a local vault.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant