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:
parent
a621fbaffd
commit
6382139890
27 changed files with 1455 additions and 107 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue