All checks were successful
Deploy Fly.io Proxy / deploy (push) Successful in 1m12s
## Summary Brings up the Adelaide / Heidi / Addie baby shower app on ringtail k3s with the public/private split that the app's hosting contract calls for: `shower.eblu.me` (public, via Fly proxy) and `shower.ops.eblu.me` (tailnet). App is consumed as a wheel from the Forgejo PyPI index — source lives at [`adelaide-baby-shower-app`](https://forge.eblu.me/eblume/adelaide-baby-shower-app). ### What's included - **ArgoCD app + manifests** under `argocd/manifests/shower/` (deployment, service, ProxyGroup ingress, ConfigMap for `DJANGO_DEBUG`/`DJANGO_ADMIN_URL`, ExternalSecret for `DJANGO_SECRET_KEY` from 1Password item `Shower (blumeops)`, NFS PV on sifaka, RWX media PVC, RWO local-path data PVC for SQLite). Recreate rollout because SQLite is single-writer. - **Public surface** (`fly/`): new `shower.eblu.me` server block proxying to `shower.ops.eblu.me`. `/admin/` returns 403 at the edge except `/admin/login/` and `/admin/logout/`, which are rate-limited via a new `shower_auth` zone. `X-Clacks-Overhead` on. GNU Terry Pratchett. - **fail2ban** filter (`shower-admin-login.conf`) matching 401/403/429 on `/admin/login/` and jail (`shower.conf`) with `maxretry=5/findtime=600/bantime=3600`. The `nginx-deny` action was generalized to take a per-jail `nginx_deny_file` so the shower has its own deny list (forge keeps using the legacy default). - **Caddy** route on indri (`shower.ops.eblu.me` → `https://shower.tail8d86e.ts.net`). - **Pulumi** Gandi CNAME `shower.eblu.me → blumeops-proxy.fly.dev.`. - **Grafana** APM dashboard `configmap-shower-apm.yaml` (request rate, error rate, failed admin login count, latency percentiles, bandwidth, access logs) mirroring `docs-apm.json` with a `host="shower.eblu.me"` filter. - **Container** `containers/shower/default.nix` — `dockerTools.buildLayeredImage` with a nixpkgs Python and a startup wrapper that creates `/app/data/.venv`, pip-installs `adelaide-baby-shower-app==1.0.0` from the forge PyPI index on first boot, runs migrations + collectstatic, and execs gunicorn. A `local_settings.py` shim pins `DATABASES.NAME`/`MEDIA_ROOT`/`STATIC_ROOT` to absolute paths so they don't end up in site-packages. - **Docs** runbook at `docs/how-to/operations/shower-app.md` linked from the apps registry, plus changelog fragments. ### Defense layers on the public surface 1. fly nginx geo+fail2ban `$shower_banned` (per-service deny list) 2. fly nginx `limit_req zone=shower_auth` (3 r/s per Fly-Client-IP) 3. django-axes (5 fails / 1h, keyed on username+ip_address) 4. edge `/admin/` block (returns 403 for anything that isn't login/logout) ## Prerequisites for the user to do (NOT in this PR) Halted on these per request — they touch shared/manual systems: - [x] **NFS share** on sifaka: `/volume1/shower`, NFS rule for ringtail RW, `chown 1000:1000` - [ ] **1Password item** `Shower (blumeops)` in the blumeops vault with a freshly minted `secret-key` field (`openssl rand -base64 48`) — do NOT reuse anything that has lived in git - [ ] **Container build**: `mise run container-build-and-release shower`, then update `images[].newTag` in `argocd/manifests/shower/kustomization.yaml` to the resulting `v1.0.0-<sha>-nix` - [x] **DNS**: `mise run dns-up` after merge - [x] **Fly cert**: `fly certs add shower.eblu.me -a blumeops-proxy` - [ ] **Caddy push**: `mise run provision-indri -- --tags caddy` - [ ] **Fly redeploy** to pick up the new nginx block + fail2ban jail: `mise run fly-deploy` - [ ] **ArgoCD sync**: `argocd app set shower --revision shower-app-deploy && argocd app sync shower` to test from this branch before merging ## Test plan - [ ] Container builds successfully on nix-container-builder runner - [ ] Pod starts, migrations run, gunicorn answers on :8000 - [ ] `kubectl --context=k3s-ringtail -n shower logs deploy/shower` clean - [ ] `curl -sf https://shower.ops.eblu.me/` returns the splash page (tailnet) - [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/` returns 200 (pre-DNS verification) - [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/admin/users/` returns 403 (edge block) - [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/admin/login/` returns a Django login response - [ ] After DNS is up: `curl -I https://shower.eblu.me/` returns 200 with `X-Clacks-Overhead` - [ ] Grafana dashboard "Shower APM" appears and starts showing traffic - [ ] `mise run services-check` passes Reviewed-on: #349
123 lines
4 KiB
YAML
123 lines
4 KiB
YAML
---
|
|
# Caddy reverse proxy configuration
|
|
# Caddy is built from ~/code/3rd/caddy with Gandi DNS and Layer 4 plugins
|
|
|
|
caddy_repo_dir: /Users/erichblume/code/3rd/caddy
|
|
caddy_binary: "{{ caddy_repo_dir }}/bin/caddy"
|
|
caddy_config_dir: /Users/erichblume/.config/caddy
|
|
caddy_data_dir: /Users/erichblume/.local/share/caddy
|
|
caddy_log_dir: /Users/erichblume/Library/Logs
|
|
|
|
# Gandi API token file (written by ansible, chmod 0600)
|
|
# Caddy reads this file for ACME DNS-01 challenges
|
|
caddy_gandi_token_file: /Users/erichblume/.config/caddy/gandi-token
|
|
|
|
# Domain configuration
|
|
caddy_domain: ops.eblu.me
|
|
|
|
# HTTPS port (443 is standard)
|
|
caddy_https_port: 443
|
|
|
|
# Services to proxy
|
|
# Format: { name: "service", host: "hostname", backend: "url" }
|
|
caddy_services:
|
|
# Indri-local services
|
|
- name: forge
|
|
host: "forge.{{ caddy_domain }}"
|
|
backend: "http://localhost:3001"
|
|
- name: registry
|
|
host: "registry.{{ caddy_domain }}"
|
|
backend: "http://localhost:5050"
|
|
- name: jellyfin
|
|
host: "jellyfin.{{ caddy_domain }}"
|
|
backend: "http://localhost:8096"
|
|
|
|
# K8s services (via Tailscale Ingress)
|
|
# Caddy proxies to existing Tailscale endpoints - traffic stays local
|
|
- name: grafana
|
|
host: "grafana.{{ caddy_domain }}"
|
|
backend: "https://grafana.tail8d86e.ts.net"
|
|
- name: argocd
|
|
host: "argocd.{{ caddy_domain }}"
|
|
backend: "https://argocd.tail8d86e.ts.net"
|
|
- name: prometheus
|
|
host: "prometheus.{{ caddy_domain }}"
|
|
backend: "https://prometheus.tail8d86e.ts.net"
|
|
- name: loki
|
|
host: "loki.{{ caddy_domain }}"
|
|
backend: "https://loki.tail8d86e.ts.net"
|
|
- name: miniflux
|
|
host: "feed.{{ caddy_domain }}"
|
|
backend: "https://feed.tail8d86e.ts.net"
|
|
- name: devpi
|
|
host: "pypi.{{ caddy_domain }}"
|
|
backend: "http://localhost:3141"
|
|
- name: kiwix
|
|
host: "kiwix.{{ caddy_domain }}"
|
|
backend: "https://kiwix.tail8d86e.ts.net"
|
|
- name: torrent
|
|
host: "torrent.{{ caddy_domain }}"
|
|
backend: "https://torrent.tail8d86e.ts.net"
|
|
- name: teslamate
|
|
host: "tesla.{{ caddy_domain }}"
|
|
backend: "https://tesla.tail8d86e.ts.net"
|
|
- name: immich
|
|
host: "photos.{{ caddy_domain }}"
|
|
backend: "https://photos.tail8d86e.ts.net"
|
|
- name: navidrome
|
|
host: "dj.{{ caddy_domain }}"
|
|
backend: "https://dj.tail8d86e.ts.net"
|
|
- name: homepage
|
|
host: "go.{{ caddy_domain }}"
|
|
backend: "https://go.tail8d86e.ts.net"
|
|
- name: docs
|
|
host: "docs.{{ caddy_domain }}"
|
|
kind: static
|
|
root: "{{ docs_content_dir }}"
|
|
try_html: true # Quartz: path → path/ → path.html → 404.html
|
|
- name: cv
|
|
host: "cv.{{ caddy_domain }}"
|
|
kind: static
|
|
root: "{{ cv_content_dir }}"
|
|
download_paths:
|
|
- path: /resume.pdf
|
|
filename: erich-blume-resume.pdf
|
|
- name: nvr
|
|
host: "nvr.{{ caddy_domain }}"
|
|
backend: "https://nvr.tail8d86e.ts.net"
|
|
- name: authentik
|
|
host: "authentik.{{ caddy_domain }}"
|
|
backend: "https://authentik.tail8d86e.ts.net"
|
|
cache_policy: spa
|
|
- name: ntfy
|
|
host: "ntfy.{{ caddy_domain }}"
|
|
backend: "https://ntfy.tail8d86e.ts.net"
|
|
- name: ollama
|
|
host: "ollama.{{ caddy_domain }}"
|
|
backend: "https://ollama.tail8d86e.ts.net"
|
|
- name: mealie
|
|
host: "meals.{{ caddy_domain }}"
|
|
backend: "https://meals.tail8d86e.ts.net"
|
|
- name: paperless
|
|
host: "paperless.{{ caddy_domain }}"
|
|
backend: "https://paperless.tail8d86e.ts.net"
|
|
- name: shower
|
|
host: "shower.{{ caddy_domain }}"
|
|
backend: "https://shower.tail8d86e.ts.net"
|
|
- name: sifaka
|
|
host: "nas.{{ caddy_domain }}"
|
|
backend: "http://sifaka:5000"
|
|
|
|
# Layer 4 (TCP) services
|
|
# Format: { port: external_port, backend: "host:port" }
|
|
caddy_tcp_services:
|
|
- port: 2222
|
|
backend: "localhost:2200" # Forgejo SSH
|
|
- port: 5432
|
|
backend: "pg.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg)
|
|
- port: 5433
|
|
backend: "immich-pg.tail8d86e.ts.net:5432" # PostgreSQL (immich-pg)
|
|
- port: "{{ sifaka_node_exporter_port }}"
|
|
backend: "sifaka:{{ sifaka_node_exporter_port }}" # Sifaka node_exporter
|
|
- port: "{{ sifaka_smartctl_exporter_port }}"
|
|
backend: "sifaka:{{ sifaka_smartctl_exporter_port }}" # Sifaka smartctl_exporter
|