#!/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)