Skip to main content
OxiMail, c’est un binaire, une unité systemd, une base SQLite et un répertoire de blobs. L’exploitation quotidienne est à l’avenant : lire journald, collecter un point de métriques, garder des sauvegardes, et remplacer le binaire pour mettre à jour.

Les journaux

Le serveur écrit sur stdout et journald capture ; il n’y a pas de /var/log/oximail :
Le niveau se monte pour une durée bornée, jamais jusqu’à nouvel ordre : oximail log elevate --directives oximail_imap=debug --duration-secs 600 (ou PUT /admin/v1/logs/level) et il revient de lui-même ; l’élévation et le retour sont deux événements du journal. Un changement durable va dans le TOML, où il reste visible. L’échange brut d’un compte ou d’un pair se capture par oximail log trace, borné en portée, en durée, caviardé et purgé à l’arrêt (CLI, API d’administration). La journalisation utilise des champs tracing structurés (account_id, tenant_id, method, error), et les messages décrivent ce qui s’est réellement passé, pas ce qui était prévu : une règle dure du projet. Le contexte de diagnostic est volontairement attribuable : une session IMAP qui sonde un dossier inexistant journalise le compte et l’adresse du pair, un SQLITE_BUSY nomme la table et l’opération concernées.

Ce qu’une ligne enregistre

Au niveau info, le journal enregistre des événements, pas des échanges (ADR-140) :
  • Un message sortant s’écrit en une ligne queued (expéditeur, nombre de destinataires, taille, origine), puis une ligne delivered par destinataire, à la façon de Postfix : le relais atteint (host[ip]:port), l’adresse locale de départ, la version TLS, la tentative, le temps passé en file et la réponse du pair. Les lignes « attempting » et « connected », et le rapport post-quantique de chaque remise, passent en debug (ce dernier est aussi un compteur).
  • Un message entrant s’écrit en une ligne message accepted qui porte son verdict anti-spam (spam_score, flag_junk) et dit si la session était en TLS ; les lignes séparées de l’anti-spam et de STARTTLS passent en debug.
  • Une session WebSocket écrit sa ligne de fermeture avec sa durée et sa cause ; la ligne de fermeture d’une session IMAP nomme le client d’après sa commande ID.
  • Une connexion CalDAV ou CardDAV écrit une ligne par appareil et par heure : le compte, le secret utilisé (l’identifiant d’un mot de passe d’application, ou le mot de passe du compte), l’adresse du client et le client que la requête annonce. IMAP et SMTP en disent déjà autant par session : un administrateur distingue ainsi un appareil d’un autre sur chaque protocole, et voit quel appareil utilise encore le mot de passe du compte.
  • Un état répété s’écrit une fois. Un abonnement Web Push qui ne correspond à aucun onglet ouvert est signalé une fois par abonnement en info, puis en debug ; l’accès à un compte partagé s’écrit une fois par session et par paire (bénéficiaire, propriétaire, collection, niveau), et à nouveau quand le niveau change, tandis que les refus gardent une ligne à chaque fois et que oximail_jmap_shared_access_calls_total compte toujours chaque appel. Un client qui présente une adresse IP comme SNI TLS n’est plus journalisé par rustls.
  • Les plafonds de débit en force sont nommés. La ligne de démarrage donne les valeurs TOML de [rate_limit] sous http_per_ip_baseline et http_per_account_baseline ; chaque application des surcharges rate_limit.* stockées (au démarrage et sur PUT /admin/v1/config) journalise ensuite rate-limit overrides in force avec champ=effectif (baseline b) pour chaque surcharge qui diffère du TOML, ou none in force.

Les journaux de poignée de main TLS

Les quatre écouteurs qui terminent TLS (IMAPS, IMAP STARTTLS, SMTPS, SMTP STARTTLS) séparent deux événements qui partageaient jusqu’ici un unique avertissement :
  • Un pair parti avant d’avoir rien négocié. Un contrôle de disponibilité TCP, un scan de port, un client qui renonce. Ce n’est pas un échec et ce n’est plus journalisé comme tel : la ligne descend d’un niveau, garde son pair et son texte d’erreur, et RUST_LOG=debug en restitue toujours chaque octet.
  • Un vrai échec de négociation. Aucune suite de chiffrement ni aucun groupe en commun, un message malformé, une alerte reçue, un certificat refusé. Cela reste un avertissement, parce que c’en est un.
Les deux branches portent un champ reason stable. Accrochez votre supervision à ce champ plutôt qu’au texte du message. Ce qui justifie la distinction : une instance de production émettait 480 de ces avertissements par jour pour une seule sonde de supervision qui ouvrait le 993 toutes les trois minutes et raccrochait. Un avertissement qui se déclenche 480 fois par jour de façon parfaitement inoffensive n’est pas une gêne esthétique : il entraîne à ignorer la catégorie entière, et un vrai échec de poignée de main s’y cache ensuite, même port, même message, même niveau. Cela compte surtout juste après un changement des groupes négociés, dont le mode de panne est exactement une poignée de main qui cesse d’aboutir avec un client incompatible. Les deux branches de délai explicite (120 s en IMAPS, 30 s en STARTTLS) restent des avertissements : un pair qui tient une connexion deux minutes sans conclure est rare et mérite réellement un coup d’œil.

Les métriques

Un point Prometheus est servi sur la surface d’administration à /metrics. Donnez-lui son propre jeton : [admin] metrics_token_file = "/etc/oximail/metrics.token" (un fichier à côté, mode 0600, une ligne, jamais une clé en ligne, puisque le TOML est sauvegardé et comparé) fait accepter à /metrics ce bearer en lecture seule et, une fois posé, le jeton d’administration cesse d’ouvrir /metrics sur cette box pour de bon, même si le fichier est retiré ensuite. Avant ce réglage, une configuration de collecte Prometheus détenait un jeton capable de lire le courrier, de modifier la configuration et de lever des bannissements. doctor présente le jeton des métriques à sa propre sonde, et rapporte un refus comme une dégradation plutôt que skipped. Les jauges qui méritent une alerte : Où passe le temps d’une écriture. Cinq histogrammes (intervalles de 0,5 ms à 30 s), publiés dès le démarrage, montrent ce qu’un client attend. Aucune règle d’alerte ne les lit : ils servent à mesurer avant de changer un réglage du stockage. oximail_durability_barrier_seconds{door,result} mesure la synchronisation qui précède une réponse promettant que les données sont sur un stockage stable (stockage). Chaque étiquette est un ensemble fermé : ni adresse, ni compte, ni chemin, ni boîte aux lettres. Les métriques par organisation peuvent gagner une étiquette domain (config [metrics]) ; laissez-la désactivée sauf besoin précis, le coût en cardinalité est réel. Deux étiquettes sont des ensembles fermés par construction. L’étiquette method de oximail_jmap_requests_total est un nom de méthode enregistré ou unknown : un nom de méthode qu’un client invente ne crée aucune série à lui. L’étiquette client de oximail_client_requests_total (la répartition des clients) ne compte que les requêtes des clients de courrier, d’agenda et de contacts, sur /jmap, /caldav, /carddav et leurs documents de découverte, jamais une collecte Prometheus ni une sonde de santé ; un navigateur web est Browser, les outils d’OxiMail eux-mêmes sont CLI/API, et chaque session IMAP authentifiée compte une fois, à sa fin, en imap:<famille> lue dans sa commande ID (imap:Unknown sans elle), jamais additionnée aux requêtes HTTP.

Les workers d’arrière-plan

Le binaire exécute sa maintenance lui-même ; il n’y a aucun cron externe à installer :
  • Worker de la file de remise : réessais sortants avec backoff, génération de DSN (couche SMTP).
  • Worker d’index de recherche : groupe les commits Tantivy ; valide au repos de la file. Son rattrapage indexe ce qui est dû, par lots de 256 lignes avec un budget de lecture de 2 secondes, pour qu’une écriture en direct n’attende jamais longtemps derrière un commit ; c’est aussi lui qui reconstruit l’index après une mise à jour (l’index de recherche).
  • Worker de checkpoint WAL : publie les jauges de taille, fait un checkpoint TRUNCATE au-delà de 64 Mio.
  • Nettoyage fail2ban : expire les bannissements toutes les quelques minutes.
  • Worker de réindexation : fait converger la file de réindexation ; les conditions terminales résolvent les entrées au lieu de réessayer sans fin. Il ne sélectionne rien tant qu’aucun index n’est ouvert, si bien que les marques qui attendent l’index ne sont pas consommées d’un coup.
  • Revérification des domaines de relais : recontrôle les preuves DNS des domaines de relais et d’enveloppe deux minutes après le démarrage, puis toutes les six heures ; une preuve suspendue, ou non revérifiée depuis 72 heures, diffère le courrier qu’elle couvrait en 451 4.7.1 jusqu’à ce qu’elle soit de nouveau prouvée (relais sortant).
  • Maintenance des effacements : adopte et reprend les travaux d’effacement dont le pilote est mort ; balayage des orphelins au démarrage.
  • Rétention d’audit : purge les lignes d’audit au-delà de l’horizon.
  • Ordonnanceurs d’alertes : déclenchent les rappels d’agenda et de tâches.

Les sauvegardes

Une sauvegarde doit capturer à la fois la base SQLite et le répertoire de blobs (et /etc/oximail, qui porte la configuration, les clés DKIM et le matériel de clé racine : il ne garde plus le contenu, mais il enveloppe des secrets valables ailleurs, hachages d’app passwords et clés d’API de fournisseurs entre autres). La base d’une sauvegarde est un SQLite ordinaire et les blobs sont en clair, donc la copie qui quitte la box doit elle-même être chiffrée : la sauvegarde de l’hyperviseur, chiffrée côté client, est la sauvegarde de référence (chiffrement au repos). verify-backup contrôle l’intégrité d’une sauvegarde avant d’en avoir besoin. Testez une restauration sur une machine de test, pas pendant un incident.

Sans hyperviseur : le dépôt chiffré

Une box dont l’hyperviseur ne prend pas d’instantanés (un VPS chez un hébergeur, un Raspberry Pi, une petite box de cabinet) se sauvegarde avec oximail backup --to <dépôt>. Le dépôt contient des objets chiffrés en age (chaque pièce jointe, la base, le répertoire de configuration, le séquestre de clé, le registre des effacements) et un instantané par passage, qui les liste.
  • La box écrit des sauvegardes qu’elle ne peut jamais relire. Elle ne détient que les clés publiques de [backup] recipients (age1…) ; l’identité qui les ouvre, écrite une fois par oximail backup key generate --out <fichier> --apply, quitte la box. Plusieurs destinataires ouvrent chacun les sauvegardes seuls.
  • Incrémental, et repris après une coupure. Les pièces jointes ne changent jamais (elles sont nommées par leur contenu) : une pièce déjà dans le dépôt ne repart pas ; un passage écrit son instantané en dernier, et un passage interrompu reprend où il s’était arrêté.
  • Authentifié. age ne dit pas qui a chiffré un fichier : chaque instantané porte donc un HMAC sous une clé de la box, dont la copie est dans le fichier d’identité ; un instantané forgé ou altéré est refusé. --json donne le SHA-256 de l’instantané : notez-le hors de la box, et restore --expect-sha256 refuse tout autre.
  • La box ne supprime jamais. Un rançongiciel root sur la box peut faire ce que la box peut faire : donnez-lui une destination où elle ne peut qu’ajouter, et lancez oximail backup prune --repository <dépôt> --identity <fichier> (rétention par jour, semaine et mois, simulation par défaut) là où vit l’identité.
  • Relisible sans OxiMail. Chaque fichier s’ouvre avec l’outil age : age -d -i identite.txt snapshots/<nom>.age donne la liste des objets en lignes JSON, age -d … objects/ab/ab12… une pièce jointe, age -d … | gunzip la base.
restore authentifie l’instantané et vérifie chaque objet avant d’écrire quoi que ce soit, fusionne le registre des effacements, gèle les comptes effacés revenus, et laisse la configuration dans restored-config/ pour relecture (--with-config l’écrit en place). Ce que le dépôt montre à qui le détient : le nombre et la taille des objets, et quand de nouveaux arrivent ; les noms et les contenus sont chiffrés. L’ancien --output --encrypt (une phrase de passe, tout lu en mémoire) ne reste que pour restaurer les sauvegardes existantes. Dans un bucket. --to s3://bucket/préfixe, avec [backup] s3_endpoint, s3_region, s3_access_key_id et s3_secret_access_key_file (0600). Le bucket doit être créé avec l’Object Lock et une rétention par défaut d’au moins 7 jours, et la clé de la box ne peut qu’ajouter et lister. Avant chaque passage, la box vérifie les deux sur le bucket lui-même : elle lit le versioning et le verrou, puis écrit un objet sonde qu’elle doit être incapable de supprimer et de relire. Sinon, le passage n’écrit rien. Sans versioning, « ajouter seulement » ne veut rien dire (une écriture sur un nom existant le remplace) : un fournisseur sans Object Lock est refusé : gardez-y un répertoire, tiré par une autre machine. Hors de la box, restauration, vérification et prune prennent la clé de l’opérateur dans les variables AWS_* standard ; les suppressions de prune sont des marqueurs (ajoutez une règle de cycle de vie pour les versions non courantes), et il prolonge le verrou de tout ce qui est gardé. COMPLIANCE ou GOVERNANCE. Les deux modes de verrou sont acceptés. Un verrou en mode GOVERNANCE tient face à la box, dont la clé ne peut pas supprimer (la sonde le prouve), et permet à un opérateur d’annuler une rétention posée par erreur, que COMPLIANCE rend définitive. Mais une clé qui détient la permission de contournement peut le lever : un passage vers un tel bucket le dit donc à chaque fois, par une ligne WARNING, dans le journal et par un champ warnings en --json. L’enregistrement du passage garde le mode du verrou, et doctor backup_repository le rappelle dans son détail sans faire du contrôle un avertissement permanent. COMPLIANCE est le mode recommandé ; sous GOVERNANCE, la clé qui lance prune ne détient jamais s3:BypassGovernanceRetention. Les verrous comptent le temps à l’horloge du stockage. Chaque requête porte l’heure de la box et le stockage refuse un écart de plus de 15 minutes : une horloge décalée arrête donc déjà le passage. Ce refus (RequestTimeTooSkewed) nomme désormais les deux horloges et dit que les verrous dépendent de celle du stockage. Une box compromise ne réécrit pas l’historique. La clé qui authentifie les instantanés vit sur la box : une fois la box prise, des instantanés falsifiés sont authentiques. Les instantanés sont donc datés par l’horloge du dépôt, jamais par la date qu’ils portent. Un instantané qui se dit créé après que le dépôt l’a reçu, qui partage sa séquence ou qui a été écrit deux fois est suspect : prune ne supprime rien et le nomme, et la restauration ne le prend jamais sauf s’il est nommé par --snapshot. prune ne supprime un objet inutilisé qu’après l’avoir vu tel à deux passages séparés du délai de grâce, et jamais un objet reçu après le dernier instantané. Ramener un compte. oximail account import --from-backup <dépôt> --identity <fichier> --account <adresse> [--into <adresse>] [--apply] ramène le courrier, les contacts, les événements, les tâches, les fichiers du Drive et les notes d’un compte à côté de ce qu’il contient, sous un dossier au nom du jour de l’instantané (Restored 2026-09-20) : rien n’est remplacé, et ce que le compte contient encore n’est pas ramené deux fois. --into écrit dans un autre compte (un collègue parti). Refusés, sans rien écrire : un compte effacé, une adresse qui appartenait à un autre compte dans l’instantané, un autre tenant, un compte gelé. Filtres, partages et droits ne sont pas restaurés ; un message qui nomme une personne effacée est d’abord caviardé ; le quota est imputé. Simulation par défaut. Un salon de chat revient entier, pas compte par compte : oximail chat restore-room --room <id> --from-backup <dépôt> --identity <fichier> [--apply] sort du dépôt les lignes du salon et ses pièces jointes. Un exercice de restauration que doctor voit. verify-backup --from <dépôt> --identity <fichier> --full --record, là où vit l’identité, déchiffre chaque objet et, si rien ne cloche, laisse un reçu daté dans le dépôt. La box liste les reçus avec sa clé (elle ne peut pas les lire), et doctor backup_restore_drill avertit quand aucun n’a moins de 90 jours. Un objet de plus de 256 Mio part vers un bucket en parties de 64 Mio. Débit et durée. [backup] max_rate_mbps plafonne ce qu’un passage envoie ; max_duration_minutes l’arrête entre deux objets, sans instantané, et le passage suivant continue (faites le premier envoi sans). doctor backup_repository lit le dépôt avec la clé de la box : le dernier instantané qu’elle a écrit doit y être, et un premier envoi encore inachevé après un jour est un échec — rien ne peut encore être restauré. Le plan est déclaré, et doctor vous y tient. L’assistant d’installation demande comment la box sera sauvegardée et écrit la réponse en [backup] strategy ("hypervisor-snapshot", le défaut, "oximail-backup" ou "none"). doctor backup_plan échoue sur une box sans plan ou en "none", et sur un plan oximail-backup qui n’a jamais tourné ; une exécution de plus de sept jours est un avertissement. Un instantané de l’hyperviseur est invisible depuis la box, donc l’hyperviseur le dit : son hook vzdump, en backup-end, lance oximail backup mark --source hypervisor --apply, qui inscrit la date et la source dans le marqueur que lit backup_plan (--source external pour un autre outil de l’hôte). Sans marque, le contrôle est un avertissement, « déclaré à l’installation, jamais confirmé de l’extérieur » : une déclaration que rien n’a jamais confirmée n’est pas une sauvegarde ; avec une marque, il montre la date de la dernière confirmation, et une confirmation de plus de sept jours est un avertissement (le hook ou la tâche s’est arrêté). Un plan oximail-backup se juge sur le seul enregistrement du verbe : une marque de l’hyperviseur n’en tient pas lieu. Le séquestre restaure la même box. Le séquestre de récupération de la clé racine porte aussi le secret d’instance, la graine de tous les jetons bearer : une box restaurée sur une machine neuve depuis son séquestre est la même box et garde ses sessions. Chaque instance enregistre une identité stable ; doctor instance_identity échoue quand le secret a changé depuis la création de la base (les jetons émis avant ne sont plus valides) et nomme les deux issues : remettre le secret d’origine depuis le séquestre (oximail key recover), ou accepter le changement (oximail key adopt-identity --yes). Un séquestre fait avant cela ne porte que la clé racine : doctor server_key_escrow avertit là où cela compte (une box dont la clé a été tournée), et oximail key upgrade-escrow --yes le réécrit sous la même clé de secours (chiffrement au repos). oximail backup --tenant <id> restreint une sauvegarde à une seule organisation. Deux propriétés de ce mode méritent d’être connues, car toutes deux ont été un temps discrètement fausses :
  • Il emporte les blobs de pièces jointes de l’organisation, et pas seulement ses lignes. Une archive par organisation dont les messages se restaurent sans leurs pièces jointes se vérifie sans erreur tout en étant irrestaurable au seul sens qui compte.
  • L’ensemble des tables filtrées est dérivé du schéma et gardé par un test, non tenu à la main. Une liste manuelle devient périmée dès qu’une table est ajoutée, et la panne est silencieuse dans le mauvais sens : des lignes appartenant à d’autres organisations voyagent dans un export censé n’en concerner qu’une.

Les mises à jour

Une mise à jour est un remplacement de binaire ; il n’y a aucune étape de migration à lancer :
Au démarrage, les migrations de schéma en attente s’appliquent automatiquement dans une transaction exclusive ; un échec annule tout et le serveur refuse de démarrer plutôt que de tourner à moitié migré. Vérifiez ensuite le déploiement :
oximail status, oximail health et oximail doctor offrent des contrôles locaux (processus, ports, configuration, stockage) sans l’aller-retour HTTP. Trois gardes se tiennent entre un binaire et une base qu’il ne devrait pas toucher :
  • Un binaire plus ancien ne tourne jamais sur une base plus récente. La garde de démarrage ADR-104 couvre désormais aussi chaque verbe qui ouvre la base en direct (sql, reindex, search purge-orphans, chat rekey, principal reconcile, doctor), et oximail schema check est cette garde en verbe : sortie 0 quand ce binaire accepte la base, 2 quand la base est plus récente ou illisible, en lecture seule, sans jamais migrer. Un espace de noms de migration que le binaire ne connaît pas (une base qu’une extension a migrée) est un refus au démarrage qui nomme l’espace, et schema check répond "verdict": "unknown-namespace".
  • restore refuse une sauvegarde faite par un binaire plus récent, et le manifeste porte la version de schéma du cœur pour ce jugement, puisque l’ancienne migration_version était le maximum sur tous les espaces de noms.
  • Un retour arrière se prouve avant le remplacement, par le binaire cible. Le chemin de déploiement lance schema check avec le binaire précédent sur chaque hôte avant de revenir en arrière, au lieu de découvrir le refus au démarrage suivant ; relay-spool reabsorb ne touche plus au schéma pour faire accepter la base à un ancien binaire.
Le fichier de configuration est un contrat : une clé inconnue est une erreur au chargement, un bloc répété s’imbrique ([[smtp.dkim_keys]] est la convention, défendue par un test), et doctor, check-config et tout autre verbe chargent les sections d’extension que le serveur lui-même lit. check-config refuse aussi une écoute loopback derrière un proxy inverse qu’elle ne reconnaît pas (tous les visiteurs partageraient un seul seau de limitation) et un MX de secours sans spool de relais. setup verify sort enfin non zéro (0 tout va bien, 1 dégradé, 2 un contrôle en échec), dit qui renouvelle le certificat (cert_strategy, renewal_owner) et ce que la box doit joindre ; l’assistant publie l’IPv6 du serveur (AAAA, ip6: dans le SPF) et ne prend plus une adresse v6 pour une v4. Aucun verbe CLI n’exige plus un secret sur sa ligne de commande : chaque jeton prend une forme --…-file, puisqu’un argument se lit dans la liste des processus et l’historique du shell. SQLite alloue chez jemalloc, dans une arène qui lui est propre. L’allocateur du serveur est jemalloc depuis juin, mais SQLite est du C et n’avait jamais quitté glibc, dont les arènes par fil gardent la mémoire libérée ; mesuré sur une box de production, jemalloc portait 0,3 % du tas. Les jauges oximail_jemalloc_* décrivent désormais le tas qu’elles nomment. Le premier démarrage après une mise à jour peut reconstruire l’index de recherche. Quand une version change la génération de l’index, le serveur met l’ancien de côté, sert un nouvel index amorcé depuis lui et recompose chaque document en arrière-plan. Les écoutes n’attendent pas, et doctor search_index montre l’avancement (l’index de recherche). Un binaire plus ancien refuse l’index plus récent, comme il refuse la base plus récente. Les copies locales ont une politique de rétention, et le déploiement l’applique. Une fois le remplacement vérifié actif, les deux binaires .bak les plus récents survivent et tout instantané de base de plus de 14 jours part, y compris les copies posées à la main. Un hôte dont le remplacement a échoué garde tout, car c’est son matériel de retour arrière. Deux choses que ce n’est pas : ce n’est pas votre sauvegarde (la couche durable reste les sauvegardes hors machine), et cela ne touche qu’aux instantanés data.db*.pre*, jamais à la base vivante. Sans gestion, l’accumulation est discrète : une machine avait amassé 85 Go de binaires de retour arrière, et un MX de secours était à 89 % de disque.

L’arrêt propre

systemctl stop (SIGTERM) déclenche un drainage : les workers terminent leurs éléments en cours dans un budget borné, l’index de recherche est vidé, et le WAL reçoit un checkpoint. Les redémarrages se terminent normalement dans le temps dont les workers ont réellement besoin, c’est-à-dire quasi instantanément. Un avertissement « drain cap reached » dans les journaux signale un worker vraiment lent et mérite une enquête. Un redémarrage n’accepte plus une connexion qu’il coupe ensuite. À l’arrêt, les écouteurs SMTP, IMAP et HTTPS cessent aussitôt d’accepter : le noyau refuse ce qui arrive ensuite, et le client réessaie. Une session qui attend sa prochaine commande en est avertie : 421 4.3.2 en SMTP, * BYE [UNAVAILABLE] en IMAP (y compris en IDLE et avant STARTTLS). Une session au milieu d’une transaction la termine : un DATA ou un BDAT en cours n’est jamais coupé par l’arrêt, et une connexion HTTPS que la box termine elle-même finit sa requête puis se ferme. Le drainage attend les workers et les sessions ouvertes dans un seul et même budget ; quand celui-ci est épuisé, l’avertissement nomme les workers et les sessions encore attendus. Avant, les écouteurs acceptaient pendant tout le budget (jusqu’à 10 s sur une box chargée) puis coupaient ces connexions, et un message stocké mais coupé avant son 250 pouvait être envoyé deux fois.

L’exploitation de la sécurité

La défense en trois couches (nftables côté noyau, limitation de débit et fail2ban dans le binaire, supervision) est décrite dans le modèle de sécurité ; la surface opérationnelle est :
[security] trusted_ips (CIDR accepté) contourne le limiteur de débit et fail2ban pour votre propre infrastructure. N’installez pas le démon fail2ban à côté d’OxiMail : le serveur suit lui-même ses échecs d’authentification sur tous les protocoles, avec la vraie IP cliente même derrière un proxy. Derrière un routeur NAT. Une box sur un réseau privé, joignable par une redirection de port (DNAT) du routeur, fonctionne comme une box directe : le routeur réécrit la destination, l’adresse source de chaque client arrive intacte. Deux comportements de routeur cassent cela, et tous deux touchent trusted_ips. Avec le hairpin NAT, un poste du LAN qui ouvre le nom public est renvoyé vers la box sous l’adresse LAN du routeur lui-même. Certains routeurs réécrivent aussi la source de toute connexion redirigée, clients d’Internet compris. Si l’adresse du routeur tombe dans une plage de trusted_ips (le 192.168.0.0/16 habituel), tout ce qu’il relaie ainsi échappe au limiteur et à fail2ban ; sinon, un seul bannissement du routeur coupe tout le monde. oximail doctor signale, sous trusted_ips_gateway, une passerelle de la table de routage de la box qui tombe dans trusted_ips. Resserrez la plage pour exclure le routeur, et vérifiez que le routeur garde la vraie adresse source (les adresses du journal de la box en sont la mesure).

La ronde doctor

oximail doctor est le contrôle composite : sortie 0 tout va bien, 1 dégradé, 2 un échec. Chaque contrôle sort en ok, warn, fail ou skipped, et skipped veut dire que rien n’a été mesuré, jamais une réussite. --json est un tableau d’objets {check, status, detail}, auxquels trois champs se sont ajoutés sans changer ceux-là : family (mail, backup, storage, security, accounts, system, ou extension pour un contrôle apporté par une extension), facts avec les faits datés en UTCDate (observedAt de la dernière sauvegarde ou du dernier exercice de récupération, expiresAt du certificat), et, sur un warn ou un fail seulement, remedy, le verbe à taper. Un champ qui n’a rien à dire est absent, pas null.
La ronde est enregistrée toutes les 15 minutes. oximail-health.timer lance oximail doctor --json --record sous l’utilisateur du service ; la ronde stockée (gardée 30 jours) est ce que servent HealthReport/query et HealthReport/get en JMAP (API d’administration). L’assistant d’installation écrit les deux unités ; sur une box installée avant elles, oximail setup health-timer --apply les écrit et active le timer. --record est refusé en root, et son code de sortie est celui de l’enregistrement (0 stocké, 2 non stocké). Un administrateur de locataire lit le relevé d’une box où son locataire est seul ; sur une box à plusieurs locataires, seul un opérateur d’instance le lit, un compte désigné à la console et nulle part ailleurs (oximail admin instance grant --account … --by <nom> --apply). instance_admin avertit quand une box à plusieurs locataires n’a aucun opérateur actif, et nomme un détenteur dont la ligne n’ouvre rien (compte rétrogradé, désactivé ou sans passkey). --reachability ajoute les sondes réseau, qu’un diagnostic lancé pendant une panne doit pouvoir éviter : sans lui elles disent skipped. dnsbl_self interroge les listes noires DNS publiques sur l’adresse d’envoi ([metrics] dnsbl_self_ips, à défaut l’IPv4 publique détectée) : listée est un avertissement, une requête refusée par la liste (un résolveur public) aussi, puisque l’état est alors inconnu. meet_join_origin vérifie que les liens de réunion ouvrent une page ; quand l’hôte de web_base_url résout sur la box elle-même derrière un proxy déclaré, la sonde ne frapperait que la box, elle est donc non concluante (skipped) et le détail donne le curl à lancer depuis l’extérieur. Quelques contrôles à connaître par leur nom :

La télémétrie communautaire

OxiMail peut envoyer à son éditeur un court rapport sur la marche du serveur (ADR-153). Rien n’est envoyé sans la réponse de l’exploitant. L’assistant d’installation demande Send reports? [y]es / [c]rashes only / [n]o / [d]etails sans réponse présélectionnée (Entrée seule repose la question, d montre ce qui est envoyé), sur un MX de secours aussi ; un setup.toml doit porter [server] telemetry = "yes" | "crashes" | "no", sinon l’exécution s’arrête et nomme la clé. La réponse s’écrit dans un marqueur de consentement du répertoire de données, à côté de l’identifiant d’installation, et une box sans marqueur n’envoie rien. [telemetry] enabled = false coupe le canal quoi que dise le marqueur.
enable et disable montrent ce qu’ils enregistreraient et n’écrivent qu’avec --apply ; ils prennent effet dans l’heure, sans redémarrage. enable réarme aussi un canal que le récepteur a retiré. Un rapport est une enveloppe (format du rapport, fenêtre couverte, identifiant d’installation, version, version du texte de consentement et catégories consenties), plus la partie de chaque catégorie consentie :
  • crash : combien d’exécutions ne se sont pas arrêtées proprement, comment systemd les a vues finir, et où le code d’OxiMail a paniqué (crate, fichier, ligne ; jamais le message ni une valeur) ;
  • usage : OS, architecture et famille de distribution tirée d’une liste fermée, build officielle ou personnalisée, rôle, fonctions actives, durée de fonctionnement, comptes et courrier en tranches, part du spam rejeté, les cinq méthodes les plus appelées avec la part de leurs appels dans chaque tranche de latence, le rapport de la latence p95 à celle mesurée sur la version précédente, types d’erreurs, protocoles et familles de clients utilisés, changements de configuration et avancement de l’installation (listes fermées). Aucun contenu de courrier, aucune adresse, aucun domaine, nom d’hôte ni clé de licence.
Un rapport part quinze minutes après le démarrage, puis une fois par jour ([telemetry] interval_secs), et à l’arrêt propre du serveur, qui envoie la fenêtre qu’il détient (une fenêtre de moins d’une minute est abandonnée). Les rapports partent en https seulement (du http en clair vers une adresse de loopback et nulle part ailleurs) et ne suivent jamais une redirection. L’identifiant d’installation dérive d’un secret aléatoire du répertoire de données : la valeur que stocke le récepteur n’est pas affichée sur la box (telemetry status --show-id ou telemetry id --show-id l’affichent), l’identifiant n’est plus écrit au journal au démarrage, un répertoire de données restauré ou cloné sur une autre machine reçoit un nouveau secret, et purge présente le secret lui-même, puis le renouvelle. Chaque champ de chaque format de rapport est listé sur oximail.ch/privacy/telemetry, vers laquelle pointe la configuration générée ; le texte de consentement dit la durée de conservation (90 jours) et qui reçoit les rapports.

Quand quelque chose ne va pas

  1. journalctl -u oximail --since -1h : l’échec est bruyant par conception ; cherchez les lignes de niveau error avec leurs champs de contexte.
  2. oximail doctor : diagnostics de configuration, de stockage et de ports.
  3. /metrics : une file grossit, un worker est bloqué, une jauge est périmée ?
  4. Pour les problèmes de remise, la liste de contrôle DNS et la CLI de file (oximail queue ...) viennent ensuite.
Si du courrier légitime part en spam, regardez l’horloge. oximail doctor rend compte de la santé de l’horloge, et c’est la deuxième chose à examiner après le DNS. Une signature DKIM entrante qui porte un horodatage est refusée si elle arrive plus de 300 secondes dans notre futur : une machine dont l’horloge retarde se met donc à refuser les signatures de courriers parfaitement valides, qui scorent alors en spam. Le symptôme ressemble à un problème de DNS ou de DKIM, et jamais à une horloge. Notez que l’exposition est unidirectionnelle : nos propres signatures sortantes ne portent pas d’horodatage, un destinataire n’a donc rien de nous à juger, et personne ne vient jamais s’en plaindre.