"""Minimal OpenBao (Vault) KV v2 reader for runtime secret injection. Mirrors the homelab pattern used by other non-k8s services: read ``/v1//data/`` with an ``X-Vault-Token``. Stdlib only — no extra dependency. Token comes from ``OPENBAO_TOKEN`` or, preferably, a mounted token file via ``OPENBAO_TOKEN_FILE`` (Docker/host secret). """ import json import logging import os import urllib.error import urllib.request logger = logging.getLogger(__name__) DEFAULT_ADDR = "https://vault.adamksmith.xyz" DEFAULT_MOUNT = "secret" def _resolve_token() -> str | None: token = os.environ.get("OPENBAO_TOKEN") if token: return token.strip() token_file = os.environ.get("OPENBAO_TOKEN_FILE") if token_file: try: return open(token_file).read().strip() except OSError as e: logger.warning("Could not read OPENBAO_TOKEN_FILE %s: %s", token_file, e) return None def read_kv2(path: str) -> dict | None: """Read a KV v2 secret. ``path`` is relative to the mount (e.g. "jbod-monitor/mqtt"). Returns the inner data dict, or None on failure. """ token = _resolve_token() if not token: logger.warning("OpenBao read requested for %s but no token configured", path) return None addr = os.environ.get("OPENBAO_ADDR", DEFAULT_ADDR).rstrip("/") mount = os.environ.get("OPENBAO_MOUNT", DEFAULT_MOUNT).strip("/") url = f"{addr}/v1/{mount}/data/{path.strip('/')}" req = urllib.request.Request(url, headers={"X-Vault-Token": token}) try: with urllib.request.urlopen(req, timeout=10) as resp: body = json.loads(resp.read()) except urllib.error.HTTPError as e: logger.warning("OpenBao read %s failed: HTTP %s", path, e.code) return None except Exception as e: logger.warning("OpenBao read %s failed: %s", path, e) return None # KV v2 wraps the secret in data.data. return body.get("data", {}).get("data")