202 lines
9.3 KiB
YAML
202 lines
9.3 KiB
YAML
---
|
|
# 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_<timestamp>.md
|
|
#
|
|
# Usage:
|
|
# ansible-playbook -i inventory/hosts.ini playbooks/docker/swarm_audit.yml
|
|
#
|
|
# Output:
|
|
# outputs/swarm_audit_<timestamp>.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 }}"
|