From 0c17b97602eb686a940c9ef78be0039694f716a2 Mon Sep 17 00:00:00 2001 From: Don Beckham Date: Sun, 26 Jul 2026 14:37:01 -0500 Subject: [PATCH 1/2] Make the SMTP relay's pinned address configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker-compose.yml hard-coded "mail.eigbox.net:66.96.134.48" in extra_hosts. The mapping is load-bearing, not decoration: the relay's certificate is only valid for the hostname, but the hostname's public DNS record now resolves to 38.113.1.135, which this network cannot reach at all. The pinned address still answers, so share-by-email works today entirely because of that line — and would break silently, with nothing else affected, the day the provider retires it. Now driven by SMTP_RELAY_HOST / SMTP_RELAY_IP with the current values as defaults, so a fresh clone still starts and the address can be corrected from .env without a code change. Added a runbook entry with a probe that reports what the container resolves, whether the address answers, and whether the certificate still verifies. Verified the interpolation renders correctly with the variables set, unset, and absent from .env entirely. --- .env.example | 7 +++++++ CHANGELOG.md | 6 ++++++ docker-compose.yml | 10 ++++++---- docs/CONFIGURATION.md | 18 +++++++++++++++--- docs/OPERATIONS.md | 28 ++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 83e4b94..f3b410b 100644 --- a/.env.example +++ b/.env.example @@ -40,3 +40,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 90ecdb0..0b5a748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ pre-1.0 scheme; dates are when the change reached `main`. ## Unreleased +### Changed +- 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. + ### Added - Full reference documentation under `docs/`: architecture, configuration, operations runbook, security model, and API reference, with a docs index and 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 b34c943..f4f4c49 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -80,9 +80,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. From ee4f2c9188e134506a5189e101d5211b2c54f6ad Mon Sep 17 00:00:00 2001 From: Don Beckham Date: Sun, 26 Jul 2026 16:36:20 -0500 Subject: [PATCH 2/2] Consolidate the Unreleased changelog sections --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0ba47..c1dcf7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,6 @@ pre-1.0 scheme; dates are when the change reached `main`. ## Unreleased -### Changed -- 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. - ### Added - Full reference documentation under `docs/`: architecture, configuration, operations runbook, security model, and API reference, with a docs index and @@ -20,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