feat: add auth-capability lanes and pilot closeout

Add the warden-sign auth-capability lane, AppRole handoff, verification guards, docs, and tests.

Point the whynot-design pilot at the canonical decision and add the real publish closeout preflight/runbook.
This commit is contained in:
tegwick 2026-06-29 16:58:16 +02:00
parent a621fbaffd
commit 6382139890
27 changed files with 1455 additions and 107 deletions

View file

@ -143,28 +143,49 @@ class OpenBaoClient:
if enable.returncode != 0 and "already in use" not in enable.stderr:
raise BackendError(f"could not enable approle: {enable.stderr.strip()}")
def write_approle(self, role_name: str, policies: list[str], ttl: str = "30m") -> None:
self._run_ok(
[
"write",
f"auth/approle/role/{role_name}",
f"token_policies={','.join(policies)}",
f"token_ttl={ttl}",
f"token_max_ttl={ttl}",
"secret_id_num_uses=0",
"token_num_uses=0",
]
)
def write_approle(
self,
role_name: str,
policies: list[str],
ttl: str = "30m",
*,
max_ttl: str | None = None,
secret_id_ttl: str | None = None,
secret_id_num_uses: int = 0,
token_num_uses: int = 0,
) -> None:
args = [
"write",
f"auth/approle/role/{role_name}",
f"token_policies={','.join(policies)}",
f"token_ttl={ttl}",
f"token_max_ttl={max_ttl or ttl}",
f"secret_id_num_uses={secret_id_num_uses}",
f"token_num_uses={token_num_uses}",
]
if secret_id_ttl:
args.append(f"secret_id_ttl={secret_id_ttl}")
self._run_ok(args)
def approle_exists(self, role_name: str) -> bool:
proc = self._run(["read", f"auth/approle/role/{role_name}"])
return proc.returncode == 0
def read_approle_role_id(self, role_name: str) -> str:
return self._run_ok(
["read", "-field=role_id", f"auth/approle/role/{role_name}/role-id"]
).strip()
def create_approle_secret_id(self, role_name: str) -> str:
return self._run_ok(
["write", "-field=secret_id", "-f", f"auth/approle/role/{role_name}/secret-id"]
).strip()
def approle_login_token(self, role_name: str) -> str:
"""Login as the approle and return a scoped child token. Used only for
verification / exec delivery; never logged."""
role_id = self._run_ok(
["read", "-field=role_id", f"auth/approle/role/{role_name}/role-id"]
).strip()
secret_id = self._run_ok(
["write", "-field=secret_id", "-f", f"auth/approle/role/{role_name}/secret-id"]
).strip()
role_id = self.read_approle_role_id(role_name)
secret_id = self.create_approle_secret_id(role_name)
token = self._run_ok(
[
"write",
@ -176,6 +197,28 @@ class OpenBaoClient:
).strip()
return token
def token_capabilities(self, path: str, *, token: str) -> list[str]:
"""Return token capabilities for a path without returning any secret value."""
client = OpenBaoClient(addr=self.addr, token=token, bao_bin=self.bao_bin)
out = client._run_ok(["token", "capabilities", "-format=json", path])
try:
data = json.loads(out)
except json.JSONDecodeError:
return [line.strip() for line in out.splitlines() if line.strip()]
if isinstance(data, list):
return [str(item) for item in data]
if isinstance(data, dict):
caps = data.get("capabilities", [])
if isinstance(caps, list):
return [str(item) for item in caps]
return []
def delete_policy(self, name: str) -> None:
self._run_ok(["policy", "delete", name])
def delete_approle(self, role_name: str) -> None:
self._run_ok(["delete", f"auth/approle/role/{role_name}"])
# -- KV v2 -------------------------------------------------------------
def kv_mount_exists(self, mount: str) -> bool: