smtp-relay/README.md
sirxavor 6a10eab342 docs: por que la config es como es, y fuera el CI muerto
El README describia un flujo que no existe: ./build.sh con Kaniko y un workflow de
Gitea Actions. Kaniko y Gitea Actions se estan retirando, y ademas este workflow
llevaba roto desde mayo -- era el unico del arbol que usaba actions/checkout@v4,
que necesita node, y el runner :host no lo tiene. Fallaba en 70 s en cada push sin
que nadie lo mirara: la imagen que sostuvo el correo 70 dias era del 20 de mayo.
Un CI que falla siempre y en silencio es peor que no tener CI, asi que se borra en
vez de dejarlo de adorno.

Se documenta el build con mcp-build (con el gotcha del image_name sin host, que
duplica el registro y muere con 401) y, sobre todo, POR QUE cada linea de la config
es la que es. Las cuatro que no se pueden tocar sin reabrir el incidente llevan su
sintoma exacto al lado, para que el siguiente que las vea raras sepa lo que cuesta
"limpiarlas": tlsmgr, el grupo sasl, el realm desde RELAY_AUTH_DOMAIN y chroot=n.

Y queda escrito como se prueba un cambio de esta imagen ANTES de desplegarlo, que
aqui no es opcional: desplegar es reiniciar el unico pod que sostiene el correo, y
si falla no hay alerta que lo diga porque la alerta ES el correo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 23:12:05 +02:00

106 lines
4.9 KiB
Markdown

# smtp-relay
Postfix SMTP relay with Cyrus SASL authentication and TLS.
Used by Mailu (personal + Solidaria NGO) on valhalla to route outbound mail through
hermes, which has a trusted residential IP accepted by Gmail and Hotmail.
Image: `harbor.manabo.org/library/smtp-relay`
Deployed on: hermes (`clusters/hermes/smtp-relay/` in asgard)
---
## Build
Con **mcp-build** (2026-07-29). Antes había un `scripts/build.sh` con Kaniko y un
`.gitea/workflows/build.yml`: **los dos se han borrado**. Kaniko y Gitea Actions se están
retirando, y este workflow además llevaba roto desde mayo — era el único del árbol que usaba
`actions/checkout@v4`, que necesita `node`, y el runner `:host` no lo tiene
(`Cannot find: node in PATH`). Fallaba en 70 s en cada push y **nadie se enteraba**: la imagen que
sostuvo el correo durante 70 días era del 20 de mayo.
```
trigger_build(repo_url="https://git.manabo.org/xavor/smtp-relay.git",
ref="main", image_tag="dev", image_name="library/smtp-relay")
```
⚠️ `image_name` va **sin el host**. Con `harbor.manabo.org/library/smtp-relay` el destino sale
duplicado (`harbor.manabo.org/harbor.manabo.org/...`) y el push muere con `401`.
El despliegue se pinea a mano en `asgard/clusters/hermes/smtp-relay/kustomization.yaml`.
---
## Configuration
### Required Vault secrets (`app/smtp-relay/smtp-relay-sasl`)
| Key | Description |
|-----|-------------|
| `relay_user` | SASL username (e.g. `relayuser`) |
| `relay_pass` | SASL password (plaintext — stored in Vault) |
| `relay_domain` | SASL domain (e.g. `manabo.org`) |
### TLS (`certs/smtp-relay-tls`)
Wildcard cert for `relay.manabo.org` — pushed to Vault via PushSecret on valhalla.
### Env vars (from ExternalSecret)
| Var | Source |
|-----|--------|
| `RELAY_AUTH_USER` | `relay_user` |
| `RELAY_AUTH_PASS` | `relay_pass` |
| `RELAY_AUTH_DOMAIN` | `relay_domain` |
---
## How it works
- Listens on ports **25** (SMTP, TLS optional) and **587** (submission, TLS required)
- Uses `hostNetwork: true` — ports exposed directly on the hermes host IP
- Entrypoint creates a `sasldb2` user from the env vars on every start
- Only clients authenticated via SASL can relay mail
- TLS cert mounted from Vault ExternalSecret
---
## Gotchas
- **sasldb2 recreated on every restart**: credentials are read from env vars and
`saslpasswd2` re-creates the sasldb. This is intentional (stateless SASL).
- **No DKIM**: DKIM signing is not implemented in this image. Relay delivers mail
as-is; DKIM signatures must be added by the sending MTA (Mailu).
### Por qué la config es como es (incidente del 2026-07-29)
El correo saliente de `manabo.org` estuvo **semanas sin salir** por cinco fallos encadenados, cada
uno tapando al siguiente. Cuatro de los arreglos vivieron 70 días aplicados a mano sobre el
contenedor vivo, fuera de git. Están en la imagen desde el 2026-07-29. Relato completo en
`asgard/clusters/hermes/smtp-relay/README.md`. **Lo que no se puede tocar sin reabrirlo:**
- **`tlsmgr` en `master.cf`.** Sin ese daemon, `smtpd` **anuncia** `STARTTLS` en el `EHLO` y luego
no puede hacerlo: `454 4.7.0 TLS not available due to local problem`. La capacidad anunciada no
prueba la capacidad.
- **`postfix` en el grupo `sasl`** (`usermod -aG sasl postfix` en el Dockerfile). `/etc/sasldb2` es
`root:sasl 0640`; sin el grupo, `smtpd` no puede abrirlo → `454 Temporary authentication failure`.
- **`smtpd_sasl_local_domain` sale del `entrypoint`, no de `main.cf`.** Tiene que ser el mismo
`RELAY_AUTH_DOMAIN` con el que `saslpasswd2` crea la entrada del sasldb. Estaba declarado aparte
como `$myhostname`, así que el realm era `relayuser@relay.manabo.org` y la entrada real
`relayuser@manabo.org`. **No lo devuelvas a `main.cf`**: separados, vuelven a poder divergir.
- **`chroot = n` en todos los servicios de `master.cf`.** Con chroot, Postfix necesita
`/var/spool/postfix/etc/resolv.conf` poblado en el build; sin él **no resuelve MX** y el correo se
queda en cola con `Name service error`, aunque `getent hosts` dentro del contenedor sí resuelva
(la herramienta de diagnóstico no ve lo mismo que el consumidor). En un contenedor el chroot no
aporta nada: el contenedor **es** el jail.
- **`postlogd` + `maillog_file`.** Postfix escribe a syslog y aquí no corre ninguno ⇒ `kubectl logs`
sale **vacío**. Sin esto, el incidente se diagnostica a ciegas — y es lo que permite ver si un
despliegue futuro sale mal.
**Cómo se prueba un cambio de esta imagen antes de desplegarlo** (obligatorio: desplegar aquí es
reiniciar el único pod que sostiene el correo, y si falla **no hay alerta que lo diga, porque la
alerta es el correo**): arranca la imagen candidata aparte con el entrypoint real y las env de
producción, y exige `diff` vacío de `postconf -n` y `postconf -M` contra el contenedor en marcha.
Un diff limpio no basta: ejercita también `STARTTLS` y `AUTH PLAIN` contra ella (`235
Authentication successful`), que es lo que un diff de configuración no caza.