diff --git a/CHANGELOG.md b/CHANGELOG.md index 34f9ec0..d7dcfb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ mis à jour. Réévaluer Stalwart ~2028. ### Ajouté +- **Rôle `serveur_rspamd` (rspamd 3.x) — antispam + DKIM, en milter sur Postfix.** Installé + sur le nœud edge-mta (avec Postfix), backend **Redis** local, worker proxy en **mode milter + auto-scan** (`:11332`), **signature DKIM** sortante (clé générée par le rôle de façon + idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges + `/etc/rspamd/local.d/`. Postfix branché via `smtpd_milters` (option `serveur_postfix_rspamd_milter`, + `milter_default_action = accept` → tolérant si rspamd indisponible). **Prouvé** : un courriel + traversant le milter ressort **scanné** (`rspamc stat` : 1) et **signé DKIM** (`DKIM-Signature: + d=…`), puis livré et lu en IMAP. **Étape A (courriel interne) complète** : dovecot + postfix + rspamd. - **Flux courriel interne PROUVÉ de bout en bout (Étape A).** Envoi → Postfix (`edge-mta`, validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → **lu en IMAP** (auth LDAP, TLS step_ca) : `status=sent`, message lu (sujet + corps). Réglages Dovecot 2.4 diff --git a/playbooks/groupes/serveur_rspamd.yml b/playbooks/groupes/serveur_rspamd.yml new file mode 100644 index 0000000..834d9bf --- /dev/null +++ b/playbooks/groupes/serveur_rspamd.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe serveur_rspamd + hosts: serveur_rspamd + 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_rspamd diff --git a/roles/serveur_postfix/defaults/main.yml b/roles/serveur_postfix/defaults/main.yml index 37ec9ad..4597fc0 100644 --- a/roles/serveur_postfix/defaults/main.yml +++ b/roles/serveur_postfix/defaults/main.yml @@ -24,6 +24,10 @@ serveur_postfix_ldap_base: "ou=people,dc={{ domaine_interne.split('.') | join(', serveur_postfix_ldap_bind_dn: "cn=admin,dc={{ domaine_interne.split('.') | join(',dc=') }}" serveur_postfix_ldap_bind_password: "{{ vault_openldap_admin | default('') }}" +# --- Milter rspamd (antispam + DKIM) — si rspamd est co-localisé (nœud edge-mta) --- +serveur_postfix_rspamd_milter: false +serveur_postfix_rspamd_milter_port: 11332 + # --- TLS via le certificat d'hôte step_ca (client_pki) --- serveur_postfix_tls_actif: true serveur_postfix_tls_source_cert: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.crt" diff --git a/roles/serveur_postfix/templates/main.cf.j2 b/roles/serveur_postfix/templates/main.cf.j2 index f440a3a..99c0d76 100644 --- a/roles/serveur_postfix/templates/main.cf.j2 +++ b/roles/serveur_postfix/templates/main.cf.j2 @@ -31,6 +31,14 @@ smtpd_tls_protocols = >=TLSv1.2, !SSLv2, !SSLv3 smtp_tls_security_level = may {% endif %} +{% if serveur_postfix_rspamd_milter %} +# --- Milter rspamd (antispam + signature DKIM), co-localisé sur edge-mta --- +smtpd_milters = inet:localhost:{{ serveur_postfix_rspamd_milter_port }} +non_smtpd_milters = $smtpd_milters +milter_protocol = 6 +milter_default_action = accept +{% endif %} + # --- Restrictions de base --- smtpd_helo_required = yes smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination diff --git a/roles/serveur_rspamd/defaults/main.yml b/roles/serveur_rspamd/defaults/main.yml new file mode 100644 index 0000000..55be5d7 --- /dev/null +++ b/roles/serveur_rspamd/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# rspamd 3.x (Debian 13) — antispam + signature DKIM, branché en MILTER sur Postfix +# (nœud edge-mta). Backend Redis local. Config par surcharges dans /etc/rspamd/local.d/. +# Cf. docs/courriel-conception.md. NE STOCKE AUCUNE BOÎTE. + +serveur_rspamd_paquets: + - rspamd + - redis-server + +serveur_rspamd_service: "rspamd" + +# --- Backend Redis (local à ce nœud) --- +serveur_rspamd_redis_serveur: "127.0.0.1:6379" + +# --- Milter (worker proxy, auto-scan) — Postfix s'y branche via :11332 --- +serveur_rspamd_milter_port: 11332 + +# --- Signature DKIM --- +# Domaine signé : interne en bac à sable ; domaine PUBLIC en prod (Étape B). +serveur_rspamd_dkim_actif: true +serveur_rspamd_dkim_domaine: "{{ domaine_interne }}" +serveur_rspamd_dkim_selecteur: "setops" +serveur_rspamd_dkim_repertoire: "/var/lib/rspamd/dkim" diff --git a/roles/serveur_rspamd/handlers/main.yml b/roles/serveur_rspamd/handlers/main.yml new file mode 100644 index 0000000..20c4ef3 --- /dev/null +++ b/roles/serveur_rspamd/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Redémarrer rspamd + ansible.builtin.systemd: + name: "{{ serveur_rspamd_service }}" + state: restarted + when: not ansible_check_mode diff --git a/roles/serveur_rspamd/meta/empreinte.yml b/roles/serveur_rspamd/meta/empreinte.yml new file mode 100644 index 0000000..3c272a6 --- /dev/null +++ b/roles/serveur_rspamd/meta/empreinte.yml @@ -0,0 +1,7 @@ +--- +# Empreinte ressources du logiciel — dimensionnement VM derive (scripts/instancier.py). +# rspamd + Redis (backend). Sommee au socle SE + marge. +setops_empreinte: + coeurs: 1 + memoire_mo: 1024 + disque_go: 4 diff --git a/roles/serveur_rspamd/tasks/main.yml b/roles/serveur_rspamd/tasks/main.yml new file mode 100644 index 0000000..ea2f78b --- /dev/null +++ b/roles/serveur_rspamd/tasks/main.yml @@ -0,0 +1,82 @@ +--- +- name: Installer rspamd et Redis + ansible.builtin.apt: + name: "{{ serveur_rspamd_paquets }}" + state: present + update_cache: true + +- name: Activer et démarrer Redis (backend rspamd) + ansible.builtin.systemd: + name: redis-server + enabled: true + state: started + when: not ansible_check_mode + +# --- Clé DKIM (privée locale ; publique à publier en DNS — Étape B) --- +- name: Créer le répertoire des clés DKIM + ansible.builtin.file: + path: "{{ serveur_rspamd_dkim_repertoire }}" + state: directory + owner: _rspamd + group: _rspamd + mode: "0750" + when: serveur_rspamd_dkim_actif | bool + +- name: Générer la clé DKIM (idempotent — seulement si absente) + ansible.builtin.command: + cmd: >- + rspamadm dkim_keygen + -s {{ serveur_rspamd_dkim_selecteur }} + -d {{ serveur_rspamd_dkim_domaine }} + -k {{ serveur_rspamd_dkim_repertoire }}/{{ serveur_rspamd_dkim_domaine }}.{{ serveur_rspamd_dkim_selecteur }}.key + creates: "{{ serveur_rspamd_dkim_repertoire }}/{{ serveur_rspamd_dkim_domaine }}.{{ serveur_rspamd_dkim_selecteur }}.key" + register: serveur_rspamd_dkim_gen + when: + - serveur_rspamd_dkim_actif | bool + - not ansible_check_mode + notify: Redémarrer rspamd + +- name: Fixer les droits de la clé DKIM privée + ansible.builtin.file: + path: "{{ serveur_rspamd_dkim_repertoire }}/{{ serveur_rspamd_dkim_domaine }}.{{ serveur_rspamd_dkim_selecteur }}.key" + owner: _rspamd + group: _rspamd + mode: "0640" + when: + - serveur_rspamd_dkim_actif | bool + - not ansible_check_mode + +- name: Afficher l'enregistrement DNS DKIM à publier (Étape B) + ansible.builtin.debug: + msg: "{{ serveur_rspamd_dkim_gen.stdout_lines | default(['(clé déjà présente — cf. ' + serveur_rspamd_dkim_repertoire + ')']) }}" + when: serveur_rspamd_dkim_actif | bool + +# --- Surcharges de configuration (local.d) --- +- name: Déployer les surcharges local.d (redis, proxy milter) + ansible.builtin.template: + src: "{{ item }}.j2" + dest: "/etc/rspamd/local.d/{{ item }}" + owner: root + group: root + mode: "0644" + loop: + - redis.conf + - worker-proxy.inc + notify: Redémarrer rspamd + +- name: Déployer la signature DKIM (local.d) + ansible.builtin.template: + src: dkim_signing.conf.j2 + dest: /etc/rspamd/local.d/dkim_signing.conf + owner: root + group: root + mode: "0644" + when: serveur_rspamd_dkim_actif | bool + notify: Redémarrer rspamd + +- name: Activer et démarrer rspamd + when: not ansible_check_mode + ansible.builtin.systemd: + name: "{{ serveur_rspamd_service }}" + enabled: true + state: started diff --git a/roles/serveur_rspamd/templates/dkim_signing.conf.j2 b/roles/serveur_rspamd/templates/dkim_signing.conf.j2 new file mode 100644 index 0000000..381350d --- /dev/null +++ b/roles/serveur_rspamd/templates/dkim_signing.conf.j2 @@ -0,0 +1,9 @@ +# Géré par Set-OPS (rôle serveur_rspamd). Ne pas éditer à la main. +# Signature DKIM du courrier sortant. La clé privée est générée par le rôle ; +# la clé PUBLIQUE doit être publiée en DNS (enregistrement TXT) — Étape B pour le public. +enabled = true; +selector = "{{ serveur_rspamd_dkim_selecteur }}"; +path = "{{ serveur_rspamd_dkim_repertoire }}/$domain.$selector.key"; +use_domain = "header"; +allow_hdrfrom_mismatch = true; +allow_username_mismatch = true; diff --git a/roles/serveur_rspamd/templates/redis.conf.j2 b/roles/serveur_rspamd/templates/redis.conf.j2 new file mode 100644 index 0000000..a213274 --- /dev/null +++ b/roles/serveur_rspamd/templates/redis.conf.j2 @@ -0,0 +1,3 @@ +# Géré par Set-OPS (rôle serveur_rspamd). Ne pas éditer à la main. +# Backend Redis (statistiques, greylisting, ratelimit, etc.). +servers = "{{ serveur_rspamd_redis_serveur }}"; diff --git a/roles/serveur_rspamd/templates/worker-proxy.inc.j2 b/roles/serveur_rspamd/templates/worker-proxy.inc.j2 new file mode 100644 index 0000000..882b04d --- /dev/null +++ b/roles/serveur_rspamd/templates/worker-proxy.inc.j2 @@ -0,0 +1,8 @@ +# Géré par Set-OPS (rôle serveur_rspamd). Ne pas éditer à la main. +# Worker proxy en mode MILTER auto-scan : Postfix s'y branche (inet:localhost:{{ serveur_rspamd_milter_port }}). +milter = yes; +timeout = 120s; +upstream "local" { + default = yes; + self_scan = yes; +}