Add named engine auth and accessor-file session revoke
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Select service-jwt, bootstrap, or env exclusively: JWT login uses a JSON
file, self-revokes, and never falls back to bootstrap or BAO_TOKEN. The
platform JWT mount/role is still unpublished, so auto keeps named
bootstrap/env providers.

session revoke --accessor-file revokes an already-issued token with
fingerprint-only evidence. Production remains fail-closed.

Assistant: grok
Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
This commit is contained in:
tegwick 2026-09-02 01:24:08 +02:00
parent a94003de4f
commit 3abee434df
18 changed files with 698 additions and 135 deletions

View file

@ -3,10 +3,9 @@
Thin wrapper over the `bao` CLI. Isolated here so the rest of the engine speaks
in lanes/plans, not in OpenBao endpoint quirks (FR: "isolate backend adapter").
Auth resolution order for the token:
1. explicit bootstrap token file (--bootstrap-token-file), mode-checked;
2. BAO_TOKEN / VAULT_TOKEN environment variable;
3. otherwise unauthenticated (only dry-run / read-health works).
Named engine auth providers (no implicit fallback) live in ``engine_auth``.
``OpenBaoClient.resolve`` only materializes an already-selected bootstrap file
or environment token.
This adapter NEVER returns a secret value to its callers except through the
narrow `read_field_present()` (boolean) and the exec-delivery path, which writes
@ -14,6 +13,7 @@ straight into a child process and never logs.
"""
from __future__ import annotations
import hashlib
import json
import os
import shutil
@ -56,6 +56,11 @@ def _check_token_file(path: Path) -> str:
return read_strict_token_file(path, purpose="bootstrap token")
def accessor_fingerprint(accessor: str) -> str:
"""Non-secret handle for an accessor the caller already holds."""
return hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12]
@dataclass
class ScopedTokenSession:
"""One AppRole login token that revokes itself on close."""
@ -266,9 +271,7 @@ class OpenBaoClient:
raise BackendError("malformed AppRole login response") from e
if not token:
raise BackendError("AppRole login returned an empty token")
import hashlib
fingerprint = hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12]
fingerprint = accessor_fingerprint(accessor)
scoped = OpenBaoClient(addr=self.addr, token=token, bao_bin=self.bao_bin)
del token
return ScopedTokenSession(
@ -307,6 +310,31 @@ class OpenBaoClient:
def delete_approle(self, role_name: str) -> None:
self._run_ok(["delete", f"auth/approle/role/{role_name}"])
def login_jwt(self, mount: str, role: str, jwt: str) -> ScopedTokenSession:
"""Login through JWT auth without putting the JWT in argv."""
output = self._run_ok_with_json_file(
["write", "-format=json", f"auth/{mount}/login"],
{"role": role, "jwt": jwt},
)
try:
auth = json.loads(output)["auth"]
token = str(auth["client_token"])
accessor = str(auth.get("accessor", ""))
except (json.JSONDecodeError, KeyError, TypeError) as e:
raise BackendError("malformed JWT login response") from e
if not token:
raise BackendError("JWT login returned an empty token")
fingerprint = accessor_fingerprint(accessor)
scoped = OpenBaoClient(addr=self.addr, token=token, bao_bin=self.bao_bin)
del token
return ScopedTokenSession(client=scoped, accessor_fingerprint=fingerprint)
def revoke_accessor(self, accessor: str) -> None:
"""Revoke a token the operator already holds, by accessor only."""
if not accessor or any(ch.isspace() for ch in accessor):
raise BackendError("token accessor is missing or invalid")
self._run_ok(["token", "revoke", "-accessor", accessor])
# -- KV v2 -------------------------------------------------------------
def kv_mount_exists(self, mount: str) -> bool: