2026-06-28 12:28:45 +02:00
|
|
|
"""Provisioning: get a secret value into OpenBao without it touching coordination
|
|
|
|
|
surfaces.
|
|
|
|
|
|
|
|
|
|
Modes:
|
|
|
|
|
- from-file: read a value from a mode-0600 file outside the repo, write it to
|
|
|
|
|
OpenBao, and (caller's choice) leave the source file for the operator to shred.
|
|
|
|
|
- generate: mint a random non-production value for build/test lanes only.
|
|
|
|
|
|
|
|
|
|
The value is held only in process memory and passed straight to the backend. It
|
|
|
|
|
is never logged, returned, or written to evidence.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import secrets as _secrets
|
|
|
|
|
import string
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from secrets_engine.catalog import CatalogEntry
|
|
|
|
|
from secrets_engine.errors import ProvisioningError
|
|
|
|
|
from secrets_engine.openbao import OpenBaoClient
|
2026-08-21 08:20:33 +02:00
|
|
|
from secrets_engine.safe_paths import containing_git_worktree
|
2026-06-28 12:28:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_value_file(path: Path) -> str:
|
|
|
|
|
if not path.exists():
|
|
|
|
|
raise ProvisioningError(f"value file not found: {path}")
|
|
|
|
|
st = path.stat()
|
|
|
|
|
if st.st_mode & 0o077:
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"value file {path} is group/other-accessible "
|
|
|
|
|
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
|
|
|
|
|
)
|
2026-08-21 08:20:33 +02:00
|
|
|
worktree = containing_git_worktree(path)
|
|
|
|
|
if worktree is not None:
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"value file {path} is inside a Git worktree ({worktree}); "
|
|
|
|
|
"keep secret material outside repos"
|
|
|
|
|
)
|
2026-06-28 12:28:45 +02:00
|
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
|
|
|
if not value:
|
|
|
|
|
raise ProvisioningError(f"value file {path} is empty")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def provision_from_file(
|
|
|
|
|
client: OpenBaoClient, entry: CatalogEntry, field: str, file_path: Path
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Import a value from a strict-permission file. Returns the field name only."""
|
2026-06-29 16:58:16 +02:00
|
|
|
if not entry.stores_kv_value():
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"lane '{entry.id}' is {entry.kind}; it has no KV value to provision"
|
|
|
|
|
)
|
2026-06-28 12:28:45 +02:00
|
|
|
if field not in entry.fields:
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
|
|
|
|
|
)
|
|
|
|
|
value = _read_value_file(Path(file_path))
|
2026-08-21 08:20:33 +02:00
|
|
|
if entry.manages_mount:
|
|
|
|
|
client.ensure_kv_mount(entry.mount)
|
2026-08-23 12:05:58 +02:00
|
|
|
client.kv_patch_fields(entry.mount, entry.path, {field: value})
|
2026-06-28 12:28:45 +02:00
|
|
|
del value
|
|
|
|
|
return field
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def provision_generated(client: OpenBaoClient, entry: CatalogEntry, field: str) -> str:
|
|
|
|
|
"""Generate a random NON-PRODUCTION value for build/test lanes only."""
|
2026-06-29 16:58:16 +02:00
|
|
|
if not entry.stores_kv_value():
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"lane '{entry.id}' is {entry.kind}; it has no KV value to provision"
|
|
|
|
|
)
|
2026-06-28 12:28:45 +02:00
|
|
|
if entry.stage == "prod":
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"refusing to generate a value for prod lane '{entry.id}'; "
|
|
|
|
|
"production values must be provisioned, not generated"
|
|
|
|
|
)
|
|
|
|
|
if field not in entry.fields:
|
|
|
|
|
raise ProvisioningError(
|
|
|
|
|
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
|
|
|
|
|
)
|
|
|
|
|
alphabet = string.ascii_letters + string.digits
|
|
|
|
|
value = "test-" + "".join(_secrets.choice(alphabet) for _ in range(32))
|
2026-08-21 08:20:33 +02:00
|
|
|
if entry.manages_mount:
|
|
|
|
|
client.ensure_kv_mount(entry.mount)
|
2026-08-23 12:05:58 +02:00
|
|
|
client.kv_patch_fields(entry.mount, entry.path, {field: value})
|
2026-06-28 12:28:45 +02:00
|
|
|
del value
|
|
|
|
|
return field
|