feat(secrets): OpenBao-only backend via castle.yaml + per-node prefix

- backend selected by castle.yaml 'secrets:' block (env overrides); no file
  fallback in openbao mode (bootstrap token still read from file)
- route direct-read secrets (public DNS token, supabase pw) through read_secret
- OpenBaoBackend node_prefix: read <prefix>/<name> then shared <name>, so a
  shared vault serves per-node overrides (e.g. each node's postgres password)
- tests updated (fallback removed, settings + node_prefix selection)
This commit is contained in:
2026-07-07 06:34:50 -07:00
parent 813e2b01af
commit 836b1437ab
5 changed files with 90 additions and 52 deletions

View File

@@ -38,30 +38,36 @@ def test_build_backend_defaults_to_file(tmp_path: Path, monkeypatch) -> None:
assert isinstance(build_backend(tmp_path), FileSecretBackend)
def test_build_backend_openbao_selected(tmp_path: Path, monkeypatch) -> None:
def test_build_backend_openbao_via_env(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("CASTLE_SECRET_BACKEND", "openbao")
assert isinstance(build_backend(tmp_path), OpenBaoBackend)
def test_openbao_falls_back_to_file_when_unreachable(tmp_path: Path) -> None:
"""An unreachable vault (or empty token) resolves via the file fallback."""
def test_build_backend_openbao_via_settings(tmp_path: Path, monkeypatch) -> None:
"""The castle.yaml `secrets:` block selects the backend (env still overrides)."""
monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
settings = {"backend": "openbao", "addr": "https://vault:8200", "mount": "castle"}
assert isinstance(build_backend(tmp_path, settings), OpenBaoBackend)
def test_openbao_unreachable_returns_none_no_fallback(tmp_path: Path) -> None:
"""No file fallback: an unreachable vault returns None even if a file exists."""
(tmp_path / "ONLY_IN_FILE").write_text("from-file")
backend = OpenBaoBackend(
addr="http://127.0.0.1:1", # nothing listening
token="dummy",
mount="castle",
fallback=FileSecretBackend(tmp_path),
)
assert backend.read("ONLY_IN_FILE") == "from-file"
backend = OpenBaoBackend(addr="http://127.0.0.1:1", token="dummy", mount="castle")
assert backend.read("ONLY_IN_FILE") is None
assert backend.read("NOT_ANYWHERE") is None
def test_openbao_empty_token_uses_fallback(tmp_path: Path) -> None:
(tmp_path / "K").write_text("v")
backend = OpenBaoBackend(
addr="http://127.0.0.1:8200",
token="", # no token → never hits the network
mount="castle",
fallback=FileSecretBackend(tmp_path),
def test_openbao_empty_token_returns_none(tmp_path: Path) -> None:
backend = OpenBaoBackend(addr="http://127.0.0.1:8200", token="", mount="castle")
assert backend.read("K") is None
def test_openbao_node_prefix_from_settings(tmp_path: Path, monkeypatch) -> None:
monkeypatch.delenv("CASTLE_SECRET_BACKEND", raising=False)
backend = build_backend(
tmp_path,
{"backend": "openbao", "addr": "http://x", "node_prefix": "nodes/primer"},
)
assert backend.read("K") == "v"
assert isinstance(backend, OpenBaoBackend)
assert backend._node_prefix == "nodes/primer"