#!/usr/bin/env python3 """ Docker Daemon Hardening Auditor - Check Docker daemon configuration against CIS Docker Benchmark recommendations and generate remediation. """ import json import subprocess import sys import argparse from pathlib import Path HARDENING_CHECKS = { "icc_disabled": { "description": "Inter-container communication disabled (CIS 2.2)", "check_key": "icc", "expected": False, "severity": "HIGH", }, "userns_remap": { "description": "User namespace remapping enabled (CIS 2.9)", "check_key": "userns-remap", "expected_not_empty": True, "severity": "HIGH", }, "no_new_privileges": { "description": "No new privileges flag set (CIS 2.14)", "check_key": "no-new-privileges", "expected": True, "severity": "HIGH", }, "live_restore": { "description": "Live restore enabled (CIS 2.15)", "check_key": "live-restore", "expected": True, "severity": "MEDIUM", }, "userland_proxy_disabled": { "description": "Userland proxy disabled (CIS 2.16)", "check_key": "userland-proxy", "expected": False, "severity": "MEDIUM", }, "log_driver": { "description": "Logging driver configured (CIS 2.13)", "check_key": "log-driver", "expected_not_empty": True, "severity": "MEDIUM", }, "storage_driver": { "description": "Storage driver set to overlay2 (CIS 2.6)", "check_key": "storage-driver", "expected_value": "overlay2", "severity": "LOW", }, "experimental_disabled": { "description": "Experimental features disabled", "check_key": "experimental", "expected": False, "severity": "LOW", }, } RECOMMENDED_CONFIG = { "icc": False, "userns-remap": "default", "no-new-privileges": True, "log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "5"}, "storage-driver": "overlay2", "live-restore": True, "userland-proxy": False, "default-ulimits": { "nofile": {"Name": "nofile", "Hard": 65536, "Soft": 32768}, "nproc": {"Name": "nproc", "Hard": 4096, "Soft": 2048}, }, "experimental": False, "metrics-addr": "127.0.0.1:9323", } def load_daemon_config(config_path: str = "/etc/docker/daemon.json") -> dict: """Load Docker daemon.json configuration.""" path = Path(config_path) if not path.exists(): return {} try: return json.loads(path.read_text()) except json.JSONDecodeError as e: print(f"Error parsing {config_path}: {e}", file=sys.stderr) return {} def get_docker_info() -> dict: """Get Docker system info.""" result = subprocess.run(["docker", "info", "--format", "{{json .}}"], capture_output=True, text=True) if result.returncode != 0: print(f"Error running docker info: {result.stderr}", file=sys.stderr) return {} try: return json.loads(result.stdout) except json.JSONDecodeError: return {} def audit_config(config: dict) -> list: """Audit daemon.json against hardening checks.""" results = [] for check_id, check in HARDENING_CHECKS.items(): key = check["check_key"] value = config.get(key) passed = False actual = value if "expected" in check: passed = value == check["expected"] elif "expected_not_empty" in check: passed = value is not None and value != "" elif "expected_value" in check: passed = value == check["expected_value"] results.append({ "id": check_id, "description": check["description"], "severity": check["severity"], "passed": passed, "expected": check.get("expected", check.get("expected_value", "non-empty")), "actual": actual, }) return results def check_tls_config(config: dict) -> dict: """Check TLS configuration.""" tls_enabled = config.get("tls", False) tls_verify = config.get("tlsverify", False) has_ca = bool(config.get("tlscacert")) has_cert = bool(config.get("tlscert")) has_key = bool(config.get("tlskey")) return { "tls_enabled": tls_enabled, "tls_verify": tls_verify, "has_ca_cert": has_ca, "has_server_cert": has_cert, "has_server_key": has_key, "fully_configured": all([tls_enabled, tls_verify, has_ca, has_cert, has_key]), } def check_socket_permissions() -> dict: """Check Docker socket file permissions.""" import os import stat socket_path = "/var/run/docker.sock" if not os.path.exists(socket_path): return {"exists": False} st = os.stat(socket_path) mode = stat.filemode(st.st_mode) owner_uid = st.st_uid group_gid = st.st_gid world_readable = bool(st.st_mode & stat.S_IROTH) world_writable = bool(st.st_mode & stat.S_IWOTH) return { "exists": True, "permissions": mode, "owner_uid": owner_uid, "group_gid": group_gid, "world_readable": world_readable, "world_writable": world_writable, "secure": not world_readable and not world_writable, } def generate_report(audit_results: list, tls_info: dict, config_path: str) -> str: """Generate markdown audit report.""" passed = sum(1 for r in audit_results if r["passed"]) total = len(audit_results) score = (passed / total * 100) if total > 0 else 0 report = f"""# Docker Daemon Hardening Audit Report **Config File:** `{config_path}` **Score:** {passed}/{total} checks passed ({score:.0f}%) ## Audit Results | Status | Severity | Check | Expected | Actual | |--------|----------|-------|----------|--------| """ for r in sorted(audit_results, key=lambda x: (0 if not x["passed"] else 1, x["severity"])): status = "PASS" if r["passed"] else "FAIL" report += f"| {status} | {r['severity']} | {r['description']} | {r['expected']} | {r['actual']} |\n" report += f""" ## TLS Configuration | Setting | Value | |---------|-------| | TLS Enabled | {tls_info.get('tls_enabled', False)} | | TLS Verify | {tls_info.get('tls_verify', False)} | | CA Certificate | {tls_info.get('has_ca_cert', False)} | | Server Certificate | {tls_info.get('has_server_cert', False)} | | Server Key | {tls_info.get('has_server_key', False)} | | Fully Configured | {tls_info.get('fully_configured', False)} | ## Remediation Failed checks require updating `/etc/docker/daemon.json` and restarting the Docker daemon: ```bash sudo systemctl restart docker ``` """ return report def generate_hardened_config(existing: dict) -> dict: """Merge recommended settings with existing config.""" merged = existing.copy() merged.update(RECOMMENDED_CONFIG) return merged def main(): parser = argparse.ArgumentParser(description="Docker Daemon Hardening Auditor") parser.add_argument("--config", default="/etc/docker/daemon.json", help="Path to daemon.json") parser.add_argument("--audit", action="store_true", help="Run hardening audit") parser.add_argument("--generate", action="store_true", help="Generate hardened daemon.json") parser.add_argument("--report", help="Save audit report to file") parser.add_argument("--output", help="Output path for generated config") args = parser.parse_args() if args.generate: existing = load_daemon_config(args.config) hardened = generate_hardened_config(existing) output = json.dumps(hardened, indent=2) if args.output: Path(args.output).write_text(output) print(f"Hardened config written to {args.output}") else: print(output) return if args.audit: config = load_daemon_config(args.config) if not config: print(f"Warning: No config found at {args.config}, auditing empty config") audit_results = audit_config(config) tls_info = check_tls_config(config) report = generate_report(audit_results, tls_info, args.config) if args.report: Path(args.report).write_text(report) print(f"Report written to {args.report}") else: print(report) failed = sum(1 for r in audit_results if not r["passed"]) sys.exit(1 if failed > 0 else 0) parser.print_help() if __name__ == "__main__": main()