---
title: "Configuration"
weight: 20
description: "Le fichier config.yaml section par section, les surcharges d'environnement et les invariants validés au démarrage."
---

# 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](/difuzio/admin/base-de-donnees).

## 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](/difuzio/admin/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](/difuzio/admin/reverse-proxy)).
`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é](/difuzio/admin/delivrabilite).

### 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é](/difuzio/admin/delivrabilite).

## 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](/difuzio/admin/moderation).

## 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](/difuzio/admin/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](/difuzio/admin/installation).

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](/difuzio/admin/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 dans
  `keys` le 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](/difuzio/admin/boite-externe)). 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 par
  `difuzio 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: true` et `relay.auths` deviennent 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](/difuzio/admin/domaines)).
