diff --git a/CHANGELOG.md b/CHANGELOG.md index efc5820..73fbe57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,18 @@ ## 2026-07-02 +### Décidé +- **Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd.** La Phase 1 + Stalwart (`serveur_stalwart`) avait été prototypée et déployée (v0.16.11, install + + démarrage en mode récupération). Le prototypage a révélé un projet **trop jeune/volatil + pour un pilier mail critique** : config cassée entre 0.15 et 0.16, outil IaC + `stalwart config apply` **annoncé mais non livré** dans le binaire, API REST supprimée + (JMAP), gros backlog. Pivot vers la stack **mature Postfix/Dovecot/rspamd**, en prime + **100 % configurable par fichiers** (alignée au modèle déclaratif Set-OPS). Le rôle + `serveur_stalwart` est **retiré** (git en garde la trace) ; `docs/courriel-conception.md` + mis à jour. Réévaluer Stalwart ~2028. + ### Ajouté -- **Rôle `serveur_stalwart` — Phase 1 (install + démarrage).** Stalwart Mail Server - v0.16.11 (binaire unique natif, AGPL). Mécaniques réelles validées sur le binaire : - `config.json` = objet typé `{"@type":"RocksDb","path":…}` (DataStore seul), démarrage - IaC en **mode récupération** (`STALWART_RECOVERY_MODE` + `STALWART_RECOVERY_ADMIN`, - admin depuis la voûte `vault_stalwart_admin`). Install version-épinglée, user système, - unité systemd durcie (`CAP_NET_BIND_SERVICE`, `ProtectSystem`), EnvironmentFile pour le - secret. **Phase 2 à venir** (écouteurs SMTP/IMAP/JMAP, TLS step_ca, annuaire LDAP, DKIM — - provisionnés via l'API d'admin, le binaire n'ayant pas de CLI `apply`). Validé - statiquement ; déploiement réel à suivre. - **`serveur_openldap` durci pour la prod : TLS via step_ca + organisation en intrant.** - **TLS (LDAPS + STARTTLS)** : le certificat d'hôte step_ca (déposé par `client_pki`, `root:root 600`) est synchronisé vers un emplacement lisible par `openldap` (`/etc/ldap/tls`) diff --git a/docs/courriel-conception.md b/docs/courriel-conception.md index 9455871..0ac3acd 100644 --- a/docs/courriel-conception.md +++ b/docs/courriel-conception.md @@ -9,8 +9,8 @@ |---|---| | Périmètre | **Vrai service de courriel** (boîtes utilisateurs, réception publique, IMAP) — pas un simple relais sortant. | | Modèle | **Full self-host** : MX entrant **et** sortant chez Chezlepro, réputation d'IP propre. | -| Suite | **Stalwart Mail Server** (binaire unique Rust, AGPL ; SMTP/IMAP/JMAP/POP3 + anti-spam + DKIM intégrés). Adoptée et enveloppée par un rôle Set-OPS mince — pas de réimplémentation. | -| Identité | Boîtes/utilisateurs dans l'**OpenLDAP** existant (annuaire Stalwart → LDAP). Pas de comptes en double. | +| Stack | **Postfix (MTA) + Dovecot (IMAP/LMTP) + rspamd (antispam/DKIM)** — la stack libre **mature**, éprouvée depuis 20+ ans, **entièrement configurable par fichiers**. Trois rôles Set-OPS (`serveur_postfix`, `serveur_dovecot`, `serveur_rspamd`), comme on configure nginx/powerdns. | +| Identité | Boîtes/utilisateurs dans l'**OpenLDAP** existant (Postfix + Dovecot → LDAP). Pas de comptes en double. | | DNS | Enregistrements publics dans la zone `chezlepro.ca` ; noms internes dans **PowerDNS**. | | PKI | **Deux CA distinctes** (voir §6) : Let's Encrypt pour les faces publiques, **step_ca** pour l'interne. | | Démarche | **Deux étapes** (voir §11) : **(A) fonctionnement INTERNE** d'abord (step_ca, LDAP, SMTP/IMAP internes, zéro dépendance publique) ; **(B) fonctionnement EXTERNE** ensuite (Namespro, Let's Encrypt, reprise du MX `.53`). | @@ -19,6 +19,17 @@ Le rôle existant `serveur_sendmail` (relais sortant Postfix, sans boîtes) **re courrier de notification système (via `client_smtp`/msmtp). Il ne fait pas partie de ce service et ne le remplace pas. +> **Historique — pivot depuis Stalwart (2026-07-02).** On avait d'abord choisi Stalwart +> (binaire unique, moderne). Le prototypage a révélé un projet **jeune et volatil** : +> modèle de config **cassé entre 0.15 et 0.16**, outil IaC `stalwart config apply` +> **annoncé mais non livré** dans le binaire, API REST supprimée au profit de JMAP, +> backlog d'issues/PR important. Pour un **pilier critique souverain censé durer**, c'est +> trop précaire. On pivote vers la stack **mature Postfix/Dovecot/rspamd**, qui a en prime +> l'avantage d'être **100 % déclarable par fichiers** — donc parfaitement alignée avec le +> modèle Set-OPS (déclaratif, idempotent, exploitable sans IA). La Phase 1 Stalwart +> (rôle `serveur_stalwart`) est **retirée** ; git en garde la trace. Stalwart à réévaluer +> dans ~2 ans, une fois mûri et son `config apply` livré. + ## 2. Prérequis IP publique — VÉRIFIÉ (recon DNS, 2026-07-02) Le full self-host sortant exige un **PTR propre, forward-confirmed, hors blocklist**. @@ -56,28 +67,30 @@ Réserves : │ ┌───────────────┴───────────────┐ ▼ ▼ - ┌───────────────────┐ ┌───────────────────┐ - │ mail-01 (PRIMAIRE) │ │ mail-02 (MX secours)│ - │ Stalwart complet │◄──relais──│ Stalwart mode relais│ - │ SMTP 25 / 587 / 465│ quand │ SMTP 25 seulement │ - │ IMAP 993 / JMAP 443│ mail-01 │ file d'attente puis │ - │ antispam + DKIM │ est down │ transfert à mail-01 │ - │ BOÎTES stockées │ └───────────────────┘ + ┌────────────────────┐ ┌───────────────────┐ + │ mail-01 (PRIMAIRE) │ │ mail-02 (MX secours)│ + │ Postfix (MTA) │◄──relais──│ Postfix (relais) │ + │ SMTP 25 / 587 / 465│ quand │ SMTP 25 seulement │ + │ Dovecot (IMAP 993 │ mail-01 │ file d'attente puis │ + │ + LMTP + Sieve) │ est down │ transfert à mail-01 │ + │ rspamd (spam + DKIM)│ └───────────────────┘ + │ BOÎTES stockées │ └─────────┬──────────┘ │ (réseau interne, mTLS step_ca) ┌───────┼─────────────┬──────────────┐ ▼ ▼ ▼ ▼ OpenLDAP step_ca PowerDNS PostgreSQL - (identité)(certs int.)(DNS interne)(métadonnées, option HA) + (identité)(certs int.)(DNS interne)(métadonnées, option) ``` -- **mail-01 (primaire)** — Stalwart complet : réception `:25`, soumission authentifiée - `:587`/`:465`, accès client IMAP `:993` + JMAP/web `:443`, anti-spam, **signature DKIM**, - **hébergement des boîtes**. -- **mail-02 (MX de secours)** — Stalwart en mode relais, écoute seulement `:25`. Quand - mail-01 est indisponible, **accepte + met en file** l'entrant, puis **retransmet** au - rétablissement → **zéro perte à l'entrée**. À placer idéalement sur un **lien/IP - indépendant** (autre site ou VPS), sinon la résilience est illusoire. +- **mail-01 (primaire)** — **Postfix** (réception `:25`, soumission authentifiée `:587`/`:465`) + + **Dovecot** (IMAP `:993`, livraison LMTP, filtres Sieve, **hébergement des boîtes**, + auth SASL) + **rspamd** (anti-spam, **signature DKIM**, greylisting). Auth des comptes via + **LDAP**. +- **mail-02 (MX de secours)** — **Postfix** en relais, écoute seulement `:25`. Quand mail-01 + est indisponible, **accepte + met en file** l'entrant, puis **retransmet** au rétablissement + → **zéro perte à l'entrée**. À placer idéalement sur un **lien/IP indépendant** (autre site + ou VPS), sinon la résilience est illusoire. **Décision ouverte : emplacement du MX secours** (même cluster ? VPS ? autre site ?). @@ -135,15 +148,17 @@ _dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@chezlepro. - **Nomenclature** : 2 fonctions, p. ex. `comm-mail` (primaire) + `comm-mx2` (secours) ; VMID / IP / VLAN dérivés comme le reste. -- **Rôles** : - - `serveur_stalwart` — mince : installe le binaire Stalwart, dépose la config - (annuaire LDAP, ACME public, DKIM, certs internes step_ca), unité systemd, règles - nftables mail. - - variante « relais » (ou paramètre du même rôle) pour le MX de secours. -- **Intégrations** des nœuds mail : `client_ldap` (auth), `client_pki` (certs internes), - `client_dns`. -- **Secrets (voûte)** : clé/mots de passe d'admin Stalwart, éventuel secret ACME, clé - privée DKIM — via `vault_*`, câblés au rôle (comme les autres secrets). +- **Rôles** (config 100 % par fichiers, comme nginx/powerdns) : + - `serveur_postfix` — MTA : `main.cf`/`master.cf`, TLS step_ca, cartes LDAP (domaines, + boîtes, alias), remise LMTP vers Dovecot, soumission `:587`/`:465`, milter rspamd. + - `serveur_dovecot` — IMAP `:993` + LMTP + Sieve, stockage des boîtes, **auth SASL** (pour + la soumission Postfix), backend **LDAP**, TLS step_ca. + - `serveur_rspamd` — anti-spam, **signature DKIM**, greylisting ; branché à Postfix par milter. + - `serveur_postfix` en **mode relais** (paramètre) pour le MX de secours. +- **Intégrations** des nœuds mail : `client_ldap` (auth/annuaire), `client_pki` (certs + internes), `client_dns`. +- **Secrets (voûte)** : bind LDAP, **clé privée DKIM**, éventuel secret ACME (Étape B) — + via `vault_*`, câblés aux rôles. ## 10. Décisions @@ -151,7 +166,7 @@ _dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@chezlepro. - ⬜ **Emplacement du MX de secours** (`.55` existe ; lien/PTR à cadrer) (§3). - ⬜ **Stockage** : embarqué v1 vs PostgreSQL dès le départ (§4). - ⬜ **Domaine(s)** desservi(s) : `chezlepro.ca` seul, ou multi-domaines (tenants) ? -- ⬜ **Reprise du MX existant** : bascule directe de `.53` vers Stalwart, ou cohabitation transitoire ? +- ⬜ **Reprise du MX existant** : bascule directe de `.53` vers la nouvelle stack, ou cohabitation transitoire ? ## 11. Démarche en deux étapes @@ -160,13 +175,13 @@ _dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@chezlepro. But : prouver **toute la pile en interne**, en **code de prod**, dans le bac à sable. Aucune dépendance au public. -1. **Pilier identité** : `serveur_openldap` (TLS **step_ca**) validé sur un nœud du bac à sable. -2. **`serveur_stalwart`** sur `mail-01` : TLS **step_ca** (mode `fichiers`), annuaire **LDAP**, - DKIM interne, nftables mail. -3. **Prouver** : réception → boîte → accès **IMAP/JMAP** → envoi **intra-écosystème**, le tout - en TLS interne, auth LDAP. +1. **Pilier identité** : `serveur_openldap` (TLS **step_ca**) — ✅ **déployé et prouvé** en bac à sable. +2. **`serveur_postfix` + `serveur_dovecot` + `serveur_rspamd`** sur `mail-01` : TLS **step_ca**, + annuaire/auth **LDAP**, DKIM interne, nftables mail. +3. **Prouver** : réception → boîte → accès **IMAP** → envoi **intra-écosystème**, le tout en + TLS interne, auth LDAP. -*(Étape actuelle : `serveur_openldap` écrit — reste à déployer/éprouver en bac à sable.)* +*(Étape actuelle : identité OK ; on démarre `serveur_postfix`.)* ### Étape B — Fonctionnement EXTERNE (transition prod, plus tard) diff --git a/exemples/vault.exemple.yml b/exemples/vault.exemple.yml index 184b8b3..b9e2f7f 100644 --- a/exemples/vault.exemple.yml +++ b/exemples/vault.exemple.yml @@ -38,9 +38,6 @@ vault_forgejo_admin: "" vault_forgejo_secret_key: "" vault_forgejo_internal_token: "" -# --- Courriel (Stalwart) --- -vault_stalwart_admin: "" - # --- Observabilité / divers --- vault_grafana_admin: "" vault_redis: "" diff --git a/playbooks/groupes/serveur_stalwart.yml b/playbooks/groupes/serveur_stalwart.yml deleted file mode 100644 index 71006d6..0000000 --- a/playbooks/groupes/serveur_stalwart.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -- name: Appliquer le groupe serveur_stalwart - hosts: serveur_stalwart - become: true - gather_facts: true - - pre_tasks: - - name: Vérifier que la cible est Debian - ansible.builtin.assert: - that: - - ansible_facts.distribution == "Debian" - fail_msg: "Ce playbook est prévu pour Debian." - - roles: - - serveur_stalwart diff --git a/roles/serveur_stalwart/defaults/main.yml b/roles/serveur_stalwart/defaults/main.yml deleted file mode 100644 index 26cb520..0000000 --- a/roles/serveur_stalwart/defaults/main.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -# Stalwart Mail Server (binaire unique natif, AGPL). Rôle en 2 phases : -# Phase 1 (ce fichier) : installer + démarrer (mode récupération pour l'IaC). -# Phase 2 (à venir) : provisionner écouteurs/TLS/annuaire LDAP/DKIM. -# Cf. docs/courriel-conception.md. - -serveur_stalwart_version: "0.16.11" -serveur_stalwart_arch: "x86_64-unknown-linux-gnu" -serveur_stalwart_url: "https://github.com/stalwartlabs/stalwart/releases/download/v{{ serveur_stalwart_version }}/stalwart-{{ serveur_stalwart_arch }}.tar.gz" -serveur_stalwart_binaire: "/usr/local/bin/stalwart" - -serveur_stalwart_utilisateur: "stalwart" -serveur_stalwart_etc: "/etc/stalwart" -serveur_stalwart_data: "/var/lib/stalwart" -serveur_stalwart_store: "{{ serveur_stalwart_data }}/data" -serveur_stalwart_config: "{{ serveur_stalwart_etc }}/config.json" -serveur_stalwart_env: "{{ serveur_stalwart_etc }}/stalwart.env" - -# DataStore de config.json. RocksDb embarqué par défaut (v1, cf. §4 du doc conception). -serveur_stalwart_store_type: "RocksDb" - -# Mode récupération : expose l'API d'admin (port ci-dessous) pour provisionner les -# réglages (Phase 2). À basculer à false une fois la configuration chargée. -serveur_stalwart_recovery_mode: true -serveur_stalwart_recovery_port: 8080 - -# Identifiant admin de récupération. Le mot de passe vient de la voûte. -serveur_stalwart_admin_user: "admin" -serveur_stalwart_admin_password: "{{ vault_stalwart_admin | default('') }}" diff --git a/roles/serveur_stalwart/handlers/main.yml b/roles/serveur_stalwart/handlers/main.yml deleted file mode 100644 index 0c2efab..0000000 --- a/roles/serveur_stalwart/handlers/main.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Redémarrer stalwart - ansible.builtin.systemd: - name: stalwart - state: restarted - daemon_reload: true - when: not ansible_check_mode diff --git a/roles/serveur_stalwart/meta/empreinte.yml b/roles/serveur_stalwart/meta/empreinte.yml deleted file mode 100644 index 6f12041..0000000 --- a/roles/serveur_stalwart/meta/empreinte.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py). -# Sommee au socle SE + marge pour estimer coeurs/RAM/disque de la VM hote. -# Override possible par hote dans le plan (serveurs.yml: coeurs/memoire/disque). -setops_empreinte: - coeurs: 2 - memoire_mo: 2048 - disque_go: 10 diff --git a/roles/serveur_stalwart/tasks/main.yml b/roles/serveur_stalwart/tasks/main.yml deleted file mode 100644 index 1a09f4a..0000000 --- a/roles/serveur_stalwart/tasks/main.yml +++ /dev/null @@ -1,92 +0,0 @@ ---- -- name: Exiger le mot de passe admin Stalwart (Vault) - ansible.builtin.assert: - that: - - serveur_stalwart_admin_password | length > 0 - fail_msg: "serveur_stalwart_admin_password est requis (via Ansible Vault : vault_stalwart_admin)." - -- name: Créer l'utilisateur système stalwart - ansible.builtin.user: - name: "{{ serveur_stalwart_utilisateur }}" - system: true - home: "{{ serveur_stalwart_data }}" - create_home: false - shell: /usr/sbin/nologin - -- name: Créer les répertoires stalwart - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: "{{ serveur_stalwart_utilisateur }}" - group: "{{ serveur_stalwart_utilisateur }}" - mode: "0750" - loop: - - "{{ serveur_stalwart_etc }}" - - "{{ serveur_stalwart_data }}" - - "{{ serveur_stalwart_store }}" - -- name: Lire la version de Stalwart installée - ansible.builtin.command: "{{ serveur_stalwart_binaire }} --version" - register: serveur_stalwart_version_installee - changed_when: false - failed_when: false - -- name: Installer le binaire Stalwart (absent ou version différente) - when: serveur_stalwart_version not in (serveur_stalwart_version_installee.stdout | default('')) - block: - - name: Télécharger l'archive Stalwart - ansible.builtin.get_url: - url: "{{ serveur_stalwart_url }}" - dest: "/tmp/stalwart-{{ serveur_stalwart_version }}.tar.gz" - mode: "0644" - - - name: Extraire le binaire stalwart vers /usr/local/bin - ansible.builtin.unarchive: - src: "/tmp/stalwart-{{ serveur_stalwart_version }}.tar.gz" - dest: /usr/local/bin - remote_src: true - owner: root - group: root - mode: "0755" - notify: Redémarrer stalwart - - - name: Nettoyer l'archive téléchargée - ansible.builtin.file: - path: "/tmp/stalwart-{{ serveur_stalwart_version }}.tar.gz" - state: absent - -- name: Déployer config.json (DataStore) - ansible.builtin.template: - src: config.json.j2 - dest: "{{ serveur_stalwart_config }}" - owner: "{{ serveur_stalwart_utilisateur }}" - group: "{{ serveur_stalwart_utilisateur }}" - mode: "0640" - notify: Redémarrer stalwart - -- name: Déployer l'EnvironmentFile (secret admin de récupération) - ansible.builtin.template: - src: stalwart.env.j2 - dest: "{{ serveur_stalwart_env }}" - owner: root - group: "{{ serveur_stalwart_utilisateur }}" - mode: "0640" - no_log: true - notify: Redémarrer stalwart - -- name: Déployer l'unité systemd stalwart - ansible.builtin.template: - src: stalwart.service.j2 - dest: /etc/systemd/system/stalwart.service - owner: root - group: root - mode: "0644" - notify: Redémarrer stalwart - -- name: Activer et démarrer stalwart - when: not ansible_check_mode - ansible.builtin.systemd: - name: stalwart - enabled: true - state: started - daemon_reload: true diff --git a/roles/serveur_stalwart/templates/config.json.j2 b/roles/serveur_stalwart/templates/config.json.j2 deleted file mode 100644 index d293d2c..0000000 --- a/roles/serveur_stalwart/templates/config.json.j2 +++ /dev/null @@ -1 +0,0 @@ -{"@type":"{{ serveur_stalwart_store_type }}","path":"{{ serveur_stalwart_store }}"} diff --git a/roles/serveur_stalwart/templates/stalwart.env.j2 b/roles/serveur_stalwart/templates/stalwart.env.j2 deleted file mode 100644 index a4b58ed..0000000 --- a/roles/serveur_stalwart/templates/stalwart.env.j2 +++ /dev/null @@ -1,5 +0,0 @@ -{% if serveur_stalwart_recovery_mode %} -STALWART_RECOVERY_MODE=1 -STALWART_RECOVERY_MODE_PORT={{ serveur_stalwart_recovery_port }} -STALWART_RECOVERY_ADMIN={{ serveur_stalwart_admin_user }}:{{ serveur_stalwart_admin_password }} -{% endif %} diff --git a/roles/serveur_stalwart/templates/stalwart.service.j2 b/roles/serveur_stalwart/templates/stalwart.service.j2 deleted file mode 100644 index 9af0450..0000000 --- a/roles/serveur_stalwart/templates/stalwart.service.j2 +++ /dev/null @@ -1,26 +0,0 @@ -# Géré par Set-OPS (rôle serveur_stalwart). Ne pas éditer à la main. -[Unit] -Description=Stalwart Mail Server -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User={{ serveur_stalwart_utilisateur }} -Group={{ serveur_stalwart_utilisateur }} -EnvironmentFile=-{{ serveur_stalwart_env }} -ExecStart={{ serveur_stalwart_binaire }} --config {{ serveur_stalwart_config }} -Restart=on-failure -RestartSec=5 -LimitNOFILE=65536 -# Autorise l'écoute sur les ports privilégiés (25/443/465/587/993) sans être root. -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -NoNewPrivileges=true -ProtectSystem=strict -ReadWritePaths={{ serveur_stalwart_data }} -ProtectHome=true -PrivateTmp=true - -[Install] -WantedBy=multi-user.target