Introduction

Avec l’arrêt de la possibilité d’héberger Exchange en interne à une organisation, de plus en plus d’entreprises cherchent à migrer sur des solutions de messageries alternatives.

L’infrastructure d’une messagerie peut être élaborée selon deux approches :

  • Un serveur monolithique utilisant beaucoup de ressources
  • Plusieurs petits serveurs distribués

Cet article détaille, selon la seconde approche, le déploiement d’une infrastructure de messagerie basée sur Cyrus IMAP avec Murder dans un environnement Kubernetes. Il s’adresse à un public déjà connaisseur des concepts de la messagerie et l’hébergement au sein d’une solution comme Kubernetes.

Nous intégrerons OpenLDAP, Postfix, Rspamd, LL::NG, Roundcube, et Z-Push, tout en utilisant Nginx comme reverse proxy. Nous aborderons également l’importance du rate limiting pour sécuriser et optimiser les services.

Cet article décrit une solution de messagerie minimale incluant les fonctionnalités suivantes :

  • Courriel
  • Antispam / Antivirus
  • Filtres Sieve
  • Gestion des rates limits
  • Calendrier
  • Tâche
  • Contact
  • Partage des pièces jointes (WebDAV)
  • Monitoring par Prometheus / Grafana
  • Centralisation des logs via ElasticSearch / Kibana
  • ActivSync
  • Authentification via un service de SSO

Architecture du Système

Partons d’une base solide en utilisant des outils robustes :

  • Cyrus IMAP avec Murder : Murder permet de répartir la charge entre plusieurs serveurs backend Cyrus IMAP. Configuration pour supporter IMAP, LMTP, WebDAV, CalDAV, et CardDAV. Dans notre cas, chaque backend gère 2000 utilisateurs avec réplication pour la haute disponibilité.

  • OpenLDAP : utilisé pour l’authentification centralisée des utilisateurs. Intégration avec Cyrus IMAP pour la gestion des comptes utilisateurs.

  • Postfix : serveur SMTP pour le relais des emails entrants et sortants. Configuration pour utiliser Cyrus IMAP comme backend pour la livraison des emails.

  • Rspamd : solution de filtrage anti-spam intégrée avec Postfix.

  • LemonLDAP::NG : Gestion des identités et des accès pour une authentification sécurisée. Intégration avec Roundcube pour l’authentification unique (SSO).

  • Roundcube : Interface Webmail permettant aux utilisateurs d’accéder à leurs emails via un navigateur web.

  • Z-Push : Permet la synchronisation des emails, contacts et calendriers avec les clients mobiles via ActiveSync.

  • Nginx : Utilisé comme reverse proxy pour diriger le trafic vers les différents services. Assure la terminaison SSL/TLS pour sécuriser les communications.

Ce qui permet de définir comme infrastructure :

[ Nginx Ingress ]
       │
[ LemonLDAP::NG (SSO) ]
       │
[ Roundcube / Z-Push ] - [Redis]
       │
[ Cyrus Murder Frontend ] - [ Cyrus IMAP Backend 1 (2000 users) ]
       │                       [ Cyrus IMAP Backend 2 (2000 users) ]
       │                         [ ... ]
[ Mupdate Master ]
       │
[ Postfix (SMTP) ] - [ Rspamd (Antispam) ]
       │
[ Prometheus / Grafana (Surveillance) ]
       │
[ ELK Stack (Centralisation des logs) ]
       │
[ Velero (Sauvegardes) ]

Cet article se focalise sur les composants centraux de la solution. De la littérature concernant l’intégration des autres éléments est facilement trouvable.

Déploiement dans Kubernetes

Déploiement de Cyrus Murder Frontend

Le Frontend est déployé en tant que Deployment avec un Service pour exposer les ports IMAP (143) et IMAPS (993).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cyrus-murder-frontend
  namespace: mail
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cyrus-murder-frontend
  template:
    metadata:
      labels:
        app: cyrus-murder-frontend
    spec:
      containers:
      - name: cyrus-murder-frontend
        image: cyrusimap/cyrus-imapd:latest
        args: ["-f"]
        ports:
        - containerPort: 143
        - containerPort: 993
        volumeMounts:
        - name: cyrus-config
          mountPath: /etc/imapd.conf
          subPath: imapd.conf
      volumes:
      - name: cyrus-config
        configMap:
          name: cyrus-config

Déploiement des Backends avec Auto-Scaling

Chaque backend est déployé en tant que StatefulSet avec un PersistentVolume pour stocker les données des utilisateurs. Un script surveille le nombre de comptes et déclenche le déploiement d’un nouveau backend lorsque le seuil de 2000 comptes est atteint.

StatefulSet pour un Backend

Le StatefulSet doit être configuré pour permettre la découverte des backends par Cyrus Murder. Chaque backend doit avoir un nom de service unique.

Service pour les Backends

apiVersion: v1
kind: Service
metadata:
  name: cyrus-backend
  namespace: mail
spec:
  clusterIP: None
  selector:
    app: cyrus-backend
  ports:
  - port: 143
    name: imap
  - port: 993
    name: imaps

StatefulSet pour les Backends

Le StatefulSet est configuré pour créer des pods avec des noms uniques (cyrus-backend-0, cyrus-backend-1, etc.).

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cyrus-backend
  namespace: mail
spec:
  serviceName: cyrus-backend
  replicas: 1
  selector:
    matchLabels:
      app: cyrus-backend
  template:
    metadata:
      labels:
        app: cyrus-backend
    spec:
      containers:
      - name: cyrus-backend
        image: cyrusimap/cyrus-imapd:latest
        args: ["-f"]
        env:
        - name: BACKEND_USERS_LIMIT
          value: "2000"
        ports:
        - containerPort: 143
        - containerPort: 993
        volumeMounts:
        - name: cyrus-config
          mountPath: /etc/imapd.conf
          subPath: imapd.conf
        - name: cyrus-data
          mountPath: /var/lib/cyrus
      volumes:
      - name: cyrus-config
        configMap:
          name: cyrus-config
  volumeClaimTemplates:
  - metadata:
      name: cyrus-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 50Gi

Script d’Auto-Scaling

Nous allons prendre un cas simple avec la réalisation d’un script bash mis dans un cron pour gérer la scalabilité des backends. Il est envisageable de réaliser un opérateur directement au sein de Kubernetes pour rendre cela plus dynamique.

Le script surveille le nombre de comptes utilisateurs et augmente le nombre de replicas du StatefulSet si le seuil de 2000 comptes par backend est atteint. Il met également à jour la configuration de Cyrus Murder pour inclure les nouveaux backends.

!/bin/bash

 Variables
NAMESPACE="mail"
STATEFULSET_NAME="cyrus-backend"
MURDER_FRONTEND="cyrus-murder-frontend"
BACKEND_LIMIT=2000

 Récupérer le nombre actuel de replicas
CURRENT_REPLICAS=$(kubectl get statefulset -n $NAMESPACE $STATEFULSET_NAME -o jsonpath='{.spec.replicas}')

 Récupérer le nombre total de comptes utilisateurs
CURRENT_USERS=$(cyradm --user cyrus --auth PLAIN --server $MURDER_FRONTEND --cmd "sam user.count")

 Calculer le nombre de replicas nécessaires
REQUIRED_REPLICAS=$(( (CURRENT_USERS / BACKEND_LIMIT) + 1 ))

 Si le nombre de replicas nécessaires est supérieur au nombre actuel, mettre à jour le StatefulSet
if [ "$REQUIRED_REPLICAS" -gt "$CURRENT_REPLICAS" ]; then
  echo "Scaling StatefulSet to $REQUIRED_REPLICAS replicas..."
  kubectl scale statefulset -n $NAMESPACE $STATEFULSET_NAME --replicas=$REQUIRED_REPLICAS

  # Attendre que les nouveaux pods soient prêts
  echo "Waiting for new backends to be ready..."
  kubectl wait --for=condition=Ready pod -n $NAMESPACE -l app=cyrus-backend --timeout=300s

  # Mettre à jour la configuration de Cyrus Murder
  echo "Updating Cyrus Murder configuration..."
  BACKEND_LIST=""
  for i in $(seq 0 $((REQUIRED_REPLICAS - 1))); do
    BACKEND_LIST+="backend$i.${STATEFULSET_NAME}.${NAMESPACE}.svc.cluster.local:143 "
  done

  # Appliquer la nouvelle configuration
  kubectl create configmap -n $NAMESPACE cyrus-config --from-literal=imapd.conf="
    murder: yes
    mupdate_server: mupdate-master.mail.svc.cluster.local
    backend_servers: $BACKEND_LIST
  " --dry-run=client -o yaml | kubectl apply -f -

  # Redémarrer le frontend pour appliquer la nouvelle configuration
  echo "Restarting Cyrus Murder frontend..."
  kubectl rollout restart deployment -n $NAMESPACE $MURDER_FRONTEND
else
  echo "No scaling required. Current replicas: $CURRENT_REPLICAS, Required replicas: $REQUIRED_REPLICAS"
fi

Explication du script

Scaling du StatefulSet

Le script utilise kubectl scale pour augmenter le nombre de replicas du StatefulSet existant cyrus-backend au lieu de créer un nouveau StatefulSet. Il calcule le nombre de replicas nécessaires en fonction du nombre total de comptes utilisateurs et du seuil de 2000 comptes par backend.

Mise à Jour de la Configuration de Cyrus Murder

Le script génère dynamiquement une liste de backends backend_servers en fonction du nombre de replicas. Il met à jour le ConfigMap cyrus-config avec la nouvelle configuration. Il redémarre le Cyrus Murder Frontend pour appliquer la nouvelle configuration.

Attente des Nouveaux Pods

Le script utilise kubectl wait pour s’assurer que les nouveaux pods sont prêts avant de mettre à jour la configuration.

Configuration de Mupdate Master

Le Mupdate Master synchronise les métadonnées entre les backends. Il est déployé en tant que Deployment.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mupdate-master
  namespace: mail
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mupdate-master
  template:
    metadata:
      labels:
        app: mupdate-master
    spec:
      containers:
      - name: mupdate-master
        image: cyrusimap/cyrus-imapd:latest
        args: ["-f"]
        ports:
        - containerPort: 3905
        volumeMounts:
        - name: cyrus-config
          mountPath: /etc/imapd.conf
          subPath: imapd.conf
      volumes:
      - name: cyrus-config
        configMap:
          name: cyrus-config

Rate Limiting avec Nginx

Rate Limiting pour Cyrus IMAP

Nginx est configuré pour limiter le nombre de requêtes IMAP par seconde (20 requêtes/s par défaut).

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-imap-config
  namespace: mail
data:
  nginx.conf: |
    limit_req_zone $binary_remote_addr zone=imap_limit:10m rate=20r/s;

    server {
      listen 143;
      listen 993 ssl;

      location / {
        limit_req zone=imap_limit burst=5;
        proxy_pass http://cyrus-murder-frontend;
      }
    }

Rate Limiting pour Postfix

Nginx est également configuré pour limiter le nombre de connexions SMTP (10 connexions/s par défaut).

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-smtp-config
  namespace: mail
data:
  nginx.conf: |
    limit_conn_zone $binary_remote_addr zone=smtp_limit:10m;

    server {
      listen 25;

      location / {
        limit_conn smtp_limit 10;
        proxy_pass http://postfix;
      }
    }

Conclusion

L’objectif de cet article est de démontrer la faisabilité d’un tel montage dans lequel nous faisons cohabiter deux typologies d’outils que l’on a peu l’occasion de voir ensemble. En revanche, il est à noter qu’une telle approche demandera beaucoup plus de temps de maintenance qu’une solution entièrement intégrée comme BlueMind par exemple.

Le déploiement de Cyrus IMAP avec Murder dans un cluster Kubernetes, intégré avec OpenLDAP, Postfix, Rspamd, Roundcube, LL::NG, et Z-Push, offre une solution de messagerie robuste et évolutive. L’utilisation de Nginx comme reverse proxy permet de gérer efficacement le trafic entrant et de sécuriser les communications. L’implémentation du rate limiting est cruciale pour protéger les services contre les abus d’utilisation et garantir une disponibilité optimale. Cette architecture modulaire et évolutive est idéale pour gérer au mieux une scalabilité horizontale avec un nombre cohérent de services.