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

@ -26,6 +26,10 @@ STAGE_PREFIX = {
# Capabilities a stage role's *own* policy may carry. Anything else is broad.
ALLOWED_CAPABILITIES = {"create", "read", "update", "delete", "list"}
# Auth-capability lanes grant an operational action, not KV value access. Keep
# this intentionally smaller than the stage-role capability set.
AUTH_CAPABILITY_ALLOWED_CAPABILITIES = {"update"}
# Substrings that, if they appear in a policy path, mean the plan is too broad.
FORBIDDEN_PATH_MARKERS = (
"sys/",
@ -110,6 +114,63 @@ def assert_policy_safe(policy_name: str, paths: dict[str, list[str]]) -> None:
)
def assert_auth_capability_safe(
policy_name: str,
mount: str,
paths: dict[str, list[str]],
denied_probe_paths: list[str] | None = None,
) -> None:
"""Reject auth-capability grants that are broader than exact operations."""
lowered = policy_name.lower()
for marker in FORBIDDEN_NAME_MARKERS:
if marker in lowered:
raise PolicyGuardError(
f"policy name '{policy_name}' resembles broad admin (marker '{marker}')"
)
if not paths:
raise PolicyGuardError(f"policy '{policy_name}': no auth-capability paths")
mount_prefix = f"{mount}/"
for path, caps in paths.items():
if "*" in path or "+" in path or path.strip() in ("*", "/", "+"):
raise PolicyGuardError(
f"policy '{policy_name}': wildcard auth path '{path}' not allowed"
)
for marker in FORBIDDEN_PATH_MARKERS:
if marker in path:
raise PolicyGuardError(
f"policy '{policy_name}': path '{path}' is out of bounds "
f"(marker '{marker}')"
)
if not path.startswith(mount_prefix):
raise PolicyGuardError(
f"policy '{policy_name}': auth path '{path}' is outside mount '{mount}'"
)
if mount == "ssh":
parts = path.split("/")
if len(parts) != 3 or parts[1] != "sign" or not parts[2]:
raise PolicyGuardError(
f"policy '{policy_name}': ssh auth-capability path '{path}' "
"must be exactly ssh/sign/<role>"
)
bad_caps = set(caps) - AUTH_CAPABILITY_ALLOWED_CAPABILITIES
if bad_caps:
raise PolicyGuardError(
f"policy '{policy_name}': capabilities {sorted(bad_caps)} not allowed "
f"for auth-capability (allowed {sorted(AUTH_CAPABILITY_ALLOWED_CAPABILITIES)})"
)
for path in denied_probe_paths or []:
if path in paths:
raise PolicyGuardError(
f"policy '{policy_name}': denied probe '{path}' is also allowed"
)
if "*" in path or "+" in path:
raise PolicyGuardError(
f"policy '{policy_name}': denied probe '{path}' must be exact"
)
def lane_policy_paths(entry: CatalogEntry) -> dict[str, list[str]]:
"""The minimal KV v2 paths + capabilities a consumer policy needs for a lane."""
data_path = f"{entry.mount}/data/{entry.path}"
@ -136,3 +197,43 @@ def consumer_policy_for(entry: CatalogEntry) -> tuple[str, str]:
paths = lane_policy_paths(entry)
name = entry.policy_name
return name, render_policy_hcl(name, paths)
def auth_capability_policy_paths(entry: CatalogEntry) -> dict[str, list[str]]:
"""The exact operational paths an auth-capability lane may exercise."""
paths = entry.auth_allowed_paths
assert_auth_capability_safe(
entry.policy_name, entry.mount, paths, entry.auth_denied_probe_paths
)
return paths
def render_auth_capability_policy_hcl(
policy_name: str,
mount: str,
paths: dict[str, list[str]],
denied_probe_paths: list[str] | None = None,
) -> str:
"""Render a narrow auth-capability ACL policy in HCL."""
denied_probe_paths = denied_probe_paths or []
assert_auth_capability_safe(policy_name, mount, paths, denied_probe_paths)
blocks = [
f'# Generated by secrets-engine for auth-capability policy "{policy_name}"',
"# Exact allowlist below. Every path outside it is denied by OpenBao default.",
]
if denied_probe_paths:
blocks.append("# Denial probes expected to lack update capability:")
blocks.extend(f"# - {path}" for path in denied_probe_paths)
for path, caps in paths.items():
cap_list = ", ".join(f'"{c}"' for c in caps)
blocks.append(f'path "{path}" {{\n capabilities = [{cap_list}]\n}}')
return "\n\n".join(blocks) + "\n"
def auth_capability_policy_for(entry: CatalogEntry) -> tuple[str, str]:
"""Return (policy_name, hcl) for a non-KV auth-capability lane."""
paths = auth_capability_policy_paths(entry)
name = entry.policy_name
return name, render_auth_capability_policy_hcl(
name, entry.mount, paths, entry.auth_denied_probe_paths
)