#!/usr/bin/env python3 """Create first-bootstrap config with private inputs and scoped factor delivery. Never replace an existing Secret, mint a provider token or rotate a signing key. Child output is captured, including Kubernetes error bodies containing Secret data. """ import base64 import contextlib import json import os from pathlib import Path import re import secrets import stat import subprocess import sys import yaml class Refused(Exception): pass def private(info, directory=False): kind = stat.S_ISDIR if directory else stat.S_ISREG if not kind(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: raise Refused() if not directory and info.st_nlink != 1: raise Refused() @contextlib.contextmanager def custody(root): root = Path(os.path.abspath(root)) if any(p.is_symlink() for p in (root, *root.parents)): raise Refused() descriptors = [] try: flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW fd = os.open(root, flags); descriptors.append(fd) private(os.fstat(fd), True) children = {} for name in ('lldap', 'authelia', 'keycape'): child = os.open(name, flags, dir_fd=fd); descriptors.append(child) private(os.fstat(child), True) children[name] = child yield children finally: for fd in reversed(descriptors): os.close(fd) def read_file(parent, name): fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent) try: private(os.fstat(fd)) raw = os.read(fd, 65537) if len(raw) > 65536: raise Refused() return raw finally: os.close(fd) def env_value(parent, name): values = [line.split('=', 1)[1] for line in read_file(parent, 'secrets.env').decode().splitlines() if line.startswith(name + '=')] # Consume the generator's literal format, never execute a shell env file. if len(values) != 1 or not re.fullmatch(r'[A-Za-z0-9]{32,128}', values[0]): raise Refused() return values[0] def run(args, payload=None): result = subprocess.run(args, input=payload, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=60, check=False) if result.returncode or len(result.stdout) > 65536: raise Refused() return result.stdout def signing_key(parent): try: key = read_file(parent, 'key.pem') except FileNotFoundError: key = run(['openssl', 'genrsa', '2048']) run(['openssl', 'rsa', '-check', '-noout'], key) # Publish atomically without replacement, so concurrent creation cannot # overwrite a pre-existing key. Keep this private key after API failure. temp = '.key-' + secrets.token_hex(12) fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=parent) try: with os.fdopen(fd, 'wb') as file: file.write(key); file.flush(); os.fsync(file.fileno()) os.link(temp, 'key.pem', src_dir_fd=parent, dst_dir_fd=parent, follow_symlinks=False) os.fsync(parent) finally: os.unlink(temp, dir_fd=parent) run(['openssl', 'rsa', '-check', '-noout'], key) return key def main(): if len(sys.argv) > 2: raise Refused() here = Path(__file__).resolve().parent root = sys.argv[1] if len(sys.argv) == 2 else here / '../../bootstrap/secrets' with custody(root) as dirs: bind_password = env_value(dirs['lldap'], 'LLDAP_SVC_KEYCAPE_PASS') client_secret = env_value(dirs['authelia'], 'AUTHELIA_KEYCAPE_CLIENT_SECRET') config = json.loads((here / 'bootstrap-config.json').read_text()) config['lldap']['bindPW'] = bind_password config['authelia']['clientSecret'] = client_secret # Existence is not an update grant. API create also closes the race # between this preflight and publication; AlreadyExists stays a failure. existing = run(['kubectl', 'get', 'secret', 'keycape-config', '-n', 'sso', '--ignore-not-found', '-o', 'name']) if existing.strip(): raise Refused() ready = run(['kubectl', 'get', 'externalsecret', 'keycape-factor-read', '-n', 'sso', '-o', 'jsonpath={.status.conditions[?(@.type=="Ready")].status}']) factor = run(['kubectl', 'get', 'secret', 'keycape-factor-read', '-n', 'sso', '-o', 'name']) if ready.strip() != b'True' or factor.strip() != b'secret/keycape-factor-read': raise Refused() key = signing_key(dirs['keycape']) data = {'config.yaml': yaml.safe_dump(config, sort_keys=False).encode(), 'key.pem': key} manifest = {'apiVersion': 'v1', 'kind': 'Secret', 'type': 'Opaque', 'metadata': {'name': 'keycape-config', 'namespace': 'sso'}, 'data': {name: base64.b64encode(value).decode() for name, value in data.items()}} response = run(['kubectl', 'create', '-o', 'name', '-f', '-'], json.dumps(manifest).encode()) if response.strip() != b'secret/keycape-config': raise Refused() print('KeyCape bootstrap Secret created with mounted factor credential configuration.') print('Preserve the private signing key in approved custody. Native login and factor acceptance remain required.') if __name__ == '__main__': try: main() except (Refused, OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): print('ERROR: KeyCape bootstrap refused or failed. Existing configuration is never replaced; ' 'check private inputs and RPF-WP-0040 delivery using metadata-only diagnostics. ' 'If creation was attempted, its remote outcome must be reconciled before retry.', file=sys.stderr) sys.exit(1)