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

@ -16,18 +16,26 @@ import yaml
from secrets_engine.errors import CatalogError
from secrets_engine.redact import looks_secret
VALID_KINDS = ("kv", "auth-capability")
VALID_STAGES = ("build", "test", "prod")
VALID_DELIVERY_MODES = ("exec-env", "exec-file", "npm-config", "wrapped", "read-check")
VALID_DELIVERY_MODES = (
"exec-env",
"exec-file",
"npm-config",
"wrapped",
"read-check",
"approle-login",
)
VALID_APPROVAL_MODELS = ("decision", "ccr", "dual-control", "bootstrap-only")
REQUIRED_FIELDS = (
"id",
"kind",
"org",
"repo",
"stage",
"mount",
"path",
"fields",
"consumers",
"delivery_modes",
"approval",
@ -41,6 +49,7 @@ REQUIRED_FIELDS = (
@dataclass(frozen=True)
class CatalogEntry:
id: str
kind: str
# Gitea coordinates, kept explicit to avoid the overloaded word "project".
# org = the Gitea organisation (e.g. "coulomb")
# repo = the Gitea repository / product (e.g. "whynot-design")
@ -58,6 +67,7 @@ class CatalogEntry:
deactivation: dict[str, Any]
audit: dict[str, Any]
delivery_config: dict[str, Any] = field(default_factory=dict)
auth_capability: dict[str, Any] = field(default_factory=dict)
description: str = ""
raw: dict[str, Any] = field(default_factory=dict)
@ -83,22 +93,75 @@ class CatalogEntry:
@property
def policy_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("policy_name") or self.id
return f"se-{self.stage}-{self.id}"
@property
def role_name(self) -> str:
if self.kind == "auth-capability":
return self.auth_capability.get("role_name") or self.id
return f"se-{self.stage}-{self.id}"
@property
def auth_allowed_paths(self) -> dict[str, list[str]]:
"""Allowed OpenBao paths/capabilities for an auth-capability lane."""
paths: dict[str, list[str]] = {}
for item in self.auth_capability.get("allowed_paths", []):
paths[item["path"]] = list(item["capabilities"])
return paths
@property
def auth_denied_probe_paths(self) -> list[str]:
"""Non-secret denial probes proving the lane did not get broader."""
return list(self.auth_capability.get("denied_probe_paths", []))
@property
def token_ttl(self) -> str:
return str(self.auth_capability.get("token_ttl", self.rotation.get("ttl", "30m")))
@property
def token_max_ttl(self) -> str:
return str(self.auth_capability.get("token_max_ttl", self.token_ttl))
@property
def secret_id_ttl(self) -> str:
return str(self.auth_capability.get("secret_id_ttl", self.token_ttl))
@property
def secret_id_num_uses(self) -> int:
return int(self.auth_capability.get("secret_id_num_uses", 0))
@property
def token_num_uses(self) -> int:
return int(self.auth_capability.get("token_num_uses", 0))
def approval_required(self) -> bool:
return self.approval.get("model") != "bootstrap-only"
def stores_kv_value(self) -> bool:
return self.kind == "kv"
def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> CatalogEntry:
"""Validate a raw mapping and return a CatalogEntry, or raise CatalogError."""
if not isinstance(data, dict):
raise CatalogError(f"{source}: catalog entry must be a mapping")
data = dict(data)
data.setdefault("kind", "kv")
if data["kind"] not in VALID_KINDS:
raise CatalogError(
f"{source}: kind '{data['kind']}' invalid; must be one of {VALID_KINDS}"
)
if data["kind"] == "auth-capability":
data.setdefault("fields", [])
else:
data.setdefault("auth_capability", {})
missing = [k for k in REQUIRED_FIELDS if k not in data or data[k] in (None, "", [], {})]
if data["kind"] == "kv" and not data.get("fields"):
missing.append("fields")
if missing:
raise CatalogError(f"{source}: missing required fields: {', '.join(missing)}")
@ -120,6 +183,8 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
fields = data["fields"]
if not isinstance(fields, list) or not all(isinstance(f, str) for f in fields):
raise CatalogError(f"{source}: fields must be a list of strings")
if data["kind"] == "auth-capability" and fields:
raise CatalogError(f"{source}: auth-capability lanes must not declare KV fields")
consumers = data["consumers"]
if not isinstance(consumers, list) or not consumers:
@ -153,6 +218,35 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"{source}: delivery_config.npm.registry must be an http(s) URL"
)
if data["kind"] == "auth-capability":
cfg = data.get("auth_capability")
if not isinstance(cfg, dict) or not cfg.get("allowed_paths"):
raise CatalogError(
f"{source}: auth-capability lanes require auth_capability.allowed_paths"
)
if "approle-login" not in modes:
raise CatalogError(
f"{source}: auth-capability lanes require approle-login delivery mode"
)
allowed = cfg["allowed_paths"]
if not isinstance(allowed, list) or not allowed:
raise CatalogError(f"{source}: auth_capability.allowed_paths must be a list")
for item in allowed:
if not isinstance(item, dict) or "path" not in item or "capabilities" not in item:
raise CatalogError(
f"{source}: each allowed path needs path and capabilities"
)
caps = item["capabilities"]
if not isinstance(caps, list) or not all(isinstance(c, str) for c in caps):
raise CatalogError(
f"{source}: auth_capability.allowed_paths capabilities must be strings"
)
denied = cfg.get("denied_probe_paths", [])
if not isinstance(denied, list) or not all(isinstance(p, str) for p in denied):
raise CatalogError(
f"{source}: auth_capability.denied_probe_paths must be a list of strings"
)
# A path must never leak a value through a field name suggesting inline secrets.
if any(looks_secret(k) and data.get(k) for k in ("value", "secret", "token", "password")):
raise CatalogError(f"{source}: catalog entries must not contain secret values")