feat(secrets): pluggable secret backend with OpenBao read (Phase 4)

- secret_backends.py: SecretBackend protocol, FileSecretBackend (historical),
  OpenBaoBackend (KV-v2 read + file fallback), build_backend() env-selected
- _read_secret delegates to the active backend; default is file, so production
  is unchanged until CASTLE_SECRET_BACKEND=openbao
- OpenBao token bootstraps from the file backend; missing/unreachable falls back
- tests: file hit/miss, backend selection, unreachable + empty-token fallback

Read path verified live against castle-openbao. Write path, auto-unseal-on-boot,
and TLS hardening documented as remaining for full OpenBao production use.
This commit is contained in:
2026-07-07 05:08:16 -07:00
parent c67c06d8e6
commit f94517887e
4 changed files with 183 additions and 5 deletions

View File

@@ -319,10 +319,15 @@ def resolve_env_vars(
def _read_secret(name: str) -> str:
"""Read a secret from ~/.castle/secrets/<name>. Returns placeholder if not found."""
secret_path = SECRETS_DIR / name
if secret_path.exists():
return secret_path.read_text().strip()
"""Resolve a secret via the active backend (file by default; OpenBao opt-in).
Returns a ``<MISSING_SECRET:...>`` placeholder if unresolved (never raises).
"""
from castle_core.secret_backends import build_backend
value = build_backend(SECRETS_DIR).read(name)
if value is not None:
return value
return f"<MISSING_SECRET:{name}>"