Adds the ADR-025 integration-test profile that proves the askari mesh-hardening REDESIGN (INPUT-only default-deny, forward ACCEPT for Docker) is reboot-safe on a throwaway KVM VM before the live cut-over. Profile applies base (firewall + sshd) and offsite (docker_host + reverse_proxy). Post-reboot verify checks: input policy drop, forward policy accept, admin-addr break-glass SSH (192.168.150.1), Docker up, and a published port answered from the controller. GREEN on 2026-06-19. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
6.1 KiB
YAML
129 lines
6.1 KiB
YAML
---
|
|
# Integration verify (ADR-025). Outcome-based, profile-aware: the active profile is named by
|
|
# `integration_profile` (set in each profile's overlay). Each profile asserts its own success
|
|
# criteria; an unknown/unset profile fails loudly (never a silent pass).
|
|
- name: Verify the rebooted host
|
|
hosts: all
|
|
become: true
|
|
gather_facts: false
|
|
tasks:
|
|
- name: A known integration_profile must be set (no silent pass)
|
|
ansible.builtin.assert:
|
|
that:
|
|
- integration_profile is defined
|
|
- integration_profile in ['askari', 'askari_inputonly', 'ubongo']
|
|
fail_msg: "integration_profile must be set in the profile overlay (askari|askari_inputonly|ubongo)"
|
|
|
|
# ── askari profile — Docker host: published-port forwarding survives the reboot ──
|
|
# The load-bearing check probes the VM's published :80 FROM the controller (ubongo) — if
|
|
# base's forward-drop killed DNAT, this times out (the FRICTION 2026-06-17 #1 bug).
|
|
- name: (askari) Gather service facts
|
|
when: integration_profile == 'askari'
|
|
ansible.builtin.service_facts:
|
|
|
|
- name: (askari) Docker daemon is active
|
|
when: integration_profile == 'askari'
|
|
ansible.builtin.assert:
|
|
that: "ansible_facts.services['docker.service'].state == 'running'"
|
|
fail_msg: "docker.service is not running"
|
|
|
|
- name: (askari) Forward chain permits container traffic (drop-in loaded)
|
|
when: integration_profile == 'askari'
|
|
ansible.builtin.command: nft list chain inet filter forward
|
|
register: _fwd
|
|
changed_when: false
|
|
|
|
- name: (askari) Assert container forwarding is allowed (not pure drop)
|
|
when: integration_profile == 'askari'
|
|
ansible.builtin.assert:
|
|
that: "'accept' in _fwd.stdout"
|
|
fail_msg: >-
|
|
forward chain is pure drop — container forwarding will die on reboot
|
|
(FRICTION 2026-06-17 #1). docker_host container-forward drop-in missing.
|
|
|
|
- name: (askari) Published port answers from the controller (DNAT + forward alive)
|
|
when: integration_profile == 'askari'
|
|
delegate_to: localhost
|
|
become: false
|
|
ansible.builtin.uri:
|
|
# Probe :80 (plain HTTP) — any answer proves the published-port DNAT + forward path
|
|
# is alive. Don't follow caddy's HTTP->HTTPS redirect (its `tls internal` has no
|
|
# cert for a bare-IP HTTPS request); the 308 itself proves the path works.
|
|
url: "http://{{ ansible_host }}/"
|
|
follow_redirects: none
|
|
status_code: [200, 301, 308, 404, 502, 503]
|
|
timeout: 10
|
|
register: _probe
|
|
retries: 5
|
|
delay: 6
|
|
until: _probe is succeeded
|
|
|
|
# ── ubongo profile — control node: INPUT-only default-deny survives the reboot ──
|
|
# SSH reachability across the reboot is proven by the harness itself (it re-SSHes and
|
|
# checks boot_id changed before this verify runs). Here we assert the ruleset shape.
|
|
- name: (ubongo) Read the live nftables ruleset
|
|
when: integration_profile == 'ubongo'
|
|
ansible.builtin.command: nft list ruleset
|
|
register: _nft
|
|
changed_when: false
|
|
|
|
- name: (ubongo) INPUT default-deny, forward permissive, lifeline + admin-addr allow
|
|
when: integration_profile == 'ubongo'
|
|
ansible.builtin.assert:
|
|
that:
|
|
# live `nft list ruleset` prints the SYMBOLIC priority (`filter` = 0), unlike the
|
|
# rendered /etc/nftables.conf (`priority 0`) that the Molecule scenario asserts against.
|
|
- "'hook input priority filter; policy drop;' in _nft.stdout"
|
|
- "'hook forward priority filter; policy accept;' in _nft.stdout"
|
|
# the ssh-from-control lifeline (base__firewall_control_addr) — the reconnect path
|
|
- "'ip saddr 192.168.150.1 tcp dport 22 accept' in _nft.stdout"
|
|
- "'ip saddr 192.168.150.98 tcp dport 22 accept' in _nft.stdout"
|
|
- "'ip saddr 192.168.150.99 tcp dport 22 accept' in _nft.stdout"
|
|
fail_msg: >-
|
|
ubongo profile: expected input policy drop, forward policy accept (input-only),
|
|
the ssh-from-control lifeline (192.168.150.1), and both admin-addr
|
|
(192.168.150.98/99) SSH allows in the live ruleset.
|
|
|
|
# ── askari_inputonly profile — the mesh-hardening REDESIGN (2026-06-19) ──
|
|
# INPUT-only default-deny on a Docker host: input policy drop, forward policy ACCEPT
|
|
# (Docker-safe), SSH via the admin-addr break-glass, published-port DNAT survives reboot.
|
|
- name: (askari_inputonly) Read the live nftables ruleset
|
|
when: integration_profile == 'askari_inputonly'
|
|
ansible.builtin.command: nft list ruleset
|
|
register: _nft_io
|
|
changed_when: false
|
|
|
|
- name: (askari_inputonly) INPUT default-deny, forward permissive, admin-addr break-glass
|
|
when: integration_profile == 'askari_inputonly'
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'hook input priority filter; policy drop;' in _nft_io.stdout"
|
|
- "'hook forward priority filter; policy accept;' in _nft_io.stdout"
|
|
- "'ip saddr 192.168.150.1 tcp dport 22 accept' in _nft_io.stdout"
|
|
fail_msg: >-
|
|
askari_inputonly: expected input policy drop, forward policy accept (input-only),
|
|
and the admin-addr break-glass (192.168.150.1) SSH allow in the live ruleset.
|
|
|
|
- name: (askari_inputonly) Gather service facts
|
|
when: integration_profile == 'askari_inputonly'
|
|
ansible.builtin.service_facts:
|
|
|
|
- name: (askari_inputonly) Docker daemon is active
|
|
when: integration_profile == 'askari_inputonly'
|
|
ansible.builtin.assert:
|
|
that: "ansible_facts.services['docker.service'].state == 'running'"
|
|
fail_msg: "docker.service is not running"
|
|
|
|
- name: (askari_inputonly) Published port answers from the controller (DNAT + forward alive)
|
|
when: integration_profile == 'askari_inputonly'
|
|
delegate_to: localhost
|
|
become: false
|
|
ansible.builtin.uri:
|
|
url: "http://{{ ansible_host }}/"
|
|
follow_redirects: none
|
|
status_code: [200, 301, 308, 404, 502, 503]
|
|
timeout: 10
|
|
register: _probe_io
|
|
retries: 5
|
|
delay: 6
|
|
until: _probe_io is succeeded
|