21. La console d’administration¶
Pepsi livre une petite console de navigateur, servie par pepsi-httpd sous /ui. Elle existe pour ce qui est pénible en ligne de commande : surveiller la file d’attente, parcourir les clés et les correspondants, lire le journal d’audit, et modifier la configuration avec la validation et la provenance sous les yeux.
Elle est cliente de l’API d’administration documentée (L’API d’administration) et rien de plus. Chaque page appelle les mêmes fonctions de bibliothèque qu’appellent les points de terminaison /api/v1, à travers la même authentification, les mêmes vérifications de portée et le même journal d’audit, de sorte que tout ce que la console sait faire peut aussi se faire avec curl — et que les deux ne peuvent pas être en désaccord sur ce que contient la file d’attente ni sur la validité d’une valeur de configuration.
Note
Il y a deux API HTTP dans ce serveur et elles ne sont pas la même. /api/v1 est celle de Pepsi (L’API d’administration), dont cette console est un client : comptes, portées, sessions et CSRF, conditionnée à ADMIN = yes. /3.0/ et /3.1/ sont l’API REST de GNU Mailman 3 (L’API REST de GNU Mailman 3), réimplémentée pour que les logiciels Mailman fonctionnent avec Pepsi : un mot de passe partagé, aucune portée, les formes de quelqu’un d’autre, conditionnée à LIST_API = yes. Un exploitant rencontrera les deux ; elles partagent le serveur et rien d’autre, et aucune n’est une version de l’autre.
21.1. Trois drapeaux de listener, et pour deux d’entre eux le conseil est inverse¶
pepsi-httpd sert quatre choses et en conditionne trois, chacune à son propre drapeau dans [pepsi-httpd-listener-*]. Les confondre est de loin la façon la plus simple de mal déployer la surface HTTP de Pepsi : ils sont donc réunis dans un tableau :
Drapeau |
Ce qu’il sert |
À qui il est destiné |
Où le placer |
|---|---|---|---|
|
|
L’exploitant. Jetons porteurs, sessions par mot de passe, |
Derrière un tunnel. Un socket UNIX, ou la boucle locale avec un proxy inverse. |
|
|
Les logiciels de listes de diffusion ( |
Boucle locale ou TLS. Le même refus que pour |
|
|
Le public anonyme. Aucun identifiant du tout. |
Sur l’internet ouvert. C’est la seule surface destinée à être joignable : la restriction sur le clair ne s’y applique donc pas délibérément – mais un formulaire d’abonnement porte l’adresse de quelqu’un, utilisez donc le listener TLS. |
(aucun) |
MTA-STS, Web Key Directory, autoconfiguration du courrier, le portail des liens sécurisés, |
Quiconque en a besoin, par conception. |
Là où se trouve le listener public. |
Chaque route conditionnée répond un simple 404 – octet pour octet ce que répond un chemin inconnu – sur un listener dépourvu de son drapeau. C’est pourquoi publier le listener HTTPS public ne publie pas l’administration, et pourquoi un listener public ne peut pas être sondé pour savoir ce que ce serveur fait tourner d’autre.
21.2. L’atteindre en toute sécurité¶
La console se monte exactement sur les listeners où se monte l’API : ceux marqués ADMIN = yes (voir pepsi-httpd(1)). Sur tout autre listener, les chemins /ui répondent un simple 404, octet pour octet ce que répond un chemin inconnu, de sorte que publier le listener public ne publie pas la console et qu’un listener public ne peut pas être sondé pour savoir si l’administration est activée ici.
pepsi-httpd refuse de la servir sur un listener marqué qui la transporterait en clair hors de cet hôte : le TCP en clair n’est accepté que sur une adresse de boucle locale, tandis que les listeners TLS et les sockets UNIX conviennent toujours. Un listener activé par socket (SERVE = systemd) sans TLS ne convient jamais, quoi que lie son unité, car le descripteur transmis pourrait être n’importe quoi et le traiter comme autorisé ferait passer le cas le plus strict par la vérification la plus lâche — donnez à l’administration un listener à elle. Il ne s’agit pas seulement d’écoute clandestine : le cookie de session ne porte le préfixe __Host- (RFC 6265bis §4.1.3.2) que lorsqu’il est posé Secure, c’est-à-dire seulement lorsque la connexion du client est en HTTPS, puisqu’un navigateur refuse un cookie __Host- qui ne l’est pas. Sur le chemin d’amorçage en clair par la boucle locale, ni l’un ni l’autre n’est employé : l’authentification propre de la console est donc structurellement plus faible sur une origine en clair. Un navigateur ne peut pas ouvrir une socket UNIX, aussi un déploiement typique
lie-t-il un listener d’administration à
127.0.0.1et l’atteint par un tunnel SSH (ssh -L 8443:127.0.0.1:8443 mail.example.com), ou biendonne-t-il au listener d’administration du TLS et un nom à lui, et le traite comme toute autre surface d’administration exposée à l’internet.
21.3. Se connecter¶
Deux des trois mécanismes de l’API sont utiles depuis un navigateur :
Un opérateur local sur la socket UNIX d’administration est identifié par SO_PEERCRED et n’a besoin d’aucun mot de passe. Les navigateurs ne savent pas parler à une socket UNIX : cette voie est donc pour curl et pour l’amorçage au premier démarrage ; c’est aussi ce qui rend acceptable la politique de session stricte ci-dessous.
Un administrateur distant se connecte à /ui/login avec un compte de pepsi.admin_account (créez-en un avec POST /api/v1/accounts). Les sessions ont un délai d’inactivité de 30 minutes et une durée de vie ferme de 12 heures quelle que soit l’activité, et il n’y a pas de « se souvenir de moi ». Cette console peut révoquer une clé, lire qui correspond avec qui et réécrire le pipeline ; un onglet de navigateur oublié ne devrait pas détenir cela demain, et un portable volé ne devrait pas être une session d’administration permanente.
Les jetons porteurs fonctionnent aussi, mais un navigateur n’a aucun moyen d’en attacher un ; ils sont destinés à l’automatisation contre /api/v1.
21.4. Ce que la console peut et ne peut pas faire¶
Les actions sont au niveau des messages et des clés uniquement :
remettre en file un message bloqué à son étape actuelle, l’acheminer vers une étape de rebond, ou le supprimer sans le remettre ;
publier ou retirer une identité locale, en rendre une principale, la supprimer, ou demander son téléversement vers le serveur de clés ;
demander à l’applicateur une clé gérée par le serveur, l’enregistrement de la propre clé publique d’un utilisateur, ou la révocation d’une identité (les trois passant par l’applicateur privilégié, ci-dessous) ;
oublier une clé de correspondant mise en cache, et retirer une ancre de confiance. Importer une clé à la main et mettre en file une recherche de découverte ne sont pas des actions de la console : ce sont
POST /api/v1/peersetPOST /api/v1/peers/discoversur l’API, ou pepsi-keys(1), que la page elle-même indique ;révoquer un message à lien sécurisé ;
poser ou retirer une redéfinition de configuration ;
répondre au questionnaire de configuration initiale, et demander à l’applicateur privilégié d’exécuter certbot, d’installer le schéma, de provisionner des rôles ou de vérifier le DNS.
Il n’y a délibérément aucun bouton de redémarrage, d’arrêt ou de sauvegarde. Une console web capable d’arrêter le serveur de messagerie est une console que l’on peut tromper pour qu’elle arrête le serveur de messagerie, et systemd possède déjà ce rôle. Redémarrez un composant avec systemctl restart pepsi-ingress (les pages de configuration nomment l’unité lorsqu’un changement en exige un).
Tout ce qui est privilégié ici a cette forme : la console note ce qui devrait se produire et un programme root distinct le fait. La console ne peut pas écrire /etc, exécuter certbot ni créer un rôle de base de données, et ne doit pas le pouvoir. Voir « La configuration initiale dans le navigateur » ci-dessous.
Deux autres choses que la console ne peut pas faire, parce que le serveur ne le peut pas :
Générer ou révoquer le matériel de clé lui-même. Cela requiert le rôle de base de données
pepsi-crypto, qui détient le droit sur la colonne privée et que pepsi-httpd ne détient délibérément pas — un niveau web capable de créer des clés de signature est un niveau web dont la compromission crée des clés de signature. Le formulaire Générer une clé gérée par le serveur de la page des identités ne fait donc que demander : il met en file une tâchegenerate-identityquepepsi-setup applyrevalide et exécute en tant quepepsi-crypto. Enregistrer ma propre clé (coller une clé publique OpenPGP en armure) met en fileregister-client-keyde la même manière, et le bouton Révoquer d’une identité, une fois confirmé, met en filerevoke-identity— révoquer une clé de signature seule détruit sa moitié privée, ce qui est une écriture dans cette colonne. Chacun répond avec la page propre de la tâche mise en file,/ui/setup/tasks/<id>, que l’utilisateur qui l’a demandée peut ouvrir quelles que soient ses portées, et qui affiche l’issue – terminée, échouée avec la raison, ou refusée – au moment même où l’applicateur l’enregistre. Les trois répondent503lorsque le paquet de l’applicateur n’est pas installé. Chaque formulaire a aussi un champ Code du second facteur : un utilisateur qui a enrôlé un second facteur (Gestion des clés) y saisit le code courant de son application d’authentification, et l’applicateur le vérifie – la console ne le peut pas. Les CSR et les certificats émis restent du ressort de pepsi-keys(1).Écrire la surcouche de configuration, à moins que
[pepsi-admin] CONFIG_DBne nomme une connexion s’authentifiant comme le rôlepepsi-config. Sans elle, les pages de configuration sont en lecture seule et disent pourquoi.
La console ne montre jamais non plus les en-têtes ou le corps d’un message. Les pages de file d’attente chargent l’enveloppe, l’étape, le statut et le JSON state, et rien d’autre — structurellement, car la requête derrière elles ne sélectionne pas ces colonnes. Lire du courrier n’est pas une fonction d’administration.
21.5. Les pages¶
Chemin |
Portée |
Ce qu’elle montre |
|---|---|---|
|
|
Tableau de bord : profondeur de file par |
|
|
Listage filtrable (par étape et par statut), paginé, avec un rechargement toutes les 30 secondes que l’on active explicitement. |
|
|
Un message : enveloppe, étape, statut et |
|
|
Paires de clés locales, filtrables par adresse, avec les formulaires Générer une clé gérée par le serveur et Enregistrer ma propre clé ( |
|
|
Une identité, avec sa garde (clé gérée par le serveur ou propre clé de l’utilisateur) et son état sur le serveur de clés, et publier / retirer / Téléverser vers le serveur de clés / rendre principale / révoquer ( |
|
|
Clés de correspondants mises en cache : source, indicateur DNSSEC, validité, état d’épinglage. En oublier une exige |
|
|
Les ancres de confiance S/MIME ; le retrait exige |
|
|
Chaque section de la configuration effective pour une portée, avec la façon dont un changement prend effet. |
|
|
Une section : la valeur de chaque option, d’où elle vient (fichier, global, domaine, adresse), et les formulaires pour poser ou retirer une redéfinition ( |
|
|
Le journal d’audit, filtrable par nature et par acteur. |
|
|
Le journal par message, sur option, filtrable par adresse ; le dit clairement lorsque |
|
|
Issues des sessions TLS sortantes par domaine de politique. |
|
|
Adresses MX que le cache du résolveur marque actuellement comme en échec. |
|
|
Les domaines servis, le mode MTA-STS, combien d’identités sont publiées sous chacun, et les enregistrements DNS à publier avec le dernier verdict en direct pour chacun. Le bouton « vérifier le DNS maintenant » exige |
|
|
Les messages à lien sécurisé que le portail retient : correspondants, expiration, lectures, PIN erronés, verrouillages. La révocation exige |
|
|
Un message avec son historique d’accès. |
|
|
Le point d’entrée de l’entretien de configuration initiale ; chaque étape est |
|
|
Ce qui a été demandé à l’applicateur privilégié, et un formulaire pour en demander davantage. Sans |
|
|
Une action avec la progression diffusée de l’applicateur, mise à jour dès que l’applicateur écrit quelque chose. |
Aucune page ne charge un ensemble de résultats non borné : chaque listage passe par une requête portant son propre LIMIT/OFFSET, c’est donc une propriété du SQL plutôt que de ce que dessine le moteur de rendu. Les vues de file d’attente et de journal paginent avec ?limit=/?offset= (plafonnés à [pepsi-admin] PAGE_LIMIT, au plus 1000). Les listages du magasin de clés — identités, correspondants, ancres de confiance — récupèrent l’équivalent d’une page, plus une ligne pour savoir s’il y en a davantage, et disent clairement quand ils ont été tronqués ; restreignez-les avec le filtre d’adresse, ou utilisez pepsi-keys(1). La console ne demande un COUNT(*) que là où elle affiche un total (les deux vues de journal) ; l’API le fait toujours, car ses clients paginent par programme. Voir L’API d’administration.
Chaque action destructrice — supprimer un message en file d’attente, révoquer ou supprimer une identité, retirer une ancre de confiance, retirer une redéfinition de configuration — passe d’abord par une page de confirmation explicite. Chaque mutation est écrite au journal d’audit avec la même nature d’événement que celle qu’enregistre l’API, de sorte que /ui/logs montre indifféremment les changements faits depuis la console, depuis curl et depuis les outils en ligne de commande.
Les valeurs de configuration susceptibles de porter un secret sont affichées comme ***, jamais comme des valeurs, selon la règle même dont l’API se sert pour masquer. La liste est délibérément généreuse : une valeur masquée à tort coûte à un opérateur un coup d’œil dans le fichier, une valeur affichée à tort est publiée à tous ceux qui détiennent config:read.
21.5.1. La configuration initiale dans le navigateur¶
/ui/setup rend le même questionnaire que pepsi-setup --wizard pose à un terminal — littéralement le même, car les questions, leurs formes, leurs valeurs par défaut et les conditions dans lesquelles elles sont posées vivent en un seul endroit sous forme de données (pepsi-setup-model) et les deux frontaux les rendent. Une question ajoutée à l’un est une question ajoutée aux deux.
Deux mécanismes distincts la rendent sûre :
Les réponses sont des brouillons. Chaque étape écrit dans pepsi.config_override avec l’indicateur draft, que chaque lecteur de configuration filtre structurellement. Rien ne prend effet pendant que le questionnaire est en cours, de sorte qu’une session qui expire à mi-chemin n’a pas à moitié configuré un serveur de messagerie, et rouvrir la page reprend là où on s’était arrêté. Une étape avec une mauvaise réponse se réaffiche avec le message à côté du champ et ne stocke rien — pas même les bonnes réponses de la même page, car une étape à moitié stockée est une étape qu’un opérateur doit reconstruire de mémoire.
Les actions sont des intentions. /ui/setup/tasks montre ce qui a été demandé à l’applicateur privilégié et offre un formulaire pour en demander davantage, dans son ensemble fermé : run-preflight, obtain-certificate, install-schema, provision-roles. Un bouton écrit une ligne pepsi.setup_task et sonne à une sonnette ; pepsi-setup apply, tournant en root, fait le travail et rediffuse sa progression dans la ligne, qu’affiche /ui/setup/tasks/<id>. Pendant l’exécution de la tâche, la page se recharge d’elle-même (un <meta http-equiv="refresh">, pas un minuteur en script), et ce rechargement n’interroge pas en boucle : il redemande la page avec ce qu’elle affichait déjà, et le serveur retient cette requête jusqu’à 20 secondes, jusqu’à ce que la notification de l’applicateur signale une nouvelle ligne de progression ou une issue. Le résultat apparaît donc au moment même où il existe. Si le serveur a perdu sa connexion d’écoute à la base de données, il répond immédiatement et la page se rabat sur un rechargement toutes les cinq secondes. Il n’y a pas d’action de redémarrage ou d’arrêt ; un changement qui exige le redémarrage d’un composant se termine par vous le redémarrant.
Les deux exigent setup:write, lectures comprises : les réponses préparées sont la forme d’un déploiement en construction et la liste des tâches est un registre de travail privilégié. La seule exception concerne les propres demandes d’un utilisateur : les actions sur les clés des pages d’identités mettent elles aussi des tâches en file, et quiconque en a mis une en file peut l’ouvrir et la trouver dans /ui/setup/tasks, qui pour lui ne liste rien d’autre. La tâche de quelqu’un d’autre est « introuvable ».
Deux conditions indépendantes doivent être réunies avant que quoi que ce soit puisse être enregistré, et la page indique laquelle manque :
l”applicateur privilégié doit exister — le paquet Debian distinct
pepsi-httpd-admin, qui contientpepsi-setup-apply.socketet son service. C’est unRecommends, il est donc installé par défaut et peut être retiré sans emporter Pepsi avec lui ; et[pepsi-admin] CONFIG_DBdoit nommer une connexion s’authentifiant comme le rôlepepsi-config, car l’applicateur refuse toute ligne qui n’a pas été écrite par lui.
L’applicateur est vérifié en premier, car sans applicateur, configurer CONFIG_DB ne servirait à rien. Lorsque l’un ou l’autre manque, l’entretien se dégrade en lecture seule plutôt que de mettre de côté des réponses que rien n’appliquera : chaque valeur est toujours rendue, les champs de saisie reviennent disabled/readonly, le bouton d’enregistrement disparaît, et un message éclair nomme la raison. Les appels d’API correspondants répondent 503 setup_applier_unavailable ou 503 setup_write_unavailable (voir L’API d’administration). La seule exception délibérée est le rejet d’un brouillon — cela ne peut causer aucune modification privilégiée, et un entretien coincé doit rester effaçable.
Notez que les pages /ui/config ne sont pas affectées par l’absence d’applicateur : elles écrivent pepsi.config_override, une table de base de données, non /etc. Elles n’ont besoin que de CONFIG_DB, et de rien d’autre.
Une réponse de forme password est rendue vide à chaque fois, jamais renvoyée dans un attribut value=, et soumettre l’étape en la laissant vide laisse tel quel ce qui est stocké. L’entrée d’audit d’une étape nomme les identifiants des questions et aucune valeur.
21.5.2. Liens sécurisés¶
/ui/secure liste les messages que le portail de lien sécurisé retient pour des destinataires sans clé : de qui et pour qui ils sont, quand ils expirent, combien de fois le PIN a été saisi correctement et incorrectement, et si un verrouillage est en vigueur. /ui/secure/<token> y ajoute le journal d’accès.
Le message lui-même n’est sur aucune des deux pages, et ne peut pas y être. pepsi-httpd est le portail : c’est donc le seul rôle de base de données à qui la colonne de texte chiffré est accordée — ce qui est exactement pourquoi les pages d’opérateur ne la lisent pas. Les requêtes derrière elles nomment chaque colonne sauf celle-là et n’en rapportent que la longueur, de sorte que la discipline vit dans le SQL plutôt que dans un champ de structure que quelqu’un a pensé à omettre. Révoquer détruit l’unique copie du message et exige donc secure:write, une capacité distincte de secure:read.
21.5.3. Le DNS sur la page des domaines¶
/ui/domains montre, à côté de chaque domaine servi, chaque enregistrement DNS que Pepsi attend qu’il publie : le nom, la valeur à publier, si le DNS en direct le porte, et quoi faire quand ce n’est pas le cas. Le texte de zone au bas de la page est le même bloc qu’imprime pepsi-setup run.
Le verdict vient de l’applicateur, non de ce serveur : établir ce qui devrait être publié suppose de lire les clés de signature, et le comparer suppose d’interroger le DNS. La page montre donc toujours quand la réponse a été calculée, et « vérifier le DNS maintenant » (setup:write) en demande une fraîche plutôt que de bloquer la requête sur un résolveur.
21.5.4. Pages pour l’utilisateur final¶
La portée own:<address> est définie et appliquée, et un principal ne détenant qu’elle atteint les pages d’identités et de correspondants restreintes à sa propre adresse — les pages d’une autre adresse répondent 404, et les objets partagés (le cache de clés de correspondants, les ancres de confiance) sont refusés d’emblée. Il atteint aussi le journal du courrier, restreint à sa propre adresse ; y demander une autre adresse est refusé avec 403. Sur ses propres identités, il peut aussi agir : demander une clé gérée par le serveur, enregistrer sa propre clé publique, demander un téléversement vers le serveur de clés, publier, retirer et demander une révocation – tout sauf supprimer une ligne (voir Gestion des clés). Il n’existe pas d”interface pour l’utilisateur final dédiée ; pepsi-stage-edit-settings(1) permet aux utilisateurs de modifier leurs propres paramètres par e-mail, et l’adresse de contrôle pepsi-keys@ de l’étape de chiffrement leur permet de gérer leurs clés par e-mail.
21.6. Deux moteurs de modèles, deux usages¶
Pepsi rend deux sortes de texte assez différentes et emploie un moteur différent pour chacune — n’essayez donc pas de redéfinir une page de la console comme on redéfinit un rebond.
Les corps de courrier sont des modèles Mustache, livrés comme fichiers sous [pepsi] TEMPLATE_DIR (bounce-<name>.<lang>.body, payment-request.<lang>.body, edit-settings.<lang>.body, wallet.<lang>.body). Ils sont éditables par l’opérateur et par langue : on attend d’un opérateur qu’il réécrive la prose qu’un correspondant lira, et qu’il ajoute une langue, sans rien reconstruire. Le modèle sans logique est exactement ce qu’il faut ici — il n’y a rien à calculer, et un modèle qui ne peut pas exécuter de code est un modèle qu’on peut confier sans risque à un opérateur.
Les pages de la console sont des modèles askama, compilés dans le binaire et vérifiés en types au moment du build contre les structures Rust qu’ils rendent. Il n’y a rien à installer à l’exécution, rien à maintenir en phase avec le binaire, et un champ renommé est une erreur de build plutôt qu’une case vide en production. La console est surtout faite de tableaux et de conditions, ce que le modèle sans logique rend pénible.
La conséquence pratique : il n’y a aucun moyen de redéfinir une page de la console, et ce n’est pas prévu. Si une page dit quelque chose de faux, c’est un bogue à signaler, non un fichier à éditer.
21.7. Pas de script, pas de ressources distantes¶
La console ne livre aucun JavaScript. Chaque action est un formulaire ou un lien : elle fonctionne donc avec les scripts désactivés — y compris le rafraîchissement automatique facultatif de la file d’attente, qui est un <meta http-equiv="refresh"> que le lecteur active et désactive par un lien plutôt qu’un minuteur. Les graphiques sont de petits SVG en ligne écrits à la main et générés sur le serveur : ils se rendent donc dans la même requête que le reste de la page.
Rien n’est chargé depuis un autre hôte : une feuille de style, compilée dans le binaire et servie depuis /ui/static/console.css. La Content-Security-Policy interdit donc le script d’emblée plutôt que d’autoriser celui de la console, et interdit l’encadrement (frame-ancestors 'none', avec X-Frame-Options: DENY à côté). Les pages sont envoyées avec Cache-Control: no-store, de sorte que les données d’administration ne restent pas dans un cache partagé ni dans le bouton retour après une déconnexion.
La falsification de requête inter-sites est refusée deux fois. Chaque formulaire porte le jeton CSRF de la session en champ caché, vérifié contre un condensat stocké avec la session ; et une requête modifiante dont l’en-tête Origin nomme un autre hôte est refusée avant même que son corps ne soit lu — ce qui couvre le formulaire de connexion, l’unique formulaire qui par définition n’a pas encore de session.
Les valeurs venues de l’extérieur — un sujet, le nom d’affichage d’un expéditeur, une valeur d’en-tête, le texte de réponse d’un rebond, l’identifiant utilisateur d’une clé — sont échappées par défaut au rendu, et les tests l’exercent avec des entrées hostiles plutôt que de s’y fier. Le seul balisage que la console émet sans échappement est le SVG de ses propres graphiques, qui par construction ne contient que des nombres.
21.8. Accessibilité¶
La console est du HTML simple : un lien d’évitement vers le contenu principal, un <h1> par page, des tableaux avec <caption> et <th scope=…>, une étiquette pour chaque champ de formulaire, et aria-current="page" sur l’entrée de navigation où vous vous trouvez. Chaque action est un véritable <button> à l’intérieur d’un formulaire : elle est donc atteignable et actionnable au clavier sans raccourci à apprendre, et l’anneau de focus est dessiné explicitement parce que celui par défaut disparaît sur certains fonds. Le clair et le sombre viennent tous deux de prefers-color-scheme ; il n’y a pas de sélecteur de thème, car un sélecteur exige du script ou un cookie et le système d’exploitation connaît déjà la réponse.
Les graphiques portent aria-hidden et ne sont jamais l’unique présentation de quoi que ce soit : les mêmes nombres figurent dans le tableau à côté de chaque figure.
La navigation n’offre que ce que vos identifiants permettent — un lien qui répond 403 est pire que pas de lien du tout.
21.9. Les pages publiques des listes de diffusion¶
Une seconde surface de navigation, distincte, conditionnée à LISTS = yes (voir Trois drapeaux de listener, et pour deux d’entre eux le conseil est inverse) et documentée ici parce qu’un exploitant rencontre les deux :
/listsL’index des listes annoncées, groupées par domaine. Une liste dont l’attribut
advertisedest faux est absente de l’index et reste joignable par URL – c’est le sens qu’amont donne à cet attribut, et ce n’est pas une frontière de sécurité. « Non annoncée » se lit comme « cachée » ; elle ne l’est pas./lists/<list-id>La description d’une liste, son texte
info, son adresse d’envoi, les formulaires d’abonnement et de désabonnement, un lien vers les archives quand la politique en autorise un, et la liste des membres seulement simember_roster_visibilityvautpublic./lists/<list-id>/confirm/<token>Ce vers quoi pointe un lien de confirmation. Un ``GET`` n’achève rien : il affiche à quoi sert le jeton et demande un unique
POST. Les clients de messagerie, les générateurs d’aperçus de liens et les analyseurs de sécurité récupèrent les liens, plusieurs d’entre eux avant la personne. C’est une différence délibérée avec l’adresse-confirm+<token>, où l”arrivée est la confirmation parce que le jeton a voyagé dans l’enveloppe et que seul son propriétaire a pu l’y mettre./archives/list/<list@domain>/…Les archives : vue d’ensemble, mois, fil, message, pièce jointe, recherche. Les formes d’URL sont celles de HyperKitty, si bien que chaque en-tête
Archived-At:qu’un déploiement migré a émis continue de fonctionner. Voir Archives.
Trois choses au sujet de ces pages méritent d’être connues avant de les déployer.
Elles ne livrent aucun JavaScript, pas le moindre. La Content-Security-Policy n’a donc pas de script-src – pas même 'self' – et dit default-src 'none'. Le prix en est les parties interactives de HyperKitty : le repli des fils se fait par <details>, il n’y a pas de recherche en direct ni de réponse dans la page. Cela a été décidé délibérément, et une politique qui autorise ce qui n’est pas utilisé est une politique qui autorisera un jour ce qui est injecté.
Les corps de messages sont du texte échappé, jamais du balisage. L’attribut archive_rendering_mode d’amont propose text ou markdown ; Pepsi stocke et expose l’attribut – l’ensemble des attributs est le contrat de compatibilité – et rend text dans les deux cas. Rendre du markdown fourni par un utilisateur dans notre propre origine est précisément ce que la politique ci-dessus doit rendre impossible, et un moteur markdown est une surface d’injection HTML avec des étapes en plus.
Les adresses d’expéditeur sont obscurcies pour les visiteurs anonymes. Une page d’archives affiche alice@... plutôt que l’adresse entière. Les archives conservent la vraie – obscurcir à l’entrée serait irréversible, et le traitement des rejets en a besoin –, c’est donc une décision de rendu. Ce n’est pas un contrôle de sécurité et ce n’est pas censé l’être : qui est sur la liste a l’adresse dans sa propre copie du message. Ce que cela empêche, c’est la collecte en masse qui fait d’archives publiques une source de spam.
21.9.1. Les formulaires qui font envoyer du courrier par le serveur¶
S’abonner, c’est demander au serveur d’envoyer un message à une adresse dont personne n’a prouvé la maîtrise : sans limitation, c’est un amplificateur de harcèlement – et Pepsi ne livre aucun CAPTCHA, par la même décision qui exclut JavaScript. Donc :
chacun de ces formulaires est limité en débit sur l’adresse cible autant que sur l’adresse du client ;
une seconde soumission alors qu’un jeton est encore valide réutilise ce jeton au lieu d’en émettre un autre, de sorte que dix soumissions ne font pas dix messages ;
la réponse est identique selon que l’adresse est déjà membre ou non : le formulaire n’est donc pas un oracle d’appartenance.
La dernière est la raison pour laquelle la page dit « si cette adresse peut être abonnée, une confirmation lui a été envoyée » et rien de plus utile.
21.10. Deux systèmes de comptes, et aucun n’accorde rien à l’autre¶
C’est la chose la plus facile à mal comprendre dans la couche web : elle est donc énoncée aussi platement que possible.
Pepsi a deux sortes de comptes de navigateur, dans deux tables, avec deux jeux de cookies, et ils n’ont aucun rapport :
La console de l’exploitant |
Le compte du membre de liste |
|
|---|---|---|
À qui il est destiné |
Celui qui fait tourner le serveur de courrier |
Quiconque s’abonne à une liste, ainsi que les propriétaires et modérateurs de ces listes |
Table des comptes |
|
|
Table des sessions |
|
|
Cookies |
|
|
Drapeau de listener |
|
|
Où il devrait être joignable |
La boucle locale, par un tunnel SSH |
L’internet public |
Créé par |
Un opérateur, via |
L’inscription via |
Ce qu’il peut faire |
La configuration, la file d’attente, les clés et les journaux. Pas les listes : celles-ci se gèrent avec pepsi-list(1) ou par l’API REST Mailman |
Ses propres abonnements, plus les listes dont son adresse est propriétaire ou modératrice |
Aucun n’est une version affaiblie de l’autre. Un compte d’exploitant n’est membre de rien, et un compte de membre – même celui d’un propriétaire de liste – ne peut lire ni la file, ni la configuration, ni les clés de qui que ce soit. Les deux sont séparés parce que leur conseil de déploiement est inverse : la console est ce que le manuel dit de garder hors de l’internet, et on ne peut évidemment pas demander à un propriétaire de liste d’ouvrir un tunnel SSH pour approuver un message retenu.
21.10.2. Si vous servez les deux surfaces depuis un seul nom d’hôte¶
L’assistant en avertit et ne peut pas l’empêcher. Les cookies sont liés à l’hôte et non au listener : un déploiement qui place ADMIN = yes et LISTS = yes derrière un même nom n’a exactement que deux choses pour séparer les surfaces, les noms de cookies et le contrôle, par le garde, de la table d’où vient une session.
C’est une ligne mince, et elle est mince par conception et non par accident. Elle est testée dans les deux sens – une session de membre présentée à une route d’exploitant est refusée exactement comme le serait l’absence de cookie, et une session d’exploitant présentée à une route de membre de même, sans que l’une ou l’autre produise une erreur distinguable. Préférez néanmoins deux noms, ou mieux deux listeners : ADMIN sur la boucle locale et LISTS en public est l’agencement que le reste de ce chapitre suppose.
21.11. Les pages du membre¶
/lists/registerCrée un compte et envoie un lien de vérification. Non oraculaire et limité en débit, comme le formulaire d’abonnement ci-dessus et pour la même raison.
/lists/verify/<token>Confirme l’adresse et définit le mot de passe, en une seule étape. Il n’y a pas de page « choisir un mot de passe » séparée, car un lien de vérification qui ne fait que vérifier laisse un compte où personne ne peut se connecter.
/lists/sign-in,/lists/sign-outSe connecter avec n’importe quelle adresse vérifiée du compte et l’unique mot de passe du compte. Un membre peut détenir plusieurs adresses – le modèle d’amont est un utilisateur à plusieurs adresses – et c’est ce qui fait fonctionner « je me suis abonné avec mon adresse professionnelle ». Une adresse non vérifiée ne peut pas se connecter : elle n’a rien prouvé.
/lists/resetEnvoie un lien de réinitialisation. Un lien, jamais un mot de passe : un mot de passe envoyé est un mot de passe qui reste pour toujours dans une boîte.
pepsi-list owner reset-passworddemeure l’issue de secours de l’exploitant quand le courrier est en panne, et il l”affiche au lieu de l’envoyer, précisément pour cette raison./lists/meSes adresses, et ses abonnements sur toutes les listes, chacun avec son mode et son état de remise.
21.11.1. Pourquoi une préférence peut ne pas s’appliquer là où on l’attend¶
Les six préférences de remise sont cherchées le long d’une chaîne – cet abonnement, puis cette adresse, puis ce compte, puis le défaut de la liste – et tout niveau peut être non défini. Un membre qui pose une préférence sur son compte et constate qu’elle ne s’applique pas à une liste l’a donc généralement posée à un niveau qu’un niveau plus précis surcharge.
/lists/me montre donc, pour chaque abonnement, de quel niveau la valeur provient. Cette colonne est la réponse à la seule question que cette conception provoque à coup sûr.
21.12. La console du propriétaire et du modérateur¶
/lists/<list-id>/adminLa page d’où l’on part : les messages retenus, les demandes d’abonnement et (pour un propriétaire) des liens vers les écrans de réglages et la liste des membres.
/lists/<list-id>/admin/held/<request-id>Un message retenu, ses octets exacts affichés en texte. Accepter libère ces octets, non un nouveau rendu de ceux-ci – un message retenu est conservé sous sa forme réseau précisément pour qu’une libération puisse être remise pour de vrai, signatures et pièces jointes intactes.
C’est la page la plus hostile du site : le courrier d’un inconnu, rendu là où le clic d’un modérateur peut agir. Le corps est donc du texte échappé dans un
<pre>, la politique n’autorise aucun script, et chaque bouton porte un jeton CSRF./lists/<list-id>/admin/settings/<screen>Les réglages de liste modifiables, sur onze écrans suivant le regroupement de Postorius lui-même – parce qu’un propriétaire qui migre cherchera un réglage là où il se trouvait. Le texte d’aide en regard de chaque champ est celui de Postorius, repris ; voir la note d’attribution dans Listes de diffusion.
Les écrans sont générés à partir de la même déclaration qui pilote l’API REST et la ligne de commande : une valeur refusée ici est refusée là-bas, dans les mêmes termes, et un réglage ne peut pas être présent à un endroit et absent à un autre.
Les six attributs de modèle
*_urifigurent aussi sur ces écrans, de sorte qu’un propriétaire qui veut son propre message d’accueil ou son propre pied de page pose l’URI là où résident tous les autres réglages, et non sur une page à part./lists/<list-id>/admin/rosterLes membres, les propriétaires et les modérateurs ; les interdictions de la liste ; et ses règles d’en-tête, avec les interdictions et les règles du serveur affichées à côté en lecture seule – parce que celles du serveur priment, et qu’un propriétaire qui cherche pourquoi une règle ne se déclenche jamais doit voir celle qui s’est déclenchée d’abord.
Ajouter une adresse est la seule action de ce palier qui abonne quelqu’un sans son accord. C’est une action de propriétaire, elle est écrite au journal d’audit avec le propriétaire comme acteur, et l’adresse ajoutée n’est pas marquée vérifiée : la parole d’un propriétaire n’est pas la preuve qu’une boîte existe.
Tout ce qui se passe dans cette console est audité par le même journal que chaque action d’exploitant, avec le compte du membre comme acteur. detail ne porte jamais de contenu de message – accepter un message retenu consigne la demande et le condensé du message, non le corps – et le journal est élagué par la minuterie [pepsi-admin] EVENT_RETENTION_DAYS de l’exploitant, qu’il vaut la peine de comparer à la durée pendant laquelle vous voulez garder l’historique de modération.
21.13. La langue de l’interface, et pourquoi elles ne sont pas dix¶
Les pages publiques sont servies en anglais, allemand ou français, choisis d’après l’en-tête Accept-Language de la requête. Une étiquette régionale reçoit sa langue (de-CH obtient les pages allemandes, car une variante d’une langue en est plus proche que l’anglais), l’ordre des q de l’en-tête est respecté plutôt que son ordre d’écriture, et un q=0 est lu comme le refus qu’il est – de, en;q=0 signifie donc allemand et non « allemand, puis n’importe quoi ».
Trois langues ici et dix pour le courrier est une véritable couture, et elle est énoncée plutôt que cachée. Les modèles de notification – le message d’accueil, la demande de confirmation, les avertissements de rejet – existent en dix langues, parce qu’ils viennent de GNU Mailman, qui les a en trente-quatre (voir Listes de diffusion). Les libellés de l’interface sont les nôtres, et ils existent dans les trois langues où existe le manuel. Un membre qui lit un message d’accueil en allemand et suit son lien arrive sur une page allemande ; un membre suédois arrive sur une page anglaise. Ajouter une langue à l’interface revient à traduire environ 140 libellés courts : c’est un après-midi et un correctif, pas un projet.
Le choix se fait d’après l’en-tête seul, avant tout accès à la base. C’est délibéré : une page publique est joignable par n’importe qui, et choisir un mot en lisant un enregistrement signifierait qu’une requête non authentifiée touche la base, sur un serveur dont le pool de workers est d’une connexion. La préférence enregistrée d’un membre connecté ne surcharge donc pas l’en-tête.