Secrets + Keychain Audit Matrix

June 29, 2026 ยท View on GitHub

Last updated: 2026-02-25

This document captures the current, tested contract for Lemon secret resolution across keychain, encrypted store, and environment fallback paths.

Flow Matrix

LayerWrite PathRead/Resolve PathFallbacksPrimary Files
macOS Keychain master keyLemonCore.Secrets.MasterKey.init/1 -> Keychain.put_master_key/2LemonCore.Secrets.MasterKey.resolve/1 -> Keychain.get_master_key/1On :missing / :keychain_unavailable / command failures, tries env master key, then local fileapps/lemon_core/lib/lemon_core/secrets/keychain.ex, apps/lemon_core/lib/lemon_core/secrets/master_key.ex
Master key env fallbackManual set of LEMON_SECRETS_MASTER_KEY (external)MasterKey.resolve/1 via resolve_from_env/1On missing env, tries ~/.lemon/secrets_master_key; malformed env still fails as :invalid_master_keyapps/lemon_core/lib/lemon_core/secrets/master_key.ex
Local master key file fallback~/.lemon/secrets_master_keyMasterKey.resolve/1 via local file fallbackReturns :missing_master_key or :invalid_master_key when no valid source existsapps/lemon_core/lib/lemon_core/secrets/master_key.ex
Encrypted secret storeLemonCore.Secrets.set/3 (AES-256-GCM at rest)LemonCore.Secrets.get/2, resolve/2, exists?/2resolve/2 can fallback to env by same secret name (env_fallback: true)apps/lemon_core/lib/lemon_core/secrets.ex
Coding Agent provider secret refsConfigured api_key_secret names in provider configCodingAgent.Session.resolve_secret_api_key/1 -> LemonCore.Secrets.resolve/2Store first, env fallback enabledapps/coding_agent/lib/coding_agent/session.ex

Keychain Error Semantics (Current Contract)

LemonCore.Secrets.Keychain maps command outcomes to stable errors:

  • Exit code 44 -> {:error, :missing}
  • Non-zero exit codes -> {:error, {:command_failed, code, stderr_or_output}}
  • Timeout (Task.yield expiry) -> {:error, :timeout}
  • Non-macOS / missing security executable -> {:error, :unavailable}
  • Empty retrieved value -> treated as missing ({:error, :missing})

Master Key Resolution Precedence

LemonCore.Secrets.MasterKey.resolve/1 order:

  1. Keychain (:keychain source)
  2. LEMON_SECRETS_MASTER_KEY (:env source)
  3. ~/.lemon/secrets_master_key (:file source)
  4. Error (:missing_master_key, :invalid_master_key, or {:keychain_failed, reason})

Additional nuance:

  • If keychain returns malformed key material, env fallback is attempted first; only then returns :invalid_master_key.
  • status/1 suppresses expected keychain absence (:missing, :keychain_unavailable) from keychain_error while still surfacing hard failures.
  • The local source launcher bin/lemon also normalizes LEMON_SECRETS_MASTER_KEY from ~/.lemon/secrets_master_key on non-macOS systems so stale desktop/session env does not override the working local key by accident.

Operator Notes

  • mix lemon.secrets.init is the preferred bootstrap path on macOS (stores generated key in keychain).
  • For local non-macOS development, keep ~/.lemon/secrets_master_key as the canonical master key file. bin/lemon will export that value into LEMON_SECRETS_MASTER_KEY before boot when the file exists.
  • secrets.list and secrets.status return metadata only (never plaintext secret values).
  • If keychain prompts are denied (User interaction is not allowed), Lemon can still operate via env fallback when configured.

Validation References

  • apps/lemon_core/test/lemon_core/secrets/keychain_test.exs
  • apps/lemon_core/test/lemon_core/secrets/master_key_test.exs
  • apps/lemon_core/test/lemon_core/secrets_test.exs