diff --git a/ansible/roles/podman/defaults/main.yml b/ansible/roles/podman/defaults/main.yml index d212892..ba18998 100644 --- a/ansible/roles/podman/defaults/main.yml +++ b/ansible/roles/podman/defaults/main.yml @@ -27,6 +27,12 @@ hass_path: "{{ podman_volumes }}/hass" partsy_path: "{{ podman_volumes }}/partsy" partsy_skudak_path: "{{ podman_volumes }}/partsy-skudak" photos_path: "{{ podman_volumes }}/photos" +# rsvp.debyl.io -- invite-only RSVP app (~/src/rsvp-debylio). Both listeners are +# published on loopback only: Caddy proxies the public one, and the admin one +# only for LAN clients. Caddy runs with network: host, so 127.0.0.1 reaches them. +rsvp_path: "{{ podman_volumes }}/rsvp" +rsvp_public_port: 9080 +rsvp_admin_port: 9081 # Named rather than hardcoded so the ML service can be stood up beside a broken # one without editing tasks. On 2026-08-28 a worker thread wedged in # uninterruptible sleep (D state) in exit_mmap, which made the container @@ -185,6 +191,7 @@ home_server_name_io: home.debyl.io parts_server_name_io: parts.debyl.io photos_server_name_io: photos.debyl.io gitea_debyl_server_name: git.debyl.io +rsvp_server_name: rsvp.debyl.io # skudak.com domains (migration from skudakrennsport.com) parts_skudak_server_name: parts.skudak.com diff --git a/ansible/roles/podman/tasks/containers/home/rsvp.yml b/ansible/roles/podman/tasks/containers/home/rsvp.yml new file mode 100644 index 0000000..1f52003 --- /dev/null +++ b/ansible/roles/podman/tasks/containers/home/rsvp.yml @@ -0,0 +1,51 @@ +--- +# The image runs as uid 10001 (scratch, no passwd file). Rootless podman maps +# container uid N to host uid subuid_start + N - 1, so own the directory as +# that host uid directly. Setting the podman user here and chowning back +# afterwards would flip ownership on every deploy and briefly lock the running +# app out of its own database directory. +- name: create rsvp host directory volumes + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ podman_subuid.stdout | int + 10000 }}" + group: "{{ podman_subuid.stdout | int + 10000 }}" + mode: 0750 + notify: restorecon podman + loop: + - "{{ rsvp_path }}/data" + +- name: flush handlers + ansible.builtin.meta: flush_handlers + +- import_tasks: podman/podman-check.yml + vars: + container_name: rsvp + container_image: "{{ image }}" + +- name: create rsvp container + become: true + become_user: "{{ podman_user }}" + containers.podman.podman_container: + name: rsvp + image: "{{ image }}" + restart_policy: on-failure:3 + log_driver: journald + env: + RSVP_DB_PATH: /data/rsvp.db + RSVP_BASE_URL: "https://{{ rsvp_server_name }}" + RSVP_TZ: America/New_York + # Client IPs for the invite-link miss limiter come from Caddy's + # X-Forwarded-For. Safe only because both ports are loopback-only. + RSVP_TRUST_FORWARDED: "1" + volumes: + - "{{ rsvp_path }}/data:/data" + ports: + - "127.0.0.1:{{ rsvp_public_port }}:8080" + - "127.0.0.1:{{ rsvp_admin_port }}:8081" + +- name: create systemd startup job for rsvp + include_tasks: podman/systemd-generate.yml + vars: + container_name: rsvp diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index 8e513ce..66fc4ee 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -126,6 +126,13 @@ image: localhost/greg-time-bot:3.17.3 tags: gregtime +# Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to +# the VERSION it loaded. The Caddy vhost ships with the caddy-config tag. +- import_tasks: containers/home/rsvp.yml + vars: + image: localhost/rsvpd:1.0.1 + tags: rsvp + # Gated on zomboid_enabled (roles/podman/defaults/main.yml) so it can be taken # down for a CI-heavy stretch without losing the world. - import_tasks: containers/home/zomboid.yml diff --git a/ansible/roles/podman/templates/caddy/Caddyfile.j2 b/ansible/roles/podman/templates/caddy/Caddyfile.j2 index ebb0501..891f65d 100644 --- a/ansible/roles/podman/templates/caddy/Caddyfile.j2 +++ b/ansible/roles/podman/templates/caddy/Caddyfile.j2 @@ -23,6 +23,26 @@ } format {{ caddy_log_format }} level {{ caddy_log_level }} + # See rsvp-errors below. + exclude http.log.error.rsvp + } + + # {{ rsvp_server_name }}: when a proxied request fails, Caddy's error log + # records the raw request URI -- which for /i/ is a working invite + # link. Send those through the same redaction as the site's access log + # instead of the default log above. + log rsvp-errors { + output file /var/log/caddy/rsvp.log { + roll_size {{ caddy_log_roll_size }} + roll_keep {{ caddy_log_roll_keep }} + roll_keep_for {{ caddy_log_roll_keep_for }} + } + format filter { + wrap json + request>uri regexp ^/i/[^/?#]+ /i/REDACTED + request>headers>Referer delete + } + include http.log.error.rsvp } } @@ -193,6 +213,56 @@ } } +# RSVP - {{ rsvp_server_name }} (public invite links, LAN-only /admin) +# +# Does NOT import common_headers: its Referrer-Policy "same-origin" would +# replace the app's "no-referrer", and invite tokens live in the URL path. +{{ rsvp_server_name }} { + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Robots-Tag "noindex, nofollow" + Referrer-Policy "no-referrer" + Content-Security-Policy "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'" + X-Content-Type-Options "nosniff" + -Server + } + + # Admin is a second listener with no login. It is only published on + # loopback, and only proxied here for LAN clients -- two independent controls. + @admin_local { + path /admin /admin/* + remote_ip {{ caddy_local_networks | join(' ') }} + } + handle @admin_local { + reverse_proxy 127.0.0.1:{{ rsvp_admin_port }} + } + handle /admin* { + respond "Not found" 404 + } + + handle { + reverse_proxy 127.0.0.1:{{ rsvp_public_port }} + } + + # The invite token is the credential and it is in the URL path, so redact + # it from the request URI and drop the redirect Location (POSTs 303 back to + # /i/) before anything is written. Named "rsvp" so its error logger + # (http.log.error.rsvp) can be redirected in the global options. + log rsvp { + output file /var/log/caddy/rsvp.log { + roll_size {{ caddy_log_roll_size }} + roll_keep {{ caddy_log_roll_keep }} + roll_keep_for {{ caddy_log_roll_keep_for }} + } + format filter { + wrap json + request>uri regexp ^/i/[^/?#]+ /i/REDACTED + request>headers>Referer delete + resp_headers>Location delete + } + } +} + # Uptime Kuma (Debyltech) - {{ uptime_kuma_server_name }} {{ uptime_kuma_server_name }} { {{ ip_restricted_site() }}