20. Die administrative API

pepsi-httpd bedient unter /api/v1 eine HTTP-API, die alles abdeckt, was die Kommandozeilenwerkzeuge des Operators tun: die Warteschlange, die Gesundheitszusammenfassung, die Konfiguration, den Schlüsselspeicher und die Journale. Die Browser-Konsole und ein unbeaufsichtigtes Skript sind Clients derselben dokumentierten Oberfläche.

Warnung

Die API ist noch nicht stabil. Der Pfad trägt v1, damit der Versionierungsmechanismus existiert und Clients dagegen geschrieben werden, aber solange die Version von Pepsi mit 0. beginnt, kann ein Release die Gestalt einer Anfrage oder einer Antwort noch ändern; solche Änderungen sind in NEWS aufgeführt. Anders als bei der Datenbank, die jedes Release vorwärts migriert (siehe Upgrade), muss ein Client unter Umständen angepasst werden. Den Vertrag einzufrieren, bevor die Konsole, ihr erster echter Client, ihn erprobt hat, würde blind getroffene Entscheidungen festschreiben.

20.1. Wo sie bedient wird

Nur auf einem Listener mit dem Flag ADMIN = yes. Auf jedem anderen Listener antworten die /api/v1-Routen mit einem schlichten 404, das Byte für Byte dem entspricht, was jeder unbekannte Pfad bekommt — sodass die Veröffentlichung des öffentlichen HTTPS-Listeners nicht die Administration veröffentlicht und ein öffentlicher Listener nicht einmal daraufhin sondiert werden kann, ob die Administration auf dieser Installation aktiviert ist.

Die ausgelieferte Konfiguration markiert genau einen Listener, einen UNIX-Socket:

[pepsi-httpd-listener-admin]
SERVE = unix
UNIXPATH = /run/pepsi/admin.sock
UNIXPATH_MODE = 660
UNIXPATH_GROUP = pepsi-admin
MODE = plain
ADMIN = yes

pepsi-httpd weigert sich, die administrativen Routen auf einem markierten Listener zu bedienen, der sie im Klartext von diesem Host forttragen würde: Klartext-TCP wird nur auf einer Loopback-Adresse angenommen, TLS und UNIX-Sockets immer. Ein Socket-aktivierter Listener (SERVE = systemd) qualifiziert sich nur mit TLS, weil die Adresse des geerbten Deskriptors für den Server nicht sichtbar ist. Ein markierter Listener, der die Prüfung nicht besteht, protokolliert beim Start eine Warnung und bedient nur die öffentlichen Endpunkte; pepsi-setup meldet dasselbe, wenn es die Konfiguration validiert, sodass der Fehler auffällt, bevor er Folgen hat.

Bemerkung

Ein Binärprogramm bedient beide Zielgruppen. Es gibt keinen separaten administrativen Daemon: eine Sache zu bauen, zu paketieren, zu beaufsichtigen und im Gleichlauf zu halten — und im Gegenzug ist ein Fehler im gemeinsamen Serverprozess ein Fehler in beiden Oberflächen. Die Isolierung ist daher vom Operator zu konfigurieren. Binden Sie den administrativen Listener an einen UNIX-Socket oder an das Loopback und stellen Sie einen Reverse Proxy davor, wenn er von anderswo erreichbar sein muss.

Bemerkung

Was einem Front-Server geglaubt wird. In dieser Topologie terminiert dieser Prozess kein TLS und sieht keine Client-Adresse, also liest er zwei Header vom Front-Server: X-Forwarded-Proto entscheidet, ob das Sitzungs-Cookie Secure hinausgeht (und unter seinem __Host--Namen), und das rechteste X-Forwarded-For-Element ist das, was die Ratenbegrenzungen als Schlüssel verwenden und was das Secure-Link-Portal als Peer eines Zugriffsversuchs festhält. Beide werden nur auf einer Verbindung gelesen, die nicht von außerhalb dieses Hosts stammen kann — einem UNIX-Socket oder einem Loopback-TCP-Peer —, sodass ein Client, der direkt mit einem öffentlichen Listener spricht, nie eines von beiden behaupten kann. Die Sites, die pepsi-setup schreibt, setzen beide. Ohne sie hat eine über einen Proxy betriebene Installation Cookies ohne Secure-Attribut und einen einzigen Ratenbegrenzungs-Zähler, den sich das ganze Internet teilt.

20.2. Authentifizierung

Drei Mechanismen, ein Identitätsmodell. Ein ausdrücklich vorgelegtes Credential gewinnt immer: Zuerst wird der Authorization-Header versucht, dann das Sitzungs-Cookie, und SO_PEERCRED nur, wenn keines von beiden vorhanden ist. Diese Reihenfolge ist wichtig — gewönne peercred, bekäme ein Administrator, der bewusst ein eng gefasstes Token über den lokalen Socket vorlegt, stillschweigend die volle Autorität zurück.

20.2.1. Lokale Administratoren (SO_PEERCRED)

Ein Prozess, der sich über einen UNIX-Socket-Listener verbindet, wird über die Zugangsdaten identifiziert, die der Kernel bei connect(2) festgehalten hat und die der Peer nicht fälschen kann. root oder ein Mitglied von [pepsi-admin] ADMIN_GROUP (Standardwert pepsi-admin) ist ein Administrator ohne Credential, das man konfigurieren, speichern oder verlieren könnte:

# curl --unix-socket /run/pepsi/admin.sock http://localhost/api/v1/status

So funktioniert der Erstbootstrap auf einem System, das noch keine Konten hat, und deshalb darf der Socket gefahrlos den Modus 0660 mit einer Gruppe haben: Die Identität stammt vom Kernel, nicht vom Dateimodus.

20.2.2. Bearer-Token

Für die Automatisierung. Ein Token wird über die API geprägt und genau einmal angezeigt:

# curl --unix-socket /run/pepsi/admin.sock -X POST \
    -H 'Content-Type: application/json' \
    -d '{"label":"monitoring","scopes":["queue:read","logs:read"]}' \
    http://localhost/api/v1/tokens

Die vorgelegte Form ist pepsi_<id>.<secret>. Nur der SHA-256 der geheimen Hälfte wird gespeichert, sodass ein Token nicht erneut angezeigt oder aus einer Datenbanksicherung wiederhergestellt werden kann; die Id-Hälfte ist ein öffentlicher Selektor, der die Tabelle indiziert, was den Vergleich zu einer konstantzeitigen Prüfung gegen einen Datensatz statt zu einem Tabellendurchlauf macht.

Legen Sie es als Authorization: Bearer pepsi_<id>.<secret> vor — das Bearer-Schema von RFC 6750, getragen im Authorization-Feld, das RFC 9110 §11.6.2 definiert. Schemanamen beachten keine Groß- und Kleinschreibung (RFC 7235 §2.1), sodass bearer und BEARER ebenfalls angenommen werden. Token können einen Ablauf tragen (expires_in_days) und werden durch Löschen widerrufen.

Das Token ist kein OAuth-2.0-Zugriffstoken (RFC 6749), und es gibt keinen Autorisierungsserver: Es handelt sich um ein lokal geprägtes Credential, das zufällig dasselbe Drahtformat verwendet. Pepsi spricht anderswo sehr wohl OAuth 2.0 — ausgehend zu einem Smarthost, siehe pepsi-helper-token-refresh — und die beiden sollten nicht verwechselt werden.

20.2.3. Passwörter und Sitzungen

Für einen Browser. Konten liegen in pepsi.admin_account mit Argon2id-Passwort-Hashes (RFC 9106); POST /api/v1/auth/login tauscht einen Namen und ein Passwort gegen ein Sitzungs-Cookie (RFC 6265) plus ein CSRF-Token, und jede verändernde Anfrage aus einer Sitzung muss dieses Token im X-Pepsi-Csrf-Header wiederholen. Ein Bearer-Token und ein lokaler Peer brauchen kein CSRF-Token: Keines von beiden wird je von einem Browser im Auftrag eines anderen gesendet.

Das Cookie trägt HttpOnly, SameSite=Strict und das Namenspräfix __Host-. Dieses Präfix ist das stärkste der drei: Der Browser weist das Cookie zurück, sofern es nicht über HTTPS ohne Domain-Attribut und mit Path=/ gesetzt wurde, sodass ein Geschwisterhost auf derselben registrierbaren Domain kein Sitzungs-Cookie für diesen setzen kann.

Das Präfix und Secure sind eine Entscheidung, und sie richtet sich nach dem Schema des Clients, nicht nach dem dieses Prozesses: ein Listener, der selbst TLS terminiert, oder ein Klartext-Listener hinter einem Front-Server, der wie oben beschrieben X-Forwarded-Proto: https behauptet. Auf dem Klartext-Pfad des lokalen Bootstraps — ein Loopback- oder UNIX-Listener ohne Front-Server — würde ein Browser ein Secure-Cookie rundweg ablehnen, also wird keines von beiden verwendet und das Cookie heißt pepsi_session. Beide Schreibweisen werden beim Eingang angenommen, sodass das Hinzufügen oder Entfernen eines TLS-Front-Servers nicht jeden Administrator abmeldet.

Diese Anforderungen sind auch der Grund, warum das Secure-Link-Portal, das sich einen Ursprung teilt, nicht dasselbe Präfix verwenden kann: __Host- schreibt Path=/ vor, was das Sitzungs-Cookie des Portals auch an /metrics, /resume und das Web Key Directory schicken würde. Es behält seine Pfadbegrenzung, verwendet stattdessen __Secure- und erkauft sich das, was das Präfix gegeben hätte, indem es den Cookie-Wert unter dem Server-Pfeffer versiegelt — siehe Das Secure-Link-Ausweichportal.

Sitzungen sind Datensätze, kein Prozessspeicher, sodass ein Neustart nicht alle abmeldet und zwei Serverprozesse sich einig sind, wer angemeldet ist. Sie laufen zweifach ab:

  • ein Leerlauf-Timeout (SESSION_IDLE, Standardwert 30 Minuten), das bei jeder Anfrage vorgeschoben wird, und

  • eine harte Lebensdauer (SESSION_LIFETIME, Standardwert 12 Stunden), die nie verlängert wird.

Es gibt kein „Angemeldet bleiben“. Diese Konsole kann einen Schlüssel widerrufen, lesen, wer mit wem korrespondiert, und die Pipeline umschreiben; ein vergessener Browser-Tab sollte das morgen nicht immer noch halten, und ein gestohlener Laptop sollte keine dauerhafte administrative Sitzung sein. Die Richtlinie ist tragbar, weil die Leute, die eine Installation am häufigsten verwalten — lokale Operatoren —, überhaupt nie ein Passwort tippen.

Bemerkung

PAM wird nicht unterstützt. Es würde einen privilegierten Authentifizierungspfad und Systemkonto-Semantik in die Web-Schicht eines Mailservers setzen und die Sicherheit der API von einer Host-Konfiguration abhängig machen, die Pepsi nicht kontrolliert. Ein lokaler Administrator braucht es nicht; ein entfernter bekommt ein Konto in der Datenbank mit einer expliziten Liste von Geltungsbereichen.

20.3. Autorisierung: Geltungsbereiche

Jeder Endpunkt deklariert den einen Geltungsbereich, den er verlangt, in derselben Tabelle, aus der die Dispatch-Tabelle und das OpenAPI-Dokument erzeugt werden — sodass das, was dieses Handbuch verspricht, und das, was der Server durchsetzt, nicht auseinandergehen können.

Geltungsbereich

Gewährt

config:read

Die effektive Konfiguration und ihre Herkunft lesen (Geheimnisse maskiert).

config:write

Die Konfigurations-Überlagerung ändern.

keys:read

Lokale Identitäten, Korrespondentenschlüssel und Vertrauensanker lesen.

keys:write

Lokale Identitäten ändern und Vertrauensanker entfernen.

peers:write

Den Schlüssel eines Korrespondenten importieren, eine Entdeckungsabfrage in die Warteschlange stellen oder einen zwischengespeicherten Schlüssel vergessen.

queue:read

Die Gesundheitszusammenfassung und die Nachrichtenwarteschlange lesen.

queue:write

Eine eingereihte Nachricht erneut einreihen, umleiten oder löschen.

logs:read

Das Prüfprotokoll, das Maillog, TLS-Ergebnisse und den DNS-Cache lesen.

setup:write

Konten und Token verwalten und das Online-Setup steuern.

secure:read

Secure-Link-Metadaten lesen: für wen eine gespeicherte Nachricht ist, wann sie gelesen wurde, wie oft die PIN falsch eingegeben wurde. Nie ihren Inhalt.

secure:write

Eine Secure-Link-Nachricht widerrufen und damit die einzige Kopie von ihr zerstören.

own:<address>

Autorität über genau eine E-Mail-Adresse.

Ein lokaler Administrator hält jeden Geltungsbereich außer own:. Ein Prinzipal kann nie ein Credential prägen, das mächtiger ist als er selbst: Ein Konto oder ein Token mit einem Geltungsbereich zu erzeugen, den der Aufrufer nicht hält, wird mit scope_escalation abgelehnt.

20.3.1. Der Geltungsbereich own:<address>

own:<address> verengt die Autorität auf eine einzige Adresse. Keine Oberfläche stellt einen solchen Prinzipal von sich aus aus; ein Administrator erzeugt ihn als Token oder als Konto mit diesem Geltungsbereich. Der Geltungsbereich ist dennoch auf jedem Endpunkt, der eine Adresse entgegennimmt, definiert und durchgesetzt, denn ein Autorisierungsmodell nachträglich auf Endpunkte aufzusetzen, die ohne eines geschrieben wurden, ist die teure Hälfte der Arbeit.

Ein Prinzipal, der nur own:-Geltungsbereiche hält, darf seine eigenen Identitäten und Korrespondentenschlüssel lesen (eine ungefilterte Auflistung wird auf seine Adresse verengt statt abgelehnt) sowie seine eigenen Datensätze des Maillogs (dort muss er ?address= mit seiner eigenen Adresse angeben). Er darf die von niemand anderem lesen, darf die installationsweiten Objekte — den Korrespondentenschlüssel-Cache, die Vertrauensanker — nicht anfassen und darf die Konfiguration weder lesen noch schreiben. Ein Administrator kann eine Adresse delegieren, indem er ein solches Token ausstellt; ein Token, das nur Zugangsdaten erzeugen kann, kann keine Adressautorität herbeizaubern, die es selbst nicht hält.

Er darf außerdem seine eigenen Schlüssel verwalten (die Selbstbedienung im Stil von pEp; siehe Schlüsselverwaltung): einen servergeführten Schlüssel anfordern (POST /api/v1/identities), den eigenen öffentlichen Schlüssel des Benutzers registrieren (POST /api/v1/identities/client) und mit PATCH /api/v1/identities/{id} die Flags seiner eigenen Identität ändern, einschließlich einer Anforderung zum Hochladen auf einen Schlüsselserver und ihres Widerrufs. Jeder dieser Aufrufe prüft die Adresse erneut, und die beiden POST-Routen sowie ein widerrufendes PATCH stellen nur eine Aufgabe in die Warteschlange, die der Applier noch einmal gegen den own:-Geltungsbereich des Prinzipals prüft. Das Löschen des Datensatzes einer Identität bleibt keys:write vorbehalten.

Er darf die von ihm eingereihten Aufgaben bis zu ihrem Ende verfolgen: GET /api/v1/setup/tasks/{id} antwortet für eine Aufgabe, die dieser Prinzipal selbst eingereiht hat, und GET /api/v1/setup/tasks listet genau diese auf (siehe Einer Aufgabe folgen). Alles andere auf der Setup-Oberfläche bleibt setup:write.

20.4. Konventionen

  • Nur JSON. Anfragen und Antworten sind application/json.

  • Eine Fehlergestalt, bei jedem Fehlschlag:

    {"code": "scope_required", "hint": "this endpoint requires the 'queue:write' scope",
     "detail": {"scope": "queue:write"}}
    

    code ist ein stabiles Token, auf das man verzweigen kann; hint ist Prosa für einen Menschen und kann sich ändern; detail trägt strukturierte Zusatzinformation, wenn es welche gibt.

  • Einheitliche Authentifizierungsfehlschläge. Ein falsches Passwort, ein unbekanntes Konto und ein deaktiviertes Konto erzeugen alle dasselbe invalid_credentials, und ein unbekanntes Konto zahlt dennoch für eine Passwort-Verifikation, sodass weder die Meldung noch die Antwortzeit Konten aufzählt.

  • Interne Fehlschläge tragen nie eine Ursache. Der vollständige Fehler geht ins Prozessjournal; der Client bekommt internal_error.

  • Auflistungen antworten mit {"items": [...], "total": N, "limit": L, "offset": O}, und jede einzelne meint damit dasselbe. Siehe Paginierung.

  • ISO-8601-Zeitstempel, mit einem Versatz.

  • Keine nachgestellten Schrägstriche.

20.5. Paginierung

Elf Endpunkte antworten mit der Auflistungshülle, und alle elf verhalten sich gleich:

Endpunkt

Filter, die auch für total gelten

GET /api/v1/queue

?stage= ?status=

GET /api/v1/events

?kind= (Präfix) ?actor=

GET /api/v1/mail-log

?address=

GET /api/v1/secure-messages

?include_expired=

GET /api/v1/identities

?address= ?protocol=

GET /api/v1/peers

?address=

GET /api/v1/peers/unclaimed

?address=

GET /api/v1/ca-trust

—

GET /api/v1/accounts

—

GET /api/v1/tokens

—

GET /api/v1/setup/tasks

— (ohne setup:write auf die eigenen Aufgaben des Aufrufers verengt)

limit ist standardmäßig [pepsi-admin] PAGE_LIMIT (100) und wird auf 1–1000 begrenzt; offset wird auf nicht negative Werte begrenzt; ein Wert, der keine Zahl ist, ist ein 400. Die Hülle gibt das angewandte Fenster wieder, sodass eine Anfrage nach limit=100000 mit "limit": 1000 zurückkommt. Beide gehen ins SQL, und total ist ein COUNT(*) über dieselbe WHERE-Klausel wie die Seite — nie die Länge von items und nie die Größe eines zu groß abgerufenen Fensters. Jede Auflistung sortiert zuletzt nach etwas Eindeutigem (ihrer Datensatz-Id), sodass ein OFFSET weder einen Datensatz überspringen noch einen zweimal zeigen kann.

GET /api/v1/mail-log fügt der Hülle ein Mitglied hinzu, mode, denn eine leere Seite bedeutet etwas anderes, je nachdem, ob [pepsi] MAIL_LOG überhaupt etwas aufzeichnet.

20.5.1. total und die Seite sind zwei Abfragen

Die Zählung läuft zuerst, dann die Seite, und nichts hält über die beiden hinweg einen Schnappschuss. Eine nur lesende Transaktion je Auflistung würde sie exakt konsistent machen, um den Preis, eine gepoolte Datenbankverbindung in einer Web-Schicht für eine Garantie zu binden, die kein Client braucht. Also gilt gegen eine nebenläufig beschriebene Tabelle:

  • zwischenzeitlich gelöschte Datensätze machen total zu einer Überschätzung — und die Warteschlange wird fortlaufend geleert, sodass dies dort der Normalfall ist, keine Kuriosität. Der sichtbare Effekt ist eine letzte Seite, die leer zurückkommt.

  • zwischenzeitlich eingefügte Datensätze machen ihn zu einer Unterschätzung.

Blättern Sie, bis items kürzer als limit ist; behandeln Sie total als eine gute Zahl, die man einem Operator zeigt, nicht als Schleifengrenze, der man auf den Datensatz genau vertraut.

Die Zählung ist auf jeder dieser Tabellen ein echtes COUNT(*), keine Schätzung. Das ist vertretbar wegen dessen, was sie sind: pepsi.workqueue ist der lebende Rückstau — der Datensatz einer zugestellten Nachricht wird von der Stage gelöscht, die sie zugestellt hat, sodass die Tabelle noch in Bearbeitung befindliche Mail enthält statt einer Historie von allem je Gesendeten — und der Schlüsselspeicher, die Konten und die Token sind Objekte in Installationsgröße. event_log und mail_log sind die beiden, die mit dem Verkehr wachsen; beide werden von den Aufbewahrungsdurchläufen begrenzt, die unter Das Prüfprotokoll und Das Maillog (standardmäßig aus) beschrieben sind.

20.5.2. Nicht diese Gestalt

GET /api/v1/status nimmt ein limit — wie viele steckengebliebene Nachrichten der Bericht auflistet —, aber kein offset, und antwortet mit dem Bericht von pepsi-status statt mit einer Auflistung. GET /api/v1/tls-sessions antwortet mit {"days": D, "items": [...]} — es gibt das angewandte Fenster wieder, so wie die Auflistungshülle limit/offset wiedergibt — begrenzt durch den Parameter days (1–365, Standardwert von pepsi-status). GET /api/v1/dns-cache antwortet mit einem bloßen {"items": [...]}, nur begrenzt durch die Größe des DNS-Fehlschlag-Caches.

20.6. Endpunkte

Die maßgebliche Liste für einen gegebenen Build ist GET /api/v1/openapi.json, das aus der eigenen Routentabelle des Servers erzeugt wird — einschließlich der Parameter limit und offset, die es an genau den unter Paginierung aufgeführten Endpunkten deklariert. Die Spalte Anmerkungen unten gibt die übrigen Parameter jeder Auflistung an; jede Auflistung nimmt zusätzlich ?limit= und ?offset=.

Methode

Pfad

Geltungsbereich

Anmerkungen

POST

/api/v1/auth/login

—

Liefert ein Sitzungs-Cookie und ein CSRF-Token zurück.

POST

/api/v1/auth/logout

—

Idempotent.

GET

/api/v1/auth/whoami

—

Das Erste, was zu prüfen ist, wenn etwas mit 403 antwortet.

GET POST

/api/v1/accounts

setup:write

Passwort-Hashes werden nie zurückgegeben.

PATCH DELETE

/api/v1/accounts/{id}

setup:write

Ein Konto zu löschen beendet seine Sitzungen.

GET POST

/api/v1/tokens

setup:write

Das Geheimnis steht in der POST-Antwort und sonst nirgends.

DELETE

/api/v1/tokens/{id}

setup:write

Widerruf.

GET

/api/v1/status

queue:read

Identisch zu pepsi-status --json. ?limit= begrenzt die Liste steckengebliebener Nachrichten; keine Auflistung, daher kein ?offset=.

GET

/api/v1/queue

queue:read

?stage= ?status=

GET

/api/v1/queue/{id}

queue:read

Umschlag, Stage, Status und state. Nie die Nachricht.

POST

/api/v1/queue/{id}/requeue

queue:write

Wieder ausstehend an seiner aktuellen Stage.

POST

/api/v1/queue/{id}/bounce

queue:write

Body {"stage": "<bounce stage>"}.

POST

/api/v1/queue/{id}/cancel

queue:write

Löscht die Nachricht.

GET

/api/v1/events

logs:read

Das Prüfprotokoll. ?kind= (Präfix) ?actor=

GET

/api/v1/mail-log

logs:read

?address= Leer, sofern [pepsi] MAIL_LOG nicht an ist; meldet den Modus.

GET

/api/v1/tls-sessions

logs:read

?days=

GET

/api/v1/dns-cache

logs:read

MX-Adressen, die derzeit scheitern.

GET

/api/v1/config

config:read

?scope= Effektive Werte mit Herkunft; Geheimnisse maskiert.

GET

/api/v1/config/{section}

config:read

Ein Abschnitt.

PUT DELETE

/api/v1/config/{section}/{option}

config:write

Vor dem Speichern validiert. Siehe unten.

POST

/api/v1/config/validate

config:read

Trockenlauf; antwortet in beiden Fällen mit 200 und einem Urteil.

GET

/api/v1/identities

keys:read

?address= ?protocol=

GET PATCH DELETE

/api/v1/identities/{id}

keys:read / keys:write

PATCH setzt is_primary (true), published, vks_wanted; oder, für sich allein, status (nur revoked, mit einem optionalen revocation_reason), was eine revoke-identity-Aufgabe in die Warteschlange stellt. Siehe unten. GET und PATCH stehen own:<address> für diese Adresse offen; DELETE nicht.

GET

/api/v1/identities/{id}/public

keys:read

Die öffentliche Hälfte, base64.

POST

/api/v1/identities

keys:write oder own:<address>

Eine generate-identity-Aufgabe in die Warteschlange stellen: {"address": ..., "vks": true|false}. Siehe unten.

POST

/api/v1/identities/client

keys:write oder own:<address>

Eine register-client-key-Aufgabe in die Warteschlange stellen: {"address": ..., "key": "<armoured public key>"}. Siehe unten.

DELETE

/api/v1/otp/{address}

keys:write

Eine reset-otp-Aufgabe in die Warteschlange stellen, die den zweiten Faktor der Adresse entfernt (und sie damit entsperrt). Siehe unten.

GET

/api/v1/peers

keys:read

?address=

POST

/api/v1/peers

peers:write

Den Schlüssel eines Korrespondenten von Hand importieren. Siehe unten.

POST

/api/v1/peers/discover

peers:write

Reiht eine Entdeckungsabfrage ein; es holt nicht ab. Siehe unten.

DELETE

/api/v1/peers/{id}

peers:write

Einen zwischengespeicherten Schlüssel vergessen.

GET

/api/v1/peers/unclaimed

keys:read

?address= — bediente Adressen, für die wir keine Identität halten, für die aber dennoch ein Schlüssel zwischengespeichert ist. Eine paginierte Auflistung.

GET

/api/v1/ca-trust

keys:read

Die S/MIME-Vertrauensanker.

DELETE

/api/v1/ca-trust/{id}

keys:write

Ändert, welche eingehenden Signaturen validieren.

GET

/api/v1/secure-messages

secure:read

Ausstehende Secure-Link-Nachrichten, nur Metadaten. ?include_expired=1.

GET

/api/v1/secure-messages/{token}

secure:read

Eine Nachricht mit ihrer Zugriffshistorie. Nie ihr Inhalt.

DELETE

/api/v1/secure-messages/{token}

secure:write

Widerrufen; das zerstört die einzige Kopie der Nachricht.

GET

/api/v1/dns-records

config:read

Die zu veröffentlichenden Einträge und das letzte Live-Urteil. Siehe unten.

GET

/api/v1/setup/questions

setup:write

Das Setup-Interview als Daten.

GET

/api/v1/setup/telemetry

setup:write

Ob das Einschalten der Funktionstelemetrie wirksam werden könnte und, falls nicht, was auf dem Server zu tun ist. Siehe unten.

GET

/api/v1/setup/answers

setup:write

Die vorgemerkten Antworten; Zugangsdaten maskiert.

PUT

/api/v1/setup/answers

setup:write

Antworten in die vorgemerkte (Entwurfs-)Menge einmischen.

DELETE

/api/v1/setup/answers

setup:write

Das vorgemerkte Interview verwerfen.

GET

/api/v1/setup/tasks

setup:write oder ein beliebiger Prinzipal für seine eigenen Aufgaben

Privilegierte Einrichtungsaufgaben, neueste zuerst; ohne setup:write nur die eigenen des Aufrufers.

POST

/api/v1/setup/tasks

setup:write

Den Applier um eine Aktion aus seiner geschlossenen Menge bitten.

GET

/api/v1/setup/tasks/{id}

setup:write oder ein beliebiger Prinzipal für seine eigenen Aufgaben

Eine Aufgabe, mit ?since=, das ihren Fortschritt streamt, und ?wait=, das die Antwort zurückhält, bis es Neuigkeiten gibt.

POST

/api/v1/setup/preflight

setup:write

Die Umgebung prüfen (Ports, Resolver-DNSSEC).

POST

/api/v1/setup/dns-check

setup:write

Live-DNS mit dem vergleichen, was das Setup veröffentlichen würde.

POST

/api/v1/setup/certificates

setup:write

Die von der Konfiguration benötigten Zertifikate beziehen.

GET

/api/v1/openapi.json

—

Aus der Routentabelle erzeugt.

Die Überwachungsendpunkte rufen die eigenen Funktionen von pepsi-status und pepsi-queue auf und serialisieren dieselben Strukturen, sodass pepsi-status --json und GET /api/v1/status konstruktionsbedingt dieselben Bytes sind. Die Konfigurationsendpunkte verwenden den Herkunftsdurchlauf von pepsi-config und dessen Validierung wieder.

20.7. Was dieser Server nicht kann

20.7.1. Schlüsselmaterial erzeugen oder widerrufen – es fragt stattdessen den Applier

Privates Schlüsselmaterial liegt in crypto_identity.private_wrapped, das pepsi-setup allein der Rolle pepsi-crypto gewährt (vom Schema-Eigentümer abgesehen) und jedem Dienstkonto entzieht, pepsi-httpd eingeschlossen: Eine Web-Schicht, die Signierschlüssel prägen kann, prägt sie für jeden, der sie kompromittiert.

POST /api/v1/identities erzeugt daher nichts. Es stellt eine generate-identity-Aufgabe in die Warteschlange ({"address": ..., "vks": true|false}; vks ist optional und hat als Standard [pepsi-keys] VKS_PUBLISH) und antwortet mit der Aufgabe ({"task": ..., "kind": "generate-identity", "applier_notified": ...}). Der root-Applier validiert sie erneut und erzeugt als Rolle pepsi-crypto einen OpenPGP-Schlüssel. Die Erzeugung auf Anfrage ist immer erlaubt, auch neben dem eigenen Schlüssel des Benutzers.

POST /api/v1/identities/client stellt auf dieselbe Weise eine register-client-key-Aufgabe in die Warteschlange ({"address": ..., "key": ...}, der Schlüssel ein ASCII-armierter öffentlicher OpenPGP-Schlüssel von höchstens 64 KiB). Sie läuft über den Applier, obwohl kein privates Material beteiligt ist, weil ein als eigener Schlüssel des Benutzers registrierter Schlüssel vom MTA als vertrauenswürdig behandelt wird – er wird zum öffentlichen Gesicht der Adresse, und damit erstellte Signaturen werden als die des Benutzers verifiziert –, sodass eine kompromittierte Web-Schicht keinen einschleusen können darf. Der Applier verlangt ein OpenPGP-Zertifikat, dessen User-IDs die Adresse nennen, und registriert keinen Fingerabdruck, den die Adresse bereits hat, ob ausgemustert oder nicht.

PATCH /api/v1/identities/{id} mit {"status": "revoked"} (und einem optionalen "revocation_reason", einer Zeile von höchstens 1024 Byte) ist aus demselben Grund asynchron: Der Widerruf eines reinen Signierschlüssels vernichtet dessen private Hälfte, und dieses UPDATE nennt private_wrapped, was pepsi-httpd nicht darf. Es stellt eine revoke-identity-Aufgabe in die Warteschlange ({"address": ..., "identity_id": ..., "reason": ...}) und antwortet mit der Aufgabe, nicht mit dem geänderten Datensatz: {"identity_id": ..., "task": ..., "kind": "revoke-identity", "applier_notified": ...}. Der status der Identität lautet revoked, sobald der Applier gelaufen ist; GET /api/v1/setup/tasks/{task} zeigt die Aufgabe selbst, für setup:write ebenso wie für den Prinzipal, der sie angefordert hat, und deren result nennt den Fingerabdruck und gibt an, ob die Identität durch diese Aufgabe widerrufen wurde oder bereits widerrufen war. Der Applier prüft, dass die Identität zur Adresse der Aufgabe gehört, und wendet dann dieselbe Regel an wie pepsi-keys identity revoke: Die private Hälfte eines reinen Signierschlüssels wird vernichtet, die eines Verschlüsselungsschlüssels behalten, damit bereits an ihn verschlüsselte Mail lesbar bleibt. Weil die Antwort eine Aufgabe ist, trägt ein widerrufendes PATCH nur status; zusammen mit is_primary, published oder vks_wanted wird es mit 400 abgelehnt, ebenso ein revocation_reason ohne status.

Für alle drei ist die Autorisierungsschranke des Appliers keys:write oder own:<address> für genau die Adresse in den Parametern der Aufgabe – setup:write allein genügt nicht, da die Verwaltung des Schlüssels eines Benutzers nicht die Konfiguration des Servers ist –, und die Adresse muss in einer Domain aus [pepsi-ingress] ACCEPTED_DOMAINS liegen. Eine Aufgabe kann mit done, failed (der Applier hat es versucht und konnte nicht, der Grund steht in error) oder refused (der Applier hat den Datensatz gar nicht angenommen) enden, und der anfragende Prinzipal kann lesen, womit: Eine Widerrufsanfrage für einen kompromittierten Schlüssel wird mit GET /api/v1/setup/tasks/{task}?wait=60 verfolgt, bis status einer der drei ist, wie Einer Aufgabe folgen beschreibt. Ohne pepsi-httpd-admin antworten alle drei mit 503 setup_applier_unavailable, wie jeder Applier-Pfad, und ohne eine [pepsi-admin] CONFIG_DB-Verbindung mit 503 setup_write_unavailable (siehe Online-Setup: nur die Rolle pepsi-config darf einreihen). Das Erzeugen eines CSR und das Importieren eines ausgestellten Zertifikats werden hier nicht angeboten; verwenden Sie pepsi-keys.

Der zweite Faktor des Eigentümers. Eine Adresse, deren Eigentümer einen zweiten Faktor eingerichtet hat (Schlüsselverwaltung, „Ein zweiter Faktor für Schlüsseländerungen“), benötigt für jede der drei den aktuellen Code, wenn die Aufgabe unter own:<address> angefordert wird: Fügen Sie dem Rumpf "otp": "123456" hinzu (die Formulare der Konsole haben ein Feld dafür). pepsi-httpd kann den Code nicht prüfen – pepsi.otp_key ist allein pepsi-crypto gewährt –, daher wird er in den Parametern der Aufgabe mitgeführt, und der Applier prüft ihn, bevor er handelt; ein fehlender, falscher, wiederverwendeter oder gesperrter Code lässt die Aufgabe mit dem Grund in error fehlschlagen. Eine mit keys:write angeforderte Aufgabe wird nicht auf den zweiten Faktor geprüft: Der Operator kann den zweiten Faktor ohnehin zurücksetzen. DELETE /api/v1/otp/{address} (nur keys:write, nie own:) reiht eine reset-otp-Aufgabe ein, die den zweiten Faktor der Adresse entfernt und sie damit entsperrt; ihr Eigentümer richtet ihn anschließend neu ein. Die Flag-Änderungen, die ein PATCH direkt vornimmt (is_primary, published, vks_wanted), sind nicht geschützt, da dieser Prozess sie selbst ausführt.

20.7.2. Die Konfiguration schreiben, sofern Sie nicht darum bitten

pepsi.config_override zu schreiben gehört der Datenbankrolle pepsi-config, die pepsi-setup gewährt und jedem Dienstkonto ausdrücklich entzieht — eine Komponente, die Mail verarbeitet, darf die Pipeline, in der sie läuft, nicht umschreiben können. pepsi-httpd ist ein solches Konto, und die Peer-Authentifizierung richtet sich nach der effektiven uid, sodass seine eigene Verbindung nicht diese Rolle sein kann.

PUT und DELETE auf /api/v1/config antworten daher mit 503 config_write_unavailable, bis [pepsi-admin] CONFIG_DB eine Verbindung benennt, die sich als pepsi-config authentifiziert — in der Praxis ein Passwort in einem secrets.d-Fragment, das nur pepsi-httpd lesen kann, oder eine pg_ident-Abbildung. Der Server prüft das beim Start mit SELECT current_user und lehnt alles andere ab.

PUT nimmt {"value": "...", "scope": "..."} entgegen, wobei scope global (der Standard), domain:<domain> oder address:<address> ist; DELETE nimmt dasselbe als ?scope= entgegen, und GET liest die effektive Konfiguration aus der Sicht dieses Geltungsbereichs. Beide Schreibzugriffe antworten mit einem reload-Objekt, das angibt, wie die Änderung wirksam wird (hot, restart mit der neu zu startenden Unit, oder ini-only).

Abschnitte, die nur aus der Konfigurationsdatei gelesen werden ([pepsi], [pepsi-postgres], [paths], [pepsi-admin], [pepsi-httpd], [pepsi-crypto], [pepsi-srs], [pepsi-origin], [pepsi-secure-link], [pepsi-wizard] sowie jeder [pepsi-httpd-listener-*]-, [pepsi-httpd-cert-*]- und [pepsi-ingress-listener-*]-Abschnitt), werden in jedem Fall mit 403 forbidden abgelehnt. Ebenso jede Zugangsdaten tragende Option in jedem anderen Abschnitt (eine, deren Name PASSWORD, PASSPHRASE, SECRET, TOKEN, CREDENTIAL, CLIENT_ID oder PEPPER enthält – dieselbe Regel, die sie in GET maskiert und die auch die Datei abdeckt, aus der Zugangsdaten gelesen werden, sowie den Endpunkt, an den sie gesendet werden): Geheimnisse werden nie in der Datenbank gespeichert. Ein domain:- oder address:-Geltungsbereich wird nur für [stage-*]-Abschnitte angenommen, die einzigen, die pro Korrespondent gelesen werden; jeder andere Abschnitt wird dort ebenfalls mit 403 abgelehnt. POST /api/v1/config/validate meldet dieselben Ablehnungen als "valid": false. Der Leser des Overlays wendet dieselbe Regel an, sodass ein Datensatz, der auf anderem Weg in die Tabelle gelangt ist, mit einer Warnung ignoriert wird.

20.7.3. Irgendetwas Privilegiertes tun

Das Setup schreibt /etc/pepsi/pepsi.conf, übergibt jedes secrets.d-Fragment dem einen Konto, das es liest, führt certbot aus, legt Datenbankrollen an und erzeugt Schlüsselmaterial — alles als root. pepsi-httpd gibt Privilegien ab, bevor es eine Verbindung annimmt, und darf sie nie zurückerlangen können.

Also handeln die Setup-Endpunkte nicht. Sie schreiben einen Absichts-Datensatz in pepsi.setup_task, und ein separates root-Programm, pepsi-setup apply, leert ihn. Root wird über eine Datenbanktabelle erreicht, nie über einen Socket, der ein Protokoll spricht. Das vollständige Vertrauensmodell — die geschlossene Aufgabenliste, die zwei Zulassungsschranken, was bewusst fehlt und was der Entwurf nicht versprechen kann — steht in pepsi-setup(1), „Das Vertrauensmodell des Appliers“, und ist Pflichtlektüre, bevor das Browser-Setup eingesetzt wird.

20.8. Korrespondentenschlüssel

POST /api/v1/peers speichert den öffentlichen Schlüssel eines Korrespondenten von Hand — das Geschwister von pepsi-keys peer import. (Die Browser-Konsole hat keinen Import: Ihre Korrespondentenseite kann einen Schlüssel nur vergessen und verweist für alles Weitere auf pepsi-keys peer import.) Der Body benennt die address, das protocol (openpgp oder smime), das material (base64 oder Armored- bzw. PEM-Text wortgetreu) und ein optionales pin.

Zwei Ablehnungen sind wichtig:

  • Material, das kein Schlüssel ist, ist ein 422 invalid_value. OpenPGP-Eingabe läuft durch den gehärteten Parser, den die Entdeckungsmethoden verwenden, der nur die oberste Ebene durchläuft und einen Strom ablehnt, der ein komprimiertes oder verschlüsseltes Paket trägt — ein übertragbarer öffentlicher Schlüssel enthält nie eines, und eine an einen Schlüsselbund geheftete Kompressionsbombe ist kein Schlüssel, den zu haben sich lohnt.

  • Ein Schlüssel, der eine andere Adresse benennt, ist ein 422 invalid_value, das benennt, wessen Schlüssel er wirklich ist. Den Schlüssel eines Korrespondenten unter der Adresse eines anderen abzulegen leitet stillschweigend jede künftige Nachricht an ihn fehl. Ein Schlüssel, der keine Adresse benennt, wird gespeichert — Schweigen kann nichts widersprechen — und die Antwort sagt das in ihrem binding-Feld (matched / no-identity).

Der Datensatz wird mit source = api gespeichert, was die Konfliktregel neben einen von Hand eingetragenen Schlüssel einstuft: Er verdrängt einen entdeckten und verdrängt keinen gepinnten. Ein *@domain-Eintrag wird hier abgelehnt; das Schema lässt einen nur für source = manual zu, sodass das Weiten einer ganzen Domain eine an einem Terminal getroffene Entscheidung bleibt.

POST /api/v1/peers/discover stellt eine Abfrage in die Warteschlange und kehrt sofort zurück. Es holt nicht ab: Die Entdeckung spricht HTTPS, LDAP und DNS mit Hosts, die die Domain des Korrespondenten wählt, und das in einem Anfrage-Handler zu tun ließe einen Fremden die Verbindung dieses Servers offenhalten — derselbe Grund, aus dem die Verschlüsselungs- und Entschlüsselungs-Stages eine Nachricht parken, statt selbst einen Schlüssel nachzuschlagen. Der Endpunkt schreibt denselben pepsi.key_request-Datensatz, den das Parken einer Stage schreibt, über dieselbe SQL-Funktion, und ein pepsi-keydisc-Dienst erledigt die Arbeit. Die Antwort erscheint in GET /api/v1/peers, wenn einer sie gespeichert hat. {"force": true} fragt eine Adresse erneut an, deren frischer Negativ-Cache-Eintrag sagt, dass es keinen Schlüssel gibt. Es antwortet mit 501 no_discovery_method, wenn [pepsi-keydiscovery] SOURCES leer ist, da dann nie ein Dienst antworten würde, und mit 403 für eine Adresse, die ALLOW_DOMAINS/DENY_DOMAINS ausschließen.

20.10. DNS-Einträge

GET /api/v1/dns-records antwortet mit den Einträgen, die diese Installation veröffentlichen sollte, dem letzten Live-DNS-Urteil zu jedem (ok / missing / mismatch / lookup-failed), der Abhilfe für jeden falschen und dem Zonentext, den pepsi-setup run ausgeben würde.

Es bedient eine gespeicherte Antwort, statt eine zu berechnen. Abzuleiten, was veröffentlicht werden sollte, bedeutet, die validierte Konfiguration und das DKIM-Schlüsselverzeichnis zu lesen, und es zu vergleichen bedeutet, DNS abzufragen — Arbeit, die pepsi-setup gehört, das dieser Server nicht aufrufen kann (die Abhängigkeit verläuft bereits andersherum) und dessen Eingaben eine Web-Schicht nicht lesen sollte. Also berechnet der privilegierte Applier es als Teil einer run-preflight-Aufgabe und schreibt es zurück, und dieser Endpunkt meldet es mit dem Zeitpunkt seiner Berechnung, immer: Ein Urteil ohne Altersangabe lädt dazu ein, einem veralteten zu vertrauen. POST /api/v1/setup/dns-check bittet um ein frisches und braucht entsprechend setup:write — anzusehen, was gefunden wurde, ist ein Lesen, einen root-Prozess zu bitten hinzusehen nicht.

Bevor eine Prüfung gelaufen ist, ist die Antwort leer und benennt den Endpunkt, der eine ausführt, statt einer leeren Liste, die sich wie „alles ist in Ordnung“ liest.

20.11. Online-Setup

Der Browser-Weg durch pepsi-setup. Zwei Mechanismen:

Antworten werden vorgemerkt, nicht angewandt. PUT /api/v1/setup/answers mischt in eine Menge von Entwurfs-Datensätzen in pepsi.config_override ein, die jeder Konfigurationsleser strukturell herausfiltert (WHERE NOT draft). Nichts wird wirksam, während das Interview läuft, sodass eine Sitzung, die auf halbem Weg ausläuft, keinen halb konfigurierten Mailserver hinterlassen hat, und das Fortsetzen ist ein Zurücklesen der Entwürfe. Die Bezeichner sind die, die GET /api/v1/setup/questions veröffentlicht, und zugleich die Schlüssel, die pepsi-setup --answers liest und die der [pepsi-wizard]-Abschnitt hin- und zurückführt — sodass ein Interview im Browser begonnen und an einem Terminal beendet werden kann.

Der Telemetrie-Schalter wird nur dort angeboten, wo er wirksam werden kann. [pepsi] SHARE_TELEMETRY steht in der Konfigurationsdatei, wird also wie jede andere Antwort gesetzt und von der write-config-Aufgabe des Appliers geschrieben, die anschließend die Benachrichtigung telemetry_changed sendet (in beide Richtungen), sodass ein laufender pepsi-telemetry-client die Datei neu einliest. Dieser Daemon darf laufen, während die Telemetrie aus ist — ruhend, ohne etwas zu übermitteln —, und er führt in pepsi.telemetry_client einen Lebendigkeitsdatensatz, der angibt, ob er läuft und ob eine SYSTEM_ID konfiguriert ist (ein Boolean; die Konsole sieht die Kennung nie). Die Konsole erzeugt nie eine Kennung, da genau das durch Opt-in ausgeschlossen ist; solange der Daemon also nicht läuft und eine hat, kann ja nicht wirksam werden: GET /api/v1/setup/questions markiert die Frage dann mit "disabled": true und einem disabled_reason, der dem Operator sagt, was auf dem Server zu tun ist (systemctl enable --now pepsi-telemetry-client.service; SYSTEM_ID = mit 64 hexadezimalen Zeichen in [pepsi] ergänzen), die Konsole stellt sie deaktiviert dar, und ein trotzdem über PUT /api/v1/setup/answers oder eine write-config-Aufgabe gesendetes ja wird mit 409 telemetry_client_not_ready abgewiesen. Das Ausschalten oder das Beibehalten einer Antwort, die bereits ja ist, wird nie abgewiesen. GET /api/v1/setup/telemetry antwortet mit dem Gesamtbild:

{"question": "SHARE_TELEMETRY", "sharing": false, "can_enable": true,
 "reason": null,
 "client": {"running": true, "enabled": false, "submitting": false,
            "system_id_configured": true, "detail": null,
            "version": "0.1.0", "last_seen": "2026-09-27T10:00:00+00"}}

Aktionen werden angefragt, nicht ausgeführt. POST /api/v1/setup/tasks reiht eine von elf Arten ein (write-config, write-secret, obtain-certificate, install-schema, provision-roles, generate-keys, run-preflight, generate-identity, register-client-key, revoke-identity, reset-otp) mit streng validierten Parametern. Die letzten vier wirken auf die Schlüssel einer Adresse und werden normalerweise über POST /api/v1/identities, /identities/client, ein widerrufendes PATCH /api/v1/identities/{id} und DELETE /api/v1/otp/{address} erreicht; der Applier autorisiert sie über keys:write oder – alle außer reset-otp – den eigenen own:-Geltungsbereich der Adresse statt über setup:write. Es gibt keine Art für beliebige Befehle, und es gibt keine Neustart-, Reload- oder Abschaltart — eine Änderung, die den Neustart einer Komponente braucht, endet damit, dass der Operator sie neu startet, was die Konsole sagt, statt es zu verbergen. Der Applier lehnt alles andere ab, und jede Ablehnung, jeder Erfolg und jeder Fehlschlag ist ein Prüfeintrag.

Das Einreihen läuft über die [pepsi-admin] CONFIG_DB-Verbindung, genau wie Konfigurationsschreibvorgänge und aus demselben Grund: Nur die Rolle pepsi-config darf INSERT in setup_task ausführen, und der Applier lehnt jeden Datensatz ab, dessen written_by etwas anderes sagt. Ohne diese Verbindung antworten die Setup-Endpunkte mit 503 setup_write_unavailable; das eigene Konto des Servers darf die Warteschlange nur mit SELECT lesen, sodass die Konsole privilegierte Arbeit beobachten kann, ohne sie anfordern zu können.

GET /api/v1/setup/tasks/{id}?since=<seq> liefert die Fortschrittszeilen zurück, die der Applier bisher geschrieben hat, sodass ein certbot-Lauf oder eine Schemainstallation Zeile für Zeile beobachtet werden kann, ohne dass der Applier eine HTTP-Verbindung offenhält.

20.11.1. Einer Aufgabe folgen

Eine Aufgabe antwortet mit {"id", "kind", "status", "requested_by", "requested_at", "result", "error", "log"}; status ist pending oder running, bis er einer von done, failed oder refused ist, wonach der Applier nichts mehr in sie schreibt. params wird nie zurückgegeben, da der Parameter einer Art ein Credential ist.

Wer sie lesen darf. Ein Prinzipal, der setup:write hält, liest jede Aufgabe. Jeder andere authentifizierte Prinzipal liest die Aufgaben, die er selbst eingereiht hat – in der Praxis die generate-identity-, register-client-key- und revoke-identity-Aufgaben, die er über die Schlüssel-Endpunkte eingereiht hat, die einzigen Aufgaben, die er einreihen kann. GET /api/v1/setup/tasks wird auf diese verengt statt abgelehnt, und GET /api/v1/setup/tasks/{id} antwortet für eine Aufgabe, die jemand anderes eingereiht hat, mit 404 not_found, genau wie für eine, die nicht existiert. Was eine solche Aufgabe zeigt, ist die eigene Angelegenheit des Anfragenden: die Art, die Adresse und Identität, die sie benannt hat, der Fingerabdruck des Schlüssels und warum sie fehlgeschlagen ist oder abgewiesen wurde. Die Parameter – ein registrierter öffentlicher Schlüssel, ein Widerrufsgrund – werden nicht zurückgegeben, und die Schlüssel-Endpunkte geben ohnehin nichts Privates heraus.

„Selbst eingereiht“ wird anhand eines mit der Aufgabe gespeicherten Schlüssels entschieden, requester_key, nicht anhand von requested_by. requested_by (token:<label>, session:<login>, peer:<login>) ist das, was die Aufgabe anzeigt und was das Prüfprotokoll festhält, aber es ist ein Name, und Namen werden wiederverwendet: Zwei Token können dasselbe Label tragen, und ein gelöschtes und unter demselben Login neu angelegtes Konto ist ein neues Konto. Der Schlüssel wird nie wiederverwendet: token:<selector> für ein Bearer-Token (die öffentliche <id>-Hälfte von pepsi_<id>.<secret>, eindeutig unter den Token), account:<n> für eine Passwortsitzung (die Datensatznummer des Kontos, die PostgreSQL nie zweimal vergibt) und peer:<login> für einen lokalen Peer, bei dem das Systemkonto selbst der Prinzipal ist. Ein neues Token mit dem Label eines alten oder ein neu angelegtes Konto liest also keine der Aufgaben seines Vorgängers. Eine von der Kommandozeile (pepsi-setup) eingereihte Aufgabe speichert einen leeren Schlüssel, zu dem kein Prinzipal passt.

Warten statt erneut fragen. ?wait=<seconds> (höchstens 60; ein größerer Wert wird als 60 genommen) hält die Antwort zurück, bis die Aufgabe beendet ist oder eine Fortschrittszeile nach since hat (0, wenn nicht angegeben), oder bis die Zeit abgelaufen ist, je nachdem, was zuerst eintritt, und antwortet dann genau wie ohne. Ein Client, der einer Aufgabe folgt, läuft daher in einer Schleife über ?since=<last seq>&wait=60: Jede Antwort trägt die Zeilen, die er noch nicht gesehen hat, und ein status, der nicht pending oder running ist, beendet die Schleife. In der Zwischenzeit fragt nichts die Datenbank ab. Die Schreibvorgänge des Appliers lösen zwei Benachrichtigungen mit der Aufgaben-ID aus – setup_task_progress für jede Fortschrittszeile, setup_task_done, wenn die Aufgabe endet (siehe pepsi-setup) –, und pepsi-httpd hält eine Listener-Verbindung für sie, die jede auf diese Aufgabe wartende Anfrage weckt. Eine wartende Anfrage abonniert, bevor sie den Datensatz liest, sodass eine Aufgabe, die zwischen beidem endet, nie verpasst wird. Ist der Listener nicht verbunden (etwa weil PostgreSQL neu gestartet wurde), kehrt wait sofort zurück und hängt nie; der Listener verbindet sich von selbst neu. Ein wait, das keine ganze Zahl von Sekunden ist, ergibt ein 400.

Die Aufgabenseite der Konsole tut dasselbe für einen Browser, ohne Skript: Solange eine Aufgabe aussteht oder läuft, lädt sich die Seite sofort in eine Anfrage neu, die der Server bis zu 20 Sekunden lang zurückhält, bis es etwas Neues zu zeigen gibt, sodass das Ergebnis in dem Moment erscheint, in dem der Applier es festhält. Die Selbstbedienungs-Schlüsselformulare (erzeugen, registrieren, widerrufen) antworten mit dieser Seite für die Aufgabe, die sie eingereiht haben.

20.11.2. Wenn es keinen Applier gibt: 503 setup_applier_unavailable

Die privilegierte Hälfte ist ein eigenes Debian-Paket, pepsi-httpd-admin, das nur pepsi-setup-apply.socket und pepsi-setup-apply.service enthält — den gesamten Weg von einer HTTP-Anfrage zu einer Änderung unterhalb von /etc/pepsi. Das Paket pepsi empfiehlt es, es wird also standardmäßig installiert und ist entfernbar; eine Quellinstallation erhält denselben Hebel mit make install INSTALL_ADMIN_UNITS=no. Es zu entfernen durchtrennt den Kreis am Mechanismus: Nichts leert setup_task, ein INSERT bleibt also wirkungslos.

pepsi-httpd erkennt das und stuft sich auf Nur-Lesen zurück, statt Anfragen einzureihen, die niemals jemand ausführt. Jeder Endpunkt, der den Applier bitten würde, antwortet mit 503, dem code setup_applier_unavailable und einem detail, das Paket und Unit benennt, sodass ein Deployment-Skript keine Prosa parsen muss:

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

Welche Endpunkte davon abhängig sind, ist der interessante Teil:

Endpunkt

Von einem Applier abhängig?

PUT /api/v1/setup/answers

Ja. Ein Entwurfsdatensatz ist für sich genommen harmlos, aber der einzige Zweck eines Antwortsatzes ist es, angewendet zu werden; ein Assistent, der auf einem Host, auf dem nichts sie anwenden kann, klammheimlich sechs Schritte an Antworten zur Seite legt, ist genau der stille Leerlauf, den die Paketaufteilung vermeiden soll.

DELETE /api/v1/setup/answers

Nein, absichtlich. Einen Entwurf zu verwerfen kann keine privilegierte Änderung auslösen, und ein festgefahrenes Interview muss löschbar bleiben.

GET /api/v1/setup/questions, GET /api/v1/setup/answers, GET /api/v1/setup/tasks, GET /api/v1/setup/tasks/{id}, GET /api/v1/dns-records

Nein. Sie lesen. (Ein wait auf eine Aufgabe, die nie jemand abarbeiten wird, läuft einfach ab.)

POST /api/v1/setup/tasks, POST /api/v1/setup/preflight, POST /api/v1/setup/dns-check, POST /api/v1/setup/certificates, POST /api/v1/identities, POST /api/v1/identities/client, PATCH /api/v1/identities/{id} mit "status": "revoked", DELETE /api/v1/otp/{address}

Ja — jeder von ihnen erreicht das einzige ask_applier, das require_applier aufruft, bevor es irgendetwas schreibt. (Ein PATCH, das nur is_primary, published oder vks_wanted umschaltet, tut das nicht.)

PUT/DELETE /api/v1/config/{section}/{option}

Nein. Die Konfigurations-Überlagerung schreibt nach pepsi.config_override, eine Datenbanktabelle, nicht nach /etc. Sie funktionieren auch ohne Applier weiter.

503 statt 501: Die Fähigkeit ist nur ein Paket entfernt, es handelt sich also um eine Einrichtung, die hier und jetzt nicht vorhanden ist, nicht um eine Operation, die die API nicht besitzt. Es ist außerdem ein anderer Code als das obige setup_write_unavailable, weil die Abhilfen sich unterscheiden — „pepsi-httpd-admin installieren und dessen Socket starten“ gegenüber „diesem Server eine CONFIG_DB-Verbindung geben“.

Die Erkennung besteht aus zwei Dateisystemtests, beide erforderlich und keiner überflüssig: Die Unit-Datei liegt irgendwo, wo systemd sucht (installiert), und [pepsi-admin] APPLY_SOCKET (Standardwert /run/pepsi/setup-apply.sock) ist ein Socket, den dieses Konto beschreiben darf (scharfgeschaltet). Es wird nie verbunden — das Verbinden ist das Klingeln an der Tür und würde je Seitenaufbau einen root-Prozess starten. Die Hälfte mit der Unit-Datei gibt es, weil RemoveOnStop= in systemd standardmäßig aus ist, sodass ein veralteter Socket-Inode sonst als lebendige Türklingel gelesen würde und ausgerechnet in dem Szenario, für das es die Funktion gibt, offen fehlschlüge; die ausgelieferte Unit setzt zusätzlich RemoveOnStop=yes. Jede Ungewissheit wird als „nicht verfügbar“ gelesen. [pepsi-admin] APPLIER = auto|yes|no übersteuert die Prüfung für einen cron-gesteuerten oder nicht von systemd verwalteten Applier, und no schaltet das privilegierte Setup rundweg ab.

20.12. Härtung

  • Ratenbegrenzungen, je Quelladresse (RATE_LIMIT, Standardwert 120/Minute) und, auf dem Anmeldepfad, je Konto und je Quelladresse (LOGIN_RATE_LIMIT, Standardwert 10/Minute) — viele Passwörter gegen ein Konto und ein Passwort gegen viele Konten sind verschiedene Angriffe. „Quelladresse“ ist die des Clients, aus X-Forwarded-For genommen, wenn ein vertrauenswürdiger lokaler Front-Server eine behauptet; ein UNIX-Socket-Aufrufer ohne eine solche Behauptung hat keine Adresse, und der ganze Host teilt sich einen Schlüssel.

  • Body-Obergrenzen (MAX_BODY, Standardwert 1 MiB).

  • Konstantzeitiger Vergleich jedes Tokens und CSRF-Digests.

  • Kein Geheimnis wird je zurückgegeben von GET /api/v1/config: Ein Wert, der eines sein könnte, wird durch "***" mit "secret": true ersetzt. Die Maskierung ist bewusst großzügig — ein fälschlich maskierter Wert kostet einen Blick in die Datei, ein fälschlich gezeigter wird an jeden veröffentlicht, der config:read hält.

  • Kein Nachrichteninhalt ist erreichbar. Die Warteschlangenendpunkte antworten mit dem Umschlag, der Stage, dem Status und dem state-JSON, nie mit headers oder body. Mail zu lesen ist keine administrative Funktion.

  • Antworten tragen Cache-Control: no-store, X-Content-Type-Options: nosniff und eine frame-ancestors 'none'-Richtlinie.

20.13. Das Prüfprotokoll

Jede Konfigurationsänderung, Schlüsseloperation, Anmeldung, fehlgeschlagene Anmeldung, Konto- und Tokenänderung sowie administrative Warteschlangenaktion wird in pepsi.event_log festgehalten und ist über GET /api/v1/events lesbar:

event_id  at  actor  kind  subject  severity  detail

actor benennt den Prinzipal (peer:root, token:monitoring, session:alice) oder, für eine an einem Terminal vorgenommene Änderung, den aufrufenden Login (cli:alice). Die Kommandozeilenwerkzeuge des Operators schreiben in dasselbe Protokoll, sodass es vollständig ist, ganz gleich welche Oberfläche handelte; ein Protokoll, das nur bemerkte, was über HTTP geschah, lüde dazu ein, aus einem Fehlen den falschen Schluss zu ziehen.

Das Protokoll ist für alles, was Mail verarbeitet, nur anfügbar: pepsi-setup gewährt diesen Rollen INSERT und SELECT, entzieht UPDATE/DELETE und prüft den Entzug dann gegen den laufenden Server. Die Aufbewahrung wird durch [pepsi-admin] EVENT_RETENTION_DAYS (Standardwert 90) begrenzt und täglich von pepsi-httpd prune bereinigt, das pepsi-log-prune.timer unter dem Konto pepsi-httpd ausführt, unabhängig davon, ob der Webserver läuft.

Weder ein Konfigurationswert noch irgendein Schlüsselmaterial wird in einen Datensatz geschrieben: Was sich geändert hat, ist die prüfbare Tatsache, nicht, worauf es sich geändert hat.

20.14. Das Maillog (standardmäßig aus)

Warnung

``[pepsi] MAIL_LOG`` einzuschalten lässt diese Installation aufzeichnen, wer mit wem korrespondiert. Überlegen Sie, ob Sie diesen Nachweis brauchen und ob es dort, wo Sie tätig sind, rechtmäßig ist, ihn aufzubewahren, bevor Sie es einschalten.

Pepsi löscht den Datensatz einer Nachricht, wenn die Pipeline mit ihr fertig ist. Es gibt daher kein Erfolgsprotokoll je Nachricht: Eine gewöhnliche Installation sammelt keine Aufzeichnung der Korrespondenz ihrer Benutzer an und kann nicht gezwungen werden, eine herauszugeben, die sie nicht hat.

Manche Installationen brauchen diesen Nachweis wirklich. [pepsi] MAIL_LOG liefert ihn, als administrativen Akt mit ausgesprochener Konsequenz:

off

Der Standardwert. Nichts wird geschrieben; pepsi.mail_log bleibt leer.

summary

Ein Datensatz, wenn die Nachricht die Pipeline verlässt: der Umschlagabsender und die Empfänger, die Richtung (outbound für lokal eingelieferte Mail, sonst inbound), die Stage, an der sie endete, das Ergebnis und was die Pipeline über sie entschieden hat (das Authentifizierungsurteil, die Spam-/Bezahlt-Entscheidung, etwaige Fehlschlagsdetails des nächsten Hops).

full

Das Obige plus die Subject:-Zeile.

Nachrichteninhalt wird bei keiner Einstellung je aufgezeichnet. Datensätze sind über GET /api/v1/mail-log für einen Prinzipal lesbar, der logs:read hält (oder, für eine Adresse, own:<address>), sind für die mailverarbeitenden Konten genau wie das Prüfprotokoll nur anfügbar und werden nach [pepsi-admin] MAIL_LOG_RETENTION_DAYS Tagen (Standardwert 30) beschnitten.

Ein Datensatz wird geschrieben, wenn eine Stage mit einer Nachricht fertig wird (completed) und wenn eine Nachricht dauerhaft scheitert (failed) — die zwei Arten, auf die sie aufhört, sich zu bewegen. Der Schreibvorgang reitet auf derselben Anweisung wie das Terminal, das den Datensatz entfernt oder scheitern lässt, sodass das Aktivieren des Protokolls keinen zusätzlichen Datenbank-Round-Trip kostet.

20.15. Bootstrapping

Eine frische Installation hat keine Konten und braucht auch keines: Der UNIX-Socket plus SO_PEERCRED funktioniert für einen lokalen Administrator immer. Von dort aus

# curl --unix-socket /run/pepsi/admin.sock -X POST \
    -H 'Content-Type: application/json' \
    -d '{"login":"alice","password":"…","scopes":["queue:read","queue:write"]}' \
    http://localhost/api/v1/accounts

erzeugt das erste entfernte Konto. Ein verlorenes Passwort ist keine Sperre: Der lokale Socket ist immer noch da.

Wenn der Socket nicht erreichbar ist — ein Operator, der von einer anderen Maschine arbeitet, oder ein Container ohne Shell auf dem Host —, gibt pepsi-setup bootstrap, als root ausgeführt, ein Bearer-Token aus, das die volle Menge an Administrator-Geltungsbereichen hält, binnen einer Stunde abläuft und genau einmal angenommen wird: genug für ein POST /api/v1/accounts.

Es muss diese Menge halten statt setup:write allein, denn POST /api/v1/accounts weigert sich, einen Geltungsbereich auszustellen, den der Aufrufer nicht selbst hält — ein Token mit nur setup:write könnte also überhaupt keinen Administrator erzeugen, und ein von ihm erzeugtes Konto könnte sich hinterher auch nicht selbst erweitern. Was das Credential begrenzt, sind seine Lebensdauer, seine einmalige Verwendung und dass das nächste pepsi-setup bootstrap sie widerruft.

Einmalig wegen des Ortes, an dem es ausgegeben wird. Ein Token auf einem Terminal steckt in einem Scrollback-Puffer, und ein Token im Journal ist für jeden lesbar, der das Journal lesen kann, was gewöhnlich ein weiterer Kreis von Leuten ist als „darf den Mailserver verwalten“. Es wird bei Vorlage durch eine Aktualisierung verbraucht, die nur eine konkurrierende Anfrage gewinnen kann, sodass ein von zwei Personen gelesenes Token eine davon einlässt. Nur sein Digest wird gespeichert, sodass es nicht wiederhergestellt werden kann — führen Sie den Befehl erneut aus, was zugleich den vorherigen widerruft.

20.16. Die Browser-Konsole

Die Verwaltungskonsole ist ein Client dieser API, unter /ui auf denselben ADMIN = yes-Listenern eingehängt, und teilt ihre Authentifizierung, ihre Geltungsbereiche und ihr Prüfprotokoll. Sie fügt keine Fähigkeit hinzu: Alles, was sie tut, ist hier mit curl erreichbar. Zwei Details des Sitzungsmechanismus existieren ihretwegen und sind beim Schreiben eines anderen Clients von Bedeutung:

  • POST /api/v1/auth/login liefert das CSRF-Token im Antwort-Body zurück und setzt es als pepsi_csrf-Cookie (HttpOnly, SameSite=Strict; mit dem Namen __Host-pepsi_csrf, wann immer das Sitzungs-Cookie dieses Präfix trägt). Ein Programm liest den Body; die Konsole hat keinen anderen Weg, das Token in ein Formular zu bekommen, denn sie liefert kein Skript aus, das eines aus der Seite ausliest. Keine der beiden Platzierungen ist die Prüfung — die Prüfung ist, dass der Digest dessen, was eine verändernde Anfrage vorlegt, dem mit der Sitzung gespeicherten gleicht.

  • Eine verändernde Anfrage darf das Token im X-Pepsi-Csrf-Header vorlegen (was ein Programm tut) oder als _csrf-Formularfeld (was ein HTML-Formular tut). Ein Bearer-Token und ein UNIX-Socket-Peer werden nach keinem von beiden gefragt, da ein Browser sie nie im Auftrag eines anderen sendet.