85.1.3. pepsi-httpd

serve MTA-STS policy, Web Key Directory, the Outlook add-in and metrics

Section du manuel:

1

85.1.3.1.1. Nom

pepsi-httpd - serveur HTTP/HTTPS pour MTA-STS, le Web Key Directory, le module complémentaire Outlook et les métriques.

85.1.3.1.2. Synopsis

pepsi-httpd [GLOBAL-OPTIONS] serve

pepsi-httpd [GLOBAL-OPTIONS] prune

85.1.3.1.3. Description

pepsi-httpd est le serveur HTTP/HTTPS de Pepsi. Il lie un ou plusieurs listeners et achemine chaque requête à travers une table de dispatch générique, en mettant en correspondance par méthode HTTP et forme d’URL : le chemin d’une route est une liste de segments, chacun étant un littéral, une capture nommée d’un segment, ou une capture de tout le reste. Les routes sont essayées dans l’ordre d’enregistrement et la première dont la méthode et le chemin correspondent l’emporte. D’autres points de terminaison s’ajoutent comme de simples entrées de table.

Chaque section [pepsi-httpd-listener-<name>] lie une socket — SERVE = tcp (BIND_TO/PORT, port 443 par défaut), unix (UNIXPATH) ou systemd (activation par socket) — avec MODE = plain ou tls. Un listener TLS sélectionne son certificat par connexion d’après le nom d’hôte SNI du client : chaque section [pepsi-httpd-cert-<name>] liste un ou plusieurs noms d’hôte SNI avec une paire TLS_CERT/TLS_KEY, et le TLS_CERT/TLS_KEY propre au listener (le cas échéant) est le repli pour les connexions qui ne présentent aucune correspondance SNI.

Lorsqu’un autre serveur web possède déjà les ports 80/443, pepsi-setup(1) configure à la place un déploiement en reverse-proxy. Le listener reste activé par socket (SERVE = systemd) mais en HTTP en clair (MODE = plain) ; le basculement se fait plutôt dans l’unité pepsi-httpd.socket : pepsi-setup dépose un drop-in de redéfinition (/etc/systemd/system/pepsi-httpd.socket.d/10-reverse-proxy.conf) qui réinitialise le ListenStream=443 livré et le relie à une socket UNIX à /run/pepsi/httpd.sock (mode 0660, appartenant au groupe propre du serveur frontal — www-data là où ce groupe existe, sinon nginx/apache/httpd), de sorte que systemd passe le fd de cette socket à pepsi-httpd exactement comme il le ferait pour la socket du port 443. Le serveur frontal termine le TLS et lui transfère les requêtes mta-sts.<domain> ; dans ce mode, pepsi-httpd ne détient aucun certificat propre. Supprimer le drop-in et exécuter systemctl daemon-reload rétablit la liaison directe :443. Voir pepsi-setup(1) et --no-reverse-proxy.

85.1.3.1.4. Points de terminaison

GET /.well-known/mta-sts.txt

Renvoie la politique MTA-STS (RFC 8461), construite à partir des options MTA_STS_* de [pepsi] et du HOSTNAME de [pepsi-ingress] (le mx). La politique n’est servie que lorsque le Host de la requête est mta-sts.<domain> pour un domaine de ACCEPTED_DOMAINS ; tout autre hôte (ou MTA_STS_MODE = none) donne 404. La servir ici évite d’héberger le fichier de politique à la main ; pepsi-setup(1) imprime l’enregistrement TXT _mta-sts à publier dans le DNS.

GET /mail/config-v1.1.xml

GET /.well-known/autoconfig/mail/config-v1.1.xml

Renvoie le document d”autoconfiguration de compte de courrier (draft-ietf-mailmaint-autoconfig) : le XML qu’un client de messagerie récupère, ne connaissant que l’adresse de l’utilisateur, pour découvrir quels serveurs employer, sur quels ports, sous quelle sécurité de transport et avec quelle authentification.

Ce sont les deux premiers échelons de la chaîne de repli que définit le brouillon, et le Host choisit entre eux : le premier est servi lorsque le Host de la requête est autoconfig.<domain> (l’échelon obligatoire, et celui que les clients essaient en premier), le second lorsque c’est <domain> lui-même. Dans les deux cas, DOMAIN doit figurer dans ACCEPTED_DOMAINS ; tout autre hôte donne 404. Le paramètre facultatif ?emailaddress= du brouillon est accepté et ignoré — le document nomme %EMAILADDRESS%, l’espace réservé que le client substitue, de sorte qu’aucun texte contrôlé par la requête n’est jamais interpolé dans la réponse.

La réponse est text/xml; charset=utf-8, cachable une heure, et — comme l’exige le brouillon — publique : elle ne porte aucune authentification, car un client doit la lire avant de pouvoir savoir comment s’authentifier.

Le document est construit depuis [pepsi-autoconfig], et le point de terminaison répond 404 tant que cette section ne nomme pas au moins un serveur entrant (IMAP_HOST ou POP3_HOST). Le serveur sortant est dérivé du [pepsi-ingress-listener-*] marqué SUBMISSION = yes, sauf si SMTP_HOST le redéfinit. La publication exige un enregistrement DNS autoconfig.<domain> et un certificat qui le couvre ; voir pepsi.conf(5).

GET /.well-known/openpgpkey/hu/HASH

GET /.well-known/openpgpkey/policy

GET /.well-known/openpgpkey/DOMAIN/hu/HASH

GET /.well-known/openpgpkey/DOMAIN/policy

Le Web Key Directory (draft-koch-openpgp-webkey-service), dans sa forme directe (servie sous le domaine lui-même) comme dans sa forme avancée (servie sous openpgpkey.<domain>, avec le domaine répété dans le chemin). HASH est le SHA-1 en z-base-32 de la partie locale mise en minuscules, stockée et indexée à côté du domaine, de sorte qu’une requête coûte une consultation indexée.

Une clé n’est renvoyée que pour un domaine figurant dans ACCEPTED_DOMAINS et seulement depuis une identité published, active et OpenPGP. La réponse est la clé publique transférable binaire — non blindée — avec Content-Type: application/octet-stream et Access-Control-Allow-Origin: * comme l’exige la spécification. Un condensat inconnu donne 404 avec un corps vide.

Le fichier policy est un 200 au corps de longueur nulle. Il ne porte aucun indicateur, mais il doit exister : GnuPG interprète son absence comme « ce domaine n’exploite aucun Web Key Directory » et abandonne avant de demander une clé.

Le paramètre ?l=<local-part> est lu et ignoré. L’honorer transformerait le point de terminaison en une consultation par adresse fournie par l’appelant plutôt que par un condensat que l’appelant devait déjà connaître.

Le point de terminaison ne sert que les identités publiées propres à ce déploiement (crypto_identity). Il ne sert jamais une clé de correspondant mise en cache (peer_key) : ce matériel n’a jamais été vérifié par nous et n’est pas à nous pour être publié sous notre propre nom. Il n’existe aucune option pour changer cela.

Il n’y a délibérément ici aucune limitation de débit ni aucune mise en forme uniforme des 404. Un Web Key Directory est par construction un oracle public, le condensat ne couvre que la partie locale (il répond donc à des devinettes plutôt qu’il n’énumère), et les adresses figurent à l’extérieur de chaque message que le domaine envoie. Voir pepsi-keys(1) et le chapitre de gestion des clés du manuel.

Pour la forme avancée, openpgpkey.<domain> doit se résoudre vers cet hôte et le certificat servi doit couvrir ce nom ; c’est un nom SNI sur le même listener HTTPS, non un second listener. pepsi-setup(1) l’ajoute à la demande certbot et signale tout nom qui ne se résout pas.

GET /addin/manifest.xml

GET /addin/taskpane.html

Le module complémentaire Outlook : un manifeste et un volet des tâches d’une seule page qui permettent à un utilisateur d’un système de courrier situé derrière cette passerelle de demander qu’un message soit signé ou chiffré. Le volet pose X-Pepsi-Sign / X-Pepsi-Encrypt sur le message en cours de rédaction ; pepsi-stage-encrypt(1) les lit et les retire avant que le message ne parte sur le fil.

Les deux routes donnent 404 à moins que [pepsi-httpd] ADDIN ne soit défini. Deux espaces réservés sont substitués à chaque requête : l’origine publique https:// de cette passerelle (depuis ADDIN_URL, sinon le Host de la requête ; le schéma est toujours https, car Outlook refuse de charger les ressources d’un module complémentaire en HTTP simple – une origine http:// configurée est refusée au démarrage, sauf si son hôte est en boucle locale, ce qui est autorisé pour les essais locaux) et un identifiant de manifeste dérivé de cette origine — stable pour un déploiement et distinct entre déploiements, de sorte que deux passerelles chargées latéralement dans une même organisation Exchange n’entrent pas en collision. Un Host contenant autre chose que ce qu’un nom d’hôte et un port peuvent contenir est refusé par 400 plutôt qu’échappé.

La distribution se fait par chargement latéral : l’administrateur Exchange pointe « Applications intégrées » vers l’URL du manifeste. Voir le chapitre « Microsoft Exchange comme passerelle » du manuel.

GET /metrics

Statistiques du pipeline au format d’exposition texte Prometheus. Les jauges en direct (pepsi_stage_active_messages, pepsi_pause_backlog) sont lues directement depuis la file ; les compteurs (pepsi_stage_timeouts_total, pepsi_stage_crashes_total, pepsi_stage_messages_total, pepsi_stage_duration_seconds_total et les globaux pepsi_stages_executed_total / pepsi_messages_processed_total) proviennent des tables de statistiques que pepsi-dispatch(1) met à jour.

Le point de terminaison est d’administration : les profondeurs de file, les comptes de plantages et de timeouts par étape, les totaux cumulés et chaque nom d’étape choisi par l’opérateur décrivent ensemble la quantité de courrier que porte ce déploiement et la façon dont son pipeline est construit. Il n’est donc servi que sur un listener marqué ADMIN = yes, autorisé à porter effectivement la surface d’administration, et répond partout ailleurs un simple 404 — celui qu’obtient tout chemin inconnu. Contrairement à /api/v1, il ne demande aucun identifiant, car un collecteur Prometheus n’en a aucun à présenter : l’indicateur de listener est tout le contrôle d’accès, ne marquez donc qu’un listener que seul le système de supervision peut atteindre.

Pointez le collecteur vers le listener d’administration, ou ajoutez ADMIN = yes au listener qu’il interroge déjà (un listener en clair doit être sur la boucle locale ou une socket UNIX — voir La surface d’administration ci-dessous). Lier pepsi-httpd lui-même à une adresse privée n’est pas une solution de rechange : le même processus doit répondre à mta-sts.<domain> (RFC 8461) et à openpgpkey.<domain> depuis l’internet public, et il n’existe pas de liaison de listener par route. La première collecte refusée parce que son listener n’est pas marqué est journalisée avec le nom du listener.

POST /resume

Libère un message en pause dans le pipeline : la ligne correspondante passe de paused à pending et le dispatcher est notifié afin que l’étape responsable s’exécute à nouveau. C’est la cible du webhook de paiement pepsi-resume de GNU Taler (voir pepsi-setup(1) et [pepsi-payments]), appelé lorsqu’un bon de commande est payé.

La requête doit porter Authorization: Bearer <token> correspondant au RESUME_AUTHORIZATION_TOKEN configuré (comparé en temps constant), et un corps JSON {"message_id": "<token>"} nommant le jeton externe du message (l”order_id du marchand). Réponses : 200 lorsque le message existe (un message en pause a été repris, ou il n’était déjà pas en pause — l’appel est idempotent), 404 lorsque le jeton est inconnu, 401 en cas d’autorisation échouée, 400 sur un corps malformé, 413 sur un corps de plus de 4 Kio. Le point de terminaison est désactivé (404) lorsque RESUME_AUTHORIZATION_TOKEN n’est pas configuré.

/api/v1/…

L’API d’administration — la file, le résumé de santé, la configuration, le magasin de clés, le journal d’audit. Servie uniquement sur un listener marqué ADMIN = yes, et seulement à un principal authentifié. Voir La surface d’administration ci-dessous et le chapitre « L’API d’administration » du manuel pour la référence des points de terminaison, la table des portées et la forme des erreurs.

/ui, /ui/…

La console d’administration : un frontal de navigateur rendu côté serveur la file, le magasin de clés, la configuration et les journaux. Servie sur les mêmes listeners ADMIN = yes que /api/v1, sous la même authentification, les mêmes vérifications de portée et le même journal d’audit — elle est cliente de l’API et ne lui ajoute aucune capacité. Les pages sont

/ui (tableau de bord), /ui/login, /ui/logout, /ui/queue, /ui/queue/ID, /ui/queue/ID/{requeue,bounce,cancel}, /ui/identities, /ui/identities/ID, /ui/identities/ID/{publish,unpublish,vks,primary,revoke,delete}, /ui/identities/request/{generate,register}, /ui/peers, /ui/peers/ID/delete, /ui/ca-trust, /ui/ca-trust/ID/delete, /ui/config, /ui/config/SECTION, /ui/logs, /ui/logs/{mail,tls,dns}, /ui/domains, /ui/domains/check, /ui/secure, /ui/secure/TOKEN, /ui/secure/TOKEN/ACTION, /ui/setup, /ui/setup/STEP (GET et POST), /ui/setup/tasks (GET et POST), /ui/setup/tasks/ID et l’unique feuille de style /ui/static/console.css.

Attente d’une tâche de configuration. GET /api/v1/setup/tasks/ID accepte ?wait=SECONDS (60 au plus, combinable avec ?since=SEQ) et répond dès que la tâche est terminée ou a une ligne de progression postérieure à SEQ, ou lorsque le temps est écoulé ; la page de tâche de la console attend de la même façon jusqu’à 20 secondes par rechargement. Ni l’une ni l’autre n’interroge la base de données en boucle : le serveur maintient une connexion LISTEN sur les canaux setup_task_done et setup_task_progress, que les écritures de l’applicateur notifient avec l’identifiant de la tâche, et réveille les requêtes qui attendent cette tâche. La connexion est distincte du pool de requêtes et se reconnecte d’elle-même avec un délai croissant ; tant qu’elle est coupée, une attente rend la main aussitôt plutôt que de rester bloquée, et la page de la console se replie sur un rechargement toutes les cinq secondes.

L’interrupteur de télémétrie. La question SHARE_TELEMETRY de l’interview ne peut recevoir la réponse oui que lorsque pepsi-telemetry-client(1) est en cours d’exécution (en sommeil tant que la télémétrie est désactivée) et que [pepsi] SYSTEM_ID est configuré, car ce serveur ne génère jamais d’identifiant et un oui sans démon pour y donner suite ne ferait rien. Il apprend l’un et l’autre de la ligne de vivacité du démon dans pepsi.telemetry_client, qu’il peut lire mais non écrire, et qui porte un booléen pour l’identifiant, jamais sa valeur. Sinon, la case à cocher est rendue désactivée avec des indications (démarrer l’unité ; ajouter un SYSTEM_ID), GET /api/v1/setup/questions marque la question "disabled": true avec un disabled_reason, et un oui passant par PUT /api/v1/setup/answers ou par une tâche write-config est refusé avec 409 telemetry_client_not_ready. GET /api/v1/setup/telemetry rapporte le même état (sharing, can_enable, reason, client). La désactivation de la télémétrie n’est jamais refusée. Le write-config de l’applicateur notifie le démon après avoir écrit le fichier, dans un sens comme dans l’autre.

Une liste d’autorisation de chemins en reverse-proxy doit inclure aussi les familles /ui/setup* et /ui/secure* : la première est tout le questionnaire de configuration initiale du navigateur, la seconde les pages d’administration du lien sécurisé.

La console ne livre aucun JavaScript et ne charge rien depuis un autre hôte ; chaque action est un formulaire, chaque action destructrice est confirmée, et chaque formulaire modifiant porte le jeton CSRF de la session en champ caché (une requête modifiante dont l”Origin nomme un autre hôte est refusée d’emblée). Elle n’exerce aucun contrôle de service — il n’y a délibérément aucun bouton de redémarrage, d’arrêt ou de sauvegarde. Voir le chapitre « La console d’administration » du manuel.

/3.0/…, /3.1/…

L’API REST de GNU Mailman 3 – version amont 3.3.10, réimplémentée pour que les logiciels écrits pour GNU Mailman 3 (mailmanclient, Postorius, HyperKitty) fonctionnent sans modification avec Pepsi. Servie uniquement sur un listener marqué LIST_API = yes, sous la même règle de liaison que ADMIN (clair sur boucle locale, socket UNIX, ou TLS), et authentifiée par HTTP Basic contre l’unique paire [pepsi-list] API_USER/API_PASS. Ce n’est pas /api/v1 : public différent, enveloppe de collection différente, forme d’erreur différente, aucune portée. Voir le chapitre « L’API REST de GNU Mailman 3 » du manuel.

LIST_API_TESTING = yes arme en outre GET /3.1/reserved/reset, qui vide toutes les tables de listes et d’archives. Il existe pour les suites de tests de compatibilité et ne doit pas être posé sur un déploiement portant du courrier réel.

GET /favicon.ico

GET /favicon-VERSION.svg

Le badge Pepsi comme icône du navigateur, servi sur chaque écouteur quels que soient ses drapeaux : une icône ne dit rien du déploiement. La console, sa page de connexion et les pages des listes de diffusion font référence au SVG, dont le nom porte le début de son SHA-256 ; comme une nouvelle icône est une nouvelle URL, il est envoyé avec Cache-Control: public, max-age=31536000, immutable. /favicon.ico (16, 32 et 48 px) est ce qu’un navigateur va chercher de lui-même pour une page qui ne référence aucune icône ; son URL ne peut pas changer, il peut donc être mis en cache une semaine. Les deux portent un ETag et répondent 304 à un If-None-Match correspondant. Les fichiers sont compilés dans le binaire.

/lists, /lists/…, /archives/list/…, /robots.txt

L”interface web publique des listes de diffusion : l’index des listes, la page d’information d’une liste, les formulaires d’abonnement et de désabonnement, les pages de confirmation, la cible de désabonnement en un clic (RFC 8058), les archives (aux formes d’URL de HyperKitty, pour que les liens Archived-At: d’un déploiement migré continuent de fonctionner) et leur recherche. Servie uniquement sur un listener marqué LISTS = yes, à quiconque, sans identifiant.

C’est le seul drapeau dont le conseil de déploiement est l’inverse des deux autres. ADMIN est pour l’exploitant et LIST_API porte un mot de passe partagé : tous deux refusent d’être servis là où leurs identifiants traverseraient un réseau en clair ; cette surface existe pour être atteinte depuis l’internet ouvert, cette restriction ne lui est donc délibérément pas appliquée. Un listener LISTS en clair est le choix de l’exploitant et reçoit une ligne de journal plutôt qu’un refus – mais un formulaire d’abonnement porte l’adresse de quelqu’un : placez-le sur un listener TLS.

Ces pages ne livrent aucun JavaScript, et leur Content-Security-Policy n’a par conséquent aucun script-src. /robots.txt autorise les archives et interdit la recherche, la seule route qui exécute une requête plein texte par appel ; la recherche est en outre limitée en débit sur son propre budget, car un robot qui ignore le fichier doit rester supportable.

Les deux budgets s’expriment en requêtes par minute et par adresse source (un client IPv6 par /64), lues dans la section [pepsi-list] :

WEB_RATE_LIMIT

(entier, facultatif) Toutes les routes de cette interface et du niveau membre ci-dessous. Valeur par défaut 600.

WEB_SEARCH_RATE_LIMIT

(entier, facultatif) La route de recherche, en plus de la limite ci-dessus. Valeur par défaut 30.

Une valeur inférieure à 1 est relevée à 1 : aucune des deux ne peut être désactivée. Derrière un proxy inverse, chaque requête arrive par la socket UNIX sans adresse de client, de sorte que tous les clients partagent un même budget ; relevez-y les limites et laissez le proxy limiter par client.

Le même drapeau sert aussi le palier des membres – /lists/register, /lists/verify/<token>, /lists/sign-in, /lists/sign-out, /lists/reset et /lists/me – ainsi que la console du propriétaire et du modérateur à /lists/<list-id>/admin.... Ni l’un ni l’autre n’est un second drapeau : un système de comptes que personne ne peut atteindre n’est pas un choix de déploiement que quiconque ferait.

Ces pages emploient pepsi.list_session et les cookies pepsi_list_session/pepsi_list_csrf, qui ne sont pas les pepsi_session/pepsi_csrf de la console de l’exploitant et n’y accordent rien – ni l’inverse. L’autorité sur une liste est une interrogation de la liste des membres et non un drapeau de compte : un enregistrement list_member avec role = owner ou moderator pour cette liste, plus list_user.is_server_owner comme seule autorité globale du palier. Un appelant qui ne l’a pas obtient la page « introuvable » ordinaire, car sur une surface publique un refus qui se distingue a révélé que la chose existe. Voir la section « Deux systèmes de comptes » du manuel.

85.1.3.1.5. La surface d’administration

Un listener portant ADMIN = yes sert en outre /api/v1, la console /ui et /metrics. Sur tout autre listener, ces routes répondent un simple 404, octet pour octet celui qu’obtient tout chemin inconnu, de sorte que publier le listener public ne publie pas l’administration et qu’un listener public ne peut pas être sondé pour savoir si l’administration est activée sur ce déploiement.

/api/v1 et /ui authentifient et autorisent en outre chaque requête, comme ci-dessous. /metrics non : un collecteur n’a aucun identifiant à présenter, l’indicateur de listener est donc tout ce qui se dresse devant lui.

pepsi-httpd refuse de les servir sur un listener marqué qui les 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) ne convient jamais sans TLS, quoi que lie son unité — y compris un ListenStream= sur la boucle locale ou sur un chemin UNIX — car l’adresse du descripteur hérité n’est pas visible d’ici, et la traiter comme autorisée laisserait passer le cas le plus strict par la vérification la plus lâche. Puisque le déploiement livré est activé par socket, l’administration demande un listener à elle (une socket UNIX, ou une socket TLS). Un listener qui échoue au test journalise un avertissement au démarrage et ne sert que les points de terminaison publics ; pepsi-setup(1) signale la même chose au moment de la configuration.

Trois mécanismes d’authentification se résolvent en un seul modèle d’identité, essayés dans cet ordre — un identifiant explicitement présenté l’emporte toujours, de sorte qu’un jeton délibérément étroit n’est jamais silencieusement élargi :

  1. Authorization: Bearer pepsi_<id>.<secret> — une ligne de pepsi.api_token. Seul le condensat de la moitié secrète est stocké.

  2. Un cookie de session issu de POST /api/v1/auth/login — ou du formulaire POST /ui/login propre à la console, qui frappe la même session — avec un jeton CSRF requis sur chaque requête modifiante. Les sessions ont par défaut un délai d’inactivité de 30 minutes et une durée de vie dure de 12 heures ([pepsi-admin] SESSION_IDLE et SESSION_LIFETIME).

  3. SO_PEERCRED sur un listener à socket UNIX : root, ou un membre de [pepsi-admin] ADMIN_GROUP, est administrateur sans aucun identifiant. C’est ce qui fait fonctionner l’amorçage au premier démarrage et ce sur quoi s’appuie la configuration livrée.

PAM n’est délibérément pas pris en charge : cela placerait un chemin d’authentification privilégié et la sémantique des comptes système à l’intérieur de la couche web du serveur de courrier.

L’autorisation se fait par portée, déclarée par point de terminaison (config:read, config:write, keys:read, keys:write, peers:write, queue:read, queue:write, logs:read, setup:write, secure:read, secure:write, plus own:<address> pour un principal confiné à une seule adresse). GET /api/v1/openapi.json est engendré depuis la table de routes propre au serveur et constitue la liste faisant foi pour un build donné.

secure:read et secure:write sont séparées parce que révoquer un message à lien sécurisé en détruit l’unique copie ; une suppression irréversible n’a pas sa place derrière une capacité nommée « read ».

La génération ou la révocation de matériel de clé ne se fait jamais ici : le matériel privé appartient au seul rôle de base pepsi-crypto. Générer une clé gérée par le serveur (POST /api/v1/identities), enregistrer la propre clé publique d’un utilisateur (POST /api/v1/identities/client) et révoquer une identité (PATCH /api/v1/identities/{id} avec {"status": "revoked"} et un revocation_reason facultatif, ce qui détruit la moitié privée d’une clé de signature seule) sont au lieu de cela demandés, sous forme de lignes generate-identity, register-client-key et revoke-identity dans pepsi.setup_task que pepsi-setup apply revalide et exécute en tant que pepsi-crypto (voir ci-dessous). Un PATCH de révocation est donc asynchrone : il ne porte que status et répond avec la tâche mise en file ; l’identité se lit revoked une fois que l’applicateur a tourné. L’enregistrement n’implique aucun matériel privé, mais une clé enregistrée comme celle de l’utilisateur devient sa face publique et vérifie des signatures en son nom : un tiers web compromis ne doit donc pas pouvoir en implanter une. Produire une CSR et importer un certificat émis ne sont pas proposés via HTTP ; utilisez pepsi-keys(1).

Libre-service sur ses propres clés. Un principal confiné à une adresse (own:<address>) peut atteindre, pour cette seule adresse, les trois écritures de clé du libre-service à la manière de pEp : POST /api/v1/identities ({"address": ..., "vks": true|false}, vks facultatif et valant par défaut [pepsi-keys] VKS_PUBLISH), POST /api/v1/identities/client ({"address": ..., "key": "<armoured OpenPGP public key>"}, 64 Kio au plus) et PATCH /api/v1/identities/{id} (dont le champ vks_wanted, lorsqu’il vaut true, enregistre une demande de téléversement vers le serveur de clés pour une identité OpenPGP active et la publie via WKD ; la tâche de réessai pepsi-keys identity publish --retry effectue le téléversement ; et dont status: revoked demande une révocation). Chaque gestionnaire revérifie l’adresse ; l’identité d’une autre adresse répond 404. Les deux routes POST et un PATCH de révocation répondent avec la tâche mise en file ({"task": ..., "kind": ..., "applier_notified": ...}), non avec la clé, et le principal qui l’a demandée peut suivre cette tâche : GET /api/v1/setup/tasks/ID répond à tout principal pour une tâche qu’il a lui-même mise en file (404 pour celle de quelqu’un d’autre, comme pour une tâche inexistante ; la propriété est le requester_key de la tâche – le sélecteur du jeton, le numéro de ligne du compte ou le login du pair, aucun d’eux n’étant jamais réutilisé –, non le nom d’affichage requested_by), et GET /api/v1/setup/tasks ne liste que ces tâches-là à un principal dépourvu de setup:write. Les formulaires de clé de la console répondent avec la page de la tâche, /ui/setup/tasks/ID, aux mêmes conditions. DELETE /api/v1/identities/{id} reste réservé à l’opérateur (keys:write) : un utilisateur retire une clé, il n’efface pas sa ligne. La console offre la même chose via les formulaires Générer une clé gérée par le serveur et Enregistrer ma propre clé de la page des identités et les actions Téléverser vers le serveur de clés et Révoquer de la page d’une identité.

L’applicateur admet ces trois types de tâches avec sa propre barrière d’autorisation : le principal demandeur doit avoir détenu keys:write, ou own:<address> pour exactement l’adresse nommée dans les paramètres de la tâche (setup:write seul ne suffit pas). Il exige ensuite que l’adresse soit dans un domaine de [pepsi-ingress] ACCEPTED_DOMAINS et, pour un enregistrement, un unique certificat OpenPGP dont les User IDs nomment l’adresse ; pour une révocation, l’identité doit appartenir à cette adresse. Sans l’applicateur, les trois répondent 503 setup_applier_unavailable, comme tout autre chemin de l’applicateur ; la mise en file exige aussi la connexion [pepsi-admin] CONFIG_DB (503 setup_write_unavailable sinon).

Le second facteur. Chacun des trois corps de requête (et les formulaires de la console) accepte un "otp": "123456" facultatif, le code de second facteur actuel du propriétaire de l’adresse (voir pepsi-keys(1), otp). Ce serveur ne peut pas le vérifier – il ne détient aucun droit sur pepsi.otp_key –, il se contente donc de transmettre le code dans les paramètres de la tâche, où l’applicateur le vérifie, avant d’agir, pour une tâche admise au seul titre de own:<address> ; un code absent, erroné, rejoué ou verrouillé fait échouer la tâche avec le motif. Un principal détenant keys:write n’est pas sollicité. DELETE /api/v1/otp/ADDRESS (keys:write, jamais accessible sous own:) met en file une tâche reset-otp qui supprime le second facteur de l’adresse, ce qui est la façon de déverrouiller un second facteur verrouillé. Les changements d’indicateurs synchrones de PATCH /api/v1/identities/{id} (is_primary, published, vks_wanted) sont effectués ici et ne sont donc pas soumis au second facteur.

Deux opérations que ce serveur refuse délibérément : écrire la surcouche de configuration (503 à moins que [pepsi-admin] CONFIG_DB ne nomme une connexion s’authentifiant comme le rôle pepsi-config, ce que le serveur vérifie au démarrage), et toute requête de configuration privilégiée lorsque le paquet de l’applicateur n’est pas installé (503, voir L’applicateur privilégié, et s’en passer ci-dessous).

La politique de rétention du journal d’audit et, lorsqu’il est activé, du journal de courrier est appliquée par la commande prune de ce binaire, non par le serveur : chaque compte de traitement du courrier peut ajouter à ces tables sans pouvoir en effacer, et le rôle pepsi-httpd est le seul rôle de service qui y détient DELETE (l’unique autre détenteur est le rôle pepsi-config de l’opérateur). Le pepsi-log-prune.timer livré l’exécute quotidiennement sous ce compte, de sorte que la rétention s’applique que le serveur web tourne ou non — c’était auparavant une tâche de fond de serve, démarrée seulement lorsqu’un listener servait la surface d’administration, et un déploiement sans la console conservait chaque enregistrement indéfiniment.

Note

Un seul binaire sert la surface publique et la surface d’administration. Un bogue dans le processus serveur partagé est donc un bogue dans les deux ; l’isolation est à configurer par l’opérateur — liez le listener d’administration à une socket UNIX ou à la boucle locale.

GET /secure/TOKEN

La page d’entrée du portail de repli par lien sécurisé. Sans session, elle demande le PIN ; avec un cookie de session valide, elle rend le message. Répond 404 lorsque le jeton est inconnu (ou que le portail n’est pas configuré), 410 lorsque le message a expiré, et 429 tant que le jeton est verrouillé. Voir le chapitre sur le lien sécurisé du manuel et pepsi-stage-secure-link(1).

POST /secure/TOKEN

Vérifie le PIN. En cas de succès, elle pose le cookie de session et redirige (303) vers la page ci-dessus ; sur un mauvais PIN, elle réaffiche le formulaire avec 401, et sur la tentative qui épuise MAX_ATTEMPTS, elle répond 429 et verrouille le jeton pour une durée qui double à chaque nouveau verrouillage. Une soumission sans PIN reçoit également 401, mais n’est pas comptée dans MAX_ATTEMPTS : ce n’est pas une devinette, et un client qui resoumet le formulaire vide dépenserait sinon les tentatives d’un destinataire légitime à sa place. Le PIN est soumis dans le corps de la requête et n’apparaît jamais dans une URL.

GET /secure/TOKEN/part/N

Télécharge la pièce jointe N du message. Autorisé par la session, jamais par le PIN. Toujours servi en Content-Type: application/octet-stream avec Content-Disposition: attachment et une Content-Security-Policy de bac à sable, quel que soit le type de média que le message prétendait — une partie text/html rendue en ligne depuis l’origine propre du portail est précisément ce que la conception refuse.

POST /secure/TOKEN/reply

Compose une réponse, en multipart/form-data (un champ text et des files facultatifs). Autorisé par la session. Le destinataire et l’expéditeur d’enveloppe de la réponse sont pris dans la ligne stockée et ne sont pas des entrées ; le corps est toujours en text/plain ; les téléversements sont plafonnés par MAX_REPLY_SIZE / MAX_REPLY_FILES pendant que le corps s’écoule ; et le message injecté ne porte délibérément pas state.local_origin, de sorte qu’il ne puisse hériter des privilèges de soumission. Répond 404 lorsque REPLY_STAGE n’est pas défini (répondre relève d’un consentement explicite, par déploiement).

Chaque réponse du portail porte, par route plutôt que par hôte, Content-Security-Policy: default-src 'none' (avec un nonce par réponse pour l’unique feuille de style en ligne et aucune source de script), Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff, X-Frame-Options: DENY et Cache-Control: no-store. Le cookie de session est limité à Path=/secure/, HttpOnly, Secure et SameSite=Strict, de sorte que partager une origine avec les points de terminaison ci-dessus est sûr par construction et non par discipline de déploiement.

85.1.3.1.6. L’applicateur privilégié, et s’en passer

pepsi-httpd n’effectue jamais de changement privilégié. Il consigne une ligne d’intention dans pepsi.setup_task et se connecte à une socket de sonnette ; l’activation par socket de systemd transforme cette connexion en un pepsi-setup apply root de courte durée, qui valide la ligne et fait le travail. Le modèle de confiance complet est dans pepsi-setup(1), Le modèle de confiance de l’applicateur.

Cela rend la capacité retirable, et Debian l’empaquette séparément :

pepsi

Le serveur de courrier proprement dit : les binaires — y compris pepsi-setup lui-même —, le schéma, les modèles et toutes les autres unités (mis à part pepsi-stage-detect-language et pepsi-telemetry, empaquetés séparément). Un déploiement administré depuis un terminal n’a besoin de rien d’autre.

pepsi-httpd-admin

pepsi-setup-apply.socket et pepsi-setup-apply.service, et rien d’autre. C’est ce qui transforme une requête HTTP en un changement sous /etc/pepsi. pepsi le Recommends, de sorte qu’une installation par défaut l’a ; apt remove pepsi-httpd-admin réussit sans toucher à pepsi.

Avec ce paquet retiré, rien ne vide pepsi.setup_task. La boucle est rompue au mécanisme, non au bouton : une insertion dans cette table devient inerte, quoi qu’il advienne de la couche web. (Une installation depuis les sources obtient le même levier avec make install INSTALL_ADMIN_UNITS=no.)

Inerte, et cela le reste : remettre le paquet n’agit pas sur ce qui s’est accumulé pendant son absence. Son postinst exécute pepsi-setup apply --clear, qui supprime chaque ligne en file sans en exécuter une seule, de sorte qu’armer l’applicateur ne soit jamais aussi l’événement qui exécute une demande dont personne ne se souvient. Voir pepsi-setup(1).

85.1.3.1.6.1. Ce que fait alors la console

En lecture seule, et elle le dit. Chaque réglage sur lequel porte le questionnaire de configuration initiale est toujours affiché, avec sa réponse en attente ou sa valeur par défaut ; chaque contrôle qui en modifierait un est désactivé, sous une bannière nommant le paquet manquant. Les pages ne sont pas masquées : un opérateur qui a délibérément retiré l’applicateur doit encore pouvoir voir ce que le déploiement est configuré pour faire.

L’API refuse les mutations correspondantes avec un statut et un code qui leur sont propres plutôt qu’un échec générique

HTTP/1.1 503 Service Unavailable
{"code": "setup_applier_unavailable",
 "hint":  "the administrative package 'pepsi-httpd-admin' is not installed …",
 "detail": {"package": "pepsi-httpd-admin", "unit": "pepsi-setup-apply.socket"}}

Cela s’applique à POST /api/v1/setup/tasks, /setup/preflight, /setup/dns-check, /setup/certificates, PUT /api/v1/setup/answers, POST /api/v1/identities, POST /api/v1/identities/client, à un PATCH /api/v1/identities/{id} de révocation et à DELETE /api/v1/otp/{address}, ainsi qu’aux propres envois de formulaires de la console — POST /ui/setup/{step}, /ui/setup/tasks, /ui/identities/request/{generate,register}, /ui/identities/{id}/revoke et /ui/domains/check, qui est le bouton « vérifier maintenant » de la page des domaines. Chacun d’eux atteint la file par une unique fonction de mise en file qui demande d’abord, de sorte que le refus soit une seule décision plutôt qu’une par surface ; le contrôle désactivé sur la page est une présentation par-dessus cette vérification, non un substitut. La mise en attente des réponses est délibérément incluse : le seul but d’une réponse brouillon est d’être appliquée, et un assistant qui en accumule six étapes sur un hôte où rien ne peut les appliquer est une inaction silencieuse. DELETE /api/v1/setup/answers ne passe pas par cette barrière — écarter des brouillons ne peut causer aucun changement privilégié, et une console en lecture seule doit encore pouvoir vider un questionnaire à moitié terminé.

Notez le code distinct. setup_write_unavailable (pas de connexion CONFIG_DB) et setup_applier_unavailable appellent des remèdes différents, et l’applicateur est signalé en premier lorsque les deux s’appliquent : configurer un rôle de base n’aurait pas aidé.

Ce qui n’est pas affecté, c’est /api/v1/config et les pages de configuration de la console. Celles-ci écrivent pepsi.config_override, la surcouche en base que chaque composant lit à l’exécution — non un fichier de /etc. Leur seule barrière est [pepsi-admin] CONFIG_DB, car l’applicateur n’a rien à y voir et étendre la règle de lecture seule pour les couvrir retirerait une capacité dont la scission du paquet ne traitait pas. Les deux couches sont décrites sous Superposition de la configuration dans le manuel.

85.1.3.1.6.2. Comment il le détecte

En regardant deux fichiers, qui à eux deux répondent installé et armé :

  1. pepsi-setup-apply.socket existe comme fichier d’unité dans un répertoire que systemd parcourt (/etc/systemd/system, /run/systemd/system, /usr/local/lib/systemd/system, /usr/lib/systemd/system, /lib/systemd/system) ; et

  2. [pepsi-admin] APPLY_SOCKET existe, est une socket, et est accessible en écriture à ce compte — se connecter à une socket UNIX exige la permission d’écriture.

Deux stat et un access, sans privilège — ce qui compte, car le serveur est déjà descendu vers le compte non privilégié pepsi-httpd et ne peut rien demander à dpkg ni à systemd. Il ne se connecte délibérément pas : se connecter est la sonnette, et démarrerait un processus root à chaque rendu de page.

Aucun des deux tests n’est redondant. Sans le premier, un nœud de socket périmé se lit comme une sonnette vivante — et les nœuds périmés arrivent : le RemoveOnStop= de systemd vaut « off » par défaut (l’unité livrée le règle ; une unité éditée à la main peut ne pas le faire), et un paquet dont les fichiers sont supprimés plutôt qu’arrêtés laisse purement et simplement le nœud derrière lui. C’est exactement le cas pour lequel cette fonctionnalité existe, et s’y tromper échouerait de façon ouverte. Sans le second, un paquet fraîchement installé dont la socket n’a jamais été activée se lirait comme prêt, et chaque tâche resterait pending à jamais.

La vérification échoue de façon fermée. Un chemin qui ne peut pas être examiné, un chemin qui n’est pas une socket, une erreur de permission — chaque incertitude se lit comme « indisponible » et la console passe en lecture seule. L’échec récupérable est de se voir dire d’installer quelque chose qui est déjà installé ; l’irrécupérable est de croire qu’un changement a été fait.

Deux conséquences :

  • Un site qui vide la file autrement — pepsi-setup apply --once depuis cron, une unité écrite à la main, un hôte sans systemd, des fichiers d’unité quelque part où systemd ne cherche pas — n’a pas de sonnette (ou pas de fichier d’unité) et se lit comme indisponible. [pepsi-admin] APPLIER = yes outrepasse la sonde. C’est une promesse que fait l’opérateur ; rien ici ne peut la vérifier.

  • L’inverse : la socket peut être présente alors que l’unité de service est masquée ou cassée, auquel cas une tâche est mise en file et reste pending. La page des tâches montre ce statut plutôt que de prétendre autre chose. Les deux unités sont livrées dans un seul paquet : c’est donc un état fabriqué à la main plutôt qu’un résultat d’empaquetage.

[pepsi-admin] APPLIER = no rend la console en lecture seule par la seule configuration, pour un déploiement qui ne peut pas modifier son jeu de paquets.

85.1.3.1.7. Configuration

Les éléments suivants sont documentés dans pepsi.conf(5) : les options de la section [pepsi-httpd] (MAX_CONNECTIONS, MAX_CONNECTIONS_PER_IP, DB_POOL_SIZE, la paire ADDIN / ADDIN_URL qui active le module Outlook, et le RESUME_AUTHORIZATION_TOKEN facultatif qui protège POST /resume), les sections de socket et de certificat SNI [pepsi-httpd-listener-<name>] et [pepsi-httpd-cert-<name>] (y compris les drapeaux de listener ADMIN, LIST_API, LIST_API_TESTING et LISTS), la section [pepsi-admin] qui configure l’API d’administration, la section [pepsi-list] qui contient l’identifiant API_USER/API_PASS de l’API Mailman et API_RATE_LIMIT ainsi que les WEB_RATE_LIMIT/WEB_SEARCH_RATE_LIMIT de l’interface web, la section [pepsi-autoconfig] à partir de laquelle le document d’autoconfiguration est bâti, et la section [pepsi-secure-link] qui active le portail.

85.1.3.1.8. Abandon de privilèges

Pour lier les ports privilégiés (80/443), pepsi-httpd est souvent démarré en tant que root. Il ne sert pas en tant que root : une fois chaque listener configuré lié (et tout matériel de clé TLS lu), le processus abandonne ses privilèges au profit du compte de service non privilégié pepsi-httpd, abandonnant tous les groupes supplémentaires et basculant vers le gid et l’uid de ce compte avant d’accepter la moindre connexion. Le compte doit donc exister avant que le serveur ne soit démarré en tant que root ; créez-le (par exemple useradd --system --no-create-home --shell /usr/sbin/nologin pepsi-httpd) dans le cadre de l’installation.

Si l’abandon ne peut être complété en s’exécutant en tant que root — le plus souvent parce que le compte pepsi-httpd n’existe pas — le serveur journalise une erreur et quitte sans servir, plutôt que de risquer de s’exécuter en tant que root. Lorsqu’il est démarré par un utilisateur non root, aucun changement de privilège n’est effectué (le serveur s’exécute simplement en tant que l’utilisateur invoquant) ; il ne s’exécutera jamais en tant que root.

85.1.3.1.9. Matériel de clés TLS

Le certificat et la clé privée de chaque listener TLS — ses propres TLS_CERT/TLS_KEY et chaque certificat SNI [pepsi-httpd-cert-*] — sont lus une seule fois, au démarrage, avant l’abandon de privilèges décrit ci-dessus. L’endroit d’où ils sont lus dépend de la façon dont le serveur a été démarré.

Sous systemd (le déploiement empaqueté). L’unité s’exécute dès le départ sous l’utilisateur non privilégié pepsi-httpd — elle ne détient jamais root — et ne peut donc pas ouvrir /etc/letsencrypt/{live,archive}, réservé à root par certbot. Elle n’en a pas besoin : pepsi-setup(1) écrit un drop-in /etc/systemd/system/pepsi-httpd.service.d/10-tls-credentials.conf comportant une entrée LoadCredential= par fichier. systemd ouvre ces fichiers en tant que root au démarrage de l’unité et transmet au service des copies privées sous $CREDENTIALS_DIRECTORY — un répertoire tmpfs propre à l’unité, lisible par ce seul service et invisible pour toute autre unité — où pepsi-httpd recherche chaque chemin configuré avant de se rabattre sur la lecture du chemin lui-même. La configuration n’en est pas affectée : TLS_CERT/TLS_KEY désignent toujours les fichiers réels, et c’est de ces chemins que sont dérivés les noms de credentials.

Deux conséquences :

  • Relancez pepsi-setup run après avoir ajouté, déplacé ou supprimé un certificat. Le drop-in est régénéré à partir de la configuration et n’énumère délibérément que des fichiers qui existent, car systemd refuse de démarrer une unité dont la source de credential est absente.

  • Un credential est matérialisé au démarrage de l’unité ; un certificat renouvelé n’atteint donc les clients qu’après un redémarrage. Le hook de déploiement certbot installé par pepsi-setup s’en charge (systemctl try-restart).

Démarré directement, sans systemd. Le serveur est démarré en tant que root, lit les fichiers lui-même et n’abandonne ses privilèges qu’ensuite : aucun credential n’intervient et les permissions des fichiers n’ont jamais d’importance.

Un certificat qui ne peut pas être chargé est signalé et ignoré, sans être fatal : les noms d’hôte SNI qu’il aurait servis cessent d’être disponibles en HTTPS, tandis que tous les autres certificats continuent de fonctionner. Seul un listener TLS qui ne dispose plus d’aucun certificat utilisable fait sortir le serveur. Un unique fichier illisible ne peut donc pas entraîner avec lui la politique MTA-STS de domaines sans rapport — ni le point de terminaison des métriques.

85.1.3.1.10. Commandes

serve

Exécute le serveur jusqu’à interruption. Nécessite que le schéma ait été installé avec pepsi-setup(1).

prune

Supprime les enregistrements d’audit plus anciens que [pepsi-admin] EVENT_RETENTION_DAYS, les enregistrements du journal de courrier plus anciens que MAIL_LOG_RETENTION_DAYS et les sessions d’administration expirées, puis se termine. Exécuté en root, il descend d’abord au compte pepsi-httpd, dont le rôle de base de données détient le droit DELETE. pepsi-log-prune.timer l’exécute quotidiennement ; sur un système sans systemd, exécutez-le depuis cron.

85.1.3.1.11. Options globales

-c FILE, –config FILE

Lit la configuration depuis FILE au lieu de parcourir les emplacements par défaut.

-L LOGLEVEL, –log LOGLEVEL

Règle la verbosité de journalisation (par défaut info).

-v, –verbose

Affiche les messages de journal de toutes les sources.

-h, –help ; -V, –version

Affiche un résumé d’utilisation / la version et quitte.

85.1.3.1.12. Signaux

SIGINT

Amorce l’arrêt et quitte proprement.

SIGTERM

Non capturé : la disposition par défaut met fin au processus sur-le-champ, coupant toute requête en vol. Rien n’est perdu de ce fait — la file d’attente est dans PostgreSQL et ce serveur ne conserve aucun état propre — et c’est ainsi que systemctl stop met fin à l’unité.

85.1.3.1.13. Code de sortie

0

Arrêt propre.

1

Une erreur s’est produite (par exemple un fichier de configuration malformé, un certificat illisible, ou une connexion de base de données échouée). La raison est écrite dans le journal.

85.1.3.1.14. Exemples

Servir via HTTPS

pepsi-httpd -c /etc/pepsi/pepsi.conf serve

Récupérer une politique pour test (en résolvant l’hôte SNI vers le serveur)

curl --resolve mta-sts.example.org:443:203.0.113.7 \
     https://mta-sts.example.org/.well-known/mta-sts.txt

Vérifier que la clé d’un utilisateur est publiée, comme le ferait le client d’un correspondant

gpg --locate-external-key alice@example.org

85.1.3.1.15. Voir aussi

pepsi-config(1), pepsi.conf(5), pepsi-dispatch(1), pepsi-keys(1), pepsi-setup(1)

85.1.3.1.16. Bogues

Signalez les bogues au gestionnaire de tickets de Pepsi.