#!/usr/bin/env python3 """Disposable Podman confinement probe using an explicitly selected local image. No pulls, host profile loading, app/service changes, or shared bind mounts. A positive seccomp probe does not certify AppArmor, Docker, or an installed image. """ import argparse import json from pathlib import Path import subprocess import tempfile import uuid def probe(image): inspect = subprocess.run(['podman', 'image', 'inspect', '--format', '{{.Id}}', image], check=True, capture_output=True, text=True, timeout=30) image_id = inspect.stdout.strip() security = subprocess.run(['podman', 'info', '--format', '{{json .Host.Security}}'], check=True, capture_output=True, text=True, timeout=30) report = {'image_id': image_id, 'host_security': json.loads(security.stdout), 'checks': {}} with tempfile.TemporaryDirectory(prefix='archy-confinement-') as work: profile = Path(work) / 'deny-uname.json' profile.write_text(json.dumps({'defaultAction': 'SCMP_ACT_ALLOW', 'syscalls': [ {'names': ['uname'], 'action': 'SCMP_ACT_ERRNO', 'errnoRet': 1}]})) def run(options, command): name = 'archy-confinement-' + uuid.uuid4().hex args = ['podman', 'run', '--rm', '--pull=never', '--name', name, '--network=none', '--cap-drop=ALL', '--read-only', '--pids-limit=64', '--memory=128m', '--security-opt=no-new-privileges=true', *options, image_id, *command] try: return subprocess.run(args, capture_output=True, text=True, timeout=45) finally: # Scope cleanup to this unique probe only, including timeout. subprocess.run(['podman', 'rm', '--force', '--ignore', name], capture_output=True, timeout=30) baseline = run(['--security-opt=seccomp=/usr/share/containers/seccomp.json'], ['uname', '-s']) report['checks']['baseline_command_succeeds'] = baseline.returncode == 0 denied = run(['--security-opt=seccomp=' + str(profile)], ['uname', '-s']) report['checks']['seccomp_denies_uname'] = denied.returncode != 0 and 'Operation not permitted' in denied.stderr state = run(['--security-opt=seccomp=' + str(profile)], ['sh', '-c', 'cat /proc/self/status']) report['checks']['kernel_no_new_privileges_and_seccomp'] = (state.returncode == 0 and 'NoNewPrivs:\t1' in state.stdout and 'Seccomp:\t2' in state.stdout) missing = run(['--security-opt=apparmor=archipelago-probe-not-installed'], ['true']) report['checks']['missing_or_unavailable_apparmor_refuses'] = missing.returncode != 0 and 'apparmor' in missing.stderr.lower() report['apparmor_refusal'] = missing.stderr.strip() report['apparmor_positive_qualified'] = False report['docker_qualified'] = False return report if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--image', required=True, help='existing local image containing sh, uname and cat') parser.add_argument('--receipt', type=Path, required=True) args = parser.parse_args() if args.receipt.exists(): raise SystemExit('Refusing to overwrite an existing probe receipt') try: report = probe(args.image) except (subprocess.SubprocessError, OSError) as error: report = {'checks': {'probe_completed': False}, 'error': str(error), 'apparmor_positive_qualified': False, 'docker_qualified': False} args.receipt.parent.mkdir(parents=True, exist_ok=True) args.receipt.write_text(json.dumps(report, indent=2) + '\n') print(json.dumps(report, indent=2)) raise SystemExit(0 if all(report['checks'].values()) else 1)