diff --git a/CHANGELOG.md b/CHANGELOG.md index fea6aea..9656a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ mis à jour. Réévaluer Stalwart ~2028. ### Ajouté +- **Rôle `serveur_postfix` (Postfix 3.x) — MTA du nœud edge-mta.** Réception `:25`, cartes + **LDAP** (validation des boîtes via l'attribut `mail`), remise **LMTP réseau** vers le nœud + mail-store Dovecot (`virtual_transport = lmtp:inet:[…]:24`), TLS via **step_ca** (pont de + cert), aucune boîte locale. Config `main.cf` + carte `ldap-mailboxes.cf`, validée par + `postfix check`. Secret de bind : `vault_openldap_admin`. Nécessite `serveur_postfix_mailstore_hote` + (FQDN du mail-store). Validé statiquement ; déploiement réel à suivre. - **Rôle `serveur_dovecot` (Dovecot 2.4) — déployé et prouvé.** IMAP `:993`/`:143` + LMTP, **auth/annuaire LDAP** (vers `serveur_openldap`, filtre `mail`), stockage Maildir (user système `vmail`), **TLS via step_ca** (pont de cert + resync au renouvellement), diff --git a/playbooks/groupes/serveur_postfix.yml b/playbooks/groupes/serveur_postfix.yml new file mode 100644 index 0000000..650a8fa --- /dev/null +++ b/playbooks/groupes/serveur_postfix.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe serveur_postfix + hosts: serveur_postfix + 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_postfix diff --git a/roles/serveur_postfix/defaults/main.yml b/roles/serveur_postfix/defaults/main.yml new file mode 100644 index 0000000..37ec9ad --- /dev/null +++ b/roles/serveur_postfix/defaults/main.yml @@ -0,0 +1,31 @@ +--- +# Postfix 3.x (Debian 13) — MTA du nœud edge-mta : réception :25, cartes LDAP (domaines, +# boîtes, alias), remise LMTP réseau vers le nœud mail-store (Dovecot). TLS via step_ca. +# NE STOCKE AUCUNE BOÎTE. Cf. docs/courriel-conception.md + docs/identite-sso.md. + +serveur_postfix_paquets: + - postfix + - postfix-ldap + +serveur_postfix_service: "postfix" +serveur_postfix_domaine: "{{ domaine_interne }}" +serveur_postfix_nom_hote: "{{ ansible_fqdn | default(ansible_hostname) }}" +# Réseaux internes de confiance (relais autorisé). À adapter au réseau de l'instance. +serveur_postfix_reseaux_confiance: "127.0.0.0/8 [::1]/128" + +# --- Nœud mail-store (Dovecot) : remise LMTP réseau --- +# OBLIGATOIRE : le FQDN/nom du nœud qui héberge les boîtes (Dovecot LMTP). +serveur_postfix_mailstore_hote: "" +serveur_postfix_lmtp_port: 24 + +# --- Annuaire LDAP (validation domaines/boîtes/alias) --- +serveur_postfix_ldap_serveur: "ldaps://id-ldap-01.{{ domaine_interne }}:636" +serveur_postfix_ldap_base: "ou=people,dc={{ domaine_interne.split('.') | join(',dc=') }}" +serveur_postfix_ldap_bind_dn: "cn=admin,dc={{ domaine_interne.split('.') | join(',dc=') }}" +serveur_postfix_ldap_bind_password: "{{ vault_openldap_admin | default('') }}" + +# --- 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" +serveur_postfix_tls_source_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key" +serveur_postfix_tls_dir: "/etc/postfix/tls" diff --git a/roles/serveur_postfix/handlers/main.yml b/roles/serveur_postfix/handlers/main.yml new file mode 100644 index 0000000..55092f2 --- /dev/null +++ b/roles/serveur_postfix/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Recharger postfix + ansible.builtin.systemd: + name: "{{ serveur_postfix_service }}" + state: reloaded + when: not ansible_check_mode diff --git a/roles/serveur_postfix/meta/empreinte.yml b/roles/serveur_postfix/meta/empreinte.yml new file mode 100644 index 0000000..e717d0f --- /dev/null +++ b/roles/serveur_postfix/meta/empreinte.yml @@ -0,0 +1,8 @@ +--- +# 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: 1 + memoire_mo: 512 + disque_go: 3 diff --git a/roles/serveur_postfix/tasks/main.yml b/roles/serveur_postfix/tasks/main.yml new file mode 100644 index 0000000..ba3c56d --- /dev/null +++ b/roles/serveur_postfix/tasks/main.yml @@ -0,0 +1,137 @@ +--- +- name: Exiger le mot de passe de bind LDAP (Vault) + ansible.builtin.assert: + that: + - serveur_postfix_ldap_bind_password | length > 0 + fail_msg: "serveur_postfix_ldap_bind_password requis (via Ansible Vault, ex. vault_openldap_admin)." + +- name: Exiger le nœud mail-store (remise LMTP) + ansible.builtin.assert: + that: + - serveur_postfix_mailstore_hote | length > 0 + fail_msg: "serveur_postfix_mailstore_hote requis (FQDN du nœud Dovecot qui héberge les boîtes)." + +- name: Installer debconf-utils (requis par le module debconf) + ansible.builtin.apt: + name: debconf-utils + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Préconfigurer Postfix (debconf, installation non interactive) + ansible.builtin.debconf: + name: postfix + question: "{{ item.question }}" + vtype: "{{ item.vtype }}" + value: "{{ item.value }}" + loop: + - { question: "postfix/main_mailer_type", vtype: "select", value: "Internet Site" } + - { question: "postfix/mailname", vtype: "string", value: "{{ serveur_postfix_domaine }}" } + loop_control: + label: "{{ item.question }}" + +- name: Installer Postfix + ansible.builtin.apt: + name: "{{ serveur_postfix_paquets }}" + state: present + update_cache: true + +# --- Pont de certificat step_ca (déposé par client_pki) --- +- name: TLS — créer le répertoire des certificats + ansible.builtin.file: + path: "{{ serveur_postfix_tls_dir }}" + state: directory + owner: root + group: root + mode: "0755" + when: serveur_postfix_tls_actif | bool + +- name: TLS — déployer le script de synchronisation du certificat + ansible.builtin.template: + src: setops-postfix-cert-sync.sh.j2 + dest: /usr/local/sbin/setops-postfix-cert-sync + owner: root + group: root + mode: "0755" + when: serveur_postfix_tls_actif | bool + +- name: TLS — déployer les unités de synchronisation (service + path) + ansible.builtin.template: + src: "{{ item.s }}" + dest: "{{ item.d }}" + owner: root + group: root + mode: "0644" + loop: + - { s: "setops-postfix-cert-sync.service.j2", d: "/etc/systemd/system/setops-postfix-cert-sync.service" } + - { s: "setops-postfix-cert-sync.path.j2", d: "/etc/systemd/system/setops-postfix-cert-sync.path" } + loop_control: + label: "{{ item.d | basename }}" + when: serveur_postfix_tls_actif | bool + +- name: TLS — synchroniser le certificat maintenant + ansible.builtin.command: /usr/local/sbin/setops-postfix-cert-sync + changed_when: false + when: + - serveur_postfix_tls_actif | bool + - not ansible_check_mode + +- name: TLS — activer la surveillance du certificat (renouvellement) + ansible.builtin.systemd: + name: setops-postfix-cert-sync.path + enabled: true + state: started + daemon_reload: true + when: + - serveur_postfix_tls_actif | bool + - not ansible_check_mode + +- name: TLS — vérifier que le certificat est en place + ansible.builtin.stat: + path: "{{ serveur_postfix_tls_dir }}/cert.pem" + register: serveur_postfix_cert + when: serveur_postfix_tls_actif | bool + +- name: TLS — exiger le certificat (intégration client_pki requise sur ce nœud) + ansible.builtin.assert: + that: + - serveur_postfix_cert.stat.exists | default(false) + fail_msg: >- + Aucun certificat step_ca ({{ serveur_postfix_tls_source_cert }}). Le nœud Postfix doit + porter l'intégration client_pki. Déploie-la, puis relance. + when: + - serveur_postfix_tls_actif | bool + - not ansible_check_mode + +# --- Configuration --- +- name: Déployer la carte LDAP des boîtes + ansible.builtin.template: + src: ldap-mailboxes.cf.j2 + dest: /etc/postfix/ldap-mailboxes.cf + owner: root + group: postfix + mode: "0640" + no_log: true # contient le mot de passe de bind LDAP + notify: Recharger postfix + +- name: Déployer main.cf + ansible.builtin.template: + src: main.cf.j2 + dest: /etc/postfix/main.cf + owner: root + group: root + mode: "0644" + notify: Recharger postfix + +- name: Valider la configuration Postfix + ansible.builtin.command: postfix check + changed_when: false + when: not ansible_check_mode + +- name: Activer et démarrer Postfix + when: not ansible_check_mode + ansible.builtin.systemd: + name: "{{ serveur_postfix_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/serveur_postfix/templates/ldap-mailboxes.cf.j2 b/roles/serveur_postfix/templates/ldap-mailboxes.cf.j2 new file mode 100644 index 0000000..991a3bf --- /dev/null +++ b/roles/serveur_postfix/templates/ldap-mailboxes.cf.j2 @@ -0,0 +1,10 @@ +# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main. +# Carte LDAP : valide qu'une adresse correspond à une boîte (attribut mail). +server_host = {{ serveur_postfix_ldap_serveur }} +search_base = {{ serveur_postfix_ldap_base }} +query_filter = (mail=%s) +result_attribute = mail +bind = yes +bind_dn = {{ serveur_postfix_ldap_bind_dn }} +bind_pw = {{ serveur_postfix_ldap_bind_password }} +version = 3 diff --git a/roles/serveur_postfix/templates/main.cf.j2 b/roles/serveur_postfix/templates/main.cf.j2 new file mode 100644 index 0000000..d724bfb --- /dev/null +++ b/roles/serveur_postfix/templates/main.cf.j2 @@ -0,0 +1,34 @@ +# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main. +compatibility_level = 3.6 +myhostname = {{ serveur_postfix_nom_hote }} +mydomain = {{ serveur_postfix_domaine }} +myorigin = $mydomain +inet_interfaces = all +inet_protocols = ipv4 +biff = no +append_dot_mydomain = no +readme_directory = no +smtpd_banner = $myhostname ESMTP +mynetworks = {{ serveur_postfix_reseaux_confiance }} + +# Aucune remise locale : ce MTA ne fait QUE des boîtes virtuelles (via LDAP + LMTP). +mydestination = +local_recipient_maps = + +# --- Boîtes virtuelles : validation LDAP + remise LMTP vers le nœud mail-store (Dovecot) --- +virtual_mailbox_domains = {{ serveur_postfix_domaine }} +virtual_mailbox_maps = ldap:/etc/postfix/ldap-mailboxes.cf +virtual_transport = lmtp:inet:[{{ serveur_postfix_mailstore_hote }}]:{{ serveur_postfix_lmtp_port }} + +{% if serveur_postfix_tls_actif %} +# --- TLS (certificat step_ca) --- +smtpd_tls_cert_file = {{ serveur_postfix_tls_dir }}/cert.pem +smtpd_tls_key_file = {{ serveur_postfix_tls_dir }}/key.pem +smtpd_tls_security_level = may +smtpd_tls_protocols = >=TLSv1.2, !SSLv2, !SSLv3 +smtp_tls_security_level = may +{% endif %} + +# --- Restrictions de base --- +smtpd_helo_required = yes +smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination diff --git a/roles/serveur_postfix/templates/setops-postfix-cert-sync.path.j2 b/roles/serveur_postfix/templates/setops-postfix-cert-sync.path.j2 new file mode 100644 index 0000000..14115ff --- /dev/null +++ b/roles/serveur_postfix/templates/setops-postfix-cert-sync.path.j2 @@ -0,0 +1,10 @@ +# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main. +[Unit] +Description=Set-OPS — surveille le certificat step_ca de Postfix + +[Path] +PathChanged={{ serveur_postfix_tls_source_cert }} +Unit=setops-postfix-cert-sync.service + +[Install] +WantedBy=multi-user.target diff --git a/roles/serveur_postfix/templates/setops-postfix-cert-sync.service.j2 b/roles/serveur_postfix/templates/setops-postfix-cert-sync.service.j2 new file mode 100644 index 0000000..73cf708 --- /dev/null +++ b/roles/serveur_postfix/templates/setops-postfix-cert-sync.service.j2 @@ -0,0 +1,8 @@ +# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main. +[Unit] +Description=Set-OPS — synchronise le certificat step_ca vers Postfix +After=postfix.service + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/setops-postfix-cert-sync diff --git a/roles/serveur_postfix/templates/setops-postfix-cert-sync.sh.j2 b/roles/serveur_postfix/templates/setops-postfix-cert-sync.sh.j2 new file mode 100644 index 0000000..cfc2789 --- /dev/null +++ b/roles/serveur_postfix/templates/setops-postfix-cert-sync.sh.j2 @@ -0,0 +1,19 @@ +#!/bin/sh +# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main. +# Synchronise le certificat step_ca (client_pki, root:root 600) vers Postfix, puis recharge. +# Idempotent ; appelé au déploiement ET au renouvellement (unité path). La clé est lisible +# par le groupe postfix (smtpd tourne sous postfix). +set -eu + +SRC_CERT="{{ serveur_postfix_tls_source_cert }}" +SRC_KEY="{{ serveur_postfix_tls_source_cle }}" +DST="{{ serveur_postfix_tls_dir }}" + +[ -f "$SRC_CERT" ] && [ -f "$SRC_KEY" ] || exit 0 + +install -o root -g root -m 0644 "$SRC_CERT" "$DST/cert.pem" +install -o root -g postfix -m 0640 "$SRC_KEY" "$DST/key.pem" + +if systemctl --quiet is-active postfix 2>/dev/null; then + systemctl reload postfix 2>/dev/null || systemctl restart postfix +fi