Gérer les mails de plusieurs domaines avec Exim et PostgreSQL
Pour gérer plusieurs domains avec un serveur de mails, il faut passer par des comptes généralement appelés “virtuels” stockés dans une base de données (SQL, LDAP). L'utilisation d'une base permet d'être sûr qu'on ne mélange pas les comptes et les domaines. En effet, si on a un domaine a.fr et un domaine b.fr, un utilisateur bob sur le serveur de mail peut avoir deux adresses bob@a.fr et bob@b.fr. On va donc créer une base de donnée PostgreSQL et configurer Exim pour livrer les mails à partir de celle-ci.
Il existe de nombreuses documentations traitant du sujet sur le net. Par contre, il en existe peu qui utilisent Exim et PostgreSQL. On les a choisi dans cet article parce qu'Exim est le serveur de mail par défaut de Debian. Quant à PostgreSQL, il reste souvent dans l'ombre, alors qu'il a des fonctionnalités très intéressantes qui peuvent nous aider rendre la configuration d'Exim plus lisible (vues, procédures stockées, triggers…)
La configuration montrée ici est très fortement inspirée de celle décrite par Depesz dans sa suite d'articles sur Exim et PostgreSQL.
Installation
Pour installer PostgreSQL, on va suivre l'article dédié à ce sujet. On dédit une base nommée exim à un utilisateur exim :
# su - postgres postgres$ createuser -DEIlPRS exim Enter password for new role: Enter it again: postgres$ createdb -E UTF8 -O exim exim
Pour installer exim, il faut choisir le paquet exim4-daemon-heavy qui contient les fonctionnalités qui nous intéressent, contrairement à exim4-daemon-light installé par défaut :
# aptitude install exim4-daemon-heavy
En choissant d'installer le paquet, les dépendances nécessaires à la connexion à PostgreSQL (la libpq) seront ajoutées, et exim4-daemon-light sera retiré.
Configuration d'Exim
Si Exim n'a jamais été configuré, debconf pose quelques questions, on peut faire les choix suivants sachant qu'on éditera le fichier de configuration à la main juste après :
- Le type de configuration, on choisit une configuration locale, avec 127.0.0.1 comme adresse d'écoute.
- Si on souhaite découper le fichier de configuration en petits morceaux, on choisit “Oui”. Ca sera plus pratique pour la suite.
- Le format de mailbox Maildir.
exim4-config (dpkg-reconfigure exim4-config) ou en éditant /etc/exim4/update-exim4.conf.conf.
Sous Debian, la configuration d'Exim se fait dans /etc/exim4, le programme update-exim4.conf permet de générer le fichier de configuration qui sera utilisé par Exim. Le script de démarrage, le récrée au besoin lors du (re)démarrage du service. Le fichier de configuration effectif est /var/lib/exim4/config.autogenerated. Enfin, selon l'utilisation de la configuration découpée ou non, le fichier à éditer est soit /etc/exim4/exim4.conf.template, soit les fichiers présents dans /etc/exim4/conf.d et ses sous-répertoires.
Dans la configuration multi-domaines, on va modifier un certains nombres de fichiers pour utiliser la base de données. Le premier fichier à modifier est /etc/exim4/update-exim4.conf.conf, voici les paramètres les plus pertinents:
dc_eximconfig_configtype='internet' dc_other_hostnames='' dc_local_interfaces='192.168.0.1' dc_use_split_config='true' dc_localdelivery='maildir_home'
le paramètre dc_local_interfaces indique l'adresse IP sur laquelle les mails arriveront. On utilise la configuration découpée (dans /etc/exim4/conf.d) avec dc_use_split_config et le format de boîte Maildir.
Dans /etc/exim4/conf.d/, il y a un certains nombre de répertoires, contenant des fichiers préfixés par un numéro pour indiquer leur ordre dans la configuration. Lorsque update-exm4.conf crée le fichier final, il concatène les fichiers en parcurant les répertoires dans cet ordre : main, acl, router, transport, retry, rewrite et auth.
Connexion au serveur PostgreSQL
On part du principe qu'on peut se connecter au server PostgreSQL avec l'utilisateur exim depuis localhost et que PostgreSQL utilise le port TCP/5432 (le port par défaut). Ceci permet de facilement changer la configuration si on déplace le serveur PostgreSQL sur une autre machine.
Pour qu'Exim utilise notre base de données, on a ajoute la variable pgsql_servers dans /etc/exim4/conf.d/main/01_exim4-config_listmacrosdefs :
... # debconf-driven macro definitions get inserted after this line UPEX4CmacrosUPEX4C = 1 # PostgreSQL lookup configuration hide pgsql_servers = 127.0.0.1::5432/exim/exim/motDePasseExim # Create domain and host lists for relay control # '@' refers to 'the name of the local host' ...
Le format de pgsql_servers est :
<hostname>::<port>/<database>/<user>/<password> : ...
On peut donner plusieurs serveurs et/ou bases de données en séparant les informations de connexion par ”:”, c'est pour cela que le port de connexion est séparé du nom d'hôte par ”::”.
hide pgsql_servers = (/var/run/postgresql/.s.PGSQL.5432)/exim/exim/motDePasseExim
Pour finir, le paramètre pgsql_servers est préfixé du mot-clé “hide” qui permet de cache la valeur du paramètre aux utilisateurs non privilégiés qui peuvent tenter d'obtenir cette valeur avec /usr/sbin/exim4 -bP :
user$ /usr/sbin/exim4 -bP| grep pgsql pgsql_servers = <value not displayable>
Domaines
La première chose à configurer est la liste des domaines que gère notre installation. On va bien sûr stocker cette liste dans la base de données. Pour cela, on ajoute une table “domains”:
exim=> CREATE TABLE domains (
id SERIAL,
domain TEXT NOT NULL DEFAULT '' UNIQUE,
PRIMARY KEY (id)
);
Puis, on configure Exim pour qu'il aille cherche la liste des domaines dans PG, on modifie donc la variable domainlist local_domains, pour ajouter la recherche dans la base de données en plus de la configuration Debian. Dans le fichier /etc/exim4/conf.d/main/01_exim4-config_listmacrosdefs :
# List of domains considered local for exim. Domains not listed here
# need to be deliverable remotely.
domainlist local_domains = MAIN_LOCAL_DOMAINS : \
@[ ] : \
${lookup pgsql {SELECT domain FROM domains WHERE domain = '${domain}' }}
Ensuite, on rédemarre Exim, et on fait quelques tests :
# invoke-rc.d exim4 restart Stopping MTA for restart: exim4_listener. Restarting MTA: exim4.
On essaye d'abord avec root@localhost, qui est redirigé vers l'utilisateur orgrim dans /etc/aliases:
# exim4 -bt root@localhost
R: system_aliases for root@localhost
R: system_aliases for orgrim@debian.home.orgrim.net
R: userforward for orgrim@debian.home.orgrim.net
R: procmail for orgrim@debian.home.orgrim.net
R: maildrop for orgrim@debian.home.orgrim.net
R: lowuid_aliases for orgrim@debian.home.orgrim.net (UID 1000)
R: local_user for orgrim@debian.home.orgrim.net
orgrim@debian.home.orgrim.net
<-- root@localhost
router = local_user, transport = maildir_home
On voit donc que les mails locaux sont bien pris en comptes par Exim. Maintenant, on essaye avec root@a.fr, le domaine qu'on souhaite gérer :
# exim4 -bt root@a.fr R: dnslookup for root@a.fr root@a.fr is undeliverable: Unrouteable address
Exim essaye de le transmettre vers le serveur de mail de a.fr, ce qui est normal vu que notre table des domaines est vide. Maintenant, on ajoute un domaine dans la base :
exim=> INSERT INTO domains (domain) VALUES ('a.fr');
Et on teste de nouveau :
# exim4 -bt root@a.fr
R: system_aliases for root@a.fr
R: system_aliases for orgrim@debian.home.orgrim.net
R: userforward for orgrim@debian.home.orgrim.net
R: procmail for orgrim@debian.home.orgrim.net
R: maildrop for orgrim@debian.home.orgrim.net
R: lowuid_aliases for orgrim@debian.home.orgrim.net (UID 1000)
R: local_user for orgrim@debian.home.orgrim.net
orgrim@debian.home.orgrim.net
<-- root@a.fr
router = local_user, transport = maildir_home
Voilà, Exim a bien trouvé le domaine dans notre base de donnée.
Comptes
Installer le module pgcrypto, qui servira à chiffrer le mot de passe et à l'authentification par le serveur IMAP (dovecot):
# aptitude install postgresql-contrib # su - postgres $ psql -d exim -f /usr/share/postgresql/8.3/contrib/pgcrypto.sql
Créer la table des comptes utilisateurs:
CREATE TABLE accounts ( id SERIAL, username TEXT NOT NULL DEFAULT '', password TEXT NOT NULL DEFAULT '', fullname TEXT NOT NULL DEFAULT '', domain_id INTEGER NOT NULL DEFAULT 0 REFERENCES domains (id), PRIMARY KEY (id) );
Ajouter un utilisateur en chiffrant son mot de passe en MD5-CRYPT:
INSERT INTO accounts (username, password, fullname, domain_id) VALUES ('orgrim', crypt('new password', gen_salt('md5')), 'Orgrim', (SELECT id FROM domains WHERE domain = 'orgrim.net'));
Vérification:
exim=> SELECT * FROM accounts; id | username | password | fullname | domain_id ----+----------+------------------------------------+----------+----------- 1 | orgrim | $1$8gr9GqhM$7FAdtAkVBMzVzxboBZ5Jp1 | Orgrim | 2
Routeur:
Transport:
User vmail
Process séparés pour livrer en temps que vmail: QUEUERUNNER='separate' dans /etc/default/exim4
Aliases
Table des aliases:
CREATE TABLE aliases ( id SERIAL, domain_id INTEGER NOT NULL DEFAULT 0 REFERENCES domains (id), username TEXT NOT NULL DEFAULT '', destination TEXT NOT NULL DEFAULT '', PRIMARY KEY (id) );
Tests:
exim=> INSERT INTO domains (domain) VALUES ('orgrim.net');
exim=> INSERT INTO aliases (domain_id, username, destination) SELECT id, 'nico', 'orgrim@orgrim.net' FROM domains WHERE domain = 'orgrim.net';
exim=> INSERT INTO aliases (domain_id, username, destination) SELECT id, 'nico', 'admin@orgrim.net' FROM domains WHERE domain = 'orgrim.net';
exim=> SELECT * from aliases;
id | domain_id | username | destination
----+-----------+----------+-------------------
1 | 2 | nico | orgrim@orgrim.net
2 | 2 | nico | admin@orgrim.net
(2 rows)
exim=> SELECT destination FROM aliases JOIN domains ON domains.id = aliases.domain_id WHERE username = 'nico' AND domain = 'orgrim.net';
destination
-------------------
orgrim@orgrim.net
admin@orgrim.net
Config d'exim: ajout de /etc/exim4/conf.d/router/450_exim4-config_pgsql_aliases (attention au numéro, l'ordre des routers est important)
### router/450_exim4-config_pgsql_aliases
#################################
# This router handles aliasing declared in the DB
#
pgsql_aliases:
debug_print = "R: pgsql_aliases for $local_part@$domain"
driver = redirect
allow_fail
allow_defer
data = ${lookup pgsql {SELECT destination FROM aliases JOIN domains ON domains.id = aliases.domain_id WHERE username = '${local_part}' AND domain = '${domain}'} }
file_transport = address_file
pipe_transport = address_pipe
Dans, cette configuration, orgrim est un compte local, admin n'existe pas:
# exim4 -bt nico@orgrim.net
R: system_aliases for nico@orgrim.net
R: pgsql_aliases for nico@orgrim.net
R: system_aliases for orgrim@orgrim.net
R: pgsql_aliases for orgrim@orgrim.net
R: userforward for orgrim@orgrim.net
R: procmail for orgrim@orgrim.net
R: maildrop for orgrim@orgrim.net
R: lowuid_aliases for orgrim@orgrim.net (UID 1000)
R: local_user for orgrim@orgrim.net
R: system_aliases for admin@orgrim.net
R: pgsql_aliases for admin@orgrim.net
admin@orgrim.net is undeliverable: Unrouteable address
<-- nico@orgrim.net
orgrim@orgrim.net
<-- nico@orgrim.net
router = local_user, transport = maildir_home
Chiffrage des communications
Authentification
Exim:
begin authenticators
...
login:
driver = plaintext
public_name = LOGIN
server_prompts = "Username:: : Password::"
server_condition = ${lookup pgsql {SELECT verify_password('${quote_pgsql:$1}', '${quote_pgsql:$2}') }}
server_set_id = $1
PostgreSQL:
CREATE OR REPLACE FUNCTION verify_password(in_user TEXT, in_password TEXT) RETURNS INT4 AS $BODY$ DECLARE use_user TEXT; use_domain TEXT; tempint INTEGER; BEGIN use_user := split_part(in_user, '@', 1); use_domain := split_part(in_user, '@', 2); SELECT a.id INTO tempint FROM accounts a JOIN domains d ON a.domain_id = d.id WHERE a.username = use_user AND d.domain = use_domain AND a.password = crypt(in_password, a.password); IF NOT FOUND THEN RETURN NULL; END IF; RETURN 1; END; $BODY$ LANGUAGE plpgsql;
Passage en production
- DNS
- Firewall
- Tests pour de vrai