From 63808ccd20e1e6bc2401eb4280d9fd466146d68f Mon Sep 17 00:00:00 2001 From: dev_agent Date: Mon, 5 Oct 2026 17:35:39 -0400 Subject: [PATCH 1/2] Add MealQuest host firewall unit for port 8090 Restrict the published MealQuest port 8090 in DOCKER-USER to Traefik (10.60.1.33) and the Docker host (10.60.1.14), dropping all other sources. DOCKER-USER sees post-DNAT traffic, so rules match the original destination port with conntrack rather than --dport. A docker.service drop-in re-applies the rules after every docker restart. Co-Authored-By: Claude Opus 5.5 --- .../docker.service.d/mealquest-firewall.conf | 3 +++ deploy/systemd/mealquest-firewall.service | 12 +++++++++++ deploy/systemd/mealquest-firewall.sh | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 deploy/systemd/docker.service.d/mealquest-firewall.conf create mode 100644 deploy/systemd/mealquest-firewall.service create mode 100755 deploy/systemd/mealquest-firewall.sh diff --git a/deploy/systemd/docker.service.d/mealquest-firewall.conf b/deploy/systemd/docker.service.d/mealquest-firewall.conf new file mode 100644 index 0000000..afe3355 --- /dev/null +++ b/deploy/systemd/docker.service.d/mealquest-firewall.conf @@ -0,0 +1,3 @@ +# Re-apply the MealQuest DOCKER-USER rules after every docker (re)start. +[Service] +ExecStartPost=/usr/local/sbin/mealquest-firewall diff --git a/deploy/systemd/mealquest-firewall.service b/deploy/systemd/mealquest-firewall.service new file mode 100644 index 0000000..fd98254 --- /dev/null +++ b/deploy/systemd/mealquest-firewall.service @@ -0,0 +1,12 @@ +[Unit] +Description=Restrict MealQuest port 8090 to Traefik (DOCKER-USER) +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/sbin/mealquest-firewall + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/mealquest-firewall.sh b/deploy/systemd/mealquest-firewall.sh new file mode 100755 index 0000000..ca0ff66 --- /dev/null +++ b/deploy/systemd/mealquest-firewall.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Restrict the MealQuest published host port to Traefik (10.60.1.33) and the +# Docker host itself (10.60.1.14); drop every other source. +# +# DOCKER-USER runs after Docker's DNAT, so the destination port there is the +# container port (8080), not the published one. Match the original +# destination port with conntrack instead of --dport. +# +# Idempotent: each rule is checked (-C) before it is added, so re-runs add +# nothing. Installed as /usr/local/sbin/mealquest-firewall. +set -eu + +port=8090 +match="-p tcp -m conntrack --ctorigdstport $port --ctdir ORIGINAL" + +iptables -C DOCKER-USER -s 10.60.1.33/32 $match -j RETURN 2>/dev/null || + iptables -I DOCKER-USER 1 -s 10.60.1.33/32 $match -j RETURN +iptables -C DOCKER-USER -s 10.60.1.14/32 $match -j RETURN 2>/dev/null || + iptables -I DOCKER-USER 2 -s 10.60.1.14/32 $match -j RETURN +iptables -C DOCKER-USER $match -j DROP 2>/dev/null || + iptables -A DOCKER-USER $match -j DROP -- 2.55.0 From 5a1ee389dc1ee949744530c48e842f4a019e933f Mon Sep 17 00:00:00 2001 From: dev_agent Date: Mon, 5 Oct 2026 17:47:36 -0400 Subject: [PATCH 2/2] Keep MealQuest firewall rules first and off docker.service Address review of the port 8090 firewall: - Insert the three MealQuest rules at the top of DOCKER-USER, ahead of other projects' rules. Each run deletes every rule tagged "mealquest-firewall" and re-inserts them at positions 1-3, so re-runs restore the order and never duplicate. - Scope the match to MealQuest's published address with --ctorigdst 10.60.1.14, so outbound container connections to a remote :8090 are not matched. - Drop the docker.service ExecStartPost drop-in, which made a firewall error fatal to the shared Docker daemon. The unit is now PartOf= and WantedBy= docker.service: it re-runs on every Docker start or restart and fails visibly on its own without failing docker.service. Co-Authored-By: Claude Opus 5.5 --- .../docker.service.d/mealquest-firewall.conf | 3 -- deploy/systemd/mealquest-firewall.service | 12 +++++- deploy/systemd/mealquest-firewall.sh | 38 +++++++++++-------- 3 files changed, 34 insertions(+), 19 deletions(-) delete mode 100644 deploy/systemd/docker.service.d/mealquest-firewall.conf diff --git a/deploy/systemd/docker.service.d/mealquest-firewall.conf b/deploy/systemd/docker.service.d/mealquest-firewall.conf deleted file mode 100644 index afe3355..0000000 --- a/deploy/systemd/docker.service.d/mealquest-firewall.conf +++ /dev/null @@ -1,3 +0,0 @@ -# Re-apply the MealQuest DOCKER-USER rules after every docker (re)start. -[Service] -ExecStartPost=/usr/local/sbin/mealquest-firewall diff --git a/deploy/systemd/mealquest-firewall.service b/deploy/systemd/mealquest-firewall.service index fd98254..c7c5f8e 100644 --- a/deploy/systemd/mealquest-firewall.service +++ b/deploy/systemd/mealquest-firewall.service @@ -1,7 +1,17 @@ +# Applies the MealQuest DOCKER-USER rules (/usr/local/sbin/mealquest-firewall). +# +# PartOf= re-runs this unit whenever docker.service is stopped or restarted, +# and WantedBy=docker.service starts it whenever docker.service starts. Both +# are one-way: a failure here fails this unit visibly and never fails +# docker.service. +# +# Start MealQuest only after this unit has succeeded: +# systemctl start mealquest-firewall && docker compose up -d [Unit] Description=Restrict MealQuest port 8090 to Traefik (DOCKER-USER) After=docker.service Requires=docker.service +PartOf=docker.service [Service] Type=oneshot @@ -9,4 +19,4 @@ RemainAfterExit=yes ExecStart=/usr/local/sbin/mealquest-firewall [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target docker.service diff --git a/deploy/systemd/mealquest-firewall.sh b/deploy/systemd/mealquest-firewall.sh index ca0ff66..361668c 100755 --- a/deploy/systemd/mealquest-firewall.sh +++ b/deploy/systemd/mealquest-firewall.sh @@ -1,21 +1,29 @@ #!/bin/sh -# Restrict the MealQuest published host port to Traefik (10.60.1.33) and the -# Docker host itself (10.60.1.14); drop every other source. +# Restrict the MealQuest published host port (10.60.1.14:8090) to Traefik +# (10.60.1.33) and the Docker host itself (10.60.1.14); drop every other +# source. # -# DOCKER-USER runs after Docker's DNAT, so the destination port there is the -# container port (8080), not the published one. Match the original -# destination port with conntrack instead of --dport. +# DOCKER-USER runs after Docker's DNAT, so the destination there is the +# container address and port, not the published ones. Match the original +# destination address and port with conntrack instead of -d/--dport. # -# Idempotent: each rule is checked (-C) before it is added, so re-runs add -# nothing. Installed as /usr/local/sbin/mealquest-firewall. +# The rules must sit at the top of DOCKER-USER, ahead of any other project's +# rules (an earlier RETURN or DROP would decide MealQuest traffic first). +# Every run deletes all rules tagged with the mealquest-firewall comment and +# re-inserts them at positions 1-3, so re-runs leave the same ordered rules +# and no duplicates. Installed as /usr/local/sbin/mealquest-firewall. set -eu -port=8090 -match="-p tcp -m conntrack --ctorigdstport $port --ctdir ORIGINAL" +tag="-m comment --comment mealquest-firewall" +match="-m conntrack --ctorigdst 10.60.1.14 --ctorigdstport 8090 --ctdir ORIGINAL" -iptables -C DOCKER-USER -s 10.60.1.33/32 $match -j RETURN 2>/dev/null || - iptables -I DOCKER-USER 1 -s 10.60.1.33/32 $match -j RETURN -iptables -C DOCKER-USER -s 10.60.1.14/32 $match -j RETURN 2>/dev/null || - iptables -I DOCKER-USER 2 -s 10.60.1.14/32 $match -j RETURN -iptables -C DOCKER-USER $match -j DROP 2>/dev/null || - iptables -A DOCKER-USER $match -j DROP +rules=$(iptables -w -S DOCKER-USER) +printf '%s\n' "$rules" | while read -r _ chain spec; do + case " $spec " in + *" --comment mealquest-firewall "*) iptables -w -D "$chain" $spec ;; + esac +done + +iptables -w -I DOCKER-USER 1 -s 10.60.1.33/32 $match $tag -j RETURN +iptables -w -I DOCKER-USER 2 -s 10.60.1.14/32 $match $tag -j RETURN +iptables -w -I DOCKER-USER 3 $match $tag -j DROP -- 2.55.0