Compare commits

...
Author SHA1 Message Date
dev_agentandClaude Opus 5.5 5a1ee389dc Keep MealQuest firewall rules first and off docker.service
CI / ci (push) Successful in 1m33s
CI / ci (pull_request) Successful in 1m33s
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 <noreply@anthropic.com>
2026-10-05 17:47:36 -04:00
dev_agentandClaude Opus 5.5 63808ccd20 Add MealQuest host firewall unit for port 8090
CI / ci (pull_request) Successful in 1m33s
CI / ci (push) Successful in 1m33s
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 <noreply@anthropic.com>
2026-10-05 17:35:39 -04:00
dev_agent 3b524ec118 Compose stack and env template (#54)
CI / ci (push) Successful in 1m46s
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
2026-10-05 08:08:16 -04:00
5 changed files with 194 additions and 0 deletions
+2
View File
@@ -3,6 +3,8 @@ design/.DS_Store
# Secrets and local environment
.env*
!deploy/.env.example
deploy/.env
deploy/secrets/
# Build artifacts and dependencies
+80
View File
@@ -0,0 +1,80 @@
# MealQuest production environment. Copy to deploy/.env (never committed)
# and replace the placeholders. The service fails fast: a missing or invalid
# required variable stops it at boot with the variable named.
#
# Secrets are never set here. They live in deploy/secrets/ (never committed),
# one value per file, owned by the container user and readable only by it:
#
# mkdir -p deploy/secrets && chmod 700 deploy/secrets
# head -c 32 /dev/urandom | base64 > deploy/secrets/secret_key
# printf '%s\n' '<Authentik client secret>' > deploy/secrets/oidc_client_secret
# sudo chown 10001:10001 deploy/secrets/* && sudo chmod 600 deploy/secrets/*
#
# deploy/compose.yaml mounts them at /run/secrets/<name> and points the
# *_FILE variables at them.
# --- Compose -----------------------------------------------------------------
# Host address the service is published on, as <address>:8090. Production
# binds the LAN address 10.60.1.14 only; 127.0.0.1 suits a dev machine.
MEALQUEST_BIND_ADDRESS=127.0.0.1
# --- Service -----------------------------------------------------------------
# Public origin the household opens. Fixes the OIDC redirect URI
# (<url>/auth/callback) and the Secure cookies. Must be https:// (plain
# http:// is accepted only for localhost, 127.0.0.1 and [::1]).
MEALQUEST_PUBLIC_URL=https://mealquest.cubecraftlabs.com
# How long a graceful shutdown may take before in-flight requests are cut.
MEALQUEST_SHUTDOWN_TIMEOUT=10s
# Optional. Comma-separated IPs/CIDRs of reverse proxies whose
# X-Forwarded-For/-Proto headers are honoured. Production lists Traefik;
# unset trusts no peer.
MEALQUEST_TRUSTED_PROXY=10.60.1.33
# IANA zone the household settings are created with on first boot. Required
# until those settings exist; ignored afterwards (change the zone in
# household settings instead). Confirm the household's zone before the first
# deploy.
MEALQUEST_DEFAULT_TZ=America/New_York
# --- Authentik OIDC ----------------------------------------------------------
# Issuer of the Authentik application "mealquest". Discovered at boot, so it
# must be reachable from the container. Must be https://.
MEALQUEST_OIDC_ISSUER=https://auth.cubecraftlabs.com/application/o/mealquest/
MEALQUEST_OIDC_CLIENT_ID=mealquest
# Comma-separated Authentik groups whose members may sign in.
MEALQUEST_OIDC_ALLOWED_GROUPS=mealquest-admin
# Optional session lifetimes. A session ends after IDLE unused and always MAX
# after sign-in; MAX must be at least IDLE. Defaults: 720h and 4320h.
MEALQUEST_SESSION_IDLE=720h
MEALQUEST_SESSION_MAX=4320h
# --- Set by deploy/compose.yaml; do not set here -----------------------------
#
# MEALQUEST_HTTP_ADDR=:8080
# MEALQUEST_DATA_DIR=/data
# MEALQUEST_SECRET_KEY_FILE=/run/secrets/secret_key
# Base64 of 32 random bytes; encrypts secrets at rest (AES-256-GCM).
# MEALQUEST_OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret
# The Authentik provider's client secret.
# --- Planned; not read by the service yet -------------------------------------
#
# Added to deploy/compose.yaml and this file by the features that use them.
#
# Web Push (Feature 19). Generate the key pair with cmd/vapidgen.
# MEALQUEST_VAPID_PUBLIC_KEY=<base64url VAPID public key>
# MEALQUEST_VAPID_PRIVATE_KEY_FILE=/run/secrets/vapid_private_key
# MEALQUEST_VAPID_SUBJECT=mailto:<owner address>
#
# Google Calendar OAuth web client (Feature 16).
# MEALQUEST_GOOGLE_CLIENT_ID=<client id>.apps.googleusercontent.com
# MEALQUEST_GOOGLE_CLIENT_SECRET_FILE=/run/secrets/google_client_secret
#
# SearXNG recipe search sidecar on the compose network (Feature 07).
# MEALQUEST_SEARXNG_URL=http://searxng:8080
+61
View File
@@ -0,0 +1,61 @@
# Production stack for MealQuest on 10.60.1.14 (D-018). Build and start on
# the host with:
#
# docker compose -f deploy/compose.yaml up -d --build
#
# Configuration comes from deploy/.env (copy deploy/.env.example) and secrets
# from deploy/secrets/; neither is committed. Compose bind-mounts file secrets
# with their host owner and mode, so each file in deploy/secrets/ must be
# owned by the container user 10001:10001 with mode 0600.
name: mealquest
services:
mealquest:
build:
context: ..
dockerfile: Dockerfile
user: "10001:10001"
restart: unless-stopped
env_file:
- path: .env
required: true
# Fixed by this file's container shape (listen port, volume, secret
# mounts); deploy/.env must not set them.
environment:
MEALQUEST_HTTP_ADDR: ":8080"
MEALQUEST_DATA_DIR: /data
MEALQUEST_SECRET_KEY_FILE: /run/secrets/secret_key
MEALQUEST_OIDC_CLIENT_SECRET_FILE: /run/secrets/oidc_client_secret
secrets:
- secret_key
- oidc_client_secret
volumes:
- mealquest-data:/data
ports:
- "${MEALQUEST_BIND_ADDRESS:?set MEALQUEST_BIND_ADDRESS in deploy/.env}:8090:8080"
networks:
- mealquest
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 3s
start_period: 10s
retries: 3
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
secrets:
secret_key:
file: ./secrets/secret_key
oidc_client_secret:
file: ./secrets/oidc_client_secret
volumes:
mealquest-data:
name: mealquest-data
networks:
mealquest:
name: mealquest
+22
View File
@@ -0,0 +1,22 @@
# 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
RemainAfterExit=yes
ExecStart=/usr/local/sbin/mealquest-firewall
[Install]
WantedBy=multi-user.target docker.service
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
# 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 there is the
# container address and port, not the published ones. Match the original
# destination address and port with conntrack instead of -d/--dport.
#
# 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
tag="-m comment --comment mealquest-firewall"
match="-m conntrack --ctorigdst 10.60.1.14 --ctorigdstport 8090 --ctdir ORIGINAL"
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