diff --git a/.env.example b/.env.example index 72a8e46..f10db6e 100644 --- a/.env.example +++ b/.env.example @@ -49,3 +49,10 @@ SMTP_USER= SMTP_PASSWORD= SMTP_FROM=Beckham Share SMTP_USE_TLS=true + +# The relay's certificate is valid for the hostname, but its public DNS record +# points at a pool member this network can't reach, so docker-compose.yml pins +# the name to an address that answers. Change these when the pool changes — +# docs/OPERATIONS.md has the procedure. Read by Compose, not by the app. +SMTP_RELAY_HOST=mail.eigbox.net +SMTP_RELAY_IP=66.96.134.48 diff --git a/CHANGELOG.md b/CHANGELOG.md index 51628ff..c1dcf7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ pre-1.0 scheme; dates are when the change reached `main`. - Template rendering goes through a small `render()`/`error_page()` helper, which also moves the app onto Starlette's current `TemplateResponse` signature. +- The SMTP relay's pinned address is now set by `SMTP_RELAY_HOST` / + `SMTP_RELAY_IP` instead of being hard-coded in `docker-compose.yml`, so it can + be corrected from `.env` when the provider's pool changes. Added a runbook + entry for diagnosing it, since the failure looks like nothing else breaking. ### Security - Updated dependencies to clear published advisories against the pinned diff --git a/docker-compose.yml b/docker-compose.yml index acce2eb..2c7dc30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,10 +22,12 @@ services: environment: DATABASE_URL: postgresql+psycopg2://share:${DB_PASSWORD}@db:5432/share extra_hosts: - # The SMTP relay presents a shared *.eigbox.net certificate; map the - # cert-valid hostname to its IP so STARTTLS verification succeeds when - # sending share-by-email. (Set SMTP_HOST=mail.eigbox.net in .env.) - - "mail.eigbox.net:66.96.134.48" + # The SMTP relay presents a shared *.eigbox.net certificate, so we have to + # connect by that hostname for STARTTLS verification to succeed — but its + # public DNS record points at a pool member this network cannot reach. + # Pin the name to an address that answers. Override both halves from .env + # when the pool changes; see docs/OPERATIONS.md for how to find a live one. + - "${SMTP_RELAY_HOST:-mail.eigbox.net}:${SMTP_RELAY_IP:-66.96.134.48}" volumes: - share-data:/data networks: [idp_proxy, internal] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 06e07bf..24103f4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -81,9 +81,21 @@ and the server-side relay (members only) returns a "not configured" response. | `SMTP_FROM` | `Beckham Share ` | `From` header on outgoing mail. | | `SMTP_USE_TLS` | `true` | Use STARTTLS before authenticating. | -> The relay used in production presents a shared `*.eigbox.net` certificate, so -> `docker-compose.yml` maps `mail.eigbox.net` to its IP via `extra_hosts` to keep -> STARTTLS certificate verification valid. Set `SMTP_HOST=mail.eigbox.net`. +The relay used in production presents a shared `*.eigbox.net` certificate, so the +app has to connect by that hostname for STARTTLS verification to succeed — but +the hostname's public DNS record points at a pool member the container network +cannot reach. `docker-compose.yml` therefore pins the name to an address that +answers, via `extra_hosts`. Two variables control the pin: + +| Variable | Default | Purpose | +|---|---|---| +| `SMTP_RELAY_HOST` | `mail.eigbox.net` | Hostname the certificate is valid for, and the value `SMTP_HOST` should use. | +| `SMTP_RELAY_IP` | `66.96.134.48` | Address that hostname is pinned to inside the container. | + +> Read by Compose when the stack starts, not by `app/config.py` — they shape the +> container's `/etc/hosts` rather than the application's settings. When mail +> starts failing to connect, this pin is the first thing to check; +> [Operations](OPERATIONS.md) has the procedure. ## Derived settings (not environment variables) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 04eb78b..0e40467 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -124,4 +124,32 @@ certificates are issued and renewed automatically by Caddy. | `503` on `/login` | OIDC not configured | `OIDC_*` set in `.env`? | | Anonymous upload `429` | Rate limit hit | Expected; tune `ANON_UPLOADS_PER_*`. | | Email returns `email_not_configured` | SMTP unset | Set `SMTP_*`; STARTTLS host mapping present. | +| Email returns `send_failed`, logs show a connection timeout | The pinned relay address stopped answering | See "Relay address" below. | | Member sees "not authorized" | Not in the `dropbox` group | Group membership in Authentik; `groups` scope mapped. | + +### Relay address + +Share-by-email connects to `mail.eigbox.net` because that is the name its +certificate covers, but the name's public DNS record points at a pool member the +container network cannot reach. `docker-compose.yml` pins the name to a working +address (`SMTP_RELAY_IP`). If the provider retires that address, mail starts +timing out while everything else keeps working. + +Confirm what the container is using, and whether it still answers: + +```sh +docker compose exec -T app python - <<'PY' +import smtplib, ssl, socket +print("resolves to:", sorted({i[4][0] for i in socket.getaddrinfo("mail.eigbox.net", 587)})) +s = smtplib.SMTP("mail.eigbox.net", 587, timeout=15) +print("banner:", s.ehlo()[1].decode().splitlines()[0]) +s.starttls(context=ssl.create_default_context()) # verifies the certificate +print("STARTTLS ok") +s.quit() +PY +``` + +If it no longer answers, find one that does — the provider publishes several — +set `SMTP_RELAY_IP` in `.env`, and `docker compose up -d app` to rewrite the +container's `/etc/hosts`. Keep `SMTP_RELAY_HOST` as the certificate name; +connecting by address instead would fail verification.