193 lines
6.4 KiB
Python
193 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Reconcile coding-client OAuth documents between runtime storage and Vault."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
VAULT_ADDR = os.environ.get(
|
|
"VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200"
|
|
).rstrip("/")
|
|
VAULT_ROLE = os.environ.get(
|
|
"HERMES_CREDENTIAL_SYNC_VAULT_ROLE", "hermes-credential-sync"
|
|
)
|
|
SECRET_ENDPOINT = "/v1/kv/data/atlas/hermes/agent-tokens"
|
|
JWT_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
|
CREDENTIALS = {
|
|
"claude_credentials_json": (
|
|
Path("/runtime-access/claude/.credentials.json"),
|
|
("claudeAiOauth", "refreshToken"),
|
|
),
|
|
"codex_auth_json": (
|
|
Path("/runtime-access/codex/auth.json"),
|
|
("tokens", "refresh_token"),
|
|
),
|
|
}
|
|
|
|
|
|
def _request(
|
|
method: str,
|
|
path: str,
|
|
payload: dict[str, Any] | None = None,
|
|
*,
|
|
token: str = "",
|
|
) -> dict[str, Any]:
|
|
"""Issue one bounded Vault JSON request."""
|
|
headers = {"Content-Type": "application/json"}
|
|
if token:
|
|
headers["X-Vault-Token"] = token
|
|
request = Request(
|
|
VAULT_ADDR + path,
|
|
data=(json.dumps(payload).encode("utf-8") if payload is not None else None),
|
|
headers=headers,
|
|
method=method,
|
|
)
|
|
with urlopen(request, timeout=10) as response:
|
|
body = response.read(4 << 20)
|
|
value = json.loads(body)
|
|
if not isinstance(value, dict):
|
|
raise RuntimeError("Vault returned a non-object response")
|
|
return value
|
|
|
|
|
|
def _login() -> str:
|
|
"""Exchange the pod identity for the narrow credential-sync policy."""
|
|
response = _request(
|
|
"POST",
|
|
"/v1/auth/kubernetes/login",
|
|
{"role": VAULT_ROLE, "jwt": JWT_PATH.read_text(encoding="utf-8").strip()},
|
|
)
|
|
token = response.get("auth", {}).get("client_token")
|
|
if not isinstance(token, str) or not token:
|
|
raise RuntimeError("Vault login returned no client token")
|
|
return token
|
|
|
|
|
|
def _credential_document(path: Path, required: tuple[str, ...]) -> str | None:
|
|
"""Return one complete credential JSON document or ignore a partial write."""
|
|
try:
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
document = json.loads(value)
|
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
return None
|
|
current = document
|
|
for key in required:
|
|
if not isinstance(current, dict) or key not in current:
|
|
return None
|
|
current = current[key]
|
|
if not isinstance(current, str) or not current:
|
|
return None
|
|
return value
|
|
|
|
|
|
def _credential_value(value: object, required: tuple[str, ...]) -> str | None:
|
|
"""Return a complete Vault credential document or reject invalid content."""
|
|
if not isinstance(value, str) or not value.strip():
|
|
return None
|
|
try:
|
|
document = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
current = document
|
|
for key in required:
|
|
if not isinstance(current, dict) or key not in current:
|
|
return None
|
|
current = current[key]
|
|
if not isinstance(current, str) or not current:
|
|
return None
|
|
return value.strip()
|
|
|
|
|
|
def _write_credential(path: Path, value: str) -> None:
|
|
"""Atomically restore one private runtime credential from Vault."""
|
|
temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
descriptor = os.open(temporary, flags, 0o600)
|
|
try:
|
|
remaining = memoryview((value + "\n").encode("utf-8"))
|
|
while remaining:
|
|
remaining = remaining[os.write(descriptor, remaining) :]
|
|
os.fsync(descriptor)
|
|
os.fchmod(descriptor, 0o600)
|
|
finally:
|
|
os.close(descriptor)
|
|
try:
|
|
os.replace(temporary, path)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
|
def sync_once(token: str) -> tuple[list[str], list[str]]:
|
|
"""Persist valid rotations and restore incomplete runtime credentials."""
|
|
response = _request("GET", SECRET_ENDPOINT, token=token)
|
|
envelope = response.get("data")
|
|
if not isinstance(envelope, dict):
|
|
raise RuntimeError("Vault KV response has no data envelope")
|
|
data = envelope.get("data")
|
|
metadata = envelope.get("metadata")
|
|
if not isinstance(data, dict) or not isinstance(metadata, dict):
|
|
raise RuntimeError("Vault KV response has an invalid shape")
|
|
version = metadata.get("version")
|
|
if not isinstance(version, int) or version < 1:
|
|
raise RuntimeError("Vault KV response has no version")
|
|
updated = dict(data)
|
|
persisted: list[str] = []
|
|
restored: list[str] = []
|
|
for field, (path, required) in CREDENTIALS.items():
|
|
runtime_value = _credential_document(path, required)
|
|
vault_value = _credential_value(data.get(field), required)
|
|
if runtime_value is not None:
|
|
if runtime_value != vault_value:
|
|
updated[field] = runtime_value
|
|
persisted.append(field)
|
|
elif vault_value is not None:
|
|
_write_credential(path, vault_value)
|
|
restored.append(field)
|
|
if persisted:
|
|
_request(
|
|
"POST",
|
|
SECRET_ENDPOINT,
|
|
{"options": {"cas": version}, "data": updated},
|
|
token=token,
|
|
)
|
|
return persisted, restored
|
|
|
|
|
|
def main() -> int:
|
|
"""Continuously persist refresh-token rotation without exposing values."""
|
|
interval = max(30, int(os.environ.get("HERMES_CREDENTIAL_SYNC_INTERVAL", "60")))
|
|
while True:
|
|
try:
|
|
token = _login()
|
|
persisted, restored = sync_once(token)
|
|
if persisted:
|
|
print(
|
|
"Persisted refreshed runtime credentials: "
|
|
+ ", ".join(persisted),
|
|
flush=True,
|
|
)
|
|
if restored:
|
|
print(
|
|
"Restored incomplete runtime credentials from Vault: "
|
|
+ ", ".join(restored),
|
|
flush=True,
|
|
)
|
|
except (OSError, RuntimeError, HTTPError, URLError, ValueError) as error:
|
|
print(
|
|
f"Runtime credential sync deferred: {type(error).__name__}",
|
|
flush=True,
|
|
)
|
|
time.sleep(interval)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|