Add response-wrapped operator handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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
This commit is contained in:
tegwick 2026-09-02 08:12:42 +02:00
parent 2278cefbb3
commit afd1c8e593
12 changed files with 387 additions and 8 deletions

View file

@ -9,6 +9,7 @@ Command surface (FR7):
provision <catalog-id> --stage <stage> (--from-file F | --generate) --field NAME
verify <catalog-id> [--positive] [--negative] [--field NAME] [--negative-token-file F]
handoff <catalog-id> --stage <stage> --role-id-file F --secret-id-file F
wrap <catalog-id> --out F [--ttl 15m]
exec --catalog <catalog-id> [--field NAME] [--mode auto|npm-config|exec-env] -- CMD...
route <catalog-id> [--json]
revoke <catalog-id>
@ -435,6 +436,37 @@ def cmd_handoff(cfg: Config, args) -> int:
return 0
def cmd_wrap(cfg: Config, args) -> int:
from secrets_engine.wrap import write_wrapped_handoff
entry = get_entry(cfg.catalog_dir, args.catalog_id)
with _privileged_evidence(
cfg, entry, "wrap", detail={"ttl": args.ttl, "out_file": args.out}
) as evidence:
decision = _require_lane_approval(cfg, entry, "wrap", evidence)
evidence.mark_approved(decision)
with _open_backend(cfg, args, evidence) as client:
result = write_wrapped_handoff(
client, entry, out_file=Path(args.out), ttl=args.ttl
)
print(
f"wrote wrap token for lane '{result.catalog_id}' — token not displayed"
)
print(f" out: {result.out_file}")
print(f" ttl: {result.ttl}")
print(f" handle: {result.wrap_handle or '-'}")
evidence.finish(
"wrap-token-written",
detail={
"out_file": result.out_file,
"ttl": result.ttl,
"wrap_handle": result.wrap_handle,
"creation_path": result.creation_path,
},
)
return 0
def cmd_exec(cfg: Config, args) -> int:
from secrets_engine.exec_delivery import exec_with_secret
entry = get_entry(cfg.catalog_dir, args.catalog)
@ -795,6 +827,16 @@ def build_parser() -> argparse.ArgumentParser:
add_token_arg(ha)
ha.set_defaults(func=cmd_handoff)
wr = sub.add_parser(
"wrap",
help="write a single-use OpenBao wrap token to a file (never printed)",
)
wr.add_argument("catalog_id")
wr.add_argument("--out", required=True, help="mode-0600 wrap-token file outside Git")
wr.add_argument("--ttl", default="15m", help="wrap TTL, max 15m (default 15m)")
add_token_arg(wr)
wr.set_defaults(func=cmd_wrap)
ex = sub.add_parser("exec", help="run a command with the secret injected for the child only")
ex.add_argument("--catalog", required=True)
ex.add_argument("--field", default=None)