15. Modèle de sécurité

Ce chapitre dit contre quoi Pepsi se défend, contre quoi il ne se défend pas, et ce qu’un attaquant obtient de chacune des choses qu’il aurait pu prendre — il est donc aussi utile après une compromission qu’avant.

Pepsi est un MTA qui détient des clés privées et peut lire du courrier, ce qui le rend digne d’être attaqué. La conception pose sans cesse une question : lorsque ce composant est compromis, qu’est-ce qui reste hors de portée ? Là où la réponse est « rien », cela est dit ici plutôt que laissé à découvrir.

Les affirmations — quels actifs, contre quels adversaires, dans quel mode de garde — sont énoncées dans Modèle de menace ; ce chapitre expose les mécanismes qui les sous-tendent.

15.1. Ce que Pepsi présuppose

Si l’un de ces points est faux, les garanties ci-dessous ne tiennent pas :

  • L’hôte n’est pas déjà compromis. Rien ici ne défend contre root sur la machine. Root peut lire chaque clé, chaque secret et chaque message.

  • PostgreSQL authentifie par identifiants de pair sur une socket locale. Chaque composant se connecte sous son propre utilisateur système, et la base n’accorde à ce rôle que ce dont le composant a besoin. Un déploiement qui passe à l’authentification par mot de passe avec un compte partagé jette l’essentiel de Un attaquant qui a la base de données.

  • Le système d’exploitation applique la propriété des fichiers et le bit setuid. La frontière de garde des clés privées est un GRANT de base de données fondé sur l’uid effectif ; un montage nosuid ou un compte partagé la fait s’effondrer.

  • Un résolveur DNS validant. Pepsi fait confiance au bit AD plutôt que de valider DNSSEC lui-même (voir pepsi-stage-relay-to-internet). Un résolveur menteur défait DANE et rétrograde MTA-STS en confiance au premier usage.

  • Le matériel de certificat TLS n’est pas fourni par l’attaquant. L’intégration certbot écrit dans /etc/letsencrypt ; un attaquant qui peut y écrire peut usurper le serveur.

15.2. La séparation des privilèges

Garantit

Un composant compromis (ce que chaque compte compromis atteint), Un utilisateur shell local (aucun rôle accessible depuis un compte ordinaire) et Quelqu’un qui détient une copie de la base de données (ce qu’un rôle de service peut écrire).

Pepsi n’est pas un seul programme. Chaque composant s’exécute sous son propre compte, et la séparation est appliquée par la base de données autant que par le système de fichiers.

Du côté de la base de données, deux sortes de lignes sont tracées, et il vaut la peine d’être exact sur les deux.

Le rôle du pipeline est large à dessein. pepsi (le dispatcher et chaque étape ordinaire) et pepsi-crypto détiennent un droit global unique : SELECT, INSERT, UPDATE et DELETE sur chaque table du schéma pepsi, l’usage de chaque séquence, EXECUTE sur chaque fonction, et la même chose en privilèges par défaut pour les objets créés ultérieurement. Chaque étape lit et écrit la file, et à elles toutes les étapes utilisent chaque table du pipeline, de sorte que le moindre privilège par étape exigerait un compte par étape ; la conséquence est énoncée dans Modèle de menace plutôt que dissimulée. Le droit est ensuite repris là où le pipeline n’a rien à faire :

  • crypto_identity : SELECT/INSERT/UPDATE uniquement sur les colonnes autres que private_wrapped (DELETE n’a pas de granularité par colonne et reste accordé) ; seul pepsi-crypto détient cette colonne ;

  • config_override : SELECT seulement — l’écrire revient au rôle pepsi-config (pepsi-httpd écrit via une connexion [pepsi-admin] CONFIG_DB qui s’authentifie sous ce rôle) ;

  • setup_task et setup_task_log : rien ; seul pepsi-config peut faire INSERT d’une tâche ;

  • secure_message et secure_access : INSERT, DELETE et SELECT de chaque colonne sauf ciphertext sur secure_message ; SELECT et DELETE sur secure_access ;

  • admin_account, admin_session et api_token : rien ;

  • event_log et mail_log : en ajout seul (ni UPDATE ni DELETE).

Les services qui ne sont pas le pipeline obtiennent ce qu’ils utilisent. Un audit de chaque instruction que chacun peut émettre — son propre crate et chaque bibliothèque qu’il lie, en suivant les appels vers les fonctions stockées — fixe leurs droits, et pepsi-setup refuse de terminer si PostgreSQL est en désaccord :

Rôle

Accordé

Notamment pas

pepsi-ingress

INSERT dans workqueue et workqueue_body (via workqueue_add) et SELECT des nouveaux identifiants ; SELECT des deux colonnes de taille générées (header_octets, octets) qu’additionne le contrôle d’admission de la file ; SELECT sur config_override et mailbox_quota ; SELECT de list_name et mail_host de mailing_list (l’adresse d’une liste, pour le contrôle des destinataires) ; INSERT sur les deux journaux en ajout seul.

Tout autre message de la file, les réglages, les listes blanches, les clés, tout ce qui concerne une liste au-delà de son adresse. Un bogue d’analyse dans le processus qui écoute sur le port 25 atteint une seule table, pour y ajouter.

pepsi-telemetry

Sa table telemetry.

Tout le reste, de sorte qu’un collecteur partageant une base de données avec un pipeline ne peut pas atteindre le courrier.

pepsi-httpd

Les tables d’identifiants d’administration, le portail, les tables de clés (pas private_wrapped), les tables de listes et d’archives, event_log/mail_log (pepsi-httpd prune, lancé par pepsi-log-prune.timer, les purge) ; sur workqueue, les colonnes d’enveloppe, state et de routage, INSERT (le courrier qu’envoient les formulaires web) et DELETE ; sur workqueue_body, INSERT et DELETE (un corps s’en va avec le dernier message qui le porte ; la clé étrangère refuse tout autre cas) et les seules colonnes d’identifiant et de taille ; les statistiques en lecture seule, dns_address, tls_session, mailbox_quota et telemetry_client (l’enregistrement de vivacité du démon de télémétrie) ; SELECT sur setup_task.

Les en-têtes ou le corps d’un message en file — la console liste des enveloppes et n’a aucun chemin de lecture vers le contenu, et ne peut pas non plus faire pointer le body_id d’un message vers le corps d’un autre message ; settings, whitelist, vacation_reply, payment_request_reply, les tables du secrétariat, telemetry, origin_nonce, auto_pay_spend, list_post_rate ; l’écriture des statistiques, des quotas ou de l’enregistrement de vivacité du démon de télémétrie ; config_override et setup_task autrement que par sa connexion pepsi-config.

Une table qu’ajoute un correctif de schéma ultérieur n’est à la portée de pepsi-ingress ou de pepsi-telemetry qu’une fois qu’un correctif la leur accorde : leurs privilèges par défaut sont révoqués eux aussi.

Les rôles extérieurs à cet ensemble sont plus étroits encore : pepsi-keydisc, pepsi-whitelist et pepsi-config ne détiennent chacun des droits que sur quelques tables nommées. pepsi-crypto détient le droit global plus private_wrapped. Le propriétaire du schéma, pepsi-owner, sous lequel pepsi-setup se connecte lui-même, détient tout par propriété.

Composant

S’exécute sous

Ce qu’il peut atteindre

pepsi-ingress

pepsi-ingress

INSERT dans la file et les deux lectures ci-dessus ; le secret SRS ; le matériel TLS. Aucun autre message, aucun matériel de clé privée, aucun paramètre, aucune liste blanche.

pepsi-dispatch et la plupart des étapes

pepsi

Le droit du pipeline ci-dessus : la file, les paramètres, les statistiques, la liste blanche, et les tables de lien sécurisé sans leur ciphertext. Pas crypto_identity.private_wrapped.

pepsi-stage-encrypt / -decrypt

pepsi-crypto (setuid, mode 4750 pepsi-crypto:pepsi)

L’accès ordinaire au pipeline dont dispose chaque étape, plus le matériel de clé privée enveloppé et la clé de chiffrement de clés. Rien d’autre ne détient les deux : le propriétaire du schéma pepsi-owner peut lui aussi lire la colonne, mais pas la clé de chiffrement de clés.

pepsi-keys

l’opérateur appelant

Installé en 0755, sans bit setuid : un binaire capable d’ouvrir toutes les clés privées d’un déploiement est une prise bien plus grosse que l’unique table de pepsi-whitelist, de sorte que par défaut seuls root et le compte pepsi-crypto peuvent l’utiliser, et que les droits PostgreSQL font autorité. Un site qui veut que ses utilisateurs gèrent leurs propres identités peut délibérément le rendre setuid pepsi-crypto (chmod 4755), auquel cas la règle de propriété par identité du programme commence à s’appliquer — voir Gestion des clés.

pepsi-httpd

pepsi-httpd

Les tables du deuxième tableau ci-dessus — lecture et écriture, et non lecture pour l’essentiel : ses gestionnaires modifient directement le routage de la file et les métadonnées de clés. Aucun en-tête ni corps de message, aucun matériel de clé privée, aucun paramètre ni liste blanche ; aucune possibilité d’écrire la surcouche de configuration ni de mettre en file une tâche de configuration, sinon par sa connexion pepsi-config.

pepsi-keydisc

pepsi-keydisc

La file de demandes de découverte et peer_key. Jamais un UPDATE général sur pepsi.workqueue.

pepsi-whitelist

utilisateur appelant (setuid)

SELECT/INSERT/DELETE sur pepsi.whitelist uniquement.

pepsi-quota

pepsi (setgid pepsi-maildir, mode 2550)

pepsi.mailbox_quota, et — par le groupe — le helper setuid-root qui mesure une boîte aux lettres.

Le quota de boîte aux lettres tient la mesure et le refus séparés. Seul pepsi-helper-maildir-writer peut regarder à l’intérieur d’un Maildir (mode 0700 ; c’est le seul programme qui devient l’utilisateur), et seuls les membres de pepsi-maildir peuvent l’exécuter — un groupe dont pepsi-ingress ne fait délibérément pas partie et dont pepsi-setup avertit si quelqu’un l’y ajoute. Le processus qui écoute sur le port 25 peut donc lire une mesure prise par quelqu’un d’autre et refuser un destinataire sur cette base, et rien d’autre : pepsi-setup lui révoque l’accès en écriture à pepsi.mailbox_quota et lui laisse SELECT. Les deux routes vers la comptabilité lui sont fermées, et aucune de ces fermetures ne repose sur l’autre.

Les étapes cryptographiques sont setuid, non setgid : l’authentification par pair de PostgreSQL se fonde sur l”uid effectif, de sorte qu’une étape qui n’aurait gagné qu’un groupe se connecterait toujours en tant que pepsi et se verrait refuser la colonne de clés. Elle doit être pepsi-crypto.

15.2.1. Tous les programmes portant un bit

Garantit

Un utilisateur shell local : un compte local n’obtient aucun rôle Pepsi, et chaque helper n’agit qu’avec l’autorité de l’utilisateur cible (frontière B4).

Un binaire multi-appel ne peut pas porter un bit setuid ou setgid propre à chaque programme ; chacun de ceux-ci est donc installé comme exécutable à part entière plutôt que comme lien symbolique vers le binaire pepsi partagé (la liste faisant autorité est STANDALONE_BINARIES dans le Makefile ; les modes sont appliqués par make install et, sous Debian, par dpkg-statoverride depuis le postinst). Tout ce qui n’est pas listé ici est non privilégié.

Programme

Propriétaire et mode

Pourquoi

pepsi-helper-maildir-writer

root:pepsi-maildir 4750

Devient le destinataire pour écrire dans un Maildir en mode 0700.

pepsi-stage-relay-to-maildir

pepsi:pepsi-maildir 2550

Le bit setgid est le seul verrou sur l’exécution de ce helper ; le mode est donc 2550 et non 2755 : le droit d’exécution pour tous donnerait egid=pepsi-maildir à chaque compte local. L’utilisateur pepsi n’est délibérément pas membre du groupe et obtient le droit d’exécution en étant propriétaire du fichier.

pepsi-quota

pepsi:pepsi-maildir 2550

Le même helper, afin qu’un balayage reconcile puisse remesurer depuis une minuterie sans être root.

pepsi-helper-dot-forward

root:pepsi-forward 4750

Abandonne entièrement ses privilèges pour devenir l’utilisateur avant de lire son ~/.forward.

pepsi-stage-dot-forward

pepsi:pepsi-forward 2550

Le même verrou, pour ce helper.

pepsi-helper-auto-pay

root:pepsi-wallets 4750

Pilote le wallet sous le compte qui le possède.

pepsi-stage-auto-pay

pepsi:pepsi-wallets 2550

Le même verrou, pour ce helper.

pepsi-stage-relay-to-smarthost

pepsi:pepsi-token 2550

Lit les fichiers de jetons OAuth à accès restreint par groupe qu’écrit pepsi-helper-token-refresh, la clé client TLS mutuel et le cache d’identifiants Kerberos. 2550 pour la même raison que les étapes ci-dessus : l’étape accepte -c, si bien qu’un droit d’exécution pour tous permettrait à n’importe quel compte local de la pointer vers un serveur à lui et de lui envoyer ces identifiants.

pepsi-whitelist

pepsi-whitelist:pepsi-whitelist 4755

Exécutable par tous à dessein : tout utilisateur local peut gérer son propre espace de noms, et ce qui l’empêche de toucher à celui d’un autre est la règle interne au programme, non le mode du fichier.

pepsi-stage-encrypt, pepsi-stage-decrypt

pepsi-crypto:pepsi 4750

L’authentification par pair se fonde sur l’uid effectif : l’étape doit donc être le rôle auquel crypto_identity.private_wrapped est accordée. Groupe pepsi et pas de droit d’exécution pour tous : contrairement à pepsi-whitelist, ce ne sont pas des commandes utilisateur.

pepsi-keys

0755, aucun bit

Voir le tableau ci-dessus. Un site peut le rendre setuid pepsi-crypto.

pepsi-helper-mailbox-scan

0755, aucun bit

La moitié non privilégiée du couple pepsi-whitelist : il analyse des fichiers de boîtes aux lettres hostiles, il est donc lancé avec setresuid vers l’utilisateur appelant — les trois identifiants, de sorte qu’il ne peut jamais retrouver le propriétaire.

15.2.2. Le répertoire d’exécution est une ACL, non un groupe partagé

Garantit

Un utilisateur shell local et Un composant compromis : une socket est accessible aux comptes censés y accéder, et en créer une n’accorde aucune autorité sans rapport.

/run/pepsi contient des sockets appartenant à plusieurs comptes distincts — le socket de soumission locale de pepsi-ingress, le socket d’administration de pepsi-httpd, le socket de proxy inverse auquel le serveur web frontal se connecte, et la sonnette de l’applicateur — et il appartient à root, avec une ACL POSIX accordant nominativement le droit d’écriture à pepsi-ingress et pepsi-httpd (debian/pepsi.tmpfiles). Un groupe partagé ne conviendrait pas : il faudrait un groupe dont les deux soient membres, et chaque candidat porte quelque chose qu’aucun des deux ne devrait obtenir — le groupe pepsi lit les clés privées DKIM, le groupe pepsi-admin est le contrôle d’accès de l’API d’administration. Seul pepsi-httpd.service s’exécute avec pepsi-admin comme groupe supplémentaire, afin de pouvoir remettre son socket d’administration à ce groupe : le groupe ne possède aucun fichier et n’accorde rien d’autre que le statut d’administrateur sur ce socket, que le processus qui le sert détient de toute façon. pepsi-ingress ne l’obtient jamais.

Le mode 0775 sur ce répertoire est donc porteur et non un élargissement : sur un répertoire portant une ACL, les bits de groupe sont le masque de l’ACL, si bien qu’un 0755 plafonne silencieusement chaque droit nominatif à r-x et que le redémarrage suivant ne peut pas créer son socket.

15.2.2.1. Aucune unité ne peut le revendiquer comme RuntimeDirectory

Le fragment tmpfiles.d est le seul propriétaire du répertoire, et c’est une règle et non une préférence. systemd attribue un RuntimeDirectory= au User= de l’unité qui le déclare et le chowne – ainsi que tout son contenu – à chaque démarrage. Dans un répertoire à quatre locataires répartis sur trois comptes, ce chown récursif ne décide pas seulement qui possède le répertoire : il réécrit la propriété de sockets que d’autres unités ont déjà liés.

Par exemple, pepsi-httpd.socket lie /run/pepsi/httpd.sock en 0660 root:www-data pour que le serveur web frontal puisse l’atteindre en mode proxy inverse. Un RuntimeDirectory=pepsi dans pepsi-httpd.service chownerait ce socket vers pepsi-httpd:pepsi-httpd à chaque démarrage, le serveur frontal recevrait EACCES à la connexion, et chaque récupération de politique MTA-STS recevrait un 502 — avec chaque unité active et rien de journalisé, parce que rien n’aurait échoué. Le même chown remplacerait purement et simplement la propriété root du répertoire et son ACL, et confierait les sockets des autres occupants à l’unité démarrée en dernier ; et sans RuntimeDirectoryPreserve=yes, un service qui s’arrête supprimerait le répertoire, sockets vivants compris.

Ce que ces unités attendaient réellement de RuntimeDirectory= était une seule chose : /run est en lecture seule sous ProtectSystem=strict. pepsi-httpd.service et pepsi-ingress.service disent donc ReadWritePaths=/run/pepsi à la place et tirent leur permission de l’ACL ; pepsi-setup-apply.service n’a besoin ni de l’un ni de l’autre, tournant en root sans ProtectSystem=strict. Les unités .socket conservent DirectoryMode=0775 pour que le répertoire apparaisse tout de même si l’une d’elles lie avant que systemd-tmpfiles n’ait tourné, et rien ne le supprime, parce que rien ne le revendique. tests/check-systemd-units.sh (exécuté par make check) fait échouer la construction si un RuntimeDirectory=pepsi réapparaît dans une unité livrée ou dans le fragment de proxy inverse que pepsi-setup écrit.

L’accès du serveur web frontal est accordé de la même façon, et séparément. Atteindre httpd.sock demande deux permissions : le SocketGroup= du socket lui-même, et la traversée du répertoire où il se trouve. Les bits other r-x du répertoire fourniraient la seconde, mais ils existent pour une raison sans rapport (tout compte local peut soumettre du courrier par le socket de soumission), et s’y appuyer ferait dépendre la livraison de la politique d’une permission accordée pour autre chose. pepsi-setup écrit donc un droit explicite dans /etc/tmpfiles.d/pepsi-reverse-proxy.conf lorsqu’il configure le mode proxy inverse, en nommant le compte qu’il a réellement détecté (www-data sous Debian, nginx/apache ailleurs) avec r-x et rien de plus, et le retire avec le fragment de socket. C’est un fichier distinct du fragment livré parce qu’il est découvert et non livré, et conditionnel et non systématique.

Le collecteur de télémétrie n’est délibérément pas locataire ici du tout et réside dans /run/pepsi-collector – son propre répertoire, partagé avec aucune autre unité, ce qui le tient également à l’écart de /run/pepsi-telemetry, celui du client de télémétrie.

15.3. La frontière de garde des clés privées

Garantit

Garde par la passerelle (custody = local, la clé de MTA) (les clés ne sont jamais divulguées à un lecteur passif ni à une étape ordinaire ; frontière B5).

Le matériel de clé privée réside dans crypto_identity.private_wrapped et est protégé deux fois :

Par droit. pepsi-setup accorde cette unique colonne au rôle pepsi-crypto et la révoque de tout compte de service, pepsi-httpd compris ; en dehors de pepsi-crypto, seul le propriétaire du schéma pepsi-owner peut la lire, par propriété. C’est au niveau colonne et c’est absolu : PostgreSQL refuse une instruction qui ne fait que nommer la colonne, fût-ce à l’intérieur d’un test IS NULL, de sorte qu’il n’y a aucun chemin de lecture à restreindre par la suite.

Note

Cette absoluité a une arête pratique. Une requête de listage ne peut pas rapporter « cette identité a-t-elle une moitié privée ? » sous la forme private_wrapped IS NOT NULL : c’est refusé pour tout rôle autre que pepsi-crypto, et l’erreur de PostgreSQL, permission denied for table crypto_identity, ne nomme aucune colonne. Le listage dérive l’indicateur de wrap_key_id à la place, qu’un CHECK de table maintient exactement en phase.

Par enveloppement. Ce qui est stocké n’est pas une clé privée mais un chiffré AEAD, lié au (address, protocol, purpose) de cette ligne en données associées, sous une clé de chiffrement de clés qui réside hors de la base, dans un fragment secrets.d/*.secret lisible du seul pepsi-crypto — et non de pepsi-owner, de sorte que l’unique autre rôle capable de lire la colonne ne détient toujours rien qu’il puisse ouvrir. Déplacer un bloc d’une ligne à une autre échoue à l’authentification.

15.4. Un second facteur pour la gestion des clés

Garantit

Quelqu’un qui détient le mot de passe de messagerie d’un utilisateur (un mot de passe de messagerie volé ne permet pas d’enregistrer une clé pour un utilisateur qui a enrôlé un second facteur).

Un utilisateur peut enrôler un secret TOTP (RFC 6238 : HMAC-SHA-1, six chiffres, pas de 30 secondes — ce qu’implémente toute application d’authentification) pour son adresse ; dès lors, chaque modification de ses clés qu”il peut effectuer exige un code en cours de validité :

  • Par e-mail, chaque commande de clé sauf status — generate, register, publish, retire et otp enroll/otp remove eux-mêmes — prend le code sous la forme d’un mot de six chiffres dans le Subject:. L’en-tête Autocrypt: ou la clé jointe d’un message ordinaire n’enregistre plus rien pour cette adresse.

  • Dans la console web et l’API, les requêtes generate-identity, register-client-key et revoke-identity d’un principal own:<address> portent un champ otp. pepsi-httpd ne peut pas le vérifier — il ne détient aucun droit sur la table — et le transmet tel quel ; pepsi-setup apply le vérifie avant d’agir, pour une tâche admise au seul titre de own:<address>. Un opérateur détenant keys:write n’est pas sollicité.

  • Un pepsi-keys non privilégié (une installation setuid) exige --otp CODE pour chaque modification d’identité et chaque export de clé privée ; la propriété est vérifiée d’abord, de sorte qu’un utilisateur ne peut jamais faire compter des échecs contre l’enrôlement d’un autre.

Garde. Le secret est enveloppé exactement comme une clé privée — AES-256-GCM sous la KEK du magasin de clés, lié à (address, totp, second-factor) — et la table otp_key est accordée au seul rôle pepsi-crypto : pepsi-setup la révoque de tous les autres rôles, pepsi du pipeline compris, et le vérifie auprès de PostgreSQL. Une étape qui analyse du courrier hostile ne peut donc ni lire un secret, ni réinitialiser un compteur, ni supprimer un enrôlement (ce qui désactiverait la protection). pepsi-keys wrap rotate ré-enveloppe les secrets avec les clés privées.

Tentatives. Le code est vérifié par le processus capable de désenvelopper le secret ; la décision revient à la fonction SQL otp_attempt, sous le verrou de l’enregistrement. Un code correspondant à un pas de temps postérieur au dernier accepté est accepté et remet le compteur d’échecs à zéro ; le même code une seconde fois, ou un code plus ancien, est un rejeu et compte comme un échec, tout comme un code erroné. Au bout de dix échecs consécutifs, l’enrôlement est verrouillé et toute tentative est refusée sans vérification. Une commande sans aucun code est refusée sans être comptée. Chaque enrôlement porte un numéro de série jamais réutilisé, de sorte qu’une tentative vérifiée contre un secret remplacé entre-temps est écartée plutôt qu’imputée au nouveau.

L’opérateur lève un verrouillage en supprimant l’enrôlement (pepsi-keys otp reset, ou DELETE /api/v1/otp/{address}, qui met en file une tâche reset-otp que seul keys:write peut demander) ou en le remplaçant (pepsi-keys otp enroll exécuté par l’opérateur n’exige aucun code) ; un nouvel enrôlement commence toujours à zéro échec.

Ce qu’il ne fait pas. Il ne protège pas le premier enrôlement (il n’y a encore aucun code à demander), ni une réponse d’enrôlement que l’utilisateur a laissée dans sa boîte aux lettres. Il ne soumet pas à contrôle les modifications synchrones d’indicateurs de la console web (publication WKD, demande de téléversement vers un serveur de clés, clé MTA principale), que pepsi-httpd effectue lui-même. Et il n’apporte rien contre l’opérateur, qui peut le réinitialiser.

15.5. Un attaquant qui a la base de données

Garantit

Quelqu’un qui détient une copie de la base de données.

Supposons un vidage complet — une sauvegarde volée, une réplique, une injection SQL dans un chemin en lecture seule.

Ils obtiennent :

  • chaque message actuellement en file, en clair : pepsi.workqueue contient les headers et pepsi.workqueue_body le corps, non chiffrés. Pepsi est un pipeline ; un message en vol doit être lisible par l’étape qui le traitera. Les messages remis sont supprimés : il s’agit donc de l’arriéré, non de l’historique.

  • le graphe complet de la correspondance : expéditeurs, destinataires, horodatages, paramètres par adresse, listes blanches, les tables origin_nonce, auto_pay_spend, payment_request_reply et dns_address.

  • chaque clé et certificat publics, ainsi que toutes les métadonnées de clés.

  • les verdicts ARC/DKIM/DMARC et les issues des sessions TLS.

Ils n’obtiennent pas :

  • la moindre clé privée, ni le moindre secret de second facteur. Les blocs enveloppés sont des chiffrés AEAD et la clé de chiffrement de clés n’est pas dans la base.

  • le moindre corps de message à lien sécurisé. secure_message.ciphertext est en AES-256-GCM sous une clé dérivée par Argon2id du PIN du destinataire, d’un sel par message et d’un poivre côté serveur détenu dans la configuration, non dans la base. Le PIN lui-même n’est jamais stocké sous aucune forme — pas même sous forme de hachage. Un voleur de base doit forcer un PIN qu’il ne peut pas vérifier hors ligne sans disposer aussi du poivre.

  • le moindre identifiant utilisable. Les mots de passe d’administration sont des empreintes Argon2id ; les jetons d’API sont stockés sous forme de condensats, de sorte qu’un jeton ne peut pas être récupéré depuis une sauvegarde et n’est affiché qu’une seule fois.

  • les clés de signature DKIM, qui résident dans le système de fichiers, et les secrets SRS et Pepsi-Origin, qui résident dans secrets.d.

Avertissement

La file est l’exposition. Un opérateur qui juge importante la confidentialité du courrier en vol devrait chiffrer la base au repos et traiter les sauvegardes en conséquence — Pepsi ne peut pas chiffrer la file pour lui-même, car chaque étape aurait besoin de la clé, ce qui revient à ne pas en avoir.

15.6. Un attaquant qui a le fichier de configuration

Garantit

Quelqu’un qui détient la configuration ou un secret.

/etc/pepsi/pepsi.conf est en mode 0644 à dessein : des étapes non privilégiées le lisent. Il ne doit donc contenir aucun secret, et n’en contient pas — chaque secret réside dans un fragment secrets.d/<reader>.secret tiré par une directive @inline-secret@ et appartenant à l’unique compte qui en a besoin.

Un accès en lecture au fichier de configuration livre la topologie : noms d’hôtes, domaines servis, graphe du pipeline, adresses de smarthost, chemins de fichiers, fonctionnalités activées. C’est du renseignement, non une compromission.

Un accès en lecture à un fragment ``secrets.d`` livre exactement ce que détient son unique lecteur, et rien de plus — la clé SRS, ou le mot de passe du smarthost, ou la clé de chiffrement de clés, ou le poivre du lien sécurisé, ou la clé de désabonnement des listes. C’est pourquoi ils sont séparés : il n’existe aucun fichier unique dont la divulgation serait totale.

Un accès en écriture au fichier de configuration équivaut à root. Il nomme les programmes que chaque étape exécute. Traitez-le comme un fichier appartenant à root, car c’en est un.

Avertissement

Deux pièges autour d”@inline-secret@, qui tous deux ressemblent à une configuration qui fonctionne :

  • la directive termine sa section — tout ce qui est écrit après elle n’atterrit dans aucune section ;

  • un fragment illisible ne fait qu”avertir. L’option se lit alors comme non définie, et une fonctionnalité tourne silencieusement sans son secret. Vérifiez en faisant un stat du fragment sous le compte lecteur, non en lisant le journal.

15.7. Un attaquant qui compromet un worker d’étape

Garantit

Un composant compromis, y compris son résidu en matière de signature.

Supposons qu’un bogue d’analyseur donne l’exécution de code à l’intérieur d’une étape — le cas réaliste, puisque analyser des entrées hostiles est le métier des étapes.

Une étape sous l’utilisateur ``pepsi`` (la plupart) obtient la file : lire, modifier, rediriger ou supprimer tout message en vol, et lire la liste blanche et les paramètres. Elle n’obtient pas les clés privées de bout en bout enveloppées, et ne peut pas devenir un autre compte.

Elle obtient cependant la possibilité de faire signer du courrier, et cela mérite d’être dit sans détour, car c’est le plus grand résidu de la conception :

  • pepsi-stage-dkim-sign s’exécute sous pepsi et lit les clés DKIM de domaine via le groupe pepsi, de sorte que tout processus pepsi peut signer au nom de chaque domaine servi — directement, ou en modifiant un enregistrement en file avant que l’étape de signature ne le voie.

  • pepsi-stage-encrypt signe au nom de l’auteur From: de l’enregistrement. Ingress applique USERNAME_MAP à la submission, mais l’enregistrement en file est ensuite modifiable par chaque étape pepsi, et rien en aval ne peut savoir si ingress a pris cette décision. Une étape pepsi compromise peut donc réécrire le From: d’une submission en file (ou injecter un message avec workqueue_inject) et la faire signer avec la clé de bout en bout de n’importe quel utilisateur local.

L’intégrité d’une signature produite par Pepsi — DKIM ou OpenPGP/S/MIME par utilisateur — n’est donc qu’aussi forte que l’ensemble du rôle de pipeline pepsi, et non aussi forte que pepsi-crypto. Ce que la séparation borne effectivement, c’est la divulgation des clés : un détecteur de langue compromis peut faire signer des messages tant qu’il s’exécute, mais il ne peut pas emporter une clé privée, de sorte que les dégâts cessent quand l’étape est corrigée et n’exigent pas de renouveler les clés. Voir Modèle de menace pour la place de ce point parmi les autres garanties.

Une étape ``pepsi-crypto`` obtient tout ce que possède ce compte : elle peut déchiffrer le courrier entrant adressé au déploiement et signer au nom de n’importe quelle identité locale. C’est la cible de plus grande valeur du système, raison pour laquelle les deux étapes cryptographiques sont petites, ne font aucune E/S réseau (la découverte est un programme distinct), et font l’objet du fuzzing décrit dans Suite de tests.

``pepsi-ingress`` voit chaque message à son arrivée et décide qui l’a soumis ; il peut donc admettre du courrier au nom de n’importe quel utilisateur local. Son rôle de base de données peut ajouter à la file sans rien en lire, de sorte que ce qui est déjà en file, les paramètres, les listes blanches et les clés restent hors de sa portée.

``pepsi-httpd`` obtient la surface web et ses droits en base — les enveloppes et le routage de la file, jamais les en-têtes ni le corps d’un message. Il ne peut notamment pas écrire de configuration ni exécuter d’étapes de configuration privilégiées : l’assistant du navigateur n’agit pas, il insère une ligne dans pepsi.setup_task décrivant ce qui devrait être vrai, et le pepsi-setup apply démarré séparément et tournant en root décide de le faire ou non. Une couche web compromise peut donc demander une reconfiguration mais non l’effectuer — et l’applicateur valide chaque tâche plutôt que de se fier à sa description.

Cette dernière défense peut être rendue absolue, ce qui est la bonne posture sur tout déploiement administré depuis un terminal. Les deux unités systemd de l’applicateur forment leur propre paquet Debian, pepsi-httpd-admin (pepsi ne fait que le Recommends, de sorte qu”apt remove pepsi-httpd-admin réussit et laisse le serveur de courrier tourner) ; une installation depuis les sources obtient le même levier avec make install INSTALL_ADMIN_UNITS=no. Une fois ces unités parties, rien ne vide pepsi.setup_task, de sorte qu’une insertion dedans est inerte quoi qu’il advienne de pepsi-httpd — la boucle est rompue au mécanisme plutôt qu’à l’interface, et aucun chemin de code de la couche web ne peut démarrer un processus root. La console le détecte, reste fermée si elle ne peut pas trancher, et sert ses pages de configuration initiale en lecture seule. pepsi-setup reste dans le paquet de base, donc administrer l’hôte à la main n’est pas affecté. Voir La scission comme levier de durcissement.

15.8. Défenses spécifiques

15.8.1. L’identité de submission

Garantit

Un utilisateur shell local et Un utilisateur limité au courrier (frontière B2) : un expéditeur soumettant du courrier n’envoie qu’au nom des identités que l’opérateur lui a associées.

Ingress décide qui a soumis un message par l’un de quatre moyens — SASL via le backend configuré, un certificat client TLS, une adresse dans MYNETWORKS, ou les identifiants SO_PEERCRED de la socket de submission locale, qui est la façon dont sendmail s’authentifie sans mot de passe et la raison pour laquelle cette socket peut être accessible en écriture à tous. Quel que soit le moyen, USERNAME_MAP décide ensuite quelles adresses From: et d’enveloppe cette identité peut utiliser (RFC 6409 §6/§8.1), et une identité associée à rien peut s’authentifier mais pas envoyer. Parler au nom d’une adresse — enregistrer une clé pour elle, ou gérer ses clés par e-mail — exige davantage que d’être autorisé à envoyer en son nom : le From: doit être exactement une boîte aux lettres nommée par une entrée concrète (sans joker) de la table de correspondance (state.origin.from_bound). Une autorisation *@example.org permet à un compte d’envoyer au nom de chaque collègue, jamais d’implanter une clé pour eux.

La décision est consignée dans le state du message (local_origin, l’identité authentifiée) et crue par chaque étape ultérieure — c’est précisément pourquoi Un composant compromis considère qu’une étape pepsi compromise est capable de la falsifier.

15.8.2. Rescellement du courrier déchiffré pour le stockage

Garantit

Garde par le client (custody = client, la clé de MUA) (le courrier que la passerelle a ouvert est classé lisible par le seul client de l’utilisateur) et Les garanties en un coup d’œil (la colonne Courrier classé).

pepsi-stage-decrypt doit produire du texte en clair : les étapes de spam, de langue, de liste blanche et d’apprentissage de clés qui le suivent lisent le contenu. pepsi-stage-reencrypt s’exécute une fois qu’elles l’ont fait, juste avant la remise locale, et scelle le message pour la clé custody = client active la plus récente du destinataire. Le mécanisme repose sur quatre choix :

  • Il ne requiert aucun privilège. Sceller pour une clé publique n’utilise aucun matériel privé, de sorte que l’étape s’exécute sous pepsi, est intégrée au binaire multi-appel, et ne lit que les colonnes publiques de crypto_identity sous le droit ordinaire du pipeline. Elle ne nomme jamais private_wrapped, et obtenir l’identité pepsi-crypto ne lui apporterait rien qu’elle utilise.

  • Il ne signe rien. Une signature de la passerelle dirait seulement « la passerelle a scellé ceci », indiscernable d’une signature d’auteur dans un client, et exigerait l’identité setuid. Le verdict sur la signature de l’expéditeur est celui que decrypt a consigné, dans state.crypto.in et dans X-Pepsi-Crypto — qui reste sur le bloc extérieur et que decrypt retire de chaque message entrant avant d’écrire le sien, de sorte qu’il est toujours le nôtre quand le client le lit.

  • Il n’agit que sur ce que decrypt a ouvert (state.crypto.in indique chiffré et déchiffré), et consigne state.crypto.store, ce qui l’empêche aussi de jamais sceller un enregistrement deux fois. Le courrier arrivé en clair n’est pas scellé.

  • Un refus ne renvoie rien de ce qui était chiffré. Sous ON_NO_CLIENT_KEY = bounce, le bloc d’en-têtes du DSN est réduit aux champs d’adressage, le corps est vidé et RET=HDRS est forcé, puisque l’expéditeur a chiffré le message et qu’un DSN circule en clair. Il fait rebondir le message plutôt que de le différer, car différer garderait le texte en clair plus longtemps en file.

Le résidu est celui que Un composant compromis énonce déjà : une étape pepsi compromise — l’étape de rechiffrement comprise — voit le texte en clair tant qu’il est en file et peut faire passer un message à côté de l’étape ; la garantie porte donc sur les copies déjà classées, que rien sur le serveur ne peut ouvrir.

15.8.3. Ce dont une ancre de confiance S/MIME se porte garante

Garantit

Ce que signifie un indicateur de sécurité, face à Un expéditeur distant : une autorité de certification à laquelle l’opérateur fait confiance pour un espace de noms ne peut pas faire apparaître valid pour un autre.

Une organisation qui échange du courrier signé avec un partenaire ne fait souvent confiance à l’autorité de certification du partenaire que pour le domaine propre de celui-ci — en la certifiant croisée avec des nameConstraints, ou en ancrant une sous-autorité du partenaire qui les porte. Si cette limite était ignorée, cette autorité partenaire pourrait émettre un certificat pour l’un des utilisateurs de ce déploiement et l’étape de déchiffrement déclarerait la signature valid. pepsi-crypto::trust fait donc courir l’état de contraintes de la RFC 5280 §6.1 le long de chaque chaîne qu’il construit :

  • Les contraintes de noms s’appliquent au DN du sujet, à chaque nom alternatif du sujet et à chaque attribut emailAddress du DN du sujet — ce dernier toujours, car cet attribut est ce à quoi un message est lié quand le SAN ne nomme aucune boîte aux lettres. Les sous-arbres permis s’intersectent le long de la chaîne (deux autorités permettant des domaines disjoints ne laissent aucune boîte aux lettres permise, et non toutes) ; les sous-arbres exclus s’accumulent. Une sous-autorité ne peut pas élargir ce que son parent lui a délégué.

  • Les propres contraintes de l’ancre la lient (RFC 5937), de sorte qu’ancrer directement la sous-autorité du partenaire est aussi sûr que de la certifier croisée.

  • Les contraintes de politique (requireExplicitPolicy, inhibitPolicyMapping, inhibitAnyPolicy) sont évaluées sur l’arbre de politiques complet, sous une forme dédupliquée dont la taille est polynomiale dans les politiques que nomme un certificat, et plafonnée (64 politiques ou correspondances par certificat) — l’arbre qui croît exponentiellement dans l’algorithme naïf est le déni de service CVE-2023-0464 d’OpenSSL. La vérification des noms est plafonnée à 220 comparaisons par certificat pour la même raison.

  • Ce qui ne peut être respecté est refusé. Une contrainte non traitable — une forme de sous-arbre que Pepsi ne sait pas comparer, lorsqu’un nom de cette forme apparaît en dessous ; un minimum ou un maximum ; une extension indécodable, critique ou non — rend la chaîne policy-rejected plutôt que valide sans la limite. Là où Pepsi s’écarte de la RFC 5280 ou d’OpenSSL, c’est toujours dans ce sens : un dNSName joker pouvant désigner un hôte exclu est refusé, et une URI sans hôte sous une contrainte d’URI est refusée.

La suite de conformité NIST PKITS (sections 4.6 et 4.8–4.13, avec chaque réglage d’entrée qu’elle liste) s’exécute dans cargo test, aux côtés d’un corpus propre pour les formes que PKITS ne couvre pas (adresses IP, formes non implémentées, une ancre à contraintes de noms).

15.8.4. Falsification d’en-têtes

Garantit

Ce que signifie un indicateur de sécurité, face à Un expéditeur distant.

X-Pepsi-Crypto indique au client d’un utilisateur ce que Pepsi a conclu sur la cryptographie d’un message : un en-tête falsifié est donc un mensonge dit avec notre autorité. Le courrier entrant se voit retirer l’en-tête avant que l’étape de déchiffrement puisse ajouter le sien, et les en-têtes de demande sortants sont retirés par STRIP_REQUEST_HEADERS. Un expéditeur distant ne peut pas en faire apparaître un.

Le même raisonnement couvre Authentication-Results et Pepsi-Origin : un en-tête qui signifie « le système local a vérifié ceci » n’a de sens que si le système local est la seule chose qui puisse l’écrire.

15.8.5. Destinataires inconnus et backscatter

Garantit

Actifs (identité d’envoi et réputation), contre Un expéditeur distant.

Un rebond va à l’expéditeur d’enveloppe que revendique un message, quel qu’il soit, et le spam le falsifie. Un MTA qui accepte du courrier pour des adresses qu’il n’a pas et le fait rebondir plus tard écrit donc à des inconnus pour le compte d’un spammeur. C’est le backscatter, et les listes noires réagissent vite. Pepsi y remédie en trois couches, chacune servant de filet sous la précédente :

  • Refuser au RCPT. pepsi-ingress répond 550 5.1.1 pour une adresse d’un domaine servi dont rien sur l’hôte ne se porte garant : un compte local, un alias, une liste, une adresse de service, ou le MDA ou le backend interrogé avec RCPT TO. Les sources sont déduites du graphe d’étapes (pepsi-recipients). Toute incertitude accepte, de sorte qu’une panne ne se transforme jamais en courrier refusé.

  • Ne faire de rapport qu’à un expéditeur qui a fait ses preuves. pepsi-stage-bounce n’envoie un DSN pour un message entrant que lorsque SPF a réussi pour le domaine de l’expéditeur d’enveloppe ou qu’une signature DKIM alignée a réussi (BOUNCE_UNAUTHENTICATED = drop, la valeur par défaut). Tout le reste est supprimé et journalisé.

  • Refuser une boucle. Un message qui est déjà passé MAX_SELF_HOPS fois par l’ingress de cet hôte est refusé comme boucle de routage. Une adresse de notre propre domaine relayée vers notre propre MX coûte donc un seul rebond, et non un tour du pipeline pour chaque saut que les étapes de relais autorisent.

Répondre honnêtement au RCPT indique à un expéditeur quelles adresses existent, comme le fait tout MTA qui rejette les utilisateurs inconnus. MAX_INVALID_RECIPIENTS ferme une session après dix essais erronés, si bien que la collecte coûte des connexions, que mesurent les limites de connexion. Les sondes du MDA ou du backend sont mises en cache et plafonnées à 16 en cours (au-delà, la réponse est « incertain », ce qui accepte). Un flot d’essais ne peut donc pas non plus être retourné contre le stockage du courrier. VRFY répond toujours 252 pour tout.

Le contrôle des destinataires exige un droit que pepsi-ingress n’avait pas : les colonnes de nom et d’hôte de mailing_list, qui forment ensemble l’adresse publique d’une liste et rien de plus.

15.8.6. Le changement de clé d’un correspondant

Garantit

Clés des correspondants, face à Un expéditeur distant : une clé récoltée peut être remplacée par une plus récente, mais jamais silencieusement.

Au sein du palier Autocrypt (les échelons inbound et gossip), la règle de conflit est celle d’Autocrypt Level 1, la plus récente l’emporte, fondée sur l’en-tête From: qu’écrit un expéditeur et sur un message que l’authentification entrante a accepté en fail-open. Un message qui ne fait que prétendre venir d’un correspondant peut donc remplacer la clé avec laquelle nous lui chiffrons. Les mécanismes qui bornent cela :

  • Le remplacement est consigné par l’instruction qui l’effectue. crypto_peer_key_upsert écrit un enregistrement key.peer.rotate dans pepsi.event_log (le journal d’audit en ajout seul qu’aucun rôle traitant le courrier ne peut UPDATE ni DELETE) dans la même transaction que l’échange, en nommant l’adresse, les deux ensembles d’empreintes et les deux dates d’effet. Aucun appelant ne peut faire tourner une clé sans cet enregistrement, et une étape compromise qui voudrait en cacher un devrait être le genre d’adversaire auquel Un composant compromis concède déjà la file.

  • Les personnes sur le point de répondre sont prévenues. pepsi-stage-autocrypt-learn écrit à chaque destinataire d’enveloppe local du message de rotation (RESPONSE_STAGE, le modèle key-rotation) en indiquant les deux empreintes, de sorte qu’une rotation falsifiée est quelque chose que l’utilisateur visé peut remarquer avant d’envoyer quoi que ce soit de confidentiel. Seuls les destinataires des domaines servis sont avertis, de sorte que l’avis ne peut pas être détourné en backscatter, et au plus une fois par destinataire et par message.

  • Un opérateur peut refuser la rotation d’une clé encore valide. ACCEPT_ROTATION = expired n’accepte la clé plus récente que lorsque chaque clé stockée est consignée comme révoquée ou expirée, ou a dépassé l’expiration qu’énonce son propre matériel (stockée lorsque la clé a été apprise et, pour OpenPGP, uniquement reportée à plus tard, de sorte qu’une copie plus ancienne rejouée du certificat ne peut pas l’antidater) ; sinon l’offre est retenue, auditée comme key.peer.rotate.held et signalée aux destinataires. On échange ainsi une rotation falsifiée contre une rotation authentique refusée jusqu’à ce qu’un opérateur intervienne.

  • Le palier reste confiné. Rien de tout cela n’élargit ce qui peut être remplacé : rien de ce qu’un domaine, un annuaire, un serveur de clés ou un opérateur a publié, ni aucun enregistrement épinglé, ne peut être touché par un message, et une adresse pour laquelle nous détenons une identité est de toute façon préemptée par notre propre clé. Une adresse servie pour laquelle nous ne détenons aucune identité n’est pas préemptée, et pepsi-keys peer unclaimed liste chacune de ces adresses ayant une clé récoltée ou issue de gossip (Repérer les utilisateurs qui apportent leur propre clé).

  • Une promotion n’est pas une rotation. Lorsque le propriétaire d’une clé issue de gossip annonce lui-même la même clé, crypto_peer_key_upsert fait passer l’enregistrement de l’échelon gossip à l’échelon inbound et écrit un enregistrement key.peer.promote (jamais key.peer.rotate, et sans avis, puisque la clé n’a pas changé). La décision est prise sous le même verrou par adresse, fondée sur les noms de source, et correspond au même verdict TrustSource::Autocrypt / Untrusted que toute clé récoltée (Une clé issue de gossip que son propriétaire confirme est promue).

pepsi-status liste les 30 derniers jours des deux sortes d’enregistrements.

15.8.7. SSRF de la découverte de clés

Garantit

Un expéditeur distant et Clés des correspondants : une adresse à laquelle quelqu’un écrit ne peut pas diriger le serveur vers son propre réseau.

La découverte de clés récupère des URL dérivées d’une adresse e-mail — WKD, et les serveurs de clés HTTP — ce qui est par construction une primitive de falsification de requête côté serveur. pepsi-keydisc résout lui-même le nom d’hôte et refuse toute adresse candidate située en boucle locale, en RFC 1918, en CGNAT RFC 6598, en lien-local ou en unique-local, et réapplique la vérification à chaque redirection (la redirection est le trou que laisse ouvert une conception « vérifier l’adresse puis se connecter »). La vérification n’est désactivée que par une couture de test explicite, car un serveur HTTPS bouchon écoute nécessairement sur la boucle locale.

15.8.9. L’API d’administration

Garantit

Un administrateur délégué et les frontières B6/B7.

Trois mécanismes d’authentification, un seul modèle d’identité : SO_PEERCRED sur une socket UNIX (infalsifiable, et c’est ainsi que fonctionne l’amorçage au premier démarrage lorsqu’il n’existe encore aucun compte), des jetons porteurs pour l’automatisation (stockés en condensat, comparés en temps constant), et un mot de passe plus un cookie de session pour les navigateurs (Argon2id, HttpOnly et SameSite=Strict, jeton CSRF sur chaque demande modifiante). Les routes d’administration ne se montent que sur des listeners marqués ADMIN = yes, de sorte qu’une installation de relais qui n’active jamais un tel listener n’expose pas du tout cette surface.

Les réponses ne contiennent jamais de secrets : GET /api/v1/config masque les options porteuses d’identifiants plutôt que de les omettre, de sorte qu’un opérateur puisse voir qu’un mot de passe est défini sans que le point de terminaison devienne un moyen d’en relire un.

15.8.10. Activer la télémétrie des fonctionnalités depuis la console

Garantit

Actifs (confidentialité envers les tiers : rien à quoi l’opérateur n’a pas consenti) et Un composant compromis (pepsi-httpd).

[pepsi] SHARE_TELEMETRY réside dans le fichier de configuration, de sorte que la console ne le modifie que comme elle y modifie toute chose : par une requête write-config qu’exécute l’applicateur de root. Ce qui rend cette offre sûre est une précondition que l’opérateur met en place à la main et que la console ne peut qu”observer : pepsi-telemetry-client doit être en cours d’exécution (en sommeil tant que la télémétrie est désactivée) et [pepsi] SYSTEM_ID doit être présent. Le démon rapporte les deux dans la table à un seul enregistrement pepsi.telemetry_client, que pepsi-httpd peut lire mais pas écrire — une console ne peut donc pas falsifier « prêt » — et l’enregistrement porte un booléen pour l’identifiant, jamais sa valeur. En son absence, la question est désactivée avec des indications et une requête d’API visant l’activation est refusée (409 telemetry_client_not_ready) ; la désactivation n’est jamais refusée.

Ce contrôle est une question d’utilité, pas la frontière. La frontière, c’est le démon : il ne croit que le fichier (par l’intermédiaire de l’unique lecteur de l’interrupteur), qu’il relit sur la notification telemetry_changed, sur SIGHUP et chaque minute ; une notification ne peut pas l’activer. Un pepsi-httpd compromis détenant CONFIG_DB pouvait déjà demander n’importe quelle configuration, cet interrupteur compris, et cela ne change pas ; ce qu’il ne peut pas faire, c’est amener un déploiement dont l’opérateur n’a jamais démarré le démon à soumettre quoi que ce soit. Rien sur ce chemin ne crée un identifiant que l’installation n’avait pas déjà : l’applicateur reporte au contraire un SYSTEM_ID existant d’un enregistrement de la console au suivant, et pepsi-setup n’en génère un que sur un oui. La désactivation écarte ce que le démon avait recueilli sans l’avoir encore envoyé.

15.8.11. Limites de ressources

Garantit

Disponibilité : un inconnu ne peut pas faire coûter au serveur sans limite un message, une connexion ou une adresse.

Les limites elles-mêmes sont présentées en tableau dans Disponibilité ; ce qui compte ici, c’est l’endroit où chacune est appliquée et la raison pour laquelle on ne peut pas la contourner.

  • Les connexions sont bornées par source, et pas seulement au total. pepsi-ingress compte les connexions ouvertes par plage de sources (MAX_CONNECTIONS_PER_IP) et, pour les pairs sur socket UNIX qui sont exemptés de sa limitation de débit et jamais évincés, pour l’ensemble d’entre eux (MAX_LOCAL_CONNECTIONS) ; pepsi-httpd fait de même par adresse client. Le contrôle a lieu avant qu’une connexion prenne une place sous le plafond global, de sorte qu’une source ayant atteint sa part ne peut pas en plus faire la queue pour davantage.

  • Les essais de mots de passe et les inondations sont comptés d’une session à l’autre. Un seau à jetons par plage (AUTH_FAILURES_PER_HOUR) est consommé par chaque AUTH échoué, et une fois vide, le AUTH suivant est refusé avant que le backend SASL soit interrogé ; un seau par uid, fondé sur l’uid SO_PEERCRED rapporté par le noyau (LOCAL_MESSAGES_PER_MINUTE), mesure la submission locale, et une session ne peut entamer que MAX_MESSAGES_PER_SESSION transactions avant de devoir se reconnecter et repasser par la limitation de débit.

  • La rafale d’un expéditeur ne retarde pas tous les autres. pepsi-dispatch prend en charge le travail de chaque étape selon le plus petit temps d’exécution virtuel par expéditeur plutôt que le plus ancien d’abord (FAIR_SCHEDULING, activé par défaut). L’expéditeur est la colonne générée workqueue.sender_key : le compte authentifié pour une submission, sinon l’adresse qui se connecte — un pair IPv6 par son /64, puisqu’un seul hôte en reçoit couramment un — et jamais le domaine d’enveloppe du courrier venant de l’extérieur, que l’expéditeur choisit à chaque message. Le temps de worker est imputé à l’expéditeur qui l’a occasionné, un nouveau venu part à égalité avec l’expéditeur le moins servi, de sorte que le silence n’accumule aucun crédit, et une part réservée au plus ancien d’abord (FAIR_FIFO_PERCENT) borne l’attente de tout message. La comptabilité réside uniquement dans la mémoire du dispatcher ; elle n’accorde rien et ne modifie aucun droit.

  • La vérification entrante a une borne de travail qui n’est pas un moyen de contourner DMARC. Au plus MAX_DKIM_SIGNATURES signatures sont vérifiées, en commençant par celles qui peuvent s’aligner sur le domaine du From:, et SPF et chaque signature s’exécutent en parallèle sous VERIFY_TIMEOUT, DMARC ayant une seconde échéance. Une vérification à court de temps devient temperror pour cette vérification : DMARC ne tient compte d’une erreur temporaire que sur un identifiant aligné sur le domaine du From:, et le DNS que DMARC lui-même lit est celui de ce domaine, de sorte qu’un falsificateur qui ralentit son propre DNS n’y gagne que son propre temperror.

  • Le matériel de clé provenant d’inconnus a une taille plafonnée. Chaque certificat OpenPGP venant de l’extérieur — WKD, un serveur de clés, DANE, un en-tête récolté ou issu de gossip, une clé jointe, une clé collée — passe par un unique analyseur qui refuse une clé RSA de plus de 8192 bits, et le vérificateur écarte également un tel certificat de son magasin de confiance. S/MIME a la même borne à 4096 bits, issue du crate rsa.

  • La rétention ne dépend pas de la couche web. Les journaux d’audit et de courrier sont purgés par pepsi-httpd prune depuis pepsi-log-prune.timer — sous le compte pepsi-httpd, le seul rôle de service détenant DELETE sur eux, ce qui garde les journaux en ajout seul pour tout ce qui traite le courrier — et les compteurs de rapports TLS et les entrées expirées du cache MX par leurs propres minuteries, toutes nommées par pepsi.target.

  • La file a un plafond, et le contrôle ne peut être ni affamé ni contourné par scission. pepsi-ingress refuse avec 452 4.3.1 au RCPT, au DATA/BDAT et après la fin des données tant que la file est à [pepsi] MAX_QUEUE_ROWS/MAX_QUEUE_BYTES ou que le système de fichiers de la base est sous MIN_FREE_SPACE. La mesure (pepsi.queue_usage()) ne lit que deux colonnes de taille générées, de sorte que le rôle ingress au moindre privilège n’obtient pour cela aucun accès au courrier en file, et elle est mise en cache par processus (QUEUE_CHECK_INTERVAL), de sorte qu’un flot de commandes RCPT ne coûte aucune requête. Les octets sont comptés par corps stocké : les corps résident dans pepsi.workqueue_body, partagés par tous les enregistrements que crée une scission, un clone ou une diffusion de liste, de sorte qu’un gros message adressé à de nombreux destinataires ne peut plus multiplier son corps à travers la file — l’amplification qu’offrait autrefois la scission par destinataire. Le seul amplificateur restant sous le contrôle d’un expéditeur, un message posté sur une grande liste, retient sa diffusion (pepsi-stage-list-post, QUEUE_THROTTLE_DELAY) tant que la file est pleine. Supprimer le dernier enregistrement qui référence un corps supprime ce corps (un trigger), et la clé étrangère fait qu’aucun rôle — le DELETE de la couche web sur workqueue_body compris — ne peut supprimer un corps que porte encore un message en file.

  • Les listes de diffusion bornent ce qu’un expéditeur peut leur faire faire. Les messages acceptés d’un expéditeur sur une liste sont comptés par heure (SENDER_POSTS_PER_HOUR) dans une seule instruction SQL qui décide et consigne, de sorte que des workers parallèles ne peuvent pas prendre tous deux la dernière place ; l’excédent est retenu plutôt que multiplié par le nombre de membres. La file de modération est plafonnée par liste (MAX_HELD_MESSAGES) en rejetant le nouveau message, de sorte qu’une inondation ne peut pas évincer les messages qu’un modérateur n’a pas encore vus.

  • Les dépenses automatiques ont un plafond agrégé, et on ne peut pas les prendre de vitesse. pepsi-stage-auto-pay impute une demande à deux budgets dans l’unique appel auto_pay_try_spend qui la consigne aussi : celui du message (MAX_TOTAL, sur l’enregistrement origin_nonce, verrouillé FOR UPDATE) et celui des 24 heures glissantes du wallet payeur (MAX_TOTAL_PER_DAY, le registre auto_pay_spend, sous un verrou consultatif de portée transactionnelle fondé sur le wallet — des demandes pour des messages différents ne partagent aucun enregistrement, de sorte que sans lui chacune lirait la même somme). Un refus par l’un n’écrit rien dans l’autre ; un paiement qui échoue à se régler est recrédité aux deux par auto_pay_refund, qui ne prend que le premier verrou, de sorte que les deux ne peuvent pas s’interbloquer. La clé du wallet est celle depuis laquelle le helper paie (uid:<n>, ou le compte partagé et la base de données du wallet), de sorte que deux expéditeurs ne peuvent partager une journée que s’ils partagent un wallet.

  • Une demande de paiement ne répond à un expéditeur qu’une fois par fenêtre. La demande est une réponse automatique à un expéditeur d’enveloppe non vérifié ; outre la suppression RFC 3834, pepsi-stage-anti-spam la réclame donc via payment_request_should_send — INSERT … ON CONFLICT DO UPDATE … WHERE décidant et consignant en une seule instruction, comme le fait vacation_should_reply — fondé sur la boîte aux lettres protégée et l’expéditeur, avec BLOCK_RESPONSE_SUPPRESS comme fenêtre. Un échec de la réclamation retient la réponse ; le message est retenu dans tous les cas.

15.9. Ce contre quoi Pepsi ne défend pas

La liste est tenue en un seul endroit, Ce que Pepsi ne protège pas, avec les résidus qu’énonce chaque section consacrée à un adversaire : root et un opérateur malveillant, l’analyse de trafic et les métadonnées, le texte en clair dans la file, les signatures face à un pipeline compromis, la compromission des correspondants eux-mêmes, et un attaquant réseau qui peut aussi mentir dans le DNS.

15.10. Signaler une vulnérabilité

Veuillez signaler les problèmes de sécurité en privé par courriel à Christian Grothoff, à l’adresse grothoff@gnunet.org, chiffrés avec la clé OpenPGP publiée à https://grothoff.org/christian/grothoff.asc, dont l’empreinte est D842 3BCB 326C 7907 0339 29C7 939E 6BE1 E29F C3CC. N’utilisez pas le gestionnaire de bogues public pour une vulnérabilité, et laissez le temps de publier un correctif avant toute divulgation. Le fichier SECURITY.md de l’arborescence des sources énonce la politique complète.

Les bogues ordinaires se signalent dans le gestionnaire de bogues public à l’adresse https://bugs.gnunet.org/view_all_bug_page.php?project_id=35.

15.11. Voir aussi

Gestion des clés — identités, garde, découverte et échelle de confiance. Le portail de repli par lien sécurisé — le portail en entier. L’API d’administration — la surface de l’API et ses portées. Interopérabilité des clients — ce que les autres clients savent réellement lire, ce qui borne la protection atteignable en pratique. Suite de tests — le fuzzing et les corpus adverses derrière les affirmations sur les analyseurs.