Record the passing hub-writer receipts.

Member and non-member verification both passed. The verifier accepts s. tokens and calls sys/capabilities with a token that is allowed to.

Assistant: grok
Assistant-Session: 01a13b2d-5f6d-7a32-b2c9-384625c55ca6
This commit is contained in:
tegwick 2026-10-14 22:14:32 +02:00
parent 05882b189f
commit 97d1fed949
7 changed files with 116 additions and 45 deletions

View file

@ -6,11 +6,12 @@
Logs in on auth/netkingdom role reuse-surface-hub-writer-workload-kv-read with
``bao login -token-only``, so the operator's token helper is left alone. The
lane token stays in memory. Capability queries run as the operator token
already present in the environment or token helper, because the workload
policy has no sys/capabilities-self grant. The receipt records policies,
entity id, path capabilities, and pass/fail. It does not record the token or
any KV value.
lane token stays in memory. Capability queries use the first local token that
may call ``sys/capabilities`` (``BAO_TOKEN`` or ``VAULT_TOKEN``, then
``~/.local/openbao/platform-admin.token``, then ``~/.vault-token``), because
the workload policy has no sys/capabilities-self grant. The receipt records
policies, entity id, path capabilities, and pass/fail. It does not record the
token or any KV value.
Sign in with a private browser window. A reused platform-admin session is the
wrong principal for ``member``.
@ -43,7 +44,8 @@ from ops_mason.reuse_surface_hub_writer import (
)
BAO_ADDR = os.environ.get("BAO_ADDR", "http://127.0.0.1:18200")
TOKEN_RE = re.compile(r"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b")
# This OpenBao issues s. service tokens. hvs/hvb/hvr are the newer Vault shapes.
TOKEN_RE = re.compile(r"\b(?:hvs|hvb|hvr|s)\.[A-Za-z0-9_-]{8,}\b")
PATHS = [DATA, METADATA, PARENT_DATA, PARENT_METADATA, SIBLING]
@ -58,14 +60,45 @@ def receipts_dir() -> Path:
return path
def operator_token() -> str:
def token_candidates() -> list[tuple[str, str]]:
found: list[tuple[str, str]] = []
for name in ("BAO_TOKEN", "VAULT_TOKEN"):
value = os.environ.get(name, "").strip()
if value:
return value
helper = Path(os.environ.get("HOME", str(Path.home()))) / ".vault-token"
if helper.is_file():
return helper.read_text(encoding="utf-8").strip()
found.append((name, value))
home = Path(os.environ.get("HOME", str(Path.home())))
for label, path in (
("~/.local/openbao/platform-admin.token", home / ".local/openbao/platform-admin.token"),
("~/.vault-token", home / ".vault-token"),
):
if path.is_file():
value = path.read_text(encoding="utf-8").strip()
if value:
found.append((label, value))
return found
def write_capabilities(caller: str, subject: str, paths: list[str]) -> subprocess.CompletedProcess[str]:
body = json.dumps({"token": subject, "paths": paths})
tmp = tempfile.NamedTemporaryFile("w", prefix="rs-caps-", delete=False)
try:
os.fchmod(tmp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
tmp.write(body)
tmp.close()
return run_bao(["write", "-format=json", "sys/capabilities", f"@{tmp.name}"], caller)
finally:
try:
os.remove(tmp.name)
except OSError:
pass
def operator_token() -> str:
"""Return a token allowed to call sys/capabilities, or an empty string."""
for _label, token in token_candidates():
probe = write_capabilities(token, token, ["auth/token/lookup-self"])
if probe.returncode == 0:
return token
return ""
@ -90,8 +123,8 @@ def login() -> str:
token="",
)
token = (proc.stdout or "").strip()
if proc.returncode != 0 or not token or TOKEN_RE.fullmatch(token) is None:
sys.stderr.write(redact(proc.stderr or "login refused\n"))
if proc.returncode != 0 or TOKEN_RE.fullmatch(token) is None:
sys.stderr.write(redact(proc.stderr or "login output was not a token\n"))
return ""
return token
@ -107,21 +140,10 @@ def lookup(token: str) -> dict:
def capabilities(operator: str, lane: str) -> dict[str, list[str]]:
if not operator:
raise RuntimeError(
"no operator token for sys/capabilities "
"(BAO_TOKEN, VAULT_TOKEN, or ~/.vault-token)"
"no token can call sys/capabilities "
"(BAO_TOKEN, VAULT_TOKEN, ~/.local/openbao/platform-admin.token, or ~/.vault-token)"
)
body = json.dumps({"token": lane, "paths": PATHS})
tmp = tempfile.NamedTemporaryFile("w", prefix="rs-caps-", delete=False)
try:
os.fchmod(tmp.fileno(), stat.S_IRUSR | stat.S_IWUSR)
tmp.write(body)
tmp.close()
proc = run_bao(["write", "-format=json", "sys/capabilities", f"@{tmp.name}"], operator)
finally:
try:
os.remove(tmp.name)
except OSError:
pass
proc = write_capabilities(operator, lane, PATHS)
if proc.returncode != 0:
raise RuntimeError(redact(proc.stderr or "sys/capabilities failed"))
payload = json.loads(proc.stdout)
@ -151,11 +173,17 @@ def main(argv: list[str]) -> int:
if mode not in {"member", "non-member"}:
sys.stderr.write("usage: verify-reuse-surface-hub-writer.py member|non-member\n")
return 2
operator = operator_token()
if not operator:
sys.stderr.write(
"no token can call sys/capabilities. Checked BAO_TOKEN, VAULT_TOKEN, "
"~/.local/openbao/platform-admin.token, and ~/.vault-token.\n"
)
return 1
sys.stderr.write(
f"Sign in as the {mode} identity in a private browser window.\n"
"A platform-admin session is the wrong principal for member.\n"
)
operator = operator_token()
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
lane = login()
report: dict = {