From 172ae37953f871775d5f5ac40c220b50d1d306b7 Mon Sep 17 00:00:00 2001 From: sjat Date: Thu, 18 Jun 2026 16:57:47 +0200 Subject: [PATCH] feat(docker_host): container-forward nftables drop-in (reboot-safe Docker forwarding) base's inet-filter forward chain is policy-drop; on a Docker host that kills published-port DNAT + inter-container forwarding ON REBOOT (nftables loads default-deny before dockerd). This drop-in (loaded via base's /etc/nftables.d/*.nft include at boot) appends the container-bridge accepts so a rebooted Docker host keeps forwarding. Resolves FRICTION 2026-06-17 #1 and the GREEN half of ADR-025's acceptance test. NB nftables wildcard is br-*, not the iptables br-+. Co-Authored-By: Claude Opus 4.8 (1M context) --- roles/docker_host/defaults/main.yml | 12 ++++++++++-- roles/docker_host/tasks/main.yml | 19 +++++++++++++++++++ .../templates/10-docker-forward.nft.j2 | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 roles/docker_host/templates/10-docker-forward.nft.j2 diff --git a/roles/docker_host/defaults/main.yml b/roles/docker_host/defaults/main.yml index 0e22ea5..50a84a6 100644 --- a/roles/docker_host/defaults/main.yml +++ b/roles/docker_host/defaults/main.yml @@ -1,8 +1,16 @@ --- -# Docker engine install (ADR-004). Cluster-specific daemon hardening + nftables.d -# integration are deferred to when the cluster + host firewall exist. +# Docker engine install (ADR-004). Cluster-specific daemon hardening is deferred to when +# the cluster exists. docker_host__packages: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin + +# Container-forward nftables drop-in (FRICTION 2026-06-17 #1 / ADR-025). base's inet-filter +# forward chain is `policy drop`; on a Docker host that kills published-port DNAT + inter- +# container forwarding ON REBOOT (nftables loads default-deny before dockerd). This drop-in +# (loaded via base's /etc/nftables.d/*.nft include) appends the accepts so a rebooted Docker +# host keeps forwarding. Only meaningful where base__firewall_apply is true. +docker_host__forward_dropin: true +docker_host__nftables_dropin_dir: /etc/nftables.d # must match base__firewall_dropin_dir diff --git a/roles/docker_host/tasks/main.yml b/roles/docker_host/tasks/main.yml index 44f04f5..a91d333 100644 --- a/roles/docker_host/tasks/main.yml +++ b/roles/docker_host/tasks/main.yml @@ -37,3 +37,22 @@ state: present update_cache: true tags: [packages] + +- name: Ensure the nftables drop-in dir exists (for the container-forward rules) + ansible.builtin.file: + path: "{{ docker_host__nftables_dropin_dir }}" + state: directory + mode: "0755" + when: docker_host__forward_dropin | bool + tags: [firewall] + +- name: Install the container-forward nftables drop-in (reboot-safe Docker forwarding) + ansible.builtin.template: + src: 10-docker-forward.nft.j2 + dest: "{{ docker_host__nftables_dropin_dir }}/10-docker-forward.nft" + mode: "0644" + when: docker_host__forward_dropin | bool + # Not reloaded here: a running host already forwards via Docker's runtime rules, so the + # drop-in only needs to protect the NEXT boot (loaded by nftables.service). Reloading nft + # now would flush Docker's NAT (FRICTION 2026-06-17 #4); the boot loads it cleanly. + tags: [firewall] diff --git a/roles/docker_host/templates/10-docker-forward.nft.j2 b/roles/docker_host/templates/10-docker-forward.nft.j2 new file mode 100644 index 0000000..9190cea --- /dev/null +++ b/roles/docker_host/templates/10-docker-forward.nft.j2 @@ -0,0 +1,14 @@ +# {{ ansible_managed }} +# Allow container forwarding through base's default-deny forward chain (ADR-025 / FRICTION +# 2026-06-17 #1). Appended to base's `table inet filter` / `chain forward` via the +# /etc/nftables.d/*.nft include, and loaded by nftables.service at boot — exactly when the +# bug bit (default-deny forward loading before dockerd on reboot). +table inet filter { + chain forward { + ct state established,related accept + iifname "docker0" accept + oifname "docker0" accept + iifname "br-*" accept + oifname "br-*" accept + } +}