From da1da34d259863af839e3590be2670227326622b Mon Sep 17 00:00:00 2001 From: ansible Date: Tue, 11 Nov 2025 07:47:06 +0100 Subject: [PATCH] Add comprehensive Docker security audit playbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement production-ready Docker security audit framework with CIS Docker Benchmark and NIST SP 800-190 alignment. Features: - Comprehensive container security checks (privileges, network, resources) - Daemon configuration audit - Image and network analysis - Security findings categorization (CRITICAL/HIGH/MEDIUM/LOW) - Automated report generation (JSON + detailed text) - Support for multiple hosts via dynamic inventory Audit Checks: - Privileged container detection (CRITICAL) - Host network mode usage (HIGH) - User namespace remapping status (MEDIUM) - Resource limits enforcement (MEDIUM) - Container capabilities audit - Security profiles (AppArmor/SELinux) - Image tag analysis (:latest usage) - Exposed ports inventory Report Outputs: - Detailed text report with recommendations - Machine-readable JSON report - CIS Benchmark compliance mapping - NIST SP 800-190 alignment - Actionable remediation roadmap Files: - playbooks/audit_docker.yml (300+ lines) - templates/docker_audit_report.j2 (comprehensive report template) Usage: ansible-playbook playbooks/audit_docker.yml ansible-playbook playbooks/audit_docker.yml --limit hostname Results: ./stats/docker_audits/{hostname}/docker_audit_{timestamp}.{txt,json} 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- playbooks/audit_docker.yml | 325 +++++++++++++++++++++++++++++++ templates/docker_audit_report.j2 | 303 ++++++++++++++++++++++++++++ 2 files changed, 628 insertions(+) create mode 100644 playbooks/audit_docker.yml create mode 100644 templates/docker_audit_report.j2 diff --git a/playbooks/audit_docker.yml b/playbooks/audit_docker.yml new file mode 100644 index 0000000..881fdcb --- /dev/null +++ b/playbooks/audit_docker.yml @@ -0,0 +1,325 @@ +--- +# ============================================================================== +# Docker Security Audit Playbook +# ============================================================================== +# Comprehensive security audit for Docker installations +# Generates detailed security reports with findings and recommendations +# ============================================================================== + +- name: Docker Security Audit + hosts: all + become: true + gather_facts: true + tags: [docker, security, audit] + + vars: + audit_output_dir: "./stats/docker_audits" + audit_timestamp: "{{ ansible_date_time.epoch }}" + + tasks: + - name: Display audit start information + ansible.builtin.debug: + msg: + - "=== Docker Security Audit ===" + - "Host: {{ inventory_hostname }}" + - "Date: {{ ansible_date_time.iso8601 }}" + tags: [always] + + - name: Check if Docker is installed + ansible.builtin.command: docker --version + register: docker_version + failed_when: false + changed_when: false + tags: [always] + + - name: Skip audit if Docker not installed + ansible.builtin.meta: end_host + when: docker_version.rc != 0 + tags: [always] + + - name: Create audit output directory on control node + ansible.builtin.file: + path: "{{ audit_output_dir }}/{{ inventory_hostname }}" + state: directory + mode: '0755' + delegate_to: localhost + become: false + tags: [always] + + # ========================================================================== + # Docker Daemon Configuration Audit + # ========================================================================== + + - name: Check if Docker daemon config exists + ansible.builtin.stat: + path: /etc/docker/daemon.json + register: daemon_config_stat + tags: [daemon] + + - name: Read Docker daemon configuration + ansible.builtin.slurp: + src: /etc/docker/daemon.json + register: docker_daemon_config + failed_when: false + when: daemon_config_stat.stat.exists + tags: [daemon] + + - name: Get Docker daemon info + ansible.builtin.command: docker info --format json + register: docker_info_json + changed_when: false + tags: [daemon] + + - name: Parse Docker info + ansible.builtin.set_fact: + docker_info: "{{ docker_info_json.stdout | from_json }}" + tags: [daemon] + + - name: Check Docker daemon security options + ansible.builtin.set_fact: + docker_security_options: "{{ docker_info.SecurityOptions | default([]) }}" + tags: [daemon] + + # ========================================================================== + # Container Audit + # ========================================================================== + + - name: List running containers + ansible.builtin.command: docker ps --format json + register: docker_containers_raw + changed_when: false + failed_when: false + tags: [containers] + + - name: Parse container list + ansible.builtin.set_fact: + running_containers: "{{ docker_containers_raw.stdout_lines | map('from_json') | list }}" + when: docker_containers_raw.stdout_lines | length > 0 + tags: [containers] + + - name: Get all container IDs + ansible.builtin.command: docker ps -q + register: container_ids + changed_when: false + failed_when: false + tags: [containers] + + - name: Audit container privileges + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: Privileged={{.HostConfig.Privileged}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: container_privileges + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers, privileges] + + - name: Check user namespace remapping + ansible.builtin.shell: | + docker info --format '{% raw %}{{ .SecurityOptions }}{% endraw %}' | grep -i userns || echo "Not configured" + register: userns_check + changed_when: false + failed_when: false + tags: [containers, namespaces] + + - name: Audit security profiles (AppArmor/SELinux) + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: AppArmor={{.AppArmorProfile}} SELinux={{.HostConfig.SecurityOpt}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: security_profiles + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers, profiles] + + - name: Check network modes + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: NetworkMode={{.HostConfig.NetworkMode}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: network_modes + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers, network] + + - name: Check resource limits + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: Memory={{.HostConfig.Memory}} CPU={{.HostConfig.CpuShares}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: resource_limits + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers, resources] + + - name: Check for exposed ports + ansible.builtin.shell: | + docker ps --format "{% raw %}{{.Names}}: {{.Ports}}{% endraw %}" + register: exposed_ports + changed_when: false + tags: [containers, ports] + + - name: Check container capabilities + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: container_capabilities + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers, capabilities] + + - name: Check container restart policies + ansible.builtin.shell: | + set -o pipefail + docker inspect {{ container_ids.stdout_lines | join(' ') }} --format '{% raw %}{{.Name}}: RestartPolicy={{.HostConfig.RestartPolicy.Name}}{% endraw %}' 2>/dev/null || echo "No containers" + args: + executable: /bin/bash + register: restart_policies + changed_when: false + when: container_ids.stdout_lines | length > 0 + tags: [containers] + + # ========================================================================== + # Image Audit + # ========================================================================== + + - name: List all Docker images + ansible.builtin.command: docker images --format json + register: docker_images_raw + changed_when: false + tags: [images] + + - name: Check for images with latest tag + ansible.builtin.shell: | + docker images --format "{% raw %}{{.Repository}}:{{.Tag}}{% endraw %}" | grep -c ":latest" || echo "0" + register: latest_tag_count + changed_when: false + tags: [images] + + # ========================================================================== + # Network Audit + # ========================================================================== + + - name: List Docker networks + ansible.builtin.command: docker network ls --format json + register: docker_networks_raw + changed_when: false + tags: [network] + + - name: Check Docker storage driver + ansible.builtin.set_fact: + storage_driver: "{{ docker_info.Driver | default('unknown') }}" + tags: [storage] + + # ========================================================================== + # Security Findings Analysis + # ========================================================================== + + - name: Analyze security findings + ansible.builtin.set_fact: + security_findings: + critical: [] + high: [] + medium: [] + low: [] + tags: [analysis] + + - name: Check for privileged containers (CRITICAL) + ansible.builtin.set_fact: + security_findings: "{{ security_findings | combine({'critical': security_findings.critical + ['Privileged containers detected']}) }}" + when: + - container_privileges.stdout is defined + - "'Privileged=true' in container_privileges.stdout" + tags: [analysis] + + - name: Check for host network mode (HIGH) + ansible.builtin.set_fact: + security_findings: "{{ security_findings | combine({'high': security_findings.high + ['Containers using host network mode']}) }}" + when: + - network_modes.stdout is defined + - "'NetworkMode=host' in network_modes.stdout" + tags: [analysis] + + - name: Check for missing user namespace remapping (MEDIUM) + ansible.builtin.set_fact: + security_findings: "{{ security_findings | combine({'medium': security_findings.medium + ['User namespace remapping not configured']}) }}" + when: "'userns' not in userns_check.stdout" + tags: [analysis] + + - name: Check for unlimited resources (MEDIUM) + ansible.builtin.set_fact: + security_findings: "{{ security_findings | combine({'medium': security_findings.medium + ['Containers without resource limits']}) }}" + when: + - resource_limits.stdout is defined + - "'Memory=0' in resource_limits.stdout" + tags: [analysis] + + - name: Check for latest image tags (LOW) + ansible.builtin.set_fact: + security_findings: "{{ security_findings | combine({'low': security_findings.low + ['Images using :latest tag (' + latest_tag_count.stdout + ')']}) }}" + when: latest_tag_count.stdout | int > 0 + tags: [analysis] + + # ========================================================================== + # Generate Audit Report + # ========================================================================== + + - name: Generate audit report from template + ansible.builtin.template: + src: ../templates/docker_audit_report.j2 + dest: "{{ audit_output_dir }}/{{ inventory_hostname }}/docker_audit_{{ audit_timestamp }}.txt" + mode: '0644' + delegate_to: localhost + become: false + tags: [report] + + - name: Generate JSON report + ansible.builtin.copy: + content: | + { + "timestamp": "{{ ansible_date_time.iso8601 }}", + "host": "{{ inventory_hostname }}", + "docker_version": "{{ docker_version.stdout }}", + "security_options": {{ docker_security_options | to_json }}, + "containers": { + "total": {{ container_ids.stdout_lines | length }}, + "privileged": {{ (container_privileges.stdout | default('') | regex_findall('Privileged=true')) | length }}, + "host_network": {{ (network_modes.stdout | default('') | regex_findall('NetworkMode=host')) | length }} + }, + "findings": {{ security_findings | to_json }}, + "storage_driver": "{{ storage_driver }}" + } + dest: "{{ audit_output_dir }}/{{ inventory_hostname }}/docker_audit_{{ audit_timestamp }}.json" + mode: '0644' + delegate_to: localhost + become: false + tags: [report] + + # ========================================================================== + # Display Results + # ========================================================================== + + - name: Display audit summary + ansible.builtin.debug: + msg: + - "=== Docker Security Audit Summary ===" + - "Host: {{ inventory_hostname }}" + - "Docker Version: {{ docker_version.stdout }}" + - "Running Containers: {{ container_ids.stdout_lines | length }}" + - "Security Options: {{ docker_security_options }}" + - "Storage Driver: {{ storage_driver }}" + - "" + - "Security Findings:" + - " CRITICAL: {{ security_findings.critical | length }}" + - " HIGH: {{ security_findings.high | length }}" + - " MEDIUM: {{ security_findings.medium | length }}" + - " LOW: {{ security_findings.low | length }}" + - "" + - "Report saved to: {{ audit_output_dir }}/{{ inventory_hostname }}/" + tags: [always] diff --git a/templates/docker_audit_report.j2 b/templates/docker_audit_report.j2 new file mode 100644 index 0000000..d642aad --- /dev/null +++ b/templates/docker_audit_report.j2 @@ -0,0 +1,303 @@ +================================================================================ +DOCKER SECURITY AUDIT REPORT +================================================================================ +Host: {{ inventory_hostname }} +Date: {{ ansible_date_time.iso8601 }} +Auditor: Ansible Automation Platform +Report ID: {{ audit_timestamp }} +================================================================================ + +SYSTEM INFORMATION +---------------------------------------- +Hostname: {{ ansible_hostname }} +FQDN: {{ ansible_fqdn | default('N/A') }} +OS: {{ ansible_distribution }} {{ ansible_distribution_version }} +Kernel: {{ ansible_kernel }} +Architecture: {{ ansible_architecture }} + +DOCKER INFORMATION +---------------------------------------- +Version: {{ docker_version.stdout }} +Storage Driver: {{ storage_driver }} +Security Options: {{ docker_security_options | join(', ') if docker_security_options else 'None configured' }} +Daemon Config File: {{ 'Exists' if daemon_config_stat.stat.exists else 'Not found' }} + +{% if daemon_config_stat.stat.exists and docker_daemon_config.content is defined %} +Daemon Configuration: +{{ docker_daemon_config.content | b64decode | indent(2) }} +{% endif %} + +CONTAINER INVENTORY +---------------------------------------- +Running Containers: {{ container_ids.stdout_lines | length }} + +{% if container_ids.stdout_lines | length > 0 %} +Container List: +{{ running_containers | map(attribute='Names') | join('\n') | indent(2) }} +{% else %} +No containers running +{% endif %} + +SECURITY AUDIT RESULTS +======================================== + +PRIVILEGE AUDIT +---------------------------------------- +{% if container_privileges.stdout is defined %} +{{ container_privileges.stdout }} +{% else %} +No containers to audit +{% endif %} + +USER NAMESPACE REMAPPING +---------------------------------------- +Status: {{ userns_check.stdout }} + +SECURITY PROFILES (AppArmor/SELinux) +---------------------------------------- +{% if security_profiles.stdout is defined %} +{{ security_profiles.stdout }} +{% else %} +No containers to audit +{% endif %} + +NETWORK CONFIGURATION +---------------------------------------- +{% if network_modes.stdout is defined %} +{{ network_modes.stdout }} +{% else %} +No containers to audit +{% endif %} + +RESOURCE LIMITS +---------------------------------------- +{% if resource_limits.stdout is defined %} +{{ resource_limits.stdout }} +{% else %} +No containers to audit +{% endif %} + +CONTAINER CAPABILITIES +---------------------------------------- +{% if container_capabilities.stdout is defined %} +{{ container_capabilities.stdout }} +{% else %} +No containers to audit +{% endif %} + +RESTART POLICIES +---------------------------------------- +{% if restart_policies.stdout is defined %} +{{ restart_policies.stdout }} +{% else %} +No containers to audit +{% endif %} + +EXPOSED PORTS +---------------------------------------- +{{ exposed_ports.stdout }} + +IMAGE ANALYSIS +---------------------------------------- +Total Images: {{ docker_images_raw.stdout_lines | length }} +Images using :latest tag: {{ latest_tag_count.stdout }} + +WARNING: Using :latest tag is not recommended for production as it makes + deployments non-reproducible and can lead to unexpected updates. + +NETWORK ANALYSIS +---------------------------------------- +Networks: {{ docker_networks_raw.stdout_lines | length }} + +SECURITY FINDINGS +======================================== + +{% if security_findings.critical | length > 0 %} +🔴 CRITICAL FINDINGS ({{ security_findings.critical | length }}) +---------------------------------------- +{% for finding in security_findings.critical %} + - {{ finding }} +{% endfor %} + +{% endif %} +{% if security_findings.high | length > 0 %} +🟠 HIGH SEVERITY FINDINGS ({{ security_findings.high | length }}) +---------------------------------------- +{% for finding in security_findings.high %} + - {{ finding }} +{% endfor %} + +{% endif %} +{% if security_findings.medium | length > 0 %} +🟡 MEDIUM SEVERITY FINDINGS ({{ security_findings.medium | length }}) +---------------------------------------- +{% for finding in security_findings.medium %} + - {{ finding }} +{% endfor %} + +{% endif %} +{% if security_findings.low | length > 0 %} +🟢 LOW SEVERITY FINDINGS ({{ security_findings.low | length }}) +---------------------------------------- +{% for finding in security_findings.low %} + - {{ finding }} +{% endfor %} + +{% endif %} +{% if security_findings.critical | length == 0 and security_findings.high | length == 0 and security_findings.medium | length == 0 and security_findings.low | length == 0 %} +✅ NO SECURITY FINDINGS +---------------------------------------- +No significant security issues detected. + +{% endif %} + +RECOMMENDATIONS +======================================== + +CRITICAL PRIORITY +---------------------------------------- +{% if container_privileges.stdout is defined and 'Privileged=true' in container_privileges.stdout %} +1. ⚠️ DISABLE PRIVILEGED MODE + - Privileged containers have full access to host resources + - Remove --privileged flag unless absolutely necessary + - Use specific capabilities (--cap-add) instead + - Document justification for any privileged containers + +{% endif %} +{% if network_modes.stdout is defined and 'NetworkMode=host' in network_modes.stdout %} +2. ⚠️ AVOID HOST NETWORK MODE + - Host network mode bypasses Docker network isolation + - Use bridge mode and explicit port mappings + - Consider using macvlan for performance-critical applications + +{% endif %} + +HIGH PRIORITY +---------------------------------------- +3. IMPLEMENT USER NAMESPACE REMAPPING + - Add to /etc/docker/daemon.json: + { + "userns-remap": "default" + } + - Restart Docker daemon after configuration change + - Note: Existing containers will need to be recreated + +4. ENFORCE RESOURCE LIMITS + - Set memory limits: --memory="512m" + - Set CPU limits: --cpus="1.0" + - Prevents container resource exhaustion attacks + - Example: + docker run --memory="512m" --cpus="1.0" image:tag + +5. USE SECURITY PROFILES + - Enable AppArmor (Debian/Ubuntu): + --security-opt apparmor=docker-default + - Enable SELinux (RHEL/CentOS): + --security-opt label=type:container_t + - Create custom profiles for sensitive containers + +MEDIUM PRIORITY +---------------------------------------- +6. DROP UNNECESSARY CAPABILITIES + - Drop all by default: --cap-drop=ALL + - Add only required capabilities: + --cap-add=NET_BIND_SERVICE (for ports < 1024) + --cap-add=CHOWN (for ownership changes) + - Never use --cap-add=ALL + +7. USE SPECIFIC IMAGE TAGS + - Replace :latest with specific version tags + - Ensures reproducible deployments + - Facilitates rollback procedures + - Example: nginx:1.25.3-alpine instead of nginx:latest + +8. MINIMIZE EXPOSED PORTS + - Only expose necessary ports + - Use internal networks for container-to-container communication + - Consider using reverse proxy (Traefik, nginx) for public access + +9. IMPLEMENT READ-ONLY ROOT FILESYSTEMS + - Use --read-only flag when possible + - Mount tmpfs for writable directories: + --tmpfs /tmp --tmpfs /var/run + +10. ENABLE DOCKER CONTENT TRUST + - Set environment variable: + export DOCKER_CONTENT_TRUST=1 + - Ensures images are signed and verified + - Prevents use of tampered images + +LOW PRIORITY +---------------------------------------- +11. REGULAR IMAGE UPDATES + - Schedule regular image pulls and container recreation + - Subscribe to security advisories for base images + - Consider using automated tools: Watchtower, Renovate + +12. IMPLEMENT LOGGING + - Configure centralized logging + - Use logging drivers: syslog, json-file, etc. + - Set log rotation limits to prevent disk exhaustion + +13. NETWORK SEGMENTATION + - Create separate networks for different application tiers + - Use internal networks for backend services + - Implement network policies where supported + +COMPLIANCE CHECKLIST +======================================== + +CIS Docker Benchmark Alignment: +[ ] 2.1 - Run daemon as non-root user (user namespace remapping) +[ ] 2.2 - Set default ulimit as appropriate +[ ] 2.13 - Enable user namespace support +[ ] 5.1 - Do not disable AppArmor/SELinux profile +[ ] 5.3 - Do not use privileged containers +[ ] 5.7 - Do not map privileged ports within containers +[ ] 5.12 - Mount container's root filesystem as read only +[ ] 5.15 - Do not share the host's network namespace +[ ] 5.25 - Restrict container from acquiring additional privileges +[ ] 5.28 - Use PIDs cgroup limit + +NIST 800-190 Guidelines: +[ ] Image security and integrity +[ ] Registry security +[ ] Container runtime protection +[ ] Host OS and multi-tenancy +[ ] Network isolation and segmentation + +NEXT STEPS +======================================== + +IMMEDIATE ACTIONS (This Week) +1. Review and address all CRITICAL findings +2. Document justification for any privileged containers +3. Implement resource limits on all production containers + +SHORT TERM (This Month) +1. Enable user namespace remapping +2. Implement security profiles (AppArmor/SELinux) +3. Replace :latest tags with specific versions +4. Set up automated security scanning + +LONG TERM (This Quarter) +1. Implement comprehensive container monitoring +2. Set up automated vulnerability scanning +3. Create hardened base images +4. Implement network segmentation policies +5. Regular security audits and penetration testing + +REFERENCES +======================================== + +- CIS Docker Benchmark: https://www.cisecurity.org/benchmark/docker +- NIST SP 800-190: https://csrc.nist.gov/publications/detail/sp/800-190/final +- Docker Security Best Practices: https://docs.docker.com/engine/security/ +- OWASP Docker Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html + +================================================================================ +END OF REPORT +================================================================================ +Report generated: {{ ansible_date_time.iso8601 }} +Audit tool: Ansible {{ ansible_version.full }} +================================================================================