182 lines
7.1 KiB
YAML

---
# playbooks/docker/heimdall_audit.yml
# Read-only OS and stack health audit for the Heimdall edge router.
# Safe to schedule. Makes no changes to any host.
#
# What this asserts:
# OS: kernel, distro, swap, swappiness, bridge netfilter, ip_forward
# Docker: log rotation configured
# Stack: traefik, redis, docker-socket-proxy containers are running
#
# Usage:
# ansible-playbook -i inventory/hosts.ini playbooks/docker/heimdall_audit.yml
#
# Output:
# outputs/heimdall_audit_<timestamp>.md (repo root)
- name: "Play 1: Gather Heimdall state"
hosts: heimdall
become: true
gather_facts: true
tasks:
- name: Read sysctl values
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 running container names
ansible.builtin.command: >
docker ps --format '{{ '{{' }}.Names{{ '}}' }}'
register: running_containers
changed_when: false
failed_when: false
check_mode: false
- name: Stash audit facts
ansible.builtin.set_fact:
heimdall_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 }}"
log_rotation_configured: "{{ 'max-size' in (daemon_json_content.stdout | default('{}')) }}"
running_containers: "{{ running_containers.stdout_lines | default([]) }}"
traefik_running: "{{ running_containers.stdout_lines | default([]) | select('search', 'traefik') | list | length > 0 }}"
redis_running: "{{ running_containers.stdout_lines | default([]) | select('search', 'redis') | list | length > 0 }}"
socket_proxy_running: "{{ running_containers.stdout_lines | default([]) | select('search', 'socket-proxy|socketproxy|docker-socket') | list | length > 0 }}"
- name: "Play 2: Assertions and drift report"
hosts: localhost
gather_facts: false
vars:
audit_timestamp: "{{ lookup('pipe', 'date +%Y%m%dT%H%M%S') }}"
report_path: "{{ playbook_dir }}/../../../outputs/heimdall_audit_{{ audit_timestamp }}.md"
h: "{{ hostvars['heimdall']['heimdall_audit'] }}"
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: |
# Heimdall Edge Router Audit Report
Generated: {{ audit_timestamp }}
## System
| Property | Value |
|----------|-------|
| Kernel | `{{ h.kernel }}` |
| Distro | {{ h.distro }} {{ h.distro_version }} |
| Swap | {{ h.swap_mb }}MB |
## Sysctl
| Parameter | Value | Expected |
|-----------|-------|----------|
| vm.swappiness | {{ h.swappiness }} | 0 |
| net.bridge.bridge-nf-call-iptables | {{ h.bridge_iptables }} | 1 |
| net.bridge.bridge-nf-call-ip6tables | {{ h.bridge_ip6tables }} | 1 |
| net.ipv4.ip_forward | {{ h.ip_forward }} | 1 |
## Docker
| Check | Status |
|-------|--------|
| Log rotation configured | {{ '✅' if h.log_rotation_configured | bool else '❌' }} |
## Stack Health
| Container | Status |
|-----------|--------|
| traefik | {{ '✅ running' if h.traefik_running | bool else '❌ not running' }} |
| redis | {{ '✅ running' if h.redis_running | bool else '❌ not running' }} |
| docker-socket-proxy | {{ '✅ running' if h.socket_proxy_running | bool else '❌ not running' }} |
## Running Containers
{% for c in h.running_containers %}
- {{ c }}
{% endfor %}
- name: Assert swap is disabled
ansible.builtin.assert:
that: h.swap_mb | int == 0
fail_msg: "❌ Swap enabled: {{ h.swap_mb }}MB — run heimdall_baseline.yml --tags storage"
success_msg: "✅ Heimdall: swap disabled"
- name: Assert vm.swappiness=0
ansible.builtin.assert:
that: h.swappiness | int == 0
fail_msg: "❌ vm.swappiness={{ h.swappiness }} — run heimdall_baseline.yml --tags sysctl"
success_msg: "✅ Heimdall: vm.swappiness=0"
- name: Assert bridge netfilter enabled
ansible.builtin.assert:
that:
- h.bridge_iptables | int == 1
- h.bridge_ip6tables | int == 1
fail_msg: >-
❌ Bridge netfilter not fully enabled:
bridge-nf-call-iptables={{ h.bridge_iptables }}
bridge-nf-call-ip6tables={{ h.bridge_ip6tables }}
Run heimdall_baseline.yml --tags sysctl.
success_msg: "✅ Heimdall: bridge netfilter enabled"
- name: Assert ip_forward enabled
ansible.builtin.assert:
that: h.ip_forward | int == 1
fail_msg: "❌ net.ipv4.ip_forward={{ h.ip_forward }} — run heimdall_baseline.yml --tags sysctl"
success_msg: "✅ Heimdall: ip_forward=1"
- name: Assert Docker log rotation configured
ansible.builtin.assert:
that: h.log_rotation_configured | bool
fail_msg: "❌ Docker log rotation not configured — run heimdall_baseline.yml --tags docker"
success_msg: "✅ Heimdall: Docker log rotation configured"
- name: Assert Traefik container is running
ansible.builtin.assert:
that: h.traefik_running | bool
fail_msg: "❌ Traefik container is not running — check: docker ps -a | grep traefik"
success_msg: "✅ Heimdall: Traefik running"
- name: Assert Redis container is running
ansible.builtin.assert:
that: h.redis_running | bool
fail_msg: "❌ Redis container is not running — check: docker ps -a | grep redis"
success_msg: "✅ Heimdall: Redis running"
- name: Assert docker-socket-proxy container is running
ansible.builtin.assert:
that: h.socket_proxy_running | bool
fail_msg: "❌ docker-socket-proxy container is not running — check: docker ps -a | grep socket"
success_msg: "✅ Heimdall: docker-socket-proxy running"