# 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.