From 322f2f89b59a9a9e7d20e548371beac01ff6b1ad Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Mon, 17 Aug 2026 13:35:57 +0200 Subject: [PATCH] feat(stacks): migrate mail-relay --- stacks/mail-relay/README.md | 96 ++++++++++++++++++++++++++++ stacks/mail-relay/docker-compose.yml | 22 +++++++ stacks/mail-relay/env_file | 21 ++++++ stacks/mail-relay/secrets.env.yaml | 26 ++++++++ 4 files changed, 165 insertions(+) create mode 100644 stacks/mail-relay/README.md create mode 100644 stacks/mail-relay/docker-compose.yml create mode 100644 stacks/mail-relay/env_file create mode 100644 stacks/mail-relay/secrets.env.yaml diff --git a/stacks/mail-relay/README.md b/stacks/mail-relay/README.md new file mode 100644 index 0000000..91d604a --- /dev/null +++ b/stacks/mail-relay/README.md @@ -0,0 +1,96 @@ +# mail-relay — relais SMTP interne + +Postfix en mode *satellite* : il n'accepte du courrier que depuis le réseau Docker +`smtp` et le réexpédie au relais du fournisseur, authentifié avec un compte d'envoi. + +Pas de FQDN, pas de routeur Traefik, pas de port publié, pas de volume : le dossier +porte donc le nom du service et non un domaine, comme `stacks/traefik/`. + +## Contrat avec les autres stacks — à ne pas casser + +| Élément | Valeur | Pourquoi c'est figé | +|---|---|---| +| Réseau Docker | `smtp` | déclaré `external: true` par tous les consommateurs | +| Nom d'hôte | `postfix` | valeur de `SMTP_HOST` chez les consommateurs | +| Port | `25` | sans TLS ni authentification, réseau interne uniquement | + +Le nom d'hôte `postfix` vient du **nom de service** Compose, pas du nom de projet : +renommer le dossier ne le change pas. Renommer le *service*, en revanche, casserait +tous les consommateurs en silence — les mails cesseraient de partir sans qu'aucun +conteneur ne tombe. + +Consommateurs : `vault.opytex.org`, `authentik`, `matrix_poneyworld`. + +## Le réseau `smtp` est externe + +Il n'est pas créé par cette stack, pour qu'un `docker compose down` ici ne tente pas +de supprimer un réseau auquel d'autres stacks sont attachées. À créer une seule fois, +s'il n'existe pas déjà : + +```bash +docker network ls --filter name=^smtp$ --format '{{.Name}}' +docker network create smtp # seulement si la commande ci-dessus ne renvoie rien +``` + +## Déploiement + +```bash +cd ~/infra/stacks/mail-relay +sops exec-env secrets.env.yaml 'docker compose up -d' +``` + +Après une modification d'un secret, `up -d` répond « up to date » et garde l'ancienne +valeur. Il faut forcer : + +```bash +sops exec-env secrets.env.yaml 'docker compose up -d --force-recreate' +``` + +## Configuration + +`env_file` porte le relais amont et l'identité annoncée (`SMTP_SERVER`, `SMTP_PORT`, +`SERVER_HOSTNAME`, `OVERWRITE_FROM`) ; `secrets.env.yaml` porte les identifiants du +compte d'envoi (`SMTP_USERNAME`, `SMTP_PASSWORD`), édités avec `sops edit`. + +Les noms sont ceux que l'image lit directement. Pour voir ce que le conteneur a +réellement reçu : + +```bash +docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' postfix \ + | grep -E 'SMTP|SERVER|OVERWRITE' +``` + +Un `SMTP_USERNAME` ou `SMTP_PASSWORD` vide signale un `sops exec-env` oublié. + +## Vérifier que le relais fonctionne + +```bash +# le nom d'hôte résout depuis un consommateur +docker exec vaultwarden getent hosts postfix + +# envoi de bout en bout +docker exec -i postfix sh -c 'sendmail -t' <<'EOF' +To: destinataire@example.org +Subject: test relais + +test +EOF + +# file d'attente et journal +docker exec postfix mailq +docker logs --tail 50 postfix +``` + +Une file d'attente qui ne se vide pas, ou un `status=deferred` dans le journal, pointe +l'authentification auprès du relais amont. + +## Points d'attention + +- **La file d'attente n'est pas persistée.** `/var/spool/postfix` vit dans la couche + d'écriture du conteneur : un `docker compose up --force-recreate` perd les messages + non encore remis. Acceptable pour des notifications, à revoir si le relais transporte + un jour du courrier qui compte. +- **Aucune sauvegarde.** Pas de volume, rien à restaurer : la stack se reconstruit + entièrement depuis ce dossier plus les secrets. +- Le relais fait confiance à tout ce qui est joint au réseau `smtp`. Ne pas y attacher + un service exposé qui n'a pas besoin d'envoyer du courrier. diff --git a/stacks/mail-relay/docker-compose.yml b/stacks/mail-relay/docker-compose.yml new file mode 100644 index 0000000..e88b3a1 --- /dev/null +++ b/stacks/mail-relay/docker-compose.yml @@ -0,0 +1,22 @@ +services: + postfix: + image: juanluisbaptiste/postfix:1.9.0 + container_name: postfix + restart: unless-stopped + security_opt: + - no-new-privileges:true + env_file: + - env_file + environment: + SMTP_USERNAME: ${SMTP_USERNAME:?} + SMTP_PASSWORD: ${SMTP_PASSWORD:?} + networks: + - smtp + +networks: + # Créé hors stack (`docker network create smtp`) et déclaré `external` par + # tous les consommateurs. Le garder externe ici aussi évite qu'un + # `docker compose down` sur ce relais tente de supprimer un réseau auquel + # d'autres stacks sont attachées. + smtp: + external: true diff --git a/stacks/mail-relay/env_file b/stacks/mail-relay/env_file new file mode 100644 index 0000000..8f6ec7e --- /dev/null +++ b/stacks/mail-relay/env_file @@ -0,0 +1,21 @@ +# ============================================================================= +# MAIL-RELAY (postfix) — configuration NON sensible +# ============================================================================= +# Relais SMTP interne de l'infra. Aucun port publié, aucun routeur Traefik : +# il n'est joignable que sur le réseau Docker `smtp`, sous le nom d'hôte +# `postfix`, port 25, sans authentification ni TLS. +# +# Les noms de variables ci-dessous sont ceux attendus par l'image +# juanluisbaptiste/postfix, consommés directement. +# +# SERVER_HOSTNAME : nom annoncé dans le HELO/EHLO, doit correspondre au SPF. +# OVERWRITE_FROM : réécrit l'expéditeur de tous les messages relayés. +# +# Les identifiants du compte d'envoi (SMTP_USERNAME, SMTP_PASSWORD) sont dans +# secrets.env.yaml et injectés par `sops exec-env`. +# ============================================================================= + +SMTP_SERVER=smtp.mailfence.com +SMTP_PORT=465 +SERVER_HOSTNAME=opytex.org +OVERWRITE_FROM=noreply@opytex.org diff --git a/stacks/mail-relay/secrets.env.yaml b/stacks/mail-relay/secrets.env.yaml new file mode 100644 index 0000000..1105415 --- /dev/null +++ b/stacks/mail-relay/secrets.env.yaml @@ -0,0 +1,26 @@ +SMTP_USERNAME: ENC[AES256_GCM,data:+7Kp5g9BMDo=,iv:HwSc6EfVkaipNbEKsdSYKKqu2zmKVdfGdwxmF3DeBcU=,tag:HPdOovDuvWh9+5OUep61HQ==,type:str] +SMTP_PASSWORD: ENC[AES256_GCM,data:nkxxgMlpwjoauzF9,iv:aMB9u7i5zVOoZaCCndiV+h43oxT2FiN+uTgo6fU/6/c=,tag:HE6V9Xtly6vcnWIM4fAtIQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBueXNlTFdaRWxHUjBvU0Nv + MFRJQXZobURqdWRJK0dLSmp2WEx5NDMyWFhnCnp5a1ZIN2pSbHQ2aDBVdjV1UUVu + MWNqQjloQXV4QUZoVVZDeUJvMUkvQ00KLS0tIDdSeVA2b1FqVWhHeDdrTDV5bXZ4 + T3ZIcVlGdUp1cXBETWxTRTZuVzVUdkEKFiEf3srsmNQND/dAftYWZWhzBZEVJDVN + C7sWjY+VoaUu/kLmkpSkvlU1AkMyiHuIGbFJcZVWkTo7/+nfDqd60Q== + -----END AGE ENCRYPTED FILE----- + recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXTXM5S0M5cE1mZXJqdWlC + ckx4QVE3YkplUkdUcjBDVHJMZ1lCUHJ0N0Q4CkZyUG9PclNQS1FTbTJySG1WQkRG + Vm1RdE0wN1BDc0YwdUtmYzBneGZHWVEKLS0tIDg5NzczYkR5YkpldUFtNHRUekxk + eUxHY2NTM09kRXQ1L3pkd3MwTlhTU0UKlNWwpdlqqwo7r3+gUZN4GJ3Qarr0DkFS + XUJApSBn8vYCG02BRGDBkKlm6KdqspHl36fgsB2pmtZm+oTIhG+1OQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj + lastmodified: "2026-08-17T04:51:05Z" + mac: ENC[AES256_GCM,data:Eoha31ZU3Rh4+BynONCTvz8/2PWcJ2bb8FQN1DlADnay2RcNKTa2rKkdipINLmVsMDV6BlWj+4RgQP2aVEL7XJmMVPjNKlun6tMClsT69NvJIYvlw9HbfEpXdw8U8SsNNATB2cyQ9Imayu+Zb2IWa/4GnzUUfe3PQai3rih3Yt8=,iv:0Ejixjh4A0Op/0+UpQdcsXktwwtXQoweYrPEqfVQwB0=,tag:ln4E7z6LJX2QV8rGTXIW/A==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3