Harden secret provisioning and lifecycle controls
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
This commit is contained in:
parent
0617923ff1
commit
3a1bd4f1c8
23 changed files with 1369 additions and 162 deletions
|
|
@ -12,6 +12,7 @@ Command surface (FR7):
|
|||
exec --catalog <catalog-id> [--field NAME] [--mode auto|npm-config|exec-env] -- CMD...
|
||||
route <catalog-id> [--json]
|
||||
revoke <catalog-id>
|
||||
lifecycle suspend|deactivate|destroy <catalog-id>
|
||||
|
||||
Every privileged action is decision-gated and writes non-secret evidence.
|
||||
`plan` and `apply --dry-run` never mutate OpenBao.
|
||||
|
|
@ -214,13 +215,32 @@ def cmd_verify(cfg: Config, args) -> int:
|
|||
entry = get_entry(cfg.catalog_dir, args.catalog_id)
|
||||
decision = _require_lane_approval(cfg, entry)
|
||||
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
|
||||
field = args.field or (entry.fields[0] if entry.fields else "")
|
||||
if entry.stores_kv_value() and not field:
|
||||
fields = [args.field] if args.field else list(entry.fields)
|
||||
if entry.stores_kv_value() and not fields:
|
||||
from secrets_engine.errors import VerificationError
|
||||
raise VerificationError(f"lane '{entry.id}' has no field to verify")
|
||||
positive = args.positive or not args.negative
|
||||
negative = args.negative or not args.positive
|
||||
results = run_verification(client, entry, field, positive=positive, negative=negative)
|
||||
if entry.stores_kv_value():
|
||||
results = []
|
||||
if positive:
|
||||
for field in fields:
|
||||
results.extend(
|
||||
run_verification(
|
||||
client, entry, field, positive=True, negative=False
|
||||
)
|
||||
)
|
||||
if negative:
|
||||
# Denial is path-scoped, so one probe covers every field on this path.
|
||||
results.extend(
|
||||
run_verification(
|
||||
client, entry, fields[0], positive=False, negative=True
|
||||
)
|
||||
)
|
||||
else:
|
||||
results = run_verification(
|
||||
client, entry, "", positive=positive, negative=negative
|
||||
)
|
||||
rc = 0
|
||||
for r in results:
|
||||
print(r.render())
|
||||
|
|
@ -287,10 +307,33 @@ def cmd_exec(cfg: Config, args) -> int:
|
|||
w.record("exec", result="attempt", catalog_id=entry.id, stage=entry.stage,
|
||||
decision_id=decision.id if decision else "",
|
||||
detail={"command": args.command[0], "mode": args.mode})
|
||||
rc = exec_with_secret(client, entry, field, args.command, mode=args.mode)
|
||||
session_detail: dict[str, object] = {}
|
||||
try:
|
||||
rc = exec_with_secret(
|
||||
client,
|
||||
entry,
|
||||
field,
|
||||
args.command,
|
||||
mode=args.mode,
|
||||
session_evidence=session_detail,
|
||||
)
|
||||
except SecretsEngineError as e:
|
||||
w.record(
|
||||
"exec",
|
||||
result=f"failed-{type(e).__name__}",
|
||||
catalog_id=entry.id,
|
||||
stage=entry.stage,
|
||||
decision_id=decision.id if decision else "",
|
||||
detail={
|
||||
"command": args.command[0],
|
||||
"mode": args.mode,
|
||||
"session": session_detail,
|
||||
},
|
||||
)
|
||||
raise
|
||||
w.record("exec", result=f"exit-{rc}", catalog_id=entry.id, stage=entry.stage,
|
||||
decision_id=decision.id if decision else "",
|
||||
detail={"command": args.command[0]})
|
||||
detail={"command": args.command[0], "session": session_detail})
|
||||
return rc
|
||||
|
||||
|
||||
|
|
@ -338,36 +381,81 @@ def cmd_route(cfg: Config, args) -> int:
|
|||
|
||||
|
||||
def cmd_revoke(cfg: Config, args) -> int:
|
||||
from secrets_engine.lifecycle import (
|
||||
apply_lifecycle_plan,
|
||||
build_native_deactivation_plan,
|
||||
)
|
||||
|
||||
entry = get_entry(cfg.catalog_dir, args.catalog_id)
|
||||
decision = None if args.dry_run else _require_lane_approval(cfg, entry)
|
||||
client = OpenBaoClient.resolve(cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file)
|
||||
if entry.kind == "auth-capability":
|
||||
if args.dry_run:
|
||||
print(
|
||||
f"(dry-run) would delete approle {entry.role_name} "
|
||||
f"and policy {entry.policy_name}"
|
||||
)
|
||||
return 0
|
||||
client.delete_approle(entry.role_name)
|
||||
client.delete_policy(entry.policy_name)
|
||||
print(
|
||||
f"revoked lane '{entry.id}': deleted approle {entry.role_name} "
|
||||
f"and policy {entry.policy_name}"
|
||||
)
|
||||
_writer(cfg).record(
|
||||
"revoke", result="auth-capability-deactivated", catalog_id=entry.id,
|
||||
stage=entry.stage, decision_id=decision.id if decision else ""
|
||||
)
|
||||
return 0
|
||||
plan = build_native_deactivation_plan(entry)
|
||||
if args.dry_run:
|
||||
print(f"(dry-run) would delete KV metadata {entry.mount}/{entry.path} "
|
||||
f"and approle {entry.role_name}")
|
||||
print(plan.render())
|
||||
print("\n(dry-run: no OpenBao mutation performed)")
|
||||
return 0
|
||||
client.kv_delete_metadata(entry.mount, entry.path)
|
||||
print(f"revoked lane '{entry.id}': KV metadata deleted at {entry.mount}/{entry.path}")
|
||||
client = OpenBaoClient.resolve(
|
||||
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
|
||||
)
|
||||
result = apply_lifecycle_plan(client, plan)
|
||||
print(plan.render())
|
||||
print(result.render())
|
||||
_writer(cfg).record(
|
||||
"revoke", result="deactivated", catalog_id=entry.id, stage=entry.stage,
|
||||
decision_id=decision.id if decision else ""
|
||||
"revoke",
|
||||
result="native-access-deactivated",
|
||||
catalog_id=entry.id,
|
||||
stage=entry.stage,
|
||||
decision_id=decision.id if decision else "",
|
||||
detail={
|
||||
"operation": plan.operation,
|
||||
"applied": list(result.applied),
|
||||
"preserved": list(result.preserved),
|
||||
},
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_lifecycle(cfg: Config, args) -> int:
|
||||
from secrets_engine.lifecycle import (
|
||||
apply_lifecycle_plan,
|
||||
build_lifecycle_plan,
|
||||
require_destroy_confirmation,
|
||||
)
|
||||
|
||||
entry = get_entry(cfg.catalog_dir, args.catalog_id)
|
||||
if args.operation == "destroy" and not args.dry_run:
|
||||
require_destroy_confirmation(entry, args.confirm_destroy)
|
||||
# A lane-level approval is not an authorization to erase custody. Keep
|
||||
# the destructive live path closed until T04 supplies a canonical,
|
||||
# exact-action, expiring and dual-control-capable decision contract.
|
||||
from secrets_engine.errors import PolicyGuardError
|
||||
|
||||
raise PolicyGuardError(
|
||||
"live destroy is disabled until an exact-action destruction "
|
||||
"approval contract is available; use --dry-run to inspect targets"
|
||||
)
|
||||
decision = None if args.dry_run else _require_lane_approval(cfg, entry)
|
||||
plan = build_lifecycle_plan(entry, args.operation)
|
||||
if args.dry_run:
|
||||
print(plan.render())
|
||||
print("\n(dry-run: no OpenBao mutation performed)")
|
||||
return 0
|
||||
client = OpenBaoClient.resolve(
|
||||
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
|
||||
)
|
||||
result = apply_lifecycle_plan(client, plan)
|
||||
print(plan.render())
|
||||
print(result.render())
|
||||
_writer(cfg).record(
|
||||
f"lifecycle-{args.operation}",
|
||||
result="applied",
|
||||
catalog_id=entry.id,
|
||||
stage=entry.stage,
|
||||
decision_id=decision.id if decision else "",
|
||||
detail={
|
||||
"operation": plan.operation,
|
||||
"applied": list(result.applied),
|
||||
"preserved": list(result.preserved),
|
||||
},
|
||||
)
|
||||
return 0
|
||||
|
||||
|
|
@ -455,12 +543,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
ro.add_argument("--json", action="store_true")
|
||||
ro.set_defaults(func=cmd_route)
|
||||
|
||||
rv = sub.add_parser("revoke", help="deactivate a lane (delete KV metadata)")
|
||||
rv = sub.add_parser(
|
||||
"revoke",
|
||||
help="deactivate native policy/AppRole access (preserves KV custody)",
|
||||
)
|
||||
rv.add_argument("catalog_id")
|
||||
rv.add_argument("--dry-run", action="store_true")
|
||||
add_token_arg(rv)
|
||||
rv.set_defaults(func=cmd_revoke)
|
||||
|
||||
lc = sub.add_parser("lifecycle", help="explicit lane lifecycle operations")
|
||||
lcsub = lc.add_subparsers(dest="operation", required=True)
|
||||
for operation, help_text in (
|
||||
("suspend", "stop new native logins; preserve policy and KV custody"),
|
||||
("deactivate", "remove native AppRole/policy; preserve KV custody"),
|
||||
("destroy", "irreversibly delete KV metadata after deactivation"),
|
||||
):
|
||||
lp = lcsub.add_parser(operation, help=help_text)
|
||||
lp.add_argument("catalog_id")
|
||||
lp.add_argument("--dry-run", action="store_true")
|
||||
if operation == "destroy":
|
||||
lp.add_argument(
|
||||
"--confirm-destroy",
|
||||
default="",
|
||||
help="exact catalog id required for live irreversible deletion",
|
||||
)
|
||||
add_token_arg(lp)
|
||||
lp.set_defaults(func=cmd_lifecycle)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue