net-kingdom/sso-mfa/k8s/privacyidea/bootstrap-admin.py
tegwick 19a1c75d30
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(security): make admin bootstrap private and non-overwriting
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
2026-10-12 15:02:15 +02:00

80 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Pass one private bootstrap credential to the admitted pod over stdin."""
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import sys
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()
def read_password(root):
root = Path(os.path.abspath(root))
for ancestor in (root, *root.parents):
if ancestor.is_symlink(): raise Refused()
descriptors = []
try:
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
root_fd = os.open(root, flags); descriptors.append(root_fd)
private(os.fstat(root_fd), True)
parent_fd = os.open('privacyidea', flags, dir_fd=root_fd); descriptors.append(parent_fd)
private(os.fstat(parent_fd), True)
fd = os.open('secrets.env', os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent_fd)
descriptors.append(fd); private(os.fstat(fd))
raw = os.read(fd, 65537)
if len(raw) > 65536: raise Refused()
values = [line.split('=', 1)[1] for line in raw.decode().splitlines()
if line.startswith('PI_ADMIN_PASSWORD=')]
# This entry point consumes gen-secrets.sh's format, never shell code.
if len(values) != 1 or not re.fullmatch(r'[A-Za-z0-9]{32,128}', values[0]): raise Refused()
return values[0]
finally:
for fd in reversed(descriptors): os.close(fd)
def run(args, payload=None):
completed = subprocess.run(args, input=payload, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, timeout=60, check=False)
if completed.returncode or len(completed.stdout) > 4096: raise Refused()
return completed.stdout
def main():
if len(sys.argv) > 2: raise Refused()
here = Path(__file__).resolve().parent
password = read_password(sys.argv[1] if len(sys.argv) == 2 else here / '../../bootstrap/secrets')
pod = run(['kubectl', 'get', 'pod', '-n', 'mfa', '-l', 'app.kubernetes.io/name=privacyidea',
'--field-selector=status.phase=Running', '-o', 'jsonpath={.items[*].metadata.name}']).decode().strip()
if not re.fullmatch(r'[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?', pod): raise Refused()
script = (here / 'bootstrap-admin-remote.py').read_text()
response = run(['kubectl', 'exec', '-i', '-n', 'mfa', pod, '--', 'python3', '-c', script],
json.dumps({'password': password}).encode())
receipt = json.loads(response)
if (not isinstance(receipt, dict) or set(receipt) != {'result', 'mfa_verified'}
or receipt['result'] not in ('created', 'verified')
or receipt['mfa_verified'] is not False): raise Refused()
print('pi-admin bootstrap credential and scoped bootstrap policy verified.')
print('MFA enrollment, effective-policy review and a fresh sign-in remain required; this is not MFA acceptance.')
print('Use the approved custody entry for pi-admin; no credential is printed or exported.')
print('Legacy trigger accounts and Secrets require a separate inventory/revocation review.')
if __name__ == '__main__':
try:
main()
except (Refused, OSError, ValueError, TypeError, subprocess.SubprocessError):
print('ERROR: admin bootstrap refused or failed; use approved metadata-only diagnostics. No credential was printed.', file=sys.stderr)
sys.exit(1)