Bind wrapped issuance to native metadata and preserve immutable recipient attempts
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
58129de8f1
commit
0db6adef69
8 changed files with 288 additions and 46 deletions
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
|
|
@ -400,23 +401,34 @@ class OpenBaoClient:
|
|||
self._run_ok(["lease", "revoke", lease_id])
|
||||
|
||||
def _parse_wrap_response(self, stdout: str, *, ttl: str, creation_path: str) -> WrappedResponse:
|
||||
# Verify native wrapping metadata. Never reconstruct a purported source
|
||||
# path or TTL from the caller when the backend omitted or changed it.
|
||||
try:
|
||||
payload = json.loads(stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise BackendError("wrapped response is not JSON") from exc
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
raise BackendError("wrapped response is not JSON") from None
|
||||
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")
|
||||
if payload.get("data") or payload.get("auth"):
|
||||
raise BackendError("wrapped response unexpectedly included unwrapped material")
|
||||
match = re.fullmatch(r"([1-9][0-9]*)([smh])", ttl)
|
||||
seconds = int(match[1]) * {"s": 1, "m": 60, "h": 3600}[match[2]] if match else 0
|
||||
if not 0 < seconds <= 900:
|
||||
raise BackendError("wrap TTL is outside the supported bound")
|
||||
token, accessor = wrap.get("token"), wrap.get("accessor")
|
||||
if (not isinstance(token, str) or not token or any(ch.isspace() for ch in token)
|
||||
or not isinstance(accessor, str) or not accessor
|
||||
or type(wrap.get("ttl")) is not int or wrap["ttl"] != seconds
|
||||
or wrap.get("creation_path") != creation_path):
|
||||
raise BackendError("native wrap identity, TTL or creation path unverified")
|
||||
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,
|
||||
accessor_fingerprint=accessor_fingerprint(accessor),
|
||||
ttl=str(seconds),
|
||||
creation_path=wrap["creation_path"],
|
||||
)
|
||||
|
||||
def wrap_kv_get(self, mount: str, path: str, *, ttl: str) -> WrappedResponse:
|
||||
|
|
@ -425,7 +437,7 @@ class OpenBaoClient:
|
|||
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)
|
||||
return self._parse_wrap_response(proc.stdout, ttl=ttl, creation_path=f"{mount}/data/{path}")
|
||||
|
||||
def wrap_approle_secret_id(self, role_name: str, *, ttl: str) -> WrappedResponse:
|
||||
"""Wrap a single-use AppRole secret_id. Fail if returned unwrapped."""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -43,33 +44,48 @@ def normalize_wrap_ttl(value: str) -> str:
|
|||
|
||||
|
||||
def _validate_output_path(path: Path) -> Path:
|
||||
resolved = path.expanduser().resolve()
|
||||
worktree = containing_git_worktree(resolved)
|
||||
# Do not follow a leaf/parent symlink into another recipient's private file.
|
||||
absolute = Path(os.path.abspath(path.expanduser()))
|
||||
if absolute.resolve() != absolute or absolute.is_symlink():
|
||||
raise ProvisioningError("wrap output cannot use redirected paths")
|
||||
worktree = containing_git_worktree(absolute)
|
||||
if worktree is not None:
|
||||
raise ProvisioningError(
|
||||
f"wrap file {resolved} is inside a Git worktree ({worktree}); "
|
||||
"keep wrap tokens outside repos"
|
||||
)
|
||||
if resolved.exists() and resolved.stat().st_mode & 0o077:
|
||||
raise ProvisioningError(
|
||||
f"wrap file {resolved} is group/other-accessible "
|
||||
f"(mode {oct(resolved.stat().st_mode & 0o777)}); must be 0600"
|
||||
)
|
||||
return resolved
|
||||
raise ProvisioningError("wrap output is inside a Git worktree; keep tokens outside repos")
|
||||
if os.path.lexists(absolute):
|
||||
raise ProvisioningError("wrap output already exists; reconcile the prior attempt, never overwrite or remint")
|
||||
return absolute
|
||||
|
||||
|
||||
def _write_mode_0600(path: Path, value: str) -> None:
|
||||
def _reserve_mode_0600(path: Path) -> int:
|
||||
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)
|
||||
directory_fd = os.open("/", os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fd = None
|
||||
fh.write(value)
|
||||
fh.write("\n")
|
||||
# Walk using directory descriptors so a concurrent symlink swap cannot
|
||||
# redirect either an ancestor or the final file creation.
|
||||
for component in path.parent.parts[1:]:
|
||||
next_fd = os.open(component, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
||||
dir_fd=directory_fd)
|
||||
os.close(directory_fd)
|
||||
directory_fd = next_fd
|
||||
parent = os.fstat(directory_fd)
|
||||
if parent.st_uid != os.geteuid() or stat.S_IMODE(parent.st_mode) & 0o077:
|
||||
raise ProvisioningError("wrap output parent must be owned and private")
|
||||
# Reserve before the backend call. Failure/crash leaves this private
|
||||
# marker for reconciliation, never automatic remint.
|
||||
file_fd = os.open(path.name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
||||
0o600, dir_fd=directory_fd)
|
||||
try:
|
||||
os.fchmod(file_fd, 0o600)
|
||||
os.fsync(file_fd)
|
||||
os.fsync(directory_fd)
|
||||
except BaseException:
|
||||
os.close(file_fd)
|
||||
raise
|
||||
return file_fd
|
||||
except OSError:
|
||||
raise ProvisioningError("wrap output reservation failed; no backend retry") from None
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
os.close(directory_fd)
|
||||
|
||||
|
||||
def write_wrapped_handoff(
|
||||
|
|
@ -82,20 +98,26 @@ def write_wrapped_handoff(
|
|||
"""Mint a wrap token and write it without printing it."""
|
||||
ttl = normalize_wrap_ttl(ttl)
|
||||
out_path = _validate_output_path(out_file)
|
||||
wrapped: WrappedResponse
|
||||
if entry.kind == "auth-capability":
|
||||
wrapped = client.wrap_approle_secret_id(entry.role_name, ttl=ttl)
|
||||
elif entry.stores_kv_value():
|
||||
wrapped = client.wrap_kv_get(entry.mount, entry.path, ttl=ttl)
|
||||
else:
|
||||
raise ProvisioningError(
|
||||
f"lane '{entry.id}' kind {entry.kind} has no wrapped handoff"
|
||||
)
|
||||
token = wrapped.wrap_token
|
||||
if entry.kind != "auth-capability" and not entry.stores_kv_value():
|
||||
raise ProvisioningError(f"lane '{entry.id}' kind {entry.kind} has no wrapped handoff")
|
||||
fd = _reserve_mode_0600(out_path)
|
||||
try:
|
||||
_write_mode_0600(out_path, token)
|
||||
if entry.kind == "auth-capability":
|
||||
wrapped: WrappedResponse = client.wrap_approle_secret_id(entry.role_name, ttl=ttl)
|
||||
else:
|
||||
wrapped = client.wrap_kv_get(entry.mount, entry.path, ttl=ttl)
|
||||
visible, reserved = out_path.lstat(), os.fstat(fd)
|
||||
if (not stat.S_ISREG(visible.st_mode)
|
||||
or (visible.st_dev, visible.st_ino) != (reserved.st_dev, reserved.st_ino)):
|
||||
raise ProvisioningError("wrap output reservation changed; retain and reconcile the mint")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
fd = None
|
||||
handle.write(wrapped.wrap_token + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
finally:
|
||||
token = ""
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
return WrapHandoffResult(
|
||||
catalog_id=entry.id,
|
||||
kind=entry.kind,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue