--- # 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