Add response-wrapped operator handoff
secrets-engine wrap writes a single-use OpenBao wrap token to a mode-0600 out-of-repo file and never prints it. KV reads and AppRole secret_ids are wrapped with a 15m TTL cap. Unwrapped secret payloads fail closed. Production wrap remains fail-closed. Assistant: grok Assistant-Session: 01a05f07-ae72-7781-9fcb-19efd61add00
This commit is contained in:
parent
2278cefbb3
commit
afd1c8e593
12 changed files with 387 additions and 8 deletions
|
|
@ -21,7 +21,7 @@ import stat
|
|||
import subprocess
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from secrets_engine.errors import BackendError, ProvisioningError
|
||||
|
|
@ -61,6 +61,24 @@ def accessor_fingerprint(accessor: str) -> str:
|
|||
return hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WrappedResponse:
|
||||
"""One OpenBao response-wrapped payload. The wrap token is secret."""
|
||||
|
||||
wrap_token: str = field(repr=False)
|
||||
accessor_fingerprint: str
|
||||
ttl: str
|
||||
creation_path: str
|
||||
|
||||
def evidence(self) -> dict[str, object]:
|
||||
payload: dict[str, object] = {
|
||||
"wrap_handle": self.accessor_fingerprint,
|
||||
"wrap_ttl": self.ttl,
|
||||
"creation_path": self.creation_path,
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopedTokenSession:
|
||||
"""One AppRole login token that revokes itself on close."""
|
||||
|
|
@ -335,6 +353,44 @@ class OpenBaoClient:
|
|||
raise BackendError("token accessor is missing or invalid")
|
||||
self._run_ok(["token", "revoke", "-accessor", accessor])
|
||||
|
||||
def _parse_wrap_response(self, stdout: str, *, ttl: str, creation_path: str) -> WrappedResponse:
|
||||
try:
|
||||
payload = json.loads(stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise BackendError("wrapped response is not JSON") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise BackendError("wrapped response is invalid")
|
||||
wrap = payload.get("wrap_info")
|
||||
if not isinstance(wrap, dict) or not wrap.get("token"):
|
||||
raise BackendError("response wrapping was not applied")
|
||||
token = str(wrap["token"])
|
||||
accessor = str(wrap.get("accessor") or "")
|
||||
wrap_ttl = wrap.get("ttl")
|
||||
return WrappedResponse(
|
||||
wrap_token=token,
|
||||
accessor_fingerprint=accessor_fingerprint(accessor) if accessor else "",
|
||||
ttl=str(wrap_ttl if wrap_ttl is not None else ttl),
|
||||
creation_path=creation_path,
|
||||
)
|
||||
|
||||
def wrap_kv_get(self, mount: str, path: str, *, ttl: str) -> WrappedResponse:
|
||||
"""Wrap a KV read. Fail if OpenBao returns the secret unwrapped."""
|
||||
target = f"{mount}/{path}"
|
||||
proc = self._run(["kv", "get", f"-wrap-ttl={ttl}", "-format=json", target])
|
||||
if proc.returncode != 0:
|
||||
raise BackendError("wrapped KV read failed")
|
||||
return self._parse_wrap_response(proc.stdout, ttl=ttl, creation_path=target)
|
||||
|
||||
def wrap_approle_secret_id(self, role_name: str, *, ttl: str) -> WrappedResponse:
|
||||
"""Wrap a single-use AppRole secret_id. Fail if returned unwrapped."""
|
||||
target = f"auth/approle/role/{role_name}/secret-id"
|
||||
proc = self._run(
|
||||
["write", f"-wrap-ttl={ttl}", "-format=json", "-force", target]
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise BackendError("wrapped AppRole secret-id mint failed")
|
||||
return self._parse_wrap_response(proc.stdout, ttl=ttl, creation_path=target)
|
||||
|
||||
# -- KV v2 -------------------------------------------------------------
|
||||
|
||||
def kv_mount_exists(self, mount: str) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue