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, undeine 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.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"}}
codeist ein stabiles Token, auf das man verzweigen kann;hintist Prosa für einen Menschen und kann sich ändern;detailträ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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
|
— |
|
— |
|
— (ohne |
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
totalzu 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 |
|---|---|---|---|
|
|
— |
Liefert ein Sitzungs-Cookie und ein CSRF-Token zurück. |
|
|
— |
Idempotent. |
|
|
— |
Das Erste, was zu prüfen ist, wenn etwas mit |
|
|
|
Passwort-Hashes werden nie zurückgegeben. |
|
|
|
Ein Konto zu löschen beendet seine Sitzungen. |
|
|
|
Das Geheimnis steht in der |
|
|
|
Widerruf. |
|
|
|
Identisch zu |
|
|
|
|
|
|
|
Umschlag, Stage, Status und |
|
|
|
Wieder ausstehend an seiner aktuellen Stage. |
|
|
|
Body |
|
|
|
Löscht die Nachricht. |
|
|
|
Das Prüfprotokoll. |
|
|
|
|
|
|
|
|
|
|
|
MX-Adressen, die derzeit scheitern. |
|
|
|
|
|
|
|
Ein Abschnitt. |
|
|
|
Vor dem Speichern validiert. Siehe unten. |
|
|
|
Trockenlauf; antwortet in beiden Fällen mit |
|
|
|
|
|
|
|
|
|
|
|
Die öffentliche Hälfte, base64. |
|
|
|
Eine |
|
|
|
Eine |
|
|
|
Eine |
|
|
|
|
|
|
|
Den Schlüssel eines Korrespondenten von Hand importieren. Siehe unten. |
|
|
|
Reiht eine Entdeckungsabfrage ein; es holt nicht ab. Siehe unten. |
|
|
|
Einen zwischengespeicherten Schlüssel vergessen. |
|
|
|
|
|
|
|
Die S/MIME-Vertrauensanker. |
|
|
|
Ändert, welche eingehenden Signaturen validieren. |
|
|
|
Ausstehende Secure-Link-Nachrichten, nur Metadaten. |
|
|
|
Eine Nachricht mit ihrer Zugriffshistorie. Nie ihr Inhalt. |
|
|
|
Widerrufen; das zerstört die einzige Kopie der Nachricht. |
|
|
|
Die zu veröffentlichenden Einträge und das letzte Live-Urteil. Siehe unten. |
|
|
|
Das Setup-Interview als Daten. |
|
|
|
Ob das Einschalten der Funktionstelemetrie wirksam werden könnte und, falls nicht, was auf dem Server zu tun ist. Siehe unten. |
|
|
|
Die vorgemerkten Antworten; Zugangsdaten maskiert. |
|
|
|
Antworten in die vorgemerkte (Entwurfs-)Menge einmischen. |
|
|
|
Das vorgemerkte Interview verwerfen. |
|
|
|
Privilegierte Einrichtungsaufgaben, neueste zuerst; ohne |
|
|
|
Den Applier um eine Aktion aus seiner geschlossenen Menge bitten. |
|
|
|
Eine Aufgabe, mit |
|
|
|
Die Umgebung prüfen (Ports, Resolver-DNSSEC). |
|
|
|
Live-DNS mit dem vergleichen, was das Setup veröffentlichen würde. |
|
|
|
Die von der Konfiguration benötigten Zertifikate beziehen. |
|
|
— |
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 ihrembinding-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.9. Secure Links¶
GET /api/v1/secure-messages listet die Nachrichten auf, die das Secure-Link-Portal hält: von wem und für wen sie sind, wann sie erzeugt wurden und wann sie ablaufen, wie oft die PIN richtig und wie oft falsch eingegeben wurde, ob eine Sperre in Kraft ist und wie viele Bytes gespeichert sind. GET /api/v1/secure-messages/{token} fügt das Zugriffsprotokoll hinzu — den Zeitstempel, die Peer-Adresse und das Ergebnis jedes Versuchs. Es gibt bewusst keine Geolokalisierung und keinen User Agent.
Kein Endpunkt liefert die Nachricht zurück. pepsi-httpd ist das Portal, sodass es die eine Datenbankrolle ist, der secure_message.ciphertext gewährt ist — was genau der Grund ist, warum nichts auf der Operatoroberfläche sie liest. Die Abfragen hinter diesen Endpunkten benennen jede Spalte außer dieser und melden nur length(ciphertext), sodass die Eigenschaft durch das SQL durchgesetzt wird und nicht dadurch, dass jemand daran gedacht hat, ein Feld wegzulassen. Sie zu lesen hülfe auch nicht: Der Schlüssel ist Argon2id über einer PIN, die der Server nie sieht, je Datensatz gesalzen und aus einer Datei gepfeffert, die nur das Portal liest.
DELETE /api/v1/secure-messages/{token} widerruft. Es löscht den Datensatz und mit ihm die einzige Kopie der Nachricht — es gibt keinen Hinterlegungsschlüssel, und das Portal führt keine zweite Kopie —, sodass es secure:write statt secure:read braucht. Der Absender sendet erneut, wenn es ein Versehen war.
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.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, ausX-Forwarded-Forgenommen, 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": trueersetzt. 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, derconfig:readhält.Kein Nachrichteninhalt ist erreichbar. Die Warteschlangenendpunkte antworten mit dem Umschlag, der Stage, dem Status und dem
state-JSON, nie mitheadersoderbody. Mail zu lesen ist keine administrative Funktion.Antworten tragen
Cache-Control: no-store,X-Content-Type-Options: nosniffund eineframe-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:
offDer Standardwert. Nichts wird geschrieben;
pepsi.mail_logbleibt leer.summaryEin Datensatz, wenn die Nachricht die Pipeline verlässt: der Umschlagabsender und die Empfänger, die Richtung (
outboundfür lokal eingelieferte Mail, sonstinbound), 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).fullDas 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/loginliefert das CSRF-Token im Antwort-Body zurück und setzt es alspepsi_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.