Configuration
difuzio se configure par un fichier YAML unique (typiquement
/etc/difuzio/config.yaml), partagé par les trois processus. Le fichier
config.example.yaml à la racine du dépôt sert de modèle commenté.
Chargement et surcharge
Le fichier est validé une seule fois au démarrage. Une clé requise manquante ou un invariant cassé fait échouer le démarrage : il n'y a pas de valeur par défaut silencieuse. Pour vérifier sans démarrer :
CREDENTIALS_DIRECTORY=/etc/difuzio/secrets difuzio config check --config /etc/difuzio/config.yaml
Chaque clé peut être surchargée par une variable d'environnement préfixée
DIFUZIO_, avec __ (double underscore) comme séparateur de niveau. Par exemple
DIFUZIO_DB__DSN surcharge db.dsn, et DIFUZIO_RELAY__PORT surcharge
relay.port. C'est utile pour injecter des secrets sans les écrire dans le
fichier.
Section log
log:
level: info # debug|info|warn|error
format: json # json|text
Les journaux applicatifs sont structurés (slog). En production, format: json
facilite l'ingestion. Les journaux peuvent contenir des données personnelles
(adresses email) : aligner la rétention sur la politique RGPD (90 jours).
Section db
db:
dsn: "difuzio:MOTDEPASSE@tcp(127.0.0.1:3306)/difuzio?parseTime=true&loc=UTC&time_zone=%27%2B00%3A00%27&clientFoundRows=true"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 5m
Le DSN (Data Source Name) est la chaîne de connexion à la base. Il référence le
compte applicatif à moindre privilège, sans droits DDL (Data Definition Language :
CREATE, ALTER, DROP) ; les migrations utilisent un compte DDL séparé passé à
difuzio migrate --dsn. Le DSN doit porter parseTime=true, loc=UTC,
time_zone='+00:00' et clientFoundRows=true ; ces paramètres ne sont pas
optionnels.
Avec le binaire compilé pour SQLite, dsn est un simple chemin de fichier
(/var/lib/difuzio/difuzio.db) : les pragmas et le mode de verrouillage sont
posés par difuzio, il n'y a rien à ajouter. Voir
Base de données.
Section storage
storage:
blob_backend: fs
blob_dir: /var/lib/difuzio/messages
orphan_grace: 24h
sweep_batch_size: 500
Cette section décide où vit le brut des messages : le RFC 5322 complet, pièces jointes comprises.
blob_backend: fs (le défaut) le range dans une arborescence de fichiers, et la
base ne garde que la ligne messages : métadonnées, empreinte SHA-256 et chemin
relatif de l'objet. C'est le mode recommandé pour toute installation. La raison
est la forme de la donnée : un corps de message est immuable, écrit une fois, relu
quelques fois, et peut peser jusqu'à lmtp.max_message_bytes (50 Mio par défaut).
En colonne LONGBLOB, ce corps traverse le binlog à chaque insertion répliquée,
ses pages de débordement chassent du cache InnoDB les tables réellement chaudes,
et un dump logique devient inexploitable : la taille de la base cesse de dépendre
du nombre d'abonnés pour dépendre du volume de pièces jointes qui a transité.
blob_backend: db remet les corps dans la colonne raw_blob. C'est un dernier
recours, réservé au cas d'un déploiement multi-hôte où serve-lmtp et les
worker tournent sur des machines différentes sans système de fichiers partagé.
Le démarrage journalise alors un avertissement, et difuzio doctor le rappelle.
blob_dir doit être un chemin absolu. Le répertoire est créé au démarrage en
0700 et vérifié en écriture : un chemin erroné ou un droit manquant fait échouer
le démarrage plutôt que la première réception. Sous systemd, c'est le
StateDirectory de l'unité. Le répertoire contient le contenu intégral du
courrier des abonnés : il ne doit jamais être exposé, et il fait partie du
périmètre de sauvegarde au même titre que la base (voir
Exploitation).
orphan_grace (minimum 1 h) est l'âge à partir duquel un objet que plus aucune
ligne ne référence est récupéré par le balayage périodique. Un tel objet apparaît
quand une réception est annulée après l'écriture du fichier : difuzio écrit
toujours l'objet avant la ligne qui le désigne, parce que perdre des octets est
inacceptable alors que laisser un fichier ne coûte que de l'espace, repris au
balayage suivant.
Les deux modes coexistent en lecture : changer blob_backend n'entraîne aucune
réécriture, les anciens messages restent lisibles là où ils ont été écrits.
La purge des corps expirés relève du réglage de liste archive_retention_days
(non renseigné = conservation illimitée) : elle efface le corps sur disque comme
en base et conserve l'index d'archive (auteur, sujet, date). Un message encore
nécessaire à un envoi en cours n'est jamais purgé.
Section web
web:
bind: "127.0.0.1:8080"
trusted_proxies: ["127.0.0.1/32", "::1/128"]
bind est l'adresse d'écoute de serve-web. On l'expose toujours derrière un
reverse proxy TLS (Proxy web frontal).
trusted_proxies liste les réseaux (CIDR ou IP simple) autorisés à parler au
nom du client : quand la connexion TCP vient d'une de ces adresses, l'IP
cliente consignée (audit du login notamment) est résolue depuis
X-Forwarded-For, en prenant le saut le plus à droite hors de la liste --
un préfixe forgé par le client ne peut donc jamais gagner. Le défaut couvre un
proxy sur la même machine (loopback v4 et v6) ; ajouter l'adresse du proxy s'il
est distant, ou mettre [] pour désactiver toute confiance. Une entrée
invalide est fatale au démarrage.
Section lmtp
lmtp:
socket: "/run/difuzio/lmtp.sock"
max_message_bytes: 52428800 # 50 Mio
max_recipients: 100 # == Postfix lmtp_destination_recipient_limit
max_concurrent: 200
read_timeout: 5m
write_timeout: 5m
serve-lmtp écoute sur une socket Unix (ou en TCP si socket est vide et
bind_tcp renseigné). max_recipients doit correspondre au
lmtp_destination_recipient_limit de Postfix. La socket est créée en mode 0660.
Section relay
relay:
host: "127.0.0.1"
port: 587
starttls: true
smtp_timeout: 30s
messages_per_minute: 600
per_domain_messages_per_minute: 300
default_pool: "main"
pools:
main:
selector_mode: header # header|verp_subdomain
per_pool_messages_per_minute: 600
C'est le relais submission par lequel worker réinjecte le courrier sortant.
default_pool est obligatoire dès qu'un domaine émet. Chaque pool nommé permet
d'isoler la réputation : selector_mode: header ajoute un en-tête
X-Difuzio-Pool (à mapper côté Postfix vers une IP source), verp_subdomain
utilise un sous-domaine VERP. Voir Délivrabilité.
Authentification SMTP (relay.auth, relay.auths, relay.envelope_from)
relay:
auth: # identité par défaut (profil boite externe)
mechanism: plain # plain|login
username: "robot@lists.example.org"
password_file: "/etc/difuzio/secrets/smtp-robot" # 0600, mot de passe verbatim
auths: # comptes dédiés par liste (optionnel)
membres@lists.example.org:
mechanism: plain
username: "membres@lists.example.org"
password_file: "/etc/difuzio/secrets/smtp-membres"
envelope_from: verp # verp|auth (défaut verp)
Sans bloc auth, le relais soumet à un Postfix de confiance sans
authentification (profil classique). Avec un bloc auth, le worker s'authentifie
auprès du submission de l'hébergeur (profil boite externe) ; relay.starttls: true devient obligatoire et le mot de passe est lu au démarrage depuis un
fichier 0600.
relay.auths sert les hébergeurs (OVH notamment) qui exigent que l'identité
SMTP AUTH corresponde au From du message : chaque entrée, indexée par
l'adresse de post de la liste, donne à cette liste son propre compte d'envoi.
Les messages de la liste s'authentifient alors avec ce compte ; tout le reste
(mails de service du robot, listes sans compte dédié) retombe sur relay.auth,
qui devient obligatoire dès que relay.auths est présent. Les clés sont
normalisées en minuscules (domaine en A-label) au chargement.
relay.envelope_from: auth remplace l'enveloppe MAIL FROM (normalement
l'adresse de retour VERP) par le compte authentifié de chaque copie. C'est
nécessaire chez les hébergeurs qui refusent une enveloppe n'appartenant pas au
compte ; en contrepartie, l'attribution VERP des rebonds est perdue (assumé dans
le profil boite externe, qui ne traite pas les rebonds automatiquement).
envelope_from: auth exige un bloc relay.auth.
Section worker
worker:
concurrency: 4
reclaim_seconds: 600 # >= 2x relay.smtp_timeout (fatal sinon)
max_attempts: 12
outq_retention_days: 3
inbound_retention_days: 7 # >= 7 (dedup LMTP vs file MTA amont)
concurrency est le nombre de tâches d'envoi traitées en parallèle par worker.
reclaim_seconds est le délai après lequel une tâche prise par un worker mort est
réattribuée ; l'invariant reclaim_seconds >= 2 x smtp_timeout est vérifié au
démarrage. max_attempts borne les tentatives sur verdict de remise réel.
inbound_retention_days doit valoir au moins 7 (la déduplication LMTP doit
couvrir la durée de rétention de la file MTA amont).
Section bounce
bounce:
window_days: 30
threshold: 100
verp_max_age_days: 90
Fenêtre de calcul du score de rebond, seuil de désactivation, et âge maximal d'un token VERP accepté en retour. Voir Délivrabilité.
Section reputation
reputation:
window_days: 7
min_sent: 500
complaint_rate_warn: 0.001
complaint_rate_suspend: 0.003
hard_bounce_rate_suspend: 0.05
Paramètres du kill-switch de réputation par liste. En deçà de min_sent messages
sur la fenêtre, aucune suspension n'est déclenchée (plancher anti-bruit). Au-delà,
un taux de plaintes supérieur à complaint_rate_suspend (0,3 %) ou un taux de
rebonds durs supérieur à hard_bounce_rate_suspend (5 %) suspend l'envoi de la
liste. Voir Modération.
Section metrics
metrics:
bind: "127.0.0.1:9090" # /metrics sur un port séparé (vide = désactivé)
Expose /metrics (format Prometheus) sur un port distinct du trafic applicatif.
Laisser vide pour désactiver. En multi-workers, cette valeur est surchargée par
instance dans le template systemd (DIFUZIO_METRICS__BIND=127.0.0.1:909%i,
donc 9091 pour worker@1, 9092 pour worker@2...) : un port statique partagé
ferait échouer le bind de toutes les instances sauf la première. Voir
Exploitation.
Section security (keyrings)
security:
session_keyring:
current: 1
keys:
- { version: 1, file: "${CREDENTIALS_DIRECTORY}/session.key" }
token_keyring:
current: 1
keys:
- { version: 1, file: "${CREDENTIALS_DIRECTORY}/token.key" }
verp_keyring:
current: 1
keys:
- { version: 1, file: "${CREDENTIALS_DIRECTORY}/verp.key" }
# Optionnel, voir plus bas.
credential_keyring:
current: 1
keys:
- { version: 1, file: "${CREDENTIALS_DIRECTORY}/credential.key" }
Trois keyrings séparés par durée de vie, plus un quatrième optionnel. Chaque clé est lue depuis un fichier en
mode 0600, généralement exposé par systemd via LoadCredential= ; les systemd
récents déposent cette copie en 0440 root:<groupe du service> sous
$CREDENTIALS_DIRECTORY, et ce bit de lecture groupe est toléré là, et
seulement là. Les valeurs
sont des secrets base64 d'au moins 32 octets. Le démarrage est fatal si un keyring
requis manque. Le champ current désigne la version de clé utilisée pour signer ;
les autres versions déclarées restent acceptées en vérification (rotation sans
rupture).
Le chemin d'une clé peut référencer des variables d'environnement : systemd donne
à chaque unité le chemin de ses propres credentials dans $CREDENTIALS_DIRECTORY,
si bien que la même configuration sert serve-web, serve-lmtp et worker. Une
variable non définie est une erreur explicite, jamais un chemin vide. Hors
systemd, poser la variable sur la commande :
CREDENTIALS_DIRECTORY=/etc/difuzio/secrets difuzio config check --config .... Un chemin
absolu en clair reste accepté (voir config.example-external.yaml) ; le compte de
service doit alors pouvoir lire le fichier. Génération, permissions et rotation :
Installation, section 2.
Une clé peut aussi être fournie en ligne (value: "<base64>") au lieu de file:,
mais pas les deux pour la même version : c'est réservé aux environnements de test,
le secret se retrouvant dans le fichier de configuration.
credential_keyring (optionnel)
Ce quatrième keyring ne signe rien : il chiffre les mots de passe des comptes de messagerie saisis par liste dans la console (profil boîte externe, voir Listes). Il n'est requis que si de tels comptes sont utilisés ; une installation avec son propre serveur mail n'en a pas besoin et démarre sans.
- Absent, la console refuse d'enregistrer un compte en le disant explicitement, plutôt que d'écrire un mot de passe en clair.
- Déclaré puis retiré alors que des comptes existent, le démarrage échoue en annonçant combien de comptes sont devenus illisibles : sans la clé, les listes concernées seraient muettes sans que rien ne le signale.
- Sa perte n'est pas rattrapable : les mots de passe doivent être ressaisis. Il se sauvegarde avec les trois autres.
- Rotation : ajouter une version, la désigner
current, garder l'ancienne danskeysle temps que les comptes soient rescellés (ce qui arrive à leur prochaine modification).
Section policy
policy:
external_mailboxes: allow # allow (défaut) | deny
Profils de déploiement que cette installation accepte. Cette section existe parce que deux personnes différentes décident : l'exploitant possède le fichier de configuration et la machine, l'administrateur de domaine possède la console. Ce que le premier interdit ici, le second ne peut plus faire.
external_mailboxes
Gouverne le profil boîte externe dans son ensemble (voir
Comptes de messagerie externes). Avec deny :
- aucun compte IMAP/SMTP de fournisseur ne peut être enregistré, ni depuis la
console, ni par l'API (
403 external_mailboxes_disabled), ni pardifuzio credential import; - la console cesse d'afficher la section "Comptes de messagerie du fournisseur" sur les fiches de liste, plutôt que de proposer un formulaire toujours refusé ;
ingest.enabled: trueetrelay.authsdeviennent des erreurs de configuration au démarrage, nommées telles quelles : une contradiction entre la politique et le profil qu'elle interdit n'est jamais tranchée en silence ;- les comptes déjà enregistrés ne sont plus utilisés : ni relevé IMAP, ni authentification par liste.
relay.auth (identité unique du relais authentifié) reste autorisé : un
smarthost authentifié est un réglage de relais ordinaire choisi par l'exploitant,
pas une boîte par liste qu'un administrateur peut pointer où il veut.
Basculer sur deny alors que des comptes sont encore enregistrés est une erreur
FATALE au démarrage de serve-web et de worker, qui nomme chaque liste
concernée. C'est délibéré : ces listes deviendraient muettes (boîte jamais
relevée, envoi retombant sur l'identité par défaut) sans que rien ne l'indique.
Les supprimer d'abord :
difuzio credential ls --domain lists.example.org --list membres
difuzio credential rm --domain lists.example.org --list membres --kind ingest
difuzio doctor rend le même verdict avant le redémarrage, et affiche
credentials: disabled by policy quand la situation est saine.
Le défaut est allow : une configuration écrite avant l'existence de cette
section garde exactement son comportement.
Section domains
domains:
- name: "lists.example.org"
base_url: "https://lists.example.org"
Liste des domaines connus au démarrage. base_url est obligatoire : tous les
liens insérés dans les courriers (confirmation, désabonnement, archives) pointent
vers cette URL. Un domaine sans base_url fait échouer le démarrage. On peut
aussi provisionner des domaines à chaud via difuzio add-domain (voir
Domaines).