net-kingdom/sso-mfa/bootstrap/openbao-lab.py
tegwick d9557ba90e
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Harden remaining bootstrap custody callers and prepare isolated recovery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-15 22:57:14 +02:00

148 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Contained lab-only init/unseal. Production uses the Platform custody ceremony.
The local model selection is a scope check, not an authorization grant. Caller
must supply an owner-admitted lab kubeconfig and independently observed cluster
UID. Private init output is retained for reconciliation after any unknown write.
"""
import importlib.util
import json
import os
from pathlib import Path
import re
import subprocess
import sys
spec = importlib.util.spec_from_file_location('private_service_files', Path(__file__).with_name('apply-service-secrets.py'))
private = importlib.util.module_from_spec(spec)
spec.loader.exec_module(private)
class Refused(Exception):
pass
def run(arguments, payload=None, allowed=(0,)):
# Inherit only routing/executable inputs, never ambient bearer credentials or
# shell hooks. Diagnostics and command output remain inside this process.
env = {k: os.environ[k] for k in ('PATH', 'HOME', 'KUBECONFIG') if k in os.environ}
result = subprocess.run(['kubectl', *arguments], input=payload, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, timeout=60, env=env)
if result.returncode not in allowed or len(result.stdout) > 1024 * 1024:
raise Refused()
return result.stdout
def status(namespace, pod):
data = json.loads(run(['-n', namespace, 'exec', pod, '--', 'bao', 'status', '-format=json'], allowed=(0, 2)))
if not isinstance(data, dict) or type(data.get('initialized')) is not bool or type(data.get('sealed')) is not bool:
raise Refused()
return data
def init_material(raw):
value = json.loads(raw)
shares = value.get('unseal_keys_b64')
if (not isinstance(shares, list) or not shares or len(shares) > 16
or any(not isinstance(x, str) or not re.fullmatch(r'[A-Za-z0-9+/=]{16,256}', x) for x in shares)
or len(set(shares)) != len(shares)
or not isinstance(value.get('root_token'), str) or not value['root_token']):
raise Refused()
return shares
def main(args):
if args == ['--dry-run']:
print('Plan only: validate explicit lab custody and cluster UID; retain private init material; unseal and verify. No files or cluster state inspected.')
return
if len(args) > 1 or (args and args[0].startswith('-')):
raise Refused()
root = Path(__file__).resolve().parents[2]
metadata = Path(os.environ.get('OPENBAO_CONSOLE_METADATA', str(root / '.local/security-bootstrap.json')))
model = json.loads(metadata.read_bytes())
if model.get('deployment_profile') != 'lab' or model.get('openbao_unseal_custody_model') != 'sops-held-automation':
raise Refused()
expected = os.environ.get('OPENBAO_EXPECTED_CLUSTER_UID', '')
if not re.fullmatch(r'[a-zA-Z0-9-]{1,128}', expected) or not os.environ.get('KUBECONFIG'):
raise Refused()
if os.environ.get('OPENBAO_RUN_CONFIGURE_INITIAL', '0') != '0':
raise Refused() # Platform configuration is a separate owner operation.
namespace = os.environ.get('OPENBAO_NAMESPACE', 'openbao')
release = os.environ.get('OPENBAO_RELEASE', 'openbao')
if any(not re.fullmatch(r'[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', x) for x in (namespace, release)):
raise Refused()
shares = int(os.environ.get('OPENBAO_KEY_SHARES', '3'))
threshold = int(os.environ.get('OPENBAO_KEY_THRESHOLD', '2'))
if not 2 <= threshold <= shares <= 16:
raise Refused()
# Pin the actual target before credential input or any mutation.
obj = json.loads(run(['get', 'namespace', 'kube-system', '-o', 'json']))
if obj.get('metadata', {}).get('uid') != expected:
raise Refused()
pod = release + '-0'
target = json.loads(run(['-n', namespace, 'get', 'pod', pod, '-o', 'json']))
pod_uid = target.get('metadata', {}).get('uid')
if (not isinstance(pod_uid, str) or not pod_uid or target.get('metadata', {}).get('namespace') != namespace
or target.get('metadata', {}).get('name') != pod):
raise Refused()
directory = args[0] if args else str(Path(__file__).parent / 'secrets')
did_init = False
with private.custody(directory, ()) as _:
parent = Path(directory) / 'openbao'
try:
parent.mkdir(mode=0o700)
except FileExistsError:
pass
with private.custody(directory, ('openbao',)) as fds:
fd = fds['openbao']
before = status(namespace, pod)
if not before['initialized']:
# Refuse prior custody material before sending init. An exclusive
# owner window is necessary; Kubernetes exec cannot CAS pod UID.
try:
os.stat('init.json', dir_fd=fd, follow_symlinks=False)
except FileNotFoundError:
pass
else:
raise Refused()
pinned = json.loads(run(['-n', namespace, 'get', 'pod', pod, '-o', 'json']))
if pinned.get('metadata', {}).get('uid') != pod_uid:
raise Refused()
raw = run(['-n', namespace, 'exec', pod, '--', 'bao', 'operator', 'init',
'-key-shares=' + str(shares), '-key-threshold=' + str(threshold), '-format=json'])
# Preserve even a malformed successful response privately: the
# remote initialization cannot be undone or blindly retried.
output = os.open('init.json', os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=fd)
with os.fdopen(output, 'wb') as dest:
dest.write(raw); dest.flush(); os.fsync(dest.fileno())
os.fsync(fd)
init_material(raw)
did_init = True
observed = status(namespace, pod)
if observed['sealed']:
material = init_material(private.read_private(fd, 'init.json'))
for share in material:
pinned = json.loads(run(['-n', namespace, 'get', 'pod', pod, '-o', 'json']))
if pinned.get('metadata', {}).get('uid') != pod_uid:
raise Refused()
run(['-n', namespace, 'exec', '-i', pod, '--', 'bao', 'write', 'sys/unseal', 'key=-'],
(share + '\n').encode())
observed = status(namespace, pod)
if not observed['sealed']:
break
if not observed['initialized'] or observed['sealed']:
raise Refused()
print('EVIDENCE ' + json.dumps({'custody_model': 'sops-held-automation', 'namespace': namespace,
'pod': pod, 'cluster_uid': expected, 'pod_uid': pod_uid, 'openbao_initialized': True,
'openbao_did_init_this_run': did_init, 'openbao_post_unseal_verified': True,
'recovery_custody_verified': False, 'dry_run': False}, sort_keys=True))
print('Retain init material in protected custody; encrypt and independently verify recovery. Platform configuration remains a separate owner operation.')
if __name__ == '__main__':
try:
main(sys.argv[1:])
except (Refused, private.Refused, OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError):
print('ERROR: lab init/unseal refused or failed; diagnostics suppressed. Remote outcome may be unknown. Preserve private init material and reconcile through the owner before retry.', file=sys.stderr)
raise SystemExit(1)