secrets-engine wrap writes a single-use OpenBao wrap token to a mode-0600 out-of-repo file and never prints it. KV reads and AppRole secret_ids are wrapped with a 15m TTL cap. Unwrapped secret payloads fail closed. Production wrap remains fail-closed. Assistant: grok Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
import copy
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from secrets_engine.catalog import validate_entry
|
|
from secrets_engine.config import Config
|
|
from secrets_engine.errors import BackendError, DecisionError, ProvisioningError
|
|
from secrets_engine.openbao import OpenBaoClient
|
|
from secrets_engine.wrap import normalize_wrap_ttl, write_wrapped_handoff
|
|
from tests.test_catalog import VALID
|
|
|
|
WRAP_TOKEN = "wrap-token-SUPER-SECRET"
|
|
WRAP_ACCESSOR = "wrap-accessor-value"
|
|
|
|
|
|
class FakeWrapClient:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def wrap_kv_get(self, mount, path, *, ttl):
|
|
from secrets_engine.openbao import WrappedResponse, accessor_fingerprint
|
|
|
|
self.calls.append(("kv", mount, path, ttl))
|
|
return WrappedResponse(
|
|
wrap_token=WRAP_TOKEN,
|
|
accessor_fingerprint=accessor_fingerprint(WRAP_ACCESSOR),
|
|
ttl=ttl,
|
|
creation_path=f"{mount}/{path}",
|
|
)
|
|
|
|
def wrap_approle_secret_id(self, role_name, *, ttl):
|
|
from secrets_engine.openbao import WrappedResponse, accessor_fingerprint
|
|
|
|
self.calls.append(("approle", role_name, ttl))
|
|
return WrappedResponse(
|
|
wrap_token=WRAP_TOKEN,
|
|
accessor_fingerprint=accessor_fingerprint(WRAP_ACCESSOR),
|
|
ttl=ttl,
|
|
creation_path=f"auth/approle/role/{role_name}/secret-id",
|
|
)
|
|
|
|
|
|
def test_normalize_wrap_ttl_bounds():
|
|
assert normalize_wrap_ttl("15m") == "15m"
|
|
assert normalize_wrap_ttl("60s") == "60s"
|
|
with pytest.raises(ProvisioningError, match="15m"):
|
|
normalize_wrap_ttl("16m")
|
|
with pytest.raises(ProvisioningError, match="like"):
|
|
normalize_wrap_ttl("15")
|
|
|
|
|
|
def test_wrap_kv_writes_0600_file_and_omits_token_from_result(tmp_path):
|
|
out = tmp_path / "wrap.token"
|
|
entry = validate_entry(copy.deepcopy(VALID))
|
|
result = write_wrapped_handoff(
|
|
FakeWrapClient(), entry, out_file=out, ttl="15m"
|
|
)
|
|
assert out.read_text().strip() == WRAP_TOKEN
|
|
assert (out.stat().st_mode & 0o077) == 0
|
|
assert result.wrap_handle
|
|
assert WRAP_TOKEN not in result.wrap_handle
|
|
assert WRAP_TOKEN not in result.out_file
|
|
assert WRAP_ACCESSOR not in result.wrap_handle
|
|
|
|
|
|
def test_wrap_rejects_repo_paths():
|
|
entry = validate_entry(copy.deepcopy(VALID))
|
|
with pytest.raises(ProvisioningError, match="Git worktree"):
|
|
write_wrapped_handoff(
|
|
FakeWrapClient(),
|
|
entry,
|
|
out_file=__import__("pathlib").Path("wrap.token"),
|
|
ttl="15m",
|
|
)
|
|
|
|
|
|
def test_parse_wrap_rejects_unwrapped_secret_payload():
|
|
client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao")
|
|
secret_json = json.dumps(
|
|
{"data": {"data": {"api_token": "npm_SHOULDNEVERPARSE"}}}
|
|
)
|
|
with pytest.raises(BackendError, match="was not applied") as raised:
|
|
client._parse_wrap_response(
|
|
secret_json, ttl="15m", creation_path="secret/x"
|
|
)
|
|
assert "npm_SHOULDNEVERPARSE" not in str(raised.value)
|
|
|
|
|
|
def test_parse_wrap_accepts_wrap_info_only():
|
|
client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao")
|
|
payload = json.dumps(
|
|
{
|
|
"wrap_info": {
|
|
"token": WRAP_TOKEN,
|
|
"accessor": WRAP_ACCESSOR,
|
|
"ttl": 900,
|
|
}
|
|
}
|
|
)
|
|
wrapped = client._parse_wrap_response(
|
|
payload, ttl="15m", creation_path="secret/x"
|
|
)
|
|
assert wrapped.wrap_token == WRAP_TOKEN
|
|
assert WRAP_ACCESSOR not in wrapped.accessor_fingerprint
|
|
assert WRAP_TOKEN not in repr(wrapped)
|
|
|
|
|
|
def test_wrap_kv_get_uses_wrap_ttl_flag(monkeypatch):
|
|
client = OpenBaoClient(addr="http://example.invalid", token="t", bao_bin="bao")
|
|
seen = {}
|
|
|
|
def fake_run(args, stdin=None):
|
|
seen["args"] = list(args)
|
|
return SimpleNamespace(
|
|
returncode=0,
|
|
stdout=json.dumps(
|
|
{"wrap_info": {"token": WRAP_TOKEN, "accessor": WRAP_ACCESSOR}}
|
|
),
|
|
stderr="",
|
|
)
|
|
|
|
monkeypatch.setattr(client, "_run", fake_run)
|
|
wrapped = client.wrap_kv_get("secret", "test/team/thing", ttl="15m")
|
|
assert "-wrap-ttl=15m" in seen["args"]
|
|
assert WRAP_TOKEN not in " ".join(seen["args"])
|
|
assert wrapped.wrap_token == WRAP_TOKEN
|
|
|
|
|
|
def test_production_wrap_fails_closed(tmp_path, monkeypatch):
|
|
from secrets_engine import cli
|
|
|
|
data = copy.deepcopy(VALID)
|
|
data.update(stage="prod", approval={"model": "decision", "decision_ref": "x"})
|
|
entry = validate_entry(data)
|
|
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
|
|
monkeypatch.delenv("SECRETS_ENGINE_UNSAFE_DEMO", raising=False)
|
|
monkeypatch.setattr(
|
|
cli.OpenBaoClient,
|
|
"resolve",
|
|
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
|
|
)
|
|
cfg = Config(
|
|
catalog_dir=tmp_path,
|
|
policy_dir=tmp_path,
|
|
evidence_dir=tmp_path / "evidence",
|
|
hub_url="http://127.0.0.1:8000",
|
|
bao_addr="http://127.0.0.1:8200",
|
|
topic_id="test-topic",
|
|
)
|
|
args = SimpleNamespace(
|
|
catalog_id=entry.id,
|
|
out=str(tmp_path / "wrap.token"),
|
|
ttl="15m",
|
|
bootstrap_token_file=None,
|
|
auth="auto",
|
|
)
|
|
with pytest.raises(DecisionError, match="production action 'wrap'"):
|
|
cli.cmd_wrap(cfg, args)
|