↓ Aller au contenu
  1. Posts/

QFieldCloud auto-hébergé [A03] : brancher un vrai relais de courriel

··14 mins·
Fabien ALLAMANCHE
Auteur
Fabien ALLAMANCHE
Géomaticien @ Vienne Condrieu Agglomération
Sommaire
QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 8: Cet article

À qui s’adresse cette annexe
#

L’instance de base embarque un piège à courriels : rien ne part. Cette annexe dit exactement ce que ce choix interdit, pourquoi il est plus lourd de conséquences qu’il n’y paraît, et comment brancher un vrai relais.

Les fichiers qu’on touche
#

Deux, et la bascule tient dans le premier. Chaque bloc de code plus bas rappelle en première ligne le fichier auquel il appartient.

Fichier Ce qu’on y met
.env les cinq variables EMAIL_* et DEFAULT_FROM_EMAIL
docker-compose.override.yml le profil never sur smtp4dev, ou ses trois ports dissociés tant qu’on le garde

L’effet de bord qu’on n’avait pas vu venir
#

À la fin de l’épisode 4, l’instance est passée sur AccountAdapterSignUpClosed, un adaptateur pour allauth — la bibliothèque tierce qui gère l’inscription et la connexion dans QFieldCloud. Résultat : plus personne ne peut créer son compte depuis la page d’inscription. Bonne décision de sécurité — sur une instance publiée, le défaut upstream laisse n’importe qui s’enregistrer.

Elle a une conséquence qu’il faut regarder en face : le seul chemin d’entrée en libre-service devient l’invitation par courriel. Le courriel cesse d’être un confort et devient le mécanisme d’inscription.

Info : les commandes docker compose se lancent depuis src/, le répertoire du sous-module — c’est de là que COMPOSE_FILE est résolu — et avec --env-file ../.env. Chaque bloc le rappelle par son cd src, à ne faire qu’une fois par session. Le $ en début de ligne est le symbole du terminal, il ne se copie pas.

Une autre voie subsiste, et elle mérite d’être connue avant de se croire bloqué : l’administrateur peut créer le compte lui-même, depuis l’admin Django (/admin/, modèle Person) ou en ligne de commande.

$ cd src && docker compose --env-file ../.env exec app python manage.py createuser \
    --username=dupont --email=dupont@exemple.fr --password='…'
User dupont has been successfully created

Le compte obtenu est complet : User.save() crée le UserAccount et l’abonnement courant dans la même transaction. Et comme ACCOUNT_EMAIL_VERIFICATION vaut optional, l’adresse non vérifiée n’empêche pas la connexion.

Deux réserves, cependant. La docstring de createuser annonce elle-même un usage de test, et le mot de passe passe en clair par la ligne de commande — donc par l’historique du shell : préférez l’admin Django, ou changez le mot de passe juste après. Surtout, cette voie ne remplace pas le courriel, elle le déplace : il faut transmettre l’identifiant et le mot de passe par un autre canal, et l’utilisateur qui oubliera ce dernier n’aura toujours aucun moyen de le réinitialiser seul. Elle dépanne pour trois comptes ; elle ne tient pas pour trente.

Or voici ce que fait l’instance quand on lui demande d’envoyer :

$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@exemple.fr
$ curl -s 'http://127.0.0.1:8012/api/Messages?pageSize=5'
messages piégés: 1
 - 2026-09-04T14:08:19 | webmaster@localhost -> ['vous@exemple.fr']
   | Test email from 7bd08766e2a9 on 2026-09-04 14:08:19

Le message n’est pas parti : il est dans le piège, consultable sur http://127.0.0.1:8012. Et son expéditeur est webmaster@localhost, valeur par défaut de DEFAULT_FROM_EMAIL — une adresse sans domaine pleinement qualifié, que le premier relais venu refuserait.

Ce qui est bloqué, concrètement
#

Fonctionnalité Ce qui se passe
Invitation d’un utilisateur Le message part dans le piège. Il faut y chercher le lien et le transmettre à la main
Réinitialisation de mot de passe Idem — l’utilisateur attend un courriel qui n’arrivera jamais
Vérification d’adresse (ACCOUNT_EMAIL_VERIFICATION=optional) Sans effet visible, puisqu’optionnelle
Notifications de fin de job Perdues silencieusement
Attention : ne passez pas ACCOUNT_EMAIL_VERIFICATION à mandatory en croyant durcir l’instance. Avec un piège à courriels, plus personne ne peut activer son compte — vous compris.

Pour une instance à poignée d’utilisateurs, tous connus, le compromis se défend : on relève les invitations dans l’interface de smtp4dev et on transmet le lien par le canal qu’on veut. À condition de le savoir, et de ne pas découvrir le jour J que l’invitation envoyée à un collègue n’est jamais partie.

La bascule, en cinq variables
#

# .env
EMAIL_HOST="smtp.exemple.fr"       # au lieu de smtp4dev
EMAIL_PORT=587                     # au lieu de 25
EMAIL_USE_TLS=True                 # STARTTLS ; EMAIL_USE_SSL pour du 465 implicite
EMAIL_HOST_USER="…"
EMAIL_HOST_PASSWORD="…"
DEFAULT_FROM_EMAIL="qfieldcloud@votre-domaine.fr"   # LA variable qu'on oublie

Aucune modification du sous-module. Deux confusions classiques :

EMAIL_PORT n’a rien à voir avec SMTP4DEV_SMTP_PORT. Le premier est le port que l’application compose sur le réseau Docker ; le second, le port publié sur l’hôte — déplacé sur 2525 à l’épisode 4 pour éviter le conflit avec l’agent de transport local. Changer l’un en croyant agir sur l’autre est le classique du genre.

DEFAULT_FROM_EMAIL doit appartenir à un domaine dont les enregistrements SPF autorisent le relais choisi. Faute de quoi les messages partent bien — et arrivent en indésirables, ce qui est plus difficile à diagnostiquer qu’un échec franc.

Une fois le relais en place, smtp4dev se retire exactement comme certbot :

# docker-compose.override.yml
  smtp4dev:
    profiles: ["never"]

Avec la même réserve qu’en A01 : le profil exclut le service de la configuration, il ne supprime pas le conteneur qui tourne déjà. up -d --remove-orphans le laissera en place, invisible dans docker compose config et bien présent dans docker ps. Il faut le nommer, en réactivant le profil le temps de la commande :

$ cd src && docker compose --env-file ../.env --profile never rm -sf smtp4dev

Y accéder tant qu’il est là
#

L’interface web écoute sur le port SMTP4DEV_WEB_PORT (8012 ici), et l’adresse sur laquelle elle est publiée décide de tout le reste.

Par défaut, 127.0.0.1 : elle n’est joignable que depuis la machine elle-même, donc par un tunnel SSH depuis votre poste.

$ ssh -N -L 8012:127.0.0.1:8012 utilisateur@10.0.0.10
# puis http://127.0.0.1:8012 dans le navigateur

C’est fastidieux pour une consultation quotidienne, et la tentation est grande de publier l’interface sur le LAN. Elle est légitime — à une condition près, qu’il faut avoir en tête.

Attention : smtp4dev n’a aucune authentification. Son interface affiche les invitations et les liens de réinitialisation de mot de passe de tous les comptes. La publier sur un réseau, c’est y offrir la prise de contrôle de l’instance à qui s’y trouve. Sur un LAN de confiance, le compromis se défend ; sur un réseau partagé, non.

Si vous la publiez, ne publiez qu’elle. Une seule variable pilote les trois ports dans le fichier upstream — web, SMTP et IMAP partent ensemble —, alors que l’IMAP donne accès aux mêmes messages que l’interface et que le SMTP accepterait n’importe quel envoi. Les dissocier dans l’override coûte trois lignes :

# docker-compose.override.yml
  smtp4dev:
    ports: !override
      - "${SMTP4DEV_WEB_BIND_IP}:${SMTP4DEV_WEB_PORT}:80"   # seule à sortir
      - "127.0.0.1:${SMTP4DEV_SMTP_PORT}:25"
      - "127.0.0.1:${SMTP4DEV_IMAP_PORT}:143"

Une porte au lieu de trois, pour le même service rendu. Et un contrôle qui ne coûte rien, une fois le conteneur recréé :

$ docker port qfieldcloud-smtp4dev-1
25/tcp -> 127.0.0.1:2525
80/tcp -> 10.0.0.10:8012
143/tcp -> 127.0.0.1:143

Notez qu’après cette bascule, http://127.0.0.1:8012 ne répond plus depuis la machine elle-même : le port n’y est plus publié. C’est la même URL pour tout le monde, ce qui est plutôt une bonne chose.

Vérifier que ça marche vraiment
#

$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@votre-domaine.fr

Puis regardez la boîte de réception, pas les journaux : Django considère l’envoi réussi dès que le relais a accepté le message. Le reste — SPF, DKIM, réputation — se joue après, et hors de son champ de vision.

Exemple concret : un Postfix dédié en LXC, relayé par Scaleway TEM
#

Ce que smtp.exemple.fr recouvre plus haut, dans le détail, pour une instance auto-hébergée derrière une connexion résidentielle.

Pourquoi un relais dédié, et pourquoi pas d’envoi direct
#

Postfix n’a pas besoin de tourner sur la même machine que QFieldCloud : une LXC unprivileged dédiée (512 Mo de RAM suffisent) découple la config mail d’une éventuelle réinstallation de l’appli, et permet de mutualiser ce relais avec d’autres services plus tard.

Ce Postfix ne doit en revanche jamais livrer directement aux MX des destinataires (Gmail, Outlook…). Une IP résidentielle ou même une IP de VPS sans historique part avec un désavantage rédhibitoire : pas de PTR cohérent, blacklists RBL sur les plages dynamiques, port 25 sortant souvent bloqué par le FAI. Le relais interne rebondit donc vers un smarthost externe — ici Scaleway TEM (Transactional Email), gratuit sur un volume de 300 mails — qui porte la réputation d’envoi à sa place.

QFieldCloud (Django) → Postfix interne (LXC dédiée, port 25) → Scaleway TEM (SASL, port 587) → destinataire

DNS : domaine dédié à l’envoi, zone gérée hors du registrar
#

Le domaine d’envoi (ex. mailrelay.allfabox.fr, distinct du domaine principal pour isoler la réputation) doit être validé dans la console Scaleway TEM, qui fournit les enregistrements à poser :

Enregistrement Rôle
SPF (TXT) autorise les IP de Scaleway TEM à envoyer pour ce domaine
DKIM (TXT/CNAME) signature cryptographique, générée automatiquement
DMARC (TXT sur _dmarc.) politique, p=none suffit pour démarrer
MX blackhole blackhole.tem.scaleway.com priorité 10 — déclare que ce sous-domaine n’accepte aucun mail entrant (RFC 7505)
Info : si le domaine est enregistré chez un registrar mais que sa zone DNS est déléguée ailleurs (Cloudflare, par exemple), c’est bien là qu’il faut poser ces enregistrements — pas chez le registrar. Sur Cloudflare, laissez-les en DNS only (nuage gris) : le proxy casserait leur vérification.

Pas à pas : valider le domaine d’envoi chez Scaleway TEM
#

Scaleway Transactional Email (TEM) est le smarthost retenu ici. L’offre gratuite couvre 300 courriels par mois — large pour les notifications d’une instance à poignée d’utilisateurs. Reste à lui prouver que le domaine d’envoi vous appartient.

1. Ouvrir la console. Créez un compte ou connectez-vous sur console.scaleway.com, puis rendez-vous dans Transactional Email (console.scaleway.com/transactional-email). La liste des domaines est vide au premier passage.

Console Scaleway, section Transactional Email, aucun domaine déclaré
Transactional Email, premier passage : aucun domaine, un bouton « Ajouter un domaine ».

2. Déclarer le domaine. Cliquez sur Ajouter un domaine, choisissez Ajouter un domaine externe à Scaleway et saisissez le sous-domaine dédié à l’envoi — ici mailrelay.allfabox.fr. Acceptez les conditions, puis Valider le nom de domaine.

Formulaire d'ajout d'un domaine externe mailrelay.allfabox.fr dans Scaleway TEM
Domaine externe : Scaleway ne touche pas à la zone, c’est à vous de poser les enregistrements.
Attention : même sur l’offre gratuite, Scaleway exige une carte bancaire valide enregistrée sur le compte avant de laisser valider le domaine. Aucun prélèvement tant que vous restez sous le quota, mais l’étape est bloquante.

3. Relever l’identifiant SMTP. La validation redirige vers l’aperçu du domaine. Le bloc Configuration SMTP y donne le serveur (smtp.tem.scaleway.com), les ports, et surtout le nom d’utilisateur — c’est lui que Postfix présentera au relais. Le mot de passe, lui, n’existe pas encore : ce sera la clé secrète d’une clé API, créée juste après.

Aperçu du domaine mailrelay.allfabox.fr : statut DNS non vérifié, bloc Configuration SMTP avec nom d'utilisateur masqué
L’aperçu du domaine. Le nom d’utilisateur (masqué ici) est l’identifiant d’authentification ; le mot de passe sera la clé secrète API.

4. Récupérer les enregistrements DNS. Onglet Enregistrements DNS : Scaleway liste les quatre entrées à poser — SPF, DKIM, DMARC et un MX blackhole. Toutes les valeurs sont copiables d’un clic.

Onglet Enregistrements DNS de Scaleway TEM listant SPF, DKIM, DMARC et MX pour mailrelay.allfabox.fr
Les quatre enregistrements générés par Scaleway. Le rôle de chacun est détaillé dans le tableau plus haut.

5. Ouvrir la zone chez l’hébergeur DNS. Ici la zone de allfabox.fr est déléguée à Cloudflare : le domaine est chez OVH, mais ses serveurs de noms pointent vers Cloudflare. Direction dash.cloudflare.com → le domaine → Gestion DNS → Enregistrement DNS.

Interface Cloudflare, gestion DNS de allfabox.fr, aucun enregistrement mailrelay
La zone Cloudflare de allfabox.fr, filtrée sur mailrelay : rien encore.

6. Créer les quatre enregistrements. Ajouter un enregistrement, puis pour chacun choisir le type dans la liste déroulante et coller la valeur relevée chez Scaleway.

Cloudflare, liste déroulante des types d'enregistrement DNS, TXT sélectionné
Trois entrées en TXT (SPF, DKIM, DMARC), une en MX.

Dans l’ordre : SPF (v=spf1 include:_spf.tem.scaleway.com -all), DKIM (la longue clé publique sur le sélecteur …_domainkey), DMARC (v=DMARC1; p=none sur _dmarc.mailrelay…), puis le MX blackhole.tem.scaleway.com en priorité 10. Ce dernier déclare, au sens de la RFC 7505, que le sous-domaine n’accepte aucun courriel entrant.

Cloudflare, les quatre enregistrements mailrelay créés, tous en DNS uniquement
Récapitulatif : les quatre entrées, toutes en « DNS uniquement » (nuage gris).

7. Faire vérifier par Scaleway. De retour dans l’aperçu du domaine, Vérifier le domaine. Si les enregistrements sont en place et propagés, le statut passe au vert.

Aperçu Scaleway TEM du domaine mailrelay.allfabox.fr, statut DNS Vérifié en vert
« Tous les enregistrements DNS sont vérifiés. » Le domaine peut désormais émettre.

Pas à pas : la clé API SMTP, via IAM
#

Le mot de passe SMTP manquant est la clé secrète d’une clé API Scaleway. Pas n’importe laquelle : elle doit porter la permission TransactionalEmailEmailSmtpCreate, sans quoi le relais répond 535 Invalid credentials. Trois objets à créer, dans cet ordre, chacun s’appuyant sur le précédent : une policy, une application, puis la clé.

Toute la manœuvre se passe dans IAM (console.scaleway.com/iam).

1. La policy. Onglet Policies : trois policies existent par défaut, on n’y touche pas. Créer une policy.

IAM Scaleway, onglet Policies, les trois policies par défaut
Les trois policies par défaut restent en place ; on en ajoute une quatrième.

Nommez-la — ici sans détour, TransactionalEmailEmailSmtpCreate, du nom de la permission qu’elle porte.

Dans Ajouter des règles, cherchez le produit Transactional Email et cochez l’ensemble d’autorisations TransactionalEmailEmailSmtpCreate — « Create permission for e-mails from SMTP ». Suivant.

Sélection du périmètre de la règle IAM : Tous les projets
Périmètre : « Tous les projets » suffit pour un usage aussi ciblé.

Périmètre sur Tous les projets, Suivant. Pas de condition à ajouter : Valider la règle.

Étape Ajouter des conditions, laissée vide, bouton Valider la règle
Aucune condition — la permission est déjà assez étroite.

Un dernier récapitulatif, puis Créer une policy.

La policy est créée, mais rattachée à aucun principal (Principal : Non) — c’est l’application qui va la porter.

2. L’application. Onglet Applications : une application IAM est un porteur d’identité non humain, fait pour l’accès programmatique. Créer une application.

IAM Scaleway, onglet Applications, aucune application
Aucune application au départ.

Nommez-la (mailrelay-allfabox-fr) et attachez-lui directement la policy TransactionalEmailEmailSmtpCreate créée à l’instant.

Formulaire de création d'application IAM avec la policy TransactionalEmailEmailSmtpCreate attachée
La policy s’attache dès la création : une étape de moins.

Créer une application.

IAM Applications, l'application mailrelay-allfabox-fr créée avec 0 clé API
L’application existe, sans clé API pour l’instant.

3. La clé API. Onglet Clés API, ou directement console.scaleway.com/iam/api-keys. Générer une clé API.

IAM Scaleway, onglet Clés API, aucune clé
Une clé API se compose d’un ID de clé d’accès et d’une clé secrète.

Choisissez le porteur Une application, sélectionnez mailrelay-allfabox-fr, expiration Jamais, et Non pour Object Storage.

Fenêtre Générer une clé API, porteur Une application mailrelay-allfabox-fr
La clé hérite des autorisations de son porteur — donc de la policy attachée à l’application.

Le couple ID de la clé d’accès / Clé secrète s’affiche. C’est la clé secrète qui nous intéresse : Postfix la présentera comme mot de passe SMTP. Elle n’est montrée qu’une seule fois — copiez-la maintenant.

Fenêtre Utilisation des identifiants : ID de la clé d'accès et clé secrète affichés une seule fois
La clé secrète, affichée une fois. (Celle-ci a été créée pour l’article, puis révoquée.)
Astuce : cette clé secrète est la valeur <clé_secrète> du fichier /etc/postfix/sasl_passwd de la section suivante — le <project_id> qui la précède étant le nom d’utilisateur relevé à l’étape 3 de la validation du domaine.

Postfix : deux pièges qui donnent le même symptôme
#

Configuration côté LXC dédiée (/etc/postfix/main.cf) :

postconf -e 'relayhost = [smtp.tem.scaleway.com]:587'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_tls_security_level = encrypt'
postconf -e 'inet_interfaces = all'
postconf -e 'mynetworks = 127.0.0.0/8, <IP_du_conteneur_QFieldCloud>/32'
echo '[smtp.tem.scaleway.com]:587 <project_id>:<clé_secrète>' > /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
systemctl restart postfix

Le login SASL est le Project ID Scaleway, pas l’Access Key d’une clé API existante. Une clé API générale (créée pour l’API HTTP, Terraform, le CLI scw…) est refusée en SMTP avec un 535 Invalid credentials même si elle est valide ailleurs — sa policy IAM doit explicitement inclure TransactionalEmailEmailSmtpCreate.

inet_interfaces vaut loopback-only par défaut sur une install Debian non interactive. Symptôme : Connection refused depuis la machine QFieldCloud alors que Postfix tourne (systemctl status au vert) — ss -lntp révèle 127.0.0.1:25 au lieu de 0.0.0.0:25. Un RST explicite comme celui-ci vient de la pile IP de la cible, pas d’un pare-feu réseau : sur un LAN commuté en L2 direct (même sous-réseau), l’UniFi ou tout autre pare-feu de périmètre n’est jamais dans le circuit.

Validation en couches
#

# 1. Le port répond
$ timeout 5 bash -c 'exec 3<>/dev/tcp/<ip_mailserver>/25 && head -1 <&3'
220 mailserver.allfabox.fr ESMTP Postfix

# 2. Le relais accepte et livre
$ echo "Test" | mail -s "Test" -r noreply@mailrelay.allfabox.fr vous@exemple.fr
$ journalctl -u postfix --since "1 minute ago" --no-pager
# ... status=sent (250 2.0.0 OK: queued as ...)

# 3. L'application elle-même
$ cd src && docker compose --env-file ../.env exec -T app python -c "
from django.core.mail import send_mail
send_mail('Test', 'Corps', 'noreply@mailrelay.allfabox.fr', ['vous@exemple.fr'])
"

Un premier envoi qui atterrit en indésirables (Gmail notamment) est normal — un domaine d’envoi tout juste validé n’a encore aucune réputation. Le signaler comme non-spam accélère l’apprentissage du filtre pour les suivants.

Ce qu’il faut retenir
#

  • Fermer les inscriptions fait du courriel le seul chemin d’entrée en libre-service. L’administrateur garde la création à la main (/admin/ ou manage.py createuser), mais elle ne dispense pas d’un relais : la réinitialisation de mot de passe, elle, n’a pas d’équivalent manuel.
  • DEFAULT_FROM_EMAIL par défaut vaut webmaster@localhost, refusé par n’importe quel relais.
  • EMAIL_PORT et SMTP4DEV_SMTP_PORT désignent deux choses différentes.
  • ACCOUNT_EMAIL_VERIFICATION=mandatory avec un piège à courriels verrouille l’instance entière.
  • Un envoi direct depuis une IP résidentielle finit en spam ou rejeté : le relais interne doit rebondir vers un smarthost externe.
  • 535 Invalid credentials sur un relais SMTP fraîchement configuré signale presque toujours une clé API sans la bonne permission IAM, pas un mot de passe erroné.
  • Connection refused alors que le service tourne évoque inet_interfaces = loopback-only, pas un pare-feu réseau.

Image de couverture : Scott Blake sur Unsplash.

QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 8: Cet article

Articles connexes