--- # playbooks/docker/swarm_audit.yml # Read-only cross-node consistency audit for the Docker Swarm cluster. # Safe to schedule. Makes no changes to any host. # # What this does: # Play 1 — Gathers key state from all swarm_hosts nodes (kernel, distro, # swap, sysctl, daemon.json, Docker Swarm role) # Play 2 — Asserts consistency across all 6 nodes and writes a markdown # drift report to outputs/swarm_audit_.md # # Usage: # ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_audit.yml # # Output: # outputs/swarm_audit_.md (repo root) - name: "Play 1: Gather Swarm node state" hosts: swarm_hosts become: true gather_facts: true tasks: - name: Read sysctl values for audit ansible.builtin.shell: "sysctl -n {{ item }} 2>/dev/null || echo 0" register: sysctl_raw loop: - vm.swappiness - net.bridge.bridge-nf-call-iptables - net.bridge.bridge-nf-call-ip6tables - net.ipv4.ip_forward changed_when: false check_mode: false - name: Read Docker daemon.json ansible.builtin.command: cat /etc/docker/daemon.json register: daemon_json_content changed_when: false failed_when: false check_mode: false - name: Get Docker Swarm node role ansible.builtin.shell: > docker info --format '{{ '{{' }}.Swarm.LocalNodeState{{ '}}' }}:{{ '{{' }}.Swarm.ControlAvailable{{ '}}' }}' register: docker_swarm_info changed_when: false failed_when: false check_mode: false - name: Stash per-node audit facts ansible.builtin.set_fact: swarm_audit: kernel: "{{ ansible_kernel }}" distro: "{{ ansible_distribution }}" distro_version: "{{ ansible_distribution_version }}" swap_mb: "{{ ansible_swaptotal_mb }}" swappiness: "{{ (sysctl_raw.results | selectattr('item', 'equalto', 'vm.swappiness') | first).stdout | trim }}" bridge_iptables: "{{ (sysctl_raw.results | selectattr('item', 'equalto', 'net.bridge.bridge-nf-call-iptables') | first).stdout | trim }}" bridge_ip6tables: "{{ (sysctl_raw.results | selectattr('item', 'equalto', 'net.bridge.bridge-nf-call-ip6tables') | first).stdout | trim }}" ip_forward: "{{ (sysctl_raw.results | selectattr('item', 'equalto', 'net.ipv4.ip_forward') | first).stdout | trim }}" daemon_json: "{{ daemon_json_content.stdout | default('{}') }}" log_rotation_configured: "{{ 'max-size' in (daemon_json_content.stdout | default('{}')) }}" swarm_local_state: "{{ docker_swarm_info.stdout.split(':')[0] | trim }}" swarm_is_manager: "{{ docker_swarm_info.stdout.split(':')[1] | trim | lower == 'true' }}" - name: "Play 2: Cross-node consistency assertions and drift report" hosts: localhost gather_facts: false vars: swarm_nodes: "{{ groups['swarm_hosts'] }}" managers: "{{ groups['swarm_managers'] }}" workers: "{{ groups['swarm_workers'] }}" audit_timestamp: "{{ lookup('pipe', 'date +%Y%m%dT%H%M%S') }}" report_path: "{{ playbook_dir }}/../../../outputs/swarm_audit_{{ audit_timestamp }}.md" tasks: - name: Ensure outputs directory exists ansible.builtin.file: path: "{{ playbook_dir }}/../../../outputs" state: directory mode: '0755' - name: Write drift report ansible.builtin.copy: dest: "{{ report_path }}" mode: '0644' content: | # Swarm Cluster Audit Report Generated: {{ audit_timestamp }} Nodes audited: {{ swarm_nodes | join(', ') }} ## Node Summary | Node | Role | Kernel | Distro | Swap | Swappiness | Bridge IPTables | IP Forward | Log Rotation | |------|------|--------|--------|------|------------|-----------------|------------|--------------| {% for node in swarm_nodes %} | {{ node }} | {{ 'Manager' if hostvars[node]['swarm_audit']['swarm_is_manager'] | bool else 'Worker' }} | `{{ hostvars[node]['swarm_audit']['kernel'] }}` | {{ hostvars[node]['swarm_audit']['distro'] }} {{ hostvars[node]['swarm_audit']['distro_version'] }} | {{ hostvars[node]['swarm_audit']['swap_mb'] }}MB | {{ hostvars[node]['swarm_audit']['swappiness'] }} | {{ hostvars[node]['swarm_audit']['bridge_iptables'] }} | {{ hostvars[node]['swarm_audit']['ip_forward'] }} | {{ '✅' if hostvars[node]['swarm_audit']['log_rotation_configured'] | bool else '❌' }} | {% endfor %} ## Swarm Role Mapping | Node | Inventory Role | Docker ControlAvailable | |------|----------------|------------------------| {% for node in managers %} | {{ node }} | Manager | {{ '✅ true' if hostvars[node]['swarm_audit']['swarm_is_manager'] | bool else '❌ false (DRIFT!)' }} | {% endfor %} {% for node in workers %} | {{ node }} | Worker | {{ '❌ true (UNEXPECTED!)' if hostvars[node]['swarm_audit']['swarm_is_manager'] | bool else '✅ false' }} | {% endfor %} ## Docker Swarm State | Node | LocalNodeState | |------|----------------| {% for node in swarm_nodes %} | {{ node }} | {{ hostvars[node]['swarm_audit']['swarm_local_state'] }} | {% endfor %} - name: Assert kernel consistency across all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['kernel'] == hostvars[swarm_nodes[0]]['swarm_audit']['kernel'] fail_msg: >- ❌ Kernel drift: {{ item }} has {{ hostvars[item]['swarm_audit']['kernel'] }} but {{ swarm_nodes[0] }} has {{ hostvars[swarm_nodes[0]]['swarm_audit']['kernel'] }} success_msg: "✅ {{ item }}: kernel {{ hostvars[item]['swarm_audit']['kernel'] }}" loop: "{{ swarm_nodes }}" - name: Assert distro version consistency across all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['distro_version'] == hostvars[swarm_nodes[0]]['swarm_audit']['distro_version'] fail_msg: >- ❌ Distro version drift: {{ item }} has {{ hostvars[item]['swarm_audit']['distro_version'] }} but {{ swarm_nodes[0] }} has {{ hostvars[swarm_nodes[0]]['swarm_audit']['distro_version'] }} success_msg: "✅ {{ item }}: distro {{ hostvars[item]['swarm_audit']['distro'] }} {{ hostvars[item]['swarm_audit']['distro_version'] }}" loop: "{{ swarm_nodes }}" - name: Assert swap is disabled on all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['swap_mb'] | int == 0 fail_msg: "❌ Swap is enabled on {{ item }}: {{ hostvars[item]['swarm_audit']['swap_mb'] }}MB — run swarm_baseline.yml --tags storage" success_msg: "✅ {{ item }}: swap disabled" loop: "{{ swarm_nodes }}" - name: Assert vm.swappiness=0 on all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['swappiness'] | int == 0 fail_msg: "❌ vm.swappiness={{ hostvars[item]['swarm_audit']['swappiness'] }} on {{ item }} — run swarm_baseline.yml --tags sysctl" success_msg: "✅ {{ item }}: vm.swappiness=0" loop: "{{ swarm_nodes }}" - name: Assert bridge netfilter is enabled on all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['bridge_iptables'] | int == 1 - hostvars[item]['swarm_audit']['bridge_ip6tables'] | int == 1 fail_msg: >- ❌ Bridge netfilter not fully enabled on {{ item }}: bridge-nf-call-iptables={{ hostvars[item]['swarm_audit']['bridge_iptables'] }} bridge-nf-call-ip6tables={{ hostvars[item]['swarm_audit']['bridge_ip6tables'] }} Run swarm_baseline.yml --tags sysctl to fix. success_msg: "✅ {{ item }}: bridge netfilter enabled" loop: "{{ swarm_nodes }}" - name: Assert ip_forward is enabled on all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['ip_forward'] | int == 1 fail_msg: "❌ net.ipv4.ip_forward={{ hostvars[item]['swarm_audit']['ip_forward'] }} on {{ item }} — run swarm_baseline.yml --tags sysctl" success_msg: "✅ {{ item }}: ip_forward=1" loop: "{{ swarm_nodes }}" - name: Assert Docker log rotation configured on all nodes ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['log_rotation_configured'] | bool fail_msg: "❌ Docker log rotation not configured on {{ item }} — run swarm_baseline.yml --tags docker" success_msg: "✅ {{ item }}: Docker log rotation configured" loop: "{{ swarm_nodes }}" - name: Assert swarm_managers are Docker managers ansible.builtin.assert: that: - hostvars[item]['swarm_audit']['swarm_is_manager'] | bool fail_msg: "❌ {{ item }} is in swarm_managers inventory group but Docker reports it is NOT a manager" success_msg: "✅ {{ item }}: confirmed Docker manager" loop: "{{ managers }}" - name: Assert swarm_workers are not Docker managers ansible.builtin.assert: that: - not (hostvars[item]['swarm_audit']['swarm_is_manager'] | bool) fail_msg: "❌ {{ item }} is in swarm_workers inventory group but Docker reports it is a Manager" success_msg: "✅ {{ item }}: confirmed Docker worker" loop: "{{ workers }}"