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>
106 lines
4.9 KiB
Markdown
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.
|