feat(stacks): migrate mail-relay

This commit is contained in:
2026-08-17 13:35:57 +02:00
parent 122881e539
commit 322f2f89b5
4 changed files with 165 additions and 0 deletions

View File

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

View File

@@ -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

View 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

View File

@@ -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