diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cf8d0..efc5820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ ## 2026-07-02 ### 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/exemples/vault.exemple.yml b/exemples/vault.exemple.yml index b9e2f7f..184b8b3 100644 --- a/exemples/vault.exemple.yml +++ b/exemples/vault.exemple.yml @@ -38,6 +38,9 @@ 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 new file mode 100644 index 0000000..71006d6 --- /dev/null +++ b/playbooks/groupes/serveur_stalwart.yml @@ -0,0 +1,15 @@ +--- +- 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 new file mode 100644 index 0000000..26cb520 --- /dev/null +++ b/roles/serveur_stalwart/defaults/main.yml @@ -0,0 +1,29 @@ +--- +# 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 new file mode 100644 index 0000000..0c2efab --- /dev/null +++ b/roles/serveur_stalwart/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- 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 new file mode 100644 index 0000000..6f12041 --- /dev/null +++ b/roles/serveur_stalwart/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: 2 + memoire_mo: 2048 + disque_go: 10 diff --git a/roles/serveur_stalwart/tasks/main.yml b/roles/serveur_stalwart/tasks/main.yml new file mode 100644 index 0000000..1a09f4a --- /dev/null +++ b/roles/serveur_stalwart/tasks/main.yml @@ -0,0 +1,92 @@ +--- +- 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 new file mode 100644 index 0000000..d293d2c --- /dev/null +++ b/roles/serveur_stalwart/templates/config.json.j2 @@ -0,0 +1 @@ +{"@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 new file mode 100644 index 0000000..a4b58ed --- /dev/null +++ b/roles/serveur_stalwart/templates/stalwart.env.j2 @@ -0,0 +1,5 @@ +{% 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 new file mode 100644 index 0000000..9af0450 --- /dev/null +++ b/roles/serveur_stalwart/templates/stalwart.service.j2 @@ -0,0 +1,26 @@ +# 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