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
96
src/secrets_engine/handoff.py
Normal file
96
src/secrets_engine/handoff.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Out-of-band AppRole handoff helpers for auth-capability lanes.
|
||||
|
||||
The secret_id is secret material. It is minted only after output paths have been
|
||||
validated, written to a mode-0600 file outside any Git worktree, and never
|
||||
printed or recorded in evidence.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from secrets_engine.catalog import CatalogEntry
|
||||
from secrets_engine.errors import ProvisioningError
|
||||
from secrets_engine.openbao import OpenBaoClient
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HandoffResult:
|
||||
role_name: str
|
||||
role_id_file: str
|
||||
secret_id_file: str
|
||||
token_ttl: str
|
||||
secret_id_ttl: str
|
||||
secret_id_num_uses: int
|
||||
|
||||
|
||||
def _assert_outside_git_worktree(path: Path) -> Path:
|
||||
resolved = path.expanduser().resolve()
|
||||
for parent in (resolved.parent, *resolved.parent.parents):
|
||||
if (parent / ".git").exists():
|
||||
raise ProvisioningError(
|
||||
f"handoff file {resolved} is inside a Git worktree ({parent}); "
|
||||
"keep AppRole material outside repos"
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def _validate_output_path(path: Path) -> Path:
|
||||
resolved = _assert_outside_git_worktree(path)
|
||||
if resolved.exists() and resolved.stat().st_mode & 0o077:
|
||||
raise ProvisioningError(
|
||||
f"handoff file {resolved} is group/other-accessible "
|
||||
f"(mode {oct(resolved.stat().st_mode & 0o777)}); must be 0600"
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def _write_mode_0600(path: Path, value: str) -> None:
|
||||
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
fd: int | None = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fd = None
|
||||
fh.write(value)
|
||||
fh.write("\n")
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def write_approle_handoff(
|
||||
client: OpenBaoClient,
|
||||
entry: CatalogEntry,
|
||||
*,
|
||||
role_id_file: Path,
|
||||
secret_id_file: Path,
|
||||
) -> HandoffResult:
|
||||
"""Mint and write AppRole handoff material without printing the secret_id."""
|
||||
if entry.kind != "auth-capability":
|
||||
raise ProvisioningError(
|
||||
f"lane '{entry.id}' is {entry.kind}; handoff is only for auth-capability lanes"
|
||||
)
|
||||
|
||||
role_path = _validate_output_path(role_id_file)
|
||||
secret_path = _validate_output_path(secret_id_file)
|
||||
if role_path == secret_path:
|
||||
raise ProvisioningError("role_id_file and secret_id_file must be different")
|
||||
|
||||
role_id = client.read_approle_role_id(entry.role_name)
|
||||
secret_id = client.create_approle_secret_id(entry.role_name)
|
||||
try:
|
||||
_write_mode_0600(role_path, role_id)
|
||||
_write_mode_0600(secret_path, secret_id)
|
||||
finally:
|
||||
del secret_id
|
||||
|
||||
return HandoffResult(
|
||||
role_name=entry.role_name,
|
||||
role_id_file=str(role_path),
|
||||
secret_id_file=str(secret_path),
|
||||
token_ttl=entry.token_ttl,
|
||||
secret_id_ttl=entry.secret_id_ttl,
|
||||
secret_id_num_uses=entry.secret_id_num_uses,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue