15. Sicherheitsmodell

Dieses Kapitel sagt, wogegen Pepsi sich verteidigt, wogegen nicht und was ein Angreifer aus jedem einzelnen Ding bekommt, das er erbeutet haben könnte — es ist damit nach einer Kompromittierung ebenso nützlich wie davor.

Pepsi ist ein MTA, der private Schlüssel hält und Mail lesen kann, was ihn angriffswürdig macht. Der Entwurf stellt immer wieder eine Frage: Wenn diese Komponente kompromittiert ist, was bleibt außer Reichweite? Wo die Antwort „nichts“ lautet, wird das hier gesagt, statt es entdecken zu lassen.

Die Zusicherungen — welche Schutzgüter, gegen welche Angreifer, in welchem Verwahrungsmodus — sind in Bedrohungsmodell formuliert; dieses Kapitel beschreibt die Mechanismen dahinter.

15.1. Was Pepsi voraussetzt

Ist einer davon falsch, gelten die untenstehenden Zusicherungen nicht:

  • Der Host ist nicht bereits kompromittiert. Nichts hier verteidigt gegen root auf der Maschine. Root kann jeden Schlüssel, jedes Geheimnis und jede Nachricht lesen.

  • PostgreSQL authentifiziert über Peer-Zugangsdaten auf einem lokalen Socket. Jede Komponente verbindet sich als ihr eigener Betriebssystembenutzer, und die Datenbank gewährt dieser Rolle nur das, was die Komponente braucht. Eine Installation, die auf Passwortauthentifizierung mit einem gemeinsamen Konto umstellt, wirft das meiste von Ein Angreifer, der die Datenbank hat weg.

  • Das Betriebssystem setzt Dateieigentum und das setuid-Bit durch. Die Grenze der privaten Schlüssel ist ein Datenbank-GRANT, das sich nach der effektiven uid richtet; eine nosuid-Einhängung oder ein gemeinsames Konto lässt sie zusammenbrechen.

  • Ein validierender DNS-Resolver. Pepsi vertraut dem AD-Bit, statt DNSSEC selbst zu validieren (siehe pepsi-stage-relay-to-internet). Ein lügender Resolver hebelt DANE aus und stuft MTA-STS auf Vertrauen beim ersten Gebrauch herab.

  • TLS-Zertifikatsmaterial stammt nicht vom Angreifer. Die certbot-Integration schreibt nach /etc/letsencrypt; ein Angreifer, der dort schreiben kann, kann sich als der Server ausgeben.

15.2. Die Privilegientrennung

Stützt

Eine kompromittierte Komponente (was jedes kompromittierte Konto erreicht), Ein lokaler Shell-Benutzer (keine Rolle ist von einem gewöhnlichen Konto aus erreichbar) und Wer eine Kopie der Datenbank hat (was eine Dienstrolle schreiben kann).

Pepsi ist nicht ein Programm. Jede Komponente läuft unter ihrem eigenen Konto, und die Trennung wird von der Datenbank ebenso durchgesetzt wie vom Dateisystem.

Die Datenbankseite zieht zwei Arten von Grenzen, und es lohnt sich, bei beiden genau zu sein.

Die Pipeline-Rolle ist absichtlich weit gefasst. pepsi (der Dispatcher und jede gewöhnliche Stage) und pepsi-crypto halten eine pauschale Berechtigung: SELECT, INSERT, UPDATE und DELETE auf jede Tabelle im Schema pepsi, die Nutzung jeder Sequenz, EXECUTE auf jede Funktion und dasselbe als Standardrechte für später angelegte Objekte. Jede Stage liest und schreibt die Warteschlange, und zusammen nutzen die Stages jede Pipeline-Tabelle, sodass geringste Rechte je Stage ein Konto je Stage erfordern würden; die Folge wird in Bedrohungsmodell benannt statt verschwiegen. Die Berechtigung wird dann dort wieder zurückgenommen, wo die Pipeline nichts zu suchen hat:

  • crypto_identity: SELECT/INSERT/UPDATE nur auf die Spalten außer private_wrapped (DELETE kennt keine Spaltengranularität und bleibt); nur pepsi-crypto hält die Spalte;

  • config_override: nur SELECT — das Schreiben ist Sache der Rolle pepsi-config (pepsi-httpd schreibt über eine [pepsi-admin] CONFIG_DB-Verbindung, die sich als diese Rolle authentifiziert);

  • setup_task und setup_task_log: nichts; nur pepsi-config darf eine Aufgabe per INSERT anlegen;

  • secure_message und secure_access: INSERT, DELETE und SELECT auf jede Spalte außer ciphertext bei secure_message; SELECT und DELETE bei secure_access;

  • admin_account, admin_session und api_token: nichts;

  • event_log und mail_log: nur anhängend (kein UPDATE/DELETE).

Die Dienste, die nicht zur Pipeline gehören, bekommen, was sie nutzen. Eine Prüfung jeder Anweisung, die jeder von ihnen absetzen kann — im eigenen Crate und in jeder gelinkten Bibliothek, einschließlich der Aufrufe gespeicherter Funktionen —, legt ihre Berechtigungen fest, und pepsi-setup weigert sich abzuschließen, wenn PostgreSQL davon abweicht:

Rolle

Gewährt

Ausdrücklich nicht

pepsi-ingress

INSERT in workqueue und workqueue_body (über workqueue_add) und SELECT der neuen IDs; SELECT der beiden generierten Größenspalten (header_octets, octets), die die Zulassungsprüfung der Warteschlange aufsummiert; SELECT auf config_override und mailbox_quota; SELECT von list_name und mail_host aus mailing_list (die Adresse einer Liste, für die Empfängerprüfung); INSERT in die beiden Nur-Anhänge-Logs.

Jede andere Nachricht in der Warteschlange, Einstellungen, Whitelists, Schlüssel, alles über eine Liste außer ihrer Adresse. Ein Parser-Fehler in dem Prozess, der auf Port 25 lauscht, erreicht eine Tabelle, und zwar nur, um ihr etwas hinzuzufügen.

pepsi-telemetry

Ihre Tabelle telemetry.

Alles andere, sodass ein Collector, der eine Datenbank mit einer Pipeline teilt, die Mail nicht erreichen kann.

pepsi-httpd

Die Tabellen der administrativen Zugangsdaten, das Portal, die Schlüsseltabellen (nicht private_wrapped), die Listen- und Archivtabellen, event_log/mail_log (pepsi-httpd prune, ausgeführt von pepsi-log-prune.timer, bereinigt sie); bei workqueue die Umschlag-, state- und Routing-Spalten, INSERT (Mail, die die Webformulare versenden) und DELETE; bei workqueue_body INSERT und DELETE (ein Nachrichtentext verschwindet mit der letzten Nachricht, die ihn trägt; der Fremdschlüssel verweigert jedes andere Löschen) und nur die ID- und Größenspalten; nur lesend Statistiken, dns_address, tls_session, mailbox_quota und telemetry_client (der Lebenszeichen-Datensatz des Telemetrie-Daemons); SELECT auf setup_task.

Header oder Nachrichtentext einer Nachricht in der Warteschlange — die Konsole listet Umschläge auf und hat keinen Lesepfad zum Inhalt, noch kann sie die body_id einer Nachricht auf den Nachrichtentext einer anderen umbiegen; settings, whitelist, vacation_reply, payment_request_reply, die Secretary-Tabellen, telemetry, origin_nonce, auto_pay_spend, list_post_rate; das Schreiben von Statistiken, Quota oder des Lebenszeichen-Datensatzes des Telemetrie-Daemons; config_override und setup_task außer über seine pepsi-config-Verbindung.

Eine Tabelle, die ein späterer Schema-Patch hinzufügt, liegt nicht in der Reichweite von pepsi-ingress oder pepsi-telemetry, bis ein Patch sie gewährt: Auch ihre Standardrechte sind entzogen.

Die Rollen außerhalb dieser Menge sind noch enger gefasst: pepsi-keydisc, pepsi-whitelist und pepsi-config halten jeweils nur Berechtigungen auf einigen benannten Tabellen. pepsi-crypto hält die pauschale Berechtigung plus private_wrapped. Der Schemaeigentümer pepsi-owner, als der sich pepsi-setup selbst verbindet, hält alles kraft Eigentümerschaft.

Komponente

Läuft als

Was sie erreichen kann

pepsi-ingress

pepsi-ingress

INSERT in die Warteschlange und die beiden obigen Lesezugriffe; das SRS-Geheimnis; TLS-Material. Keine andere Nachricht, kein privates Schlüsselmaterial, keine Einstellung, keine Whitelist.

pepsi-dispatch und die meisten Stages

pepsi

Die obige Pipeline-Berechtigung: die Warteschlange, Einstellungen, Statistiken, die Whitelist und die Secure-Link-Tabellen ohne ihr ciphertext. Nicht crypto_identity.private_wrapped.

pepsi-stage-encrypt / -decrypt

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

Der gewöhnliche Pipeline-Zugriff, den jede Stage hat, zusätzlich das verpackte private Schlüsselmaterial und der Schlüsselverschlüsselungsschlüssel. Nichts sonst hält beides: Der Schemaeigentümer pepsi-owner kann die Spalte ebenfalls lesen, aber nicht den Schlüsselverschlüsselungsschlüssel.

pepsi-keys

der aufrufende Operator

Mit 0755 installiert, ohne setuid-Bit: Ein Binärprogramm, das jeden privaten Schlüssel einer Installation öffnen kann, ist eine weit größere Beute als die einzelne Tabelle von pepsi-whitelist, sodass es standardmäßig nur root oder das Konto pepsi-crypto nutzen kann und die PostgreSQL-Berechtigungen maßgeblich sind. Ein Standort, der will, dass Benutzer ihre eigenen Identitäten verwalten, kann es bewusst setuid pepsi-crypto machen (chmod 4755); dann greift die programmeigene Eigentumsregel je Identität — siehe Schlüsselverwaltung.

pepsi-httpd

pepsi-httpd

Die Tabellen in der zweiten Tabelle oben — lesend und schreibend, nicht überwiegend lesend: Seine Handler ändern das Routing der Warteschlange und Schlüsselmetadaten direkt. Keine Header oder Nachrichtentexte, kein privates Schlüsselmaterial, keine Einstellungen oder Whitelists; keine Möglichkeit, das Konfigurations-Overlay zu schreiben oder eine Setup-Aufgabe einzureihen, außer über seine pepsi-config-Verbindung.

pepsi-keydisc

pepsi-keydisc

Die Anfragewarteschlange der Schlüsselentdeckung und peer_key. Nie ein allgemeines UPDATE auf pepsi.workqueue.

pepsi-whitelist

aufrufender Benutzer (setuid)

Nur SELECT/INSERT/DELETE auf pepsi.whitelist.

pepsi-quota

pepsi (setgid pepsi-maildir, Modus 2550)

pepsi.mailbox_quota und — über die Gruppe — der setuid-root-Helper, der ein Postfach vermisst.

Die Postfach-Quota hält Messen und Ablehnen auseinander. Nur pepsi-helper-maildir-writer kann in ein Maildir hineinsehen (Modus 0700; es ist das eine Programm, das zum Benutzer wird), und nur Mitglieder von pepsi-maildir dürfen ihn ausführen — eine Gruppe, in der pepsi-ingress bewusst nicht ist und vor deren Hinzufügen pepsi-setup warnt. So kann der Prozess, der auf Port 25 lauscht, eine von jemand anderem genommene Messung lesen und darauf einen Empfänger ablehnen, und sonst nichts: pepsi-setup entzieht ihm den Schreibzugriff auf pepsi.mailbox_quota und lässt SELECT übrig. Beide Wege zur Abrechnung sind ihm verschlossen, und keiner der Verschlüsse stützt sich auf den anderen.

Die Krypto-Stages sind setuid, nicht setgid: Die Peer-Authentifizierung von PostgreSQL richtet sich nach der effektiven uid, sodass eine Stage, die bloß eine Gruppe erlangte, sich weiterhin als pepsi verbände und ihr die Schlüsselspalte verweigert würde. Sie muss pepsi-crypto sein.

15.2.1. Jedes Programm, das ein Bit trägt

Stützt

Ein lokaler Shell-Benutzer: Ein lokales Konto erlangt keine Pepsi-Rolle, und jeder Helper handelt nur mit der Autorität des Zielbenutzers (Grenze B4).

Ein Multi-Call-Binary kann kein programmspezifisches setuid- oder setgid-Bit tragen, daher wird jedes dieser Programme als eigene ausführbare Datei installiert statt als symbolischer Verweis auf das gemeinsame pepsi-Binary (die maßgebliche Liste ist STANDALONE_BINARIES im Makefile; die Modi werden von make install und, unter Debian, von dpkg-statoverride aus dem postinst gesetzt). Alles, was hier nicht aufgeführt ist, ist unprivilegiert.

Programm

Eigentümer und Modus

Warum

pepsi-helper-maildir-writer

root:pepsi-maildir 4750

Wird zum Empfänger, um in ein 0700-Maildir zu schreiben.

pepsi-stage-relay-to-maildir

pepsi:pepsi-maildir 2550

Das setgid-Bit ist die einzige Schranke vor dem Ausführen dieses Helfers, daher lautet der Modus 2550 und nicht 2755: Ausführungsrecht für alle würde jedem lokalen Konto egid=pepsi-maildir verschaffen. Der Benutzer pepsi ist bewusst kein Gruppenmitglied und erhält das Ausführungsrecht dadurch, dass ihm die Datei gehört.

pepsi-quota

pepsi:pepsi-maildir 2550

Derselbe Helfer, damit ein reconcile-Durchlauf per Timer neu messen kann, ohne root zu sein.

pepsi-helper-dot-forward

root:pepsi-forward 4750

Gibt die Rechte vollständig an den Benutzer ab, bevor dessen ~/.forward gelesen wird.

pepsi-stage-dot-forward

pepsi:pepsi-forward 2550

Dieselbe Schranke, für diesen Helfer.

pepsi-helper-auto-pay

root:pepsi-wallets 4750

Steuert die Wallet als das Konto, dem sie gehört.

pepsi-stage-auto-pay

pepsi:pepsi-wallets 2550

Dieselbe Schranke, für diesen Helfer.

pepsi-stage-relay-to-smarthost

pepsi:pepsi-token 2550

Liest die gruppenbeschränkten OAuth-Token-Dateien, die pepsi-helper-token-refresh schreibt, den Client-Schlüssel für gegenseitiges TLS und den Kerberos-Credential-Cache. 2550 aus demselben Grund wie bei den obigen Stages: Die Stage akzeptiert -c, sodass Ausführungsrecht für alle jedem lokalen Konto erlauben würde, sie auf einen eigenen Server zu richten und ihr diese Zugangsdaten schicken zu lassen.

pepsi-whitelist

pepsi-whitelist:pepsi-whitelist 4755

Absichtlich für alle ausführbar: Jeder lokale Benutzer darf seinen eigenen Namensraum verwalten, und was ihn davon abhält, den eines anderen anzurühren, ist die Regel im Programm, nicht der Dateimodus.

pepsi-stage-encrypt, pepsi-stage-decrypt

pepsi-crypto:pepsi 4750

Die Peer-Authentifizierung richtet sich nach der effektiven uid, also muss die Stage die Rolle sein, der crypto_identity.private_wrapped gewährt ist. Gruppe pepsi und kein Ausführungsrecht für alle: Anders als pepsi-whitelist sind dies keine Benutzerbefehle.

pepsi-keys

0755, kein Bit

Siehe die Tabelle oben. Ein Standort kann es setuid pepsi-crypto machen.

pepsi-helper-mailbox-scan

0755, kein Bit

Die unprivilegierte Hälfte des pepsi-whitelist-Paares: Es parst feindliche Postfachdateien und wird daher mit setresuid auf den aufrufenden Benutzer gestartet — alle drei ids, sodass es den Eigentümer nie zurückerlangen kann.

15.2.2. Das Laufzeitverzeichnis ist eine ACL, keine gemeinsame Gruppe

Stützt

Ein lokaler Shell-Benutzer und Eine kompromittierte Komponente: Ein Socket ist für die Konten erreichbar, die es erreichen sollen, und das Anlegen eines Sockets gewährt keine sachfremde Autorität.

/run/pepsi enthält Sockets, die mehreren verschiedenen Konten gehören — das Socket für lokale Einlieferung von pepsi-ingress, das Admin-Socket von pepsi-httpd, das Reverse-Proxy-Socket, mit dem der vorgelagerte Webserver verbindet, und die Türklingel des Appliers — und es gehört root, mit einer POSIX-ACL, die pepsi-ingress und pepsi-httpd namentlich Schreibzugriff gewährt (debian/pepsi.tmpfiles). Eine gemeinsame Gruppe würde nicht genügen: Sie müsste eine sein, in der beide sind, und jeder Kandidat trägt etwas, das keiner von beiden erlangen sollte — die Gruppe pepsi liest die privaten DKIM-Schlüssel, die Gruppe pepsi-admin ist die Zugriffskontrolle der administrativen API. Nur pepsi-httpd.service läuft mit pepsi-admin als zusätzlicher Gruppe, damit es sein Admin-Socket dieser Gruppe übergeben kann: Die Gruppe besitzt keine Datei und gewährt nichts außer dem Administratorstatus auf diesem Socket, den der Prozess, der es bedient, ohnehin hat. pepsi-ingress erhält sie nie.

Der Modus 0775 auf diesem Verzeichnis ist daher tragend und keine Aufweichung: auf einem Verzeichnis mit ACL sind die Gruppenbits die ACL-Maske, ein 0755 begrenzt also stillschweigend jede namentliche Gewährung auf r-x, und der nächste Neustart kann sein Socket nicht anlegen.

15.2.2.1. Keine Unit darf es als RuntimeDirectory beanspruchen

Das tmpfiles.d-Bruchstück ist der einzige Eigentümer des Verzeichnisses, und das ist eine Regel und keine Vorliebe. systemd gibt ein RuntimeDirectory= dem User= der deklarierenden Unit und chownt es – und alles darin – bei jedem Start. In einem Verzeichnis mit vier Mietern unter drei Konten entscheidet dieses rekursive chown nicht bloß, wer das Verzeichnis besitzt: es schreibt die Eigentümerschaft von Sockets um, die andere Units schon gebunden haben.

Zum Beispiel bindet pepsi-httpd.socket /run/pepsi/httpd.sock als 0660 root:www-data, damit der vorgelagerte Webserver es im Reverse-Proxy-Betrieb erreichen kann. Ein RuntimeDirectory=pepsi in pepsi-httpd.service würde dieses Socket bei jedem Start auf pepsi-httpd:pepsi-httpd chownen, der vorgelagerte Server bekäme beim Verbinden EACCES, und jeder Abruf einer MTA-STS-Richtlinie erhielte ein 502 — während jede Unit active wäre und nichts protokolliert würde, denn nichts wäre fehlgeschlagen. Dasselbe chown würde die root-Eigentümerschaft des Verzeichnisses und seine ACL vollständig ersetzen und die Sockets der anderen Mieter derjenigen Unit übergeben, die zuletzt gestartet ist, und ohne RuntimeDirectoryPreserve=yes würde ein stoppender Dienst das Verzeichnis löschen, samt lebender Sockets.

Was diese Units von RuntimeDirectory= tatsächlich brauchten, war eines: /run ist unter ProtectSystem=strict nur lesbar. pepsi-httpd.service und pepsi-ingress.service sagen daher stattdessen ReadWritePaths=/run/pepsi und nehmen ihre Berechtigung aus der ACL; pepsi-setup-apply.service braucht keines von beiden, da es als root ohne ProtectSystem=strict läuft. Die .socket-Units behalten DirectoryMode=0775, damit das Verzeichnis dennoch erscheint, falls eine von ihnen bindet, bevor systemd-tmpfiles gelaufen ist, und nichts löscht es, weil nichts es beansprucht. tests/check-systemd-units.sh (von make check ausgeführt) lässt den Build scheitern, wenn ein RuntimeDirectory=pepsi in einer ausgelieferten Unit oder in dem Reverse-Proxy-Drop-in, das pepsi-setup schreibt, wieder auftaucht.

Der Zugriff des vorgelagerten Webservers wird auf dieselbe Weise und gesondert gewährt. httpd.sock zu erreichen braucht zwei Berechtigungen: die SocketGroup= des Sockets selbst und die Durchquerung des Verzeichnisses, in dem es liegt. Die other-r-x-Bits des Verzeichnisses würden die zweite liefern, existieren aber aus einem anderen Grund (jedes lokale Konto darf über das Einlieferungssocket Post einliefern), und sich darauf zu verlassen würde die Richtlinienauslieferung von einer Berechtigung abhängig machen, die für etwas anderes gewährt wurde. pepsi-setup schreibt daher eine ausdrückliche Gewährung in /etc/tmpfiles.d/pepsi-reverse-proxy.conf, wenn es den Reverse-Proxy-Betrieb einrichtet, mit dem tatsächlich erkannten Konto (www-data unter Debian, nginx/apache anderswo), mit r-x und nicht mehr, und entfernt sie mit dem Socket-Drop-in wieder. Es ist eine gesonderte Datei vom ausgelieferten Bruchstück, weil sie erkannt und nicht ausgeliefert ist und bedingt und nicht immer gilt.

Der Telemetriesammler ist hier bewusst überhaupt kein Mieter und liegt stattdessen in /run/pepsi-collector – seinem eigenen Verzeichnis, das mit keiner anderen Unit geteilt wird, was ihn auch von /run/pepsi-telemetry freihält, dem des Telemetrieclients.

15.3. Die Verwahrungsgrenze für private Schlüssel

Stützt

Gateway-Verwahrung (custody = local, der MTA-Schlüssel) (Schlüssel werden niemals einem passiven Leser oder einer gewöhnlichen Stage offengelegt; Grenze B5).

Privates Schlüsselmaterial liegt in crypto_identity.private_wrapped und ist doppelt geschützt:

Durch Berechtigung. pepsi-setup gewährt diese eine Spalte der Rolle pepsi-crypto und entzieht sie jedem Dienstkonto, pepsi-httpd eingeschlossen; außer pepsi-crypto kann sie nur der Schemaeigentümer pepsi-owner kraft Eigentümerschaft lesen. Das gilt auf Spaltenebene und ist absolut: PostgreSQL lehnt eine Anweisung ab, die die Spalte auch nur nennt, selbst innerhalb eines IS NULL-Tests, sodass es keinen Lesepfad gibt, den man später einengen müsste.

Bemerkung

Diese Absolutheit hat eine praktische Kante. Eine Auflistungsabfrage kann „hat diese Identität eine private Hälfte?“ nicht als private_wrapped IS NOT NULL melden: Das wird für jede Rolle außer pepsi-crypto abgelehnt, und PostgreSQLs Fehler, permission denied for table crypto_identity, nennt keine Spalte. Die Auflistung leitet das Flag stattdessen aus wrap_key_id ab, das ein Tabellen-CHECK exakt im Gleichschritt hält.

Durch Verpackung. Gespeichert wird kein privater Schlüssel, sondern ein AEAD-Chiffrat, gebunden an das (address, protocol, purpose) dieses Datensatzes als zugehörige Daten, unter einem Schlüsselverschlüsselungsschlüssel, der außerhalb der Datenbank liegt, in einem secrets.d/*.secret-Fragment, das nur pepsi-crypto lesen kann — nicht pepsi-owner, sodass die einzige andere Rolle, die die Spalte lesen kann, trotzdem nichts hält, was sie öffnen könnte. Ein Blob zwischen Datensätzen zu verschieben scheitert an der Authentifizierung.

15.4. Ein zweiter Faktor für die Schlüsselverwaltung

Stützt

Wer das Mail-Passwort eines Benutzers hat (ein gestohlenes Mail-Passwort kann keinen Schlüssel für einen Benutzer registrieren, der einen zweiten Faktor eingerichtet hat).

Ein Benutzer kann für seine Adresse ein TOTP-Geheimnis einrichten (RFC 6238: HMAC-SHA-1, sechs Ziffern, 30-Sekunden-Schritte — was jede Authenticator-App implementiert); von da an braucht jede Änderung an seinen Schlüsseln, die er vornehmen kann, einen aktuellen Code:

  • Per E-Mail nimmt jeder Schlüsselbefehl außer status — generate, register, publish, retire und otp enroll/otp remove selbst — den Code als sechsstelliges Wort im Subject: entgegen. Der Autocrypt:-Header oder ein angehängter Schlüssel einer gewöhnlichen Nachricht registriert für diese Adresse nichts mehr.

  • In der Web-Konsole und der API tragen die Anfragen generate-identity, register-client-key und revoke-identity eines own:<address>-Prinzipals ein Feld otp. pepsi-httpd kann es nicht prüfen — es hält überhaupt keine Berechtigung auf die Tabelle — und reicht es durch; pepsi-setup apply prüft es, bevor es handelt, bei einer Aufgabe, die allein aufgrund von own:<address> zugelassen wurde. Ein Operator, der keys:write hält, wird nicht gefragt.

  • Ein unprivilegiertes pepsi-keys (eine setuid-Installation) braucht --otp CODE für jede Identitätsänderung und jeden Export eines privaten Schlüssels; die Eigentümerschaft wird zuerst geprüft, sodass ein Benutzer niemals Fehlversuche gegen die Einrichtung eines anderen verbuchen kann.

Verwahrung. Das Geheimnis wird genau wie ein privater Schlüssel verpackt — AES-256-GCM unter dem KEK des Schlüsselspeichers, gebunden an (address, totp, second-factor) —, und die Tabelle otp_key ist allein der Rolle pepsi-crypto gewährt: pepsi-setup entzieht sie jeder anderen Rolle, pepsi der Pipeline eingeschlossen, und prüft das gegen PostgreSQL. Eine Stage, die feindliche Mail parst, kann daher weder ein Geheimnis lesen noch einen Zähler zurücksetzen noch eine Einrichtung löschen (was den Schutz abschalten würde). pepsi-keys wrap rotate verpackt die Geheimnisse zusammen mit den privaten Schlüsseln neu.

Versuche. Der Code wird von dem Prozess geprüft, der das Geheimnis entpacken kann; die Entscheidung trifft die SQL-Funktion otp_attempt unter der Datensatzsperre. Ein Code, der zu einem Zeitschritt nach dem zuletzt akzeptierten passt, wird akzeptiert und setzt den Fehlerzähler auf null zurück; derselbe Code noch einmal oder ein älterer ist eine Wiederholung (Replay) und zählt als Fehlversuch, ebenso ein falscher Code. Nach zehn aufeinanderfolgenden Fehlversuchen wird die Einrichtung gesperrt, und jeder Versuch wird ungeprüft abgelehnt. Ein Befehl ganz ohne Code wird abgelehnt, ohne gezählt zu werden. Jede Einrichtung hat eine nie wiederverwendete Seriennummer, sodass ein Versuch, der gegen ein inzwischen ersetztes Geheimnis geprüft wurde, verworfen statt gegen das neue verbucht wird.

Der Operator hebt eine Sperre auf, indem er die Einrichtung entfernt (pepsi-keys otp reset oder DELETE /api/v1/otp/{address}, das eine reset-otp-Aufgabe einreiht, die nur keys:write anfordern darf) oder sie ersetzt (pepsi-keys otp enroll braucht als Operator keinen Code); eine neue Einrichtung beginnt immer bei null Fehlversuchen.

Was er nicht leistet. Er schützt nicht die erste Einrichtung (es gibt noch keinen Code, nach dem gefragt werden könnte), und auch keine Einrichtungsantwort, die der Benutzer im Postfach liegen gelassen hat. Er sichert die synchronen Schalteränderungen der Web-Konsole nicht ab (WKD-Veröffentlichung, eine Anfrage zum Hochladen auf einen Keyserver, der primäre MTA-Schlüssel), die pepsi-httpd selbst ausführt. Und er fügt nichts gegen den Operator hinzu, der ihn zurücksetzen kann.

15.5. Ein Angreifer, der die Datenbank hat

Stützt

Wer eine Kopie der Datenbank hat.

Nehmen Sie einen vollständigen Dump an — eine gestohlene Sicherung, ein Replikat, SQL-Injektion in einen nur lesenden Pfad.

Sie bekommen:

  • jede derzeit in der Warteschlange befindliche Nachricht, im Klartext: pepsi.workqueue hält die headers und pepsi.workqueue_body den Nachrichtentext, unverschlüsselt. Pepsi ist eine Pipeline; eine Nachricht in Bearbeitung muss für die Stage lesbar sein, die sie verarbeiten wird. Zugestellte Nachrichten werden gelöscht, sodass dies der Rückstau ist und nicht die Geschichte.

  • den vollständigen Korrespondenzgraphen: Absender, Empfänger, Zeitstempel, Einstellungen je Adresse, Whitelists, die Tabellen origin_nonce, auto_pay_spend, payment_request_reply und dns_address.

  • jeden öffentlichen Schlüssel und jedes Zertifikat sowie alle Schlüsselmetadaten.

  • die ARC-/DKIM-/DMARC-Urteile und die TLS-Sitzungsergebnisse.

Sie bekommen nicht:

  • irgendeinen privaten Schlüssel, noch irgendein Geheimnis eines zweiten Faktors. Die verpackten Blobs sind AEAD-Chiffrat, und der Schlüsselverschlüsselungsschlüssel ist nicht in der Datenbank.

  • irgendeinen Secure-Link-Nachrichtentext. secure_message.ciphertext ist AES-256-GCM unter einem Schlüssel, den Argon2id aus der PIN des Empfängers, einem Salz je Nachricht und einem serverseitigen Pfeffer ableitet, der in der Konfiguration und nicht in der Datenbank liegt. Die PIN selbst wird in keinerlei Form gespeichert — nicht einmal als Hash. Ein Datenbankdieb muss eine PIN durchprobieren, die er ohne den Pfeffer offline nicht einmal prüfen kann.

  • irgendein brauchbares Credential. Admin-Passwörter sind Argon2id-Hashes; API-Tokens werden als Digests gespeichert, sodass ein Token nicht aus einer Sicherung wiederhergestellt werden kann und genau einmal angezeigt wird.

  • die DKIM-Signierschlüssel, die im Dateisystem liegen, und die SRS- und Pepsi-Origin-Geheimnisse, die in secrets.d liegen.

Warnung

Die Warteschlange ist die Angriffsfläche. Ein Operator, dem die Vertraulichkeit von Mail in Bearbeitung wichtig ist, sollte die Datenbank im Ruhezustand verschlüsseln und die Sicherungen entsprechend behandeln — Pepsi kann die Warteschlange nicht an sich selbst verschlüsseln, denn jede Stage bräuchte den Schlüssel, was dasselbe ist wie keinen zu haben.

15.6. Ein Angreifer, der die Konfigurationsdatei hat

Stützt

Wer die Konfiguration oder ein Geheimnis hat.

/etc/pepsi/pepsi.conf hat absichtlich den Modus 0644: Unprivilegierte Stages lesen sie. Sie darf daher keine Geheimnisse enthalten und tut es nicht — jedes Geheimnis liegt in einem secrets.d/<reader>.secret-Fragment, das über eine @inline-secret@-Direktive eingezogen wird und dem einen Konto gehört, das es braucht.

Lesezugriff auf die Konfigurationsdatei liefert die Topologie: Hostnamen, bediente Domains, den Pipeline-Graphen, Smarthost-Adressen, Dateipfade, welche Funktionen an sind. Das ist Aufklärung, keine Kompromittierung.

Lesezugriff auf ein ``secrets.d``-Fragment liefert genau das, was sein einer Leser hält, und nicht mehr — den SRS-Schlüssel, oder das Smarthost-Passwort, oder den Schlüsselverschlüsselungsschlüssel, oder den Secure-Link-Pfeffer, oder den Abmeldeschlüssel der Listen. Deshalb sind sie aufgeteilt: Es gibt keine einzelne Datei, deren Offenlegung total wäre.

Schreibzugriff auf die Konfigurationsdatei ist gleichbedeutend mit root. Sie benennt die Programme, die jede Stage ausführt. Behandeln Sie sie als root-eigene Datei, denn das ist sie.

Warnung

Zwei Fallen rund um @inline-secret@, die beide wie eine funktionierende Konfiguration aussehen:

  • die Direktive beendet ihren Abschnitt — alles danach Geschriebene landet in gar keinem Abschnitt;

  • ein nicht lesbares Fragment warnt nur. Die Option liest sich dann als nicht gesetzt, und eine Funktion läuft stillschweigend ohne ihr Geheimnis. Prüfen Sie, indem Sie das Fragment als das lesende Konto stat-en, nicht indem Sie das Journal lesen.

15.7. Ein Angreifer, der einen Stage-Worker kompromittiert

Stützt

Eine kompromittierte Komponente, einschließlich seines verbleibenden Signaturrisikos.

Nehmen wir an, ein Parser-Fehler ergibt Codeausführung innerhalb einer Stage — der realistische Fall, denn Stages parsen berufsmäßig feindliche Eingaben.

Eine Stage unter dem Benutzer ``pepsi`` (die meisten) bekommt die Warteschlange: jede Nachricht in Bearbeitung lesen, verändern, umleiten oder löschen und die Whitelist und die Einstellungen lesen. Sie bekommt nicht die verpackten privaten Ende-zu-Ende-Schlüssel und kann nicht zu einem anderen Konto werden.

Sie bekommt allerdings die Möglichkeit, Mail signieren zu lassen, und darüber lohnt es sich, unverblümt zu sein, denn es ist das größte verbleibende Risiko im Entwurf:

  • pepsi-stage-dkim-sign läuft als pepsi und liest die DKIM-Schlüssel der Domains über die Gruppe pepsi, sodass jeder pepsi-Prozess als jede bediente Domain signieren kann — direkt oder indem er einen Datensatz in der Warteschlange bearbeitet, bevor die signierende Stage ihn sieht.

  • pepsi-stage-encrypt signiert als der From:-Autor des Datensatzes. Ingress setzt USERNAME_MAP bei der Einlieferung durch, aber der Datensatz in der Warteschlange ist danach für jede pepsi-Stage schreibbar, und nichts weiter unten kann erkennen, ob Ingress diese Entscheidung getroffen hat. Eine kompromittierte pepsi-Stage kann daher das From: einer eingereihten Einlieferung umschreiben (oder mit workqueue_inject eine Nachricht einschleusen) und sie mit dem Ende-zu-Ende-Schlüssel jedes beliebigen lokalen Benutzers signieren lassen.

Die Integrität einer von Pepsi erzeugten Signatur — DKIM oder benutzerspezifisches OpenPGP/S/MIME — ist also nur so stark wie die gesamte pepsi-Pipeline-Rolle, nicht so stark wie pepsi-crypto. Was die Trennung sehr wohl begrenzt, ist die Offenlegung von Schlüsseln: Ein kompromittierter Spracherkenner kann Nachrichten signieren lassen, solange er läuft, aber er kann keinen privaten Schlüssel mitnehmen, sodass der Schaden endet, wenn die Stage bereinigt ist, und keinen Schlüsselwechsel erfordert. Siehe Bedrohungsmodell dazu, wo dies unter den übrigen Zusicherungen steht.

Eine ``pepsi-crypto``-Stage bekommt alles, was dieses Konto hat: Sie kann eingehende Mail an die Installation entschlüsseln und als jede lokale Identität signieren. Das ist das wertvollste Ziel im System, weshalb die beiden Krypto-Stages klein sind, überhaupt keine Netzwerk-E/A betreiben (die Entdeckung ist ein eigenes Programm) und Gegenstand des Fuzzings in Testsuite sind.

``pepsi-ingress`` sieht jede Nachricht beim Eintreffen und entscheidet, wer sie eingeliefert hat, sodass es Mail als jeder lokale Benutzer zulassen kann. Seine Datenbankrolle kann der Warteschlange etwas hinzufügen und nichts aus ihr lesen, sodass das bereits Eingereihte, die Einstellungen, die Whitelists und die Schlüssel außerhalb seiner Reichweite bleiben.

``pepsi-httpd`` bekommt die Web-Oberfläche und ihre Datenbankberechtigungen — Umschläge und Routing der Warteschlange, niemals Header oder Nachrichtentext einer Nachricht. Es kann bemerkenswerterweise keine Konfiguration schreiben und keine privilegierten Setup-Schritte ausführen: Der Browser-Assistent handelt nicht, er fügt einen Datensatz in pepsi.setup_task ein, der beschreibt, was wahr sein soll, und das separat gestartete, als root laufende pepsi-setup apply entscheidet, ob es das tut. Eine kompromittierte Web-Schicht kann daher eine Umkonfiguration anfordern, aber nicht durchführen — und der Applier prüft jede Aufgabe, statt ihrer Beschreibung zu trauen.

Diese letzte Verteidigung lässt sich in eine absolute verwandeln, und das ist die richtige Haltung auf jeder vom Terminal aus verwalteten Installation. Die beiden systemd-Units des Appliers bilden ein eigenes Debian-Paket, pepsi-httpd-admin (pepsi Recommends es nur, sodass apt remove pepsi-httpd-admin gelingt und den Mailserver weiterlaufen lässt); eine Quellinstallation bekommt denselben Hebel aus make install INSTALL_ADMIN_UNITS=no. Sind sie fort, leert nichts pepsi.setup_task, sodass ein Insert darin inaktiv ist, was auch immer mit pepsi-httpd geschieht — die Schleife wird am Mechanismus gebrochen statt an der Schnittstelle, und kein Codepfad in der Web-Schicht kann einen root-Prozess starten. Die Konsole erkennt das, scheitert geschlossen, wenn sie es nicht feststellen kann, und bedient ihre Setup-Seiten nur lesend. pepsi-setup bleibt im Basispaket, sodass die Verwaltung des Hosts von Hand unberührt bleibt. Siehe Die Aufteilung als Härtungshebel.

15.8. Konkrete Verteidigungen

15.8.1. Die Identität des Einliefernden

Stützt

Ein lokaler Shell-Benutzer und Ein reiner Mail-Benutzer (Grenze B2): Ein Einliefernder sendet nur unter den Identitäten, die der Operator ihm zugeordnet hat.

Ingress entscheidet auf einem von vier Wegen, wer eine Nachricht eingeliefert hat — SASL über das konfigurierte Backend, ein TLS-Client-Zertifikat, eine Adresse in MYNETWORKS oder die SO_PEERCRED-Zugangsdaten des Sockets für lokale Einlieferung, womit sich sendmail ohne Passwort authentifiziert und weshalb dieses Socket für alle schreibbar sein darf. Welcher es auch ist, USERNAME_MAP entscheidet dann, welche From:- und Umschlagadressen diese Identität verwenden darf (RFC 6409 §6/§8.1), und eine Identität, die auf nichts abgebildet ist, darf sich authentifizieren, aber nicht senden. Für eine Adresse zu sprechen — einen Schlüssel für sie zu registrieren oder ihre Schlüssel per E-Mail zu verwalten — erfordert mehr als die Erlaubnis, unter ihr zu senden: Das From: muss genau ein Postfach sein, das von einem konkreten (platzhalterfreien) Eintrag der Zuordnung benannt wird (state.origin.from_bound). Eine Berechtigung *@example.org lässt ein Konto als jeder Kollege senden, aber niemals einen Schlüssel für ihn unterschieben.

Die Entscheidung wird im state der Nachricht festgehalten (local_origin, die authentifizierte Identität) und von jeder späteren Stage geglaubt — genau deshalb behandelt Eine kompromittierte Komponente eine kompromittierte pepsi-Stage als fähig, sie zu fälschen.

15.8.2. Entschlüsselte Mail für die Ablage neu versiegeln

Stützt

Client-Verwahrung (custody = client, der MUA-Schlüssel) (Mail, die das Gateway geöffnet hat, wird so abgelegt, dass nur der eigene Client des Benutzers sie lesen kann) und Die Garantien auf einen Blick (die Spalte Abgelegte Mail).

pepsi-stage-decrypt muss Klartext erzeugen: Die Spam-, Sprach-, Whitelist- und Schlüssellern-Stages danach lesen den Inhalt. pepsi-stage-reencrypt läuft, sobald sie das getan haben, unmittelbar vor der lokalen Zustellung, und versiegelt die Nachricht an den neuesten aktiven custody = client-Schlüssel des Empfängers. Der Mechanismus beruht auf vier Entscheidungen:

  • Er braucht kein Privileg. Das Versiegeln an einen öffentlichen Schlüssel verwendet kein privates Material, daher läuft die Stage als pepsi, ist in das Multi-Call-Binary eingebunden und liest nur die öffentlichen Spalten von crypto_identity unter der gewöhnlichen Pipeline-Berechtigung. Sie nennt niemals private_wrapped, und die Identität pepsi-crypto zu erlangen, gäbe ihr nichts, was sie nutzt.

  • Er signiert nichts. Eine Gateway-Signatur würde nur sagen „das Gateway hat dies versiegelt“, in einem Client nicht von Urheberschaft zu unterscheiden, und bräuchte die setuid-Identität. Das Urteil über die Absendersignatur ist das von Decrypt festgehaltene, in state.crypto.in und in X-Pepsi-Crypto — das auf dem äußeren Block bleibt und das Decrypt von jeder eingehenden Nachricht entfernt, bevor es sein eigenes schreibt, sodass es noch unseres ist, wenn der Client es liest.

  • Er wirkt nur auf das, was Decrypt geöffnet hat (state.crypto.in sagt verschlüsselt und entschlüsselt), und hält state.crypto.store fest, was ihn auch daran hindert, einen Datensatz jemals zweimal zu versiegeln. Mail, die im Klartext eingetroffen ist, wird nicht versiegelt.

  • Eine Ablehnung gibt nichts zurück, was verschlüsselt war. Unter ON_NO_CLIENT_KEY = bounce wird der Header-Block der DSN auf die Adressierungsfelder gekürzt, der Nachrichtentext geleert und RET=HDRS erzwungen, da der Absender die Nachricht verschlüsselt hat und eine DSN im Klartext reist. Es wird ein Bounce erzeugt statt aufgeschoben, weil Aufschieben den Klartext länger in der Warteschlange hielte.

Das verbleibende Risiko ist dasjenige, das Eine kompromittierte Komponente bereits benennt: Eine kompromittierte pepsi-Stage — die Neuverschlüsselungs-Stage eingeschlossen — sieht den Klartext, während er in der Warteschlange liegt, und kann eine Nachricht an der Stage vorbeileiten; die Zusicherung betrifft also bereits abgelegte Kopien, die nichts auf dem Server öffnen kann.

15.8.3. Wofür ein S/MIME-Vertrauensanker bürgt

Stützt

Was ein Sicherheitsindikator bedeutet, gegen Ein entfernter Absender: Eine CA, der der Operator für einen Namensraum vertraut, kann valid nicht für einen anderen erscheinen lassen.

Eine Organisation, die signierte Mail mit einem Partner austauscht, vertraut der CA des Partners oft nur für dessen eigene Domain — indem sie sie mit nameConstraints gegensigniert oder eine Sub-CA des Partners verankert, die diese trägt. Würde die Beschränkung ignoriert, könnte diese Partner-CA ein Zertifikat für einen der Benutzer dieser Installation ausstellen, und die Decrypt-Stage würde die Signatur valid nennen. pepsi-crypto::trust führt daher den Beschränkungszustand aus RFC 5280 §6.1 entlang jeder Kette, die es aufbaut:

  • Namensbeschränkungen gelten für den Subject-DN, jeden alternativen Subject-Namen und jedes emailAddress-Attribut im Subject-DN — Letzteres immer, weil dieses Attribut das ist, woran eine Nachricht gebunden wird, wenn der SAN kein Postfach nennt. Erlaubte Teilbäume werden entlang der Kette geschnitten (zwei CAs, die disjunkte Domains erlauben, lassen kein Postfach zu, nicht jedes); ausgeschlossene Teilbäume sammeln sich an. Eine Sub-CA kann nicht erweitern, was ihr Elternteil delegiert hat.

  • Die eigenen Beschränkungen des Ankers binden ihn (RFC 5937), sodass das direkte Verankern der Sub-CA des Partners ebenso sicher ist wie ihr Gegensignieren.

  • Richtlinienbeschränkungen (requireExplicitPolicy, inhibitPolicyMapping, inhibitAnyPolicy) werden über den vollständigen Richtlinienbaum ausgewertet, in einer deduplizierten Form, deren Größe polynomiell in den Richtlinien ist, die ein Zertifikat nennt, und gedeckelt (64 Richtlinien oder Abbildungen je Zertifikat) — der Baum, der im naiven Algorithmus exponentiell wächst, ist der Denial of Service aus OpenSSL CVE-2023-0464. Die Namensprüfung ist aus demselben Grund auf 220 Vergleiche je Zertifikat gedeckelt.

  • Was nicht eingehalten werden kann, wird abgelehnt. Eine nicht verarbeitbare Beschränkung — eine Teilbaumform, die Pepsi nicht abgleicht, wenn ein Name dieser Form darunter auftaucht; ein minimum oder maximum; eine nicht dekodierbare Erweiterung, kritisch oder nicht — macht die Kette policy-rejected, statt sie ohne die Beschränkung gültig zu lassen. Wo Pepsi von RFC 5280 oder von OpenSSL abweicht, geschieht das immer in diese Richtung: Ein Platzhalter-dNSName, der für einen ausgeschlossenen Host stehen kann, wird abgelehnt, und eine URI ohne Host unter einer URI-Beschränkung wird abgelehnt.

Die NIST-PKITS-Konformitätssuite (Abschnitte 4.6 und 4.8–4.13, jede darin aufgeführte Eingabeeinstellung) läuft in cargo test, zusammen mit einem eigenen Korpus für die Formen, die PKITS nicht abdeckt (IP-Adressen, nicht implementierte Formen, ein namensbeschränkter Anker).

15.8.4. Header-Fälschung

Stützt

Was ein Sicherheitsindikator bedeutet, gegen Ein entfernter Absender.

X-Pepsi-Crypto sagt dem Client eines Benutzers, was Pepsi über die Kryptographie einer Nachricht geschlossen hat, sodass ein gefälschter eine mit unserer Autorität erzählte Lüge ist. Bei eingehender Mail wird der Header entfernt, bevor die Decrypt-Stage ihren eigenen hinzufügen kann, und ausgehende Anforderungs-Header werden durch STRIP_REQUEST_HEADERS entfernt. Ein entfernter Absender kann keinen erscheinen lassen.

Dieselbe Überlegung gilt für Authentication-Results und Pepsi-Origin: Ein Header, der bedeutet „das lokale System hat dies verifiziert“, ist nur dann aussagekräftig, wenn das lokale System das Einzige ist, das ihn schreiben kann.

15.8.5. Unbekannte Empfänger und Backscatter

Stützt

Schutzgüter (Sendeidentität und Reputation), gegen Ein entfernter Absender.

Ein Bounce geht an den Umschlagabsender, den eine Nachricht angibt, und Spam fälscht ihn. Ein MTA, der Mail für Adressen annimmt, die er nicht hat, und sie später bounct, schickt daher im Auftrag eines Spammers Mail an Fremde. Das ist Backscatter, und Blocklisten reagieren schnell darauf. Pepsi schließt das in drei Schichten, jede ein Netz unter der vorherigen:

  • Bei RCPT ablehnen. pepsi-ingress antwortet 550 5.1.1 für eine Adresse an einer bedienten Domain, für die nichts auf dem Host bürgt: ein lokales Konto, ein Alias, eine Liste, eine Dienstadresse oder der MDA bzw. das Backend, gefragt mit RCPT TO. Die Quellen werden aus dem Stage-Graphen abgeleitet (pepsi-recipients). Jede Unsicherheit nimmt an, sodass ein Ausfall nie zu abgelehnter Mail wird.

  • Nur an einen Absender berichten, der sich ausgewiesen hat. pepsi-stage-bounce sendet eine DSN für eine eingehende Nachricht nur, wenn SPF für die Domain des Umschlagabsenders bestanden hat oder eine ausgerichtete DKIM-Signatur (BOUNCE_UNAUTHENTICATED = drop, der Standard). Alles andere wird verworfen und geloggt.

  • Eine Schleife ablehnen. Eine Nachricht, die den Ingress dieses Hosts bereits MAX_SELF_HOPS Mal durchlaufen hat, wird als Routing-Schleife abgelehnt. Eine Adresse an unserer eigenen Domain, die an unseren eigenen MX weitergeleitet wird, kostet daher einen Bounce, nicht eine Runde durch die Pipeline für jeden Hop, den die Relay-Stages erlauben.

RCPT wahrheitsgemäß zu beantworten verrät einem Absender, welche Adressen existieren, wie es jeder MTA tut, der unbekannte Benutzer ablehnt. MAX_INVALID_RECIPIENTS schließt eine Sitzung nach zehn falschen Rateversuchen, sodass das Einsammeln Verbindungen kostet, die die Verbindungslimits bemessen. Proben beim MDA oder Backend werden zwischengespeichert und auf 16 gleichzeitig laufende begrenzt (darüber hinaus lautet die Antwort „unsicher“, was annimmt). Eine Flut von Rateversuchen lässt sich daher auch nicht gegen den Mailspeicher wenden. VRFY antwortet weiterhin auf alles mit 252.

Die Empfängerprüfung braucht eine Berechtigung, die pepsi-ingress bisher nicht hatte: die Namens- und Host-Spalten von mailing_list, die zusammen die öffentliche Adresse einer Liste ergeben und nichts weiter.

15.8.6. Wenn sich der Schlüssel eines Korrespondenten ändert

Stützt

Schlüssel von Korrespondenten, gegen Ein entfernter Absender: Ein geernteter Schlüssel darf durch einen neueren ersetzt werden, aber niemals stillschweigend.

Innerhalb der Autocrypt-Stufe (die Sprossen inbound und gossip) ist die Konfliktregel das Neueste-gewinnt aus Autocrypt Level 1, festgemacht am From:-Header, den ein Absender schreibt, und an einer Nachricht, die die eingehende Authentifizierung fail-open angenommen hat. Eine Nachricht, die nur behauptet, von einem Korrespondenten zu stammen, kann also den Schlüssel ersetzen, an den wir für ihn verschlüsseln. Die Mechanismen, die das begrenzen:

  • Die Ersetzung wird von der Anweisung festgehalten, die sie vornimmt. crypto_peer_key_upsert schreibt in derselben Transaktion wie den Austausch einen key.peer.rotate-Datensatz in pepsi.event_log (das Nur-Anhänge-Audit-Log, auf das keine Mail verarbeitende Rolle UPDATE oder DELETE ausführen kann), mit der Adresse, beiden Mengen von Fingerabdrücken und beiden Stichtagen. Kein Aufrufer kann einen Schlüssel ohne diesen Datensatz rotieren, und eine kompromittierte Stage, die eine Rotation verbergen wollte, müsste die Art Angreifer sein, der Eine kompromittierte Komponente die Warteschlange ohnehin schon zugesteht.

  • Wer gleich antworten will, wird informiert. pepsi-stage-autocrypt-learn schreibt jedem lokalen Umschlagempfänger der rotierenden Nachricht (RESPONSE_STAGE, die Vorlage key-rotation) mit beiden Fingerabdrücken, sodass eine gefälschte Rotation etwas ist, das der betroffene Benutzer bemerken kann, bevor er etwas Vertrauliches sendet. Nur Empfänger bei bedienten Domains werden angeschrieben, sodass der Hinweis nicht zu Backscatter werden kann, und höchstens einer je Empfänger und Nachricht.

  • Ein Operator kann die Rotation über einen lebenden Schlüssel verweigern. ACCEPT_ROTATION = expired nimmt den neueren Schlüssel nur an, wenn jeder gespeicherte als widerrufen oder abgelaufen verzeichnet ist oder das Ablaufdatum überschritten hat, das sein eigenes Material angibt (gespeichert, als der Schlüssel gelernt wurde, und bei OpenPGP immer nur nach hinten verschoben, sodass eine wiedereingespielte ältere Kopie des Zertifikats es nicht zurückdatieren kann); andernfalls wird das Angebot zurückgehalten, als key.peer.rotate.held protokolliert und den Empfängern gemeldet. Das tauscht eine gefälschte Rotation gegen eine echte, die abgelehnt wird, bis ein Operator eingreift.

  • Die Stufe bleibt eingegrenzt. Nichts davon erweitert, was ersetzt werden darf: Nichts, was eine Domain, ein Verzeichnis, ein Keyserver oder ein Operator veröffentlicht hat, und kein angehefteter Datensatz kann von einer Nachricht angetastet werden, und einer Adresse, für die wir eine Identität halten, greift unser eigener Schlüssel ohnehin vor. Einer bedienten Adresse, für die wir keine Identität halten, wird nicht vorgegriffen, und pepsi-keys peer unclaimed listet jede solche Adresse mit einem geernteten oder gegossipten Schlüssel auf (Die Benutzer finden, die ihren eigenen Schlüssel mitbringen).

  • Beförderung ist keine Rotation. Wenn der Eigentümer eines gegossipten Schlüssels denselben Schlüssel selbst ankündigt, verschiebt crypto_peer_key_upsert den Datensatz von der Sprosse gossip auf die Sprosse inbound und schreibt einen key.peer.promote-Datensatz (niemals key.peer.rotate und kein Hinweis, da sich der Schlüssel nicht geändert hat). Das wird unter derselben Sperre je Adresse entschieden, anhand der Namen der Quellen, und führt zum selben Urteil TrustSource::Autocrypt / Untrusted wie jeder geerntete Schlüssel (Ein gegossipter Schlüssel, den sein Eigentümer bestätigt, wird befördert).

pepsi-status listet die letzten 30 Tage beider Arten von Datensätzen auf.

15.8.7. SSRF bei der Schlüsselentdeckung

Stützt

Ein entfernter Absender und Schlüssel von Korrespondenten: Eine Adresse, an die jemand schreibt, kann den Server nicht auf dessen eigenes Netz richten.

Die Schlüsselentdeckung ruft URLs ab, die aus einer E-Mail-Adresse abgeleitet sind — WKD und HTTP-Keyserver —, was konstruktionsbedingt ein Server-Side-Request-Forgery-Primitiv ist. pepsi-keydisc löst den Hostnamen selbst auf und lehnt jede Kandidatenadresse in Loopback-, RFC-1918-, RFC-6598-CGNAT-, Link-Local- oder Unique-Local-Raum ab und wendet die Prüfung bei jeder Umleitung erneut an (die Umleitung ist das Loch, das ein Entwurf „Adresse prüfen, dann verbinden“ offen lässt). Die Prüfung wird nur durch eine ausdrückliche Testnaht deaktiviert, weil ein Stub-HTTPS-Server notwendigerweise auf dem Loopback lauscht.

15.8.9. Die administrative API

Stützt

Ein delegierter Administrator und die Grenzen B6/B7.

Drei Authentifizierungsmechanismen, ein Identitätsmodell: SO_PEERCRED auf einem UNIX-Socket (unfälschbar, und so funktioniert der Bootstrap beim ersten Lauf, wenn es noch keine Konten gibt), Bearer-Tokens für Automatisierung (als Digest gespeichert, zeitkonstant verglichen) und Passwort plus Sitzungs-Cookie für Browser (Argon2id, HttpOnly und SameSite=Strict, CSRF-Token bei jeder verändernden Anfrage). Administrative Routen werden nur auf Listenern mit dem Kennzeichen ADMIN = yes eingehängt, sodass eine Relay-Installation, die nie einen solchen Listener aktiviert, die Oberfläche überhaupt nicht offenlegt.

Antworten enthalten nie Geheimnisse: GET /api/v1/config maskiert Optionen, die Zugangsdaten tragen, statt sie wegzulassen, sodass ein Operator sehen kann, dass ein Passwort gesetzt ist, ohne dass der Endpunkt zu einem Weg wird, eines zurückzulesen.

15.8.10. Feature-Telemetrie über die Konsole einschalten

Stützt

Schutzgüter (Privatsphäre gegenüber Dritten: nichts, dem der Operator nicht zugestimmt hat) und Eine kompromittierte Komponente (pepsi-httpd).

[pepsi] SHARE_TELEMETRY steht in der Konfigurationsdatei, daher ändert die Konsole es nur so, wie sie dort alles ändert: durch eine write-config-Anfrage, die der Applier von root ausführt. Was es sicher macht, dies anzubieten, ist eine Vorbedingung, die der Operator von Hand herstellt und die die Konsole nur beobachten kann: pepsi-telemetry-client muss laufen (ruhend, solange Telemetrie ausgeschaltet ist), und [pepsi] SYSTEM_ID muss vorhanden sein. Der Daemon meldet beides in der einzeiligen Tabelle pepsi.telemetry_client, die pepsi-httpd lesen, aber nicht schreiben darf — sodass eine Konsole „bereit“ nicht fälschen kann —, und der Datensatz trägt für die Kennung einen Wahrheitswert, niemals ihren Wert. Ohne das ist die Frage mit Hinweisen deaktiviert, und eine API-Anfrage zum Einschalten wird abgelehnt (409 telemetry_client_not_ready); das Ausschalten wird niemals abgelehnt.

Diese Sperre ist eine Aussage über Nützlichkeit, nicht die Grenze. Die Grenze ist der Daemon: Er glaubt nur der Datei (über den einzigen Leser des Schalters) und liest sie bei der Benachrichtigung telemetry_changed, bei SIGHUP und jede Minute neu ein; eine Benachrichtigung kann ihn nicht einschalten. Ein kompromittiertes pepsi-httpd, das CONFIG_DB hält, konnte bereits jede Konfiguration anfordern, diesen Schalter eingeschlossen, und daran ändert sich nichts; was es nicht kann, ist eine Installation, deren Operator den Daemon nie gestartet hat, irgendetwas übermitteln zu lassen. Nichts auf diesem Weg erzeugt eine Kennung, die die Installation nicht bereits hatte: Der Applier übernimmt stattdessen eine vorhandene SYSTEM_ID über ein Speichern in der Konsole hinweg, und pepsi-setup erzeugt eine nur bei einem Ja. Das Ausschalten verwirft, was der Daemon gesammelt und noch nicht gesendet hatte.

15.8.11. Ressourcengrenzen

Stützt

Verfügbarkeit: Ein Fremder kann den Server durch eine Nachricht, eine Verbindung oder eine Adresse nicht unbegrenzt belasten.

Die Grenzen selbst sind in Verfügbarkeit tabelliert; worauf es hier ankommt, ist, wo jede durchgesetzt wird und warum sie sich nicht umgehen lässt.

  • Verbindungen werden je Quelle begrenzt, nicht nur insgesamt. pepsi-ingress zählt offene Verbindungen je Quellbereich (MAX_CONNECTIONS_PER_IP) und, für die UNIX-Socket-Gegenstellen, die von seinem Ratenlimit ausgenommen sind und nie verdrängt werden, für alle zusammen (MAX_LOCAL_CONNECTIONS); pepsi-httpd tut dasselbe je Client-Adresse. Die Prüfung erfolgt, bevor eine Verbindung einen Platz der globalen Obergrenze belegt, sodass eine Quelle, die ihren Anteil erreicht hat, sich nicht zusätzlich um weitere anstellen kann.

  • Raten und Fluten werden über Sitzungen hinweg gezählt. Ein Token-Bucket je Bereich (AUTH_FAILURES_PER_HOUR) wird von jedem fehlgeschlagenen AUTH verbraucht, und ist er leer, wird das nächste AUTH abgelehnt, bevor das SASL-Backend gefragt wird; ein Bucket je uid, festgemacht an der vom Kernel gemeldeten SO_PEERCRED-uid (LOCAL_MESSAGES_PER_MINUTE), bemisst die lokale Einlieferung, und eine Sitzung darf nur MAX_MESSAGES_PER_SESSION Transaktionen beginnen, bevor sie sich neu verbinden und damit wieder dem Ratenlimit stellen muss.

  • Der Schub eines Absenders verzögert nicht alle anderen. pepsi-dispatch übernimmt die Arbeit jeder Stage nach geringster virtueller Laufzeit je Absender statt nach ältestem zuerst (FAIR_SCHEDULING, standardmäßig eingeschaltet). Der Absender ist die generierte Spalte workqueue.sender_key: bei einer Einlieferung das authentifizierte Konto, andernfalls die verbindende Adresse — eine IPv6-Gegenstelle anhand ihres /64, da einem einzelnen Host routinemäßig eines zugeteilt wird — und niemals die Umschlag-Domain von Mail von außen, die der Absender je Nachricht wählt. Worker-Zeit wird dem Absender angerechnet, der sie verursacht hat, ein Neuankömmling startet gleichauf mit dem am wenigsten bedienten Absender, sodass Stille kein Guthaben anspart, und ein reservierter Anteil nach ältestem zuerst (FAIR_FIFO_PERCENT) begrenzt die Wartezeit jeder einzelnen Nachricht. Die Buchführung liegt nur im Speicher des Dispatchers; sie gewährt nichts und ändert keine Berechtigung.

  • Die eingehende Verifizierung hat eine Arbeitsgrenze, die kein Weg um DMARC herum ist. Höchstens MAX_DKIM_SIGNATURES Signaturen werden verifiziert, diejenigen zuerst, die mit der From:-Domain übereinstimmen können, und SPF und jede Signatur laufen nebenläufig unter VERIFY_TIMEOUT, DMARC unter einer zweiten Frist. Eine Prüfung, der die Zeit ausgeht, wird temperror für diese Prüfung: DMARC berücksichtigt einen temporären Fehler nur bei einem Identifier, der mit der From:-Domain übereinstimmt, und das DNS, das DMARC selbst liest, ist das dieser Domain, sodass ein Fälscher, der sein eigenes DNS verlangsamt, nichts gewinnt als sein eigenes temperror.

  • Schlüsselmaterial von Fremden hat eine Größenobergrenze. Jedes OpenPGP-Zertifikat, das von außen eintrifft — WKD, ein Keyserver, DANE, ein geernteter oder gegossipter Header, ein angehängter Schlüssel, ein eingefügter —, durchläuft einen Parser, der einen RSA-Schlüssel über 8192 Bit ablehnt, und der Verifizierer entfernt ein solches Zertifikat auch aus seinem Vertrauensspeicher. S/MIME hat dieselbe Grenze bei 4096 Bit, aus dem Crate rsa.

  • Die Aufbewahrung hängt nicht von der Web-Schicht ab. Die Audit- und Mail-Logs werden von pepsi-httpd prune aus pepsi-log-prune.timer bereinigt — als Konto pepsi-httpd, die eine Dienstrolle, die DELETE auf ihnen hält, was die Logs für alles, was Mail verarbeitet, nur anhängend hält —, und TLS-Report-Zähler und abgelaufene Datensätze des MX-Caches durch eigene Timer, alle von pepsi.target benannt.

  • Die Warteschlange hat eine Obergrenze, und die Prüfung lässt sich weder aushungern noch durch Aufteilen umgehen. pepsi-ingress lehnt mit 452 4.3.1 bei RCPT, DATA/BDAT und nach dem Ende der Daten ab, solange die Warteschlange bei [pepsi] MAX_QUEUE_ROWS/MAX_QUEUE_BYTES steht oder das Dateisystem der Datenbank unter MIN_FREE_SPACE liegt. Die Messung (pepsi.queue_usage()) liest nur zwei generierte Größenspalten, sodass die Ingress-Rolle mit den geringsten Rechten dafür keinen Zugriff auf eingereihte Mail erhält, und sie wird je Prozess zwischengespeichert (QUEUE_CHECK_INTERVAL), sodass eine Flut von RCPT-Befehlen keine Abfragen kostet. Bytes werden je gespeichertem Nachrichtentext gezählt: Nachrichtentexte liegen in pepsi.workqueue_body und werden von jedem Datensatz geteilt, den eine Aufteilung, ein Klon oder eine Listen-Auffächerung erzeugt, sodass eine große Nachricht an viele Empfänger ihren Nachrichtentext nicht mehr über die Warteschlange vervielfachen kann — die Verstärkung, die die Aufteilung je Empfänger früher bot. Der eine Verstärker, der noch unter der Kontrolle eines Absenders bleibt, ein Beitrag an eine große Liste, hält seine Auffächerung zurück (pepsi-stage-list-post, QUEUE_THROTTLE_DELAY), solange die Warteschlange voll ist. Das Löschen des letzten Datensatzes, der auf einen Nachrichtentext verweist, löscht diesen (ein Trigger), und der Fremdschlüssel bedeutet, dass keine Rolle — das DELETE der Web-Schicht auf workqueue_body eingeschlossen — einen Nachrichtentext löschen kann, den eine eingereihte Nachricht noch trägt.

  • Mailinglisten begrenzen, wozu ein Absender sie bringen kann. Die angenommenen Beiträge eines Absenders an eine Liste werden je Stunde gezählt (SENDER_POSTS_PER_HOUR), in einer SQL-Anweisung, die entscheidet und festhält, sodass parallele Worker nicht beide den letzten Platz nehmen können; der Überschuss wird zurückgehalten, statt mit der Mitgliederzahl vervielfacht zu werden. Die Moderationswarteschlange ist je Liste gedeckelt (MAX_HELD_MESSAGES), indem der neue Beitrag verworfen wird, sodass eine Flut die Beiträge, die ein Moderator noch nicht gesehen hat, nicht hinausspülen kann.

  • Automatische Ausgaben haben eine Gesamtsumme, und sie lässt sich nicht durch Wettläufe aushebeln. pepsi-stage-auto-pay bucht eine Zahlungsforderung gegen zwei Budgets in dem einen Aufruf von auto_pay_try_spend, der sie auch festhält: das der Nachricht (MAX_TOTAL, im Datensatz origin_nonce, gesperrt mit FOR UPDATE) und die gleitenden 24 Stunden des zahlenden Wallets (MAX_TOTAL_PER_DAY, das Buch auto_pay_spend, unter einer transaktionsgebundenen Advisory-Sperre, die am Wallet festgemacht ist — Forderungen für verschiedene Nachrichten teilen keinen Datensatz, sodass ohne sie jede dieselbe Summe läse). Eine Ablehnung durch eines der beiden schreibt nichts in das andere; eine Zahlung, die sich nicht abwickeln lässt, wird beiden durch auto_pay_refund gutgeschrieben, das nur die erste Sperre nimmt, sodass sich die beiden nicht verklemmen können. Der Wallet-Schlüssel ist derjenige, aus dem der Helper zahlt (uid:<n> oder das gemeinsame Konto und die Wallet-Datenbank), sodass zwei Absender sich keinen Tag teilen können, es sei denn, sie teilen sich ein Wallet.

  • Eine Zahlungsaufforderung antwortet einem Absender einmal je Zeitfenster. Die Aufforderung ist eine automatische Antwort an einen unverifizierten Umschlagabsender, daher beansprucht pepsi-stage-anti-spam sie neben der Unterdrückung nach RFC 3834 über payment_request_should_send — INSERT … ON CONFLICT DO UPDATE … WHERE, das in einer Anweisung entscheidet und festhält, wie es vacation_should_reply tut —, festgemacht am geschützten Postfach und am Absender, mit BLOCK_RESPONSE_SUPPRESS als Zeitfenster. Schlägt das Beanspruchen fehl, unterbleibt die Antwort; die Nachricht wird so oder so zurückgehalten.

15.9. Wogegen Pepsi sich nicht verteidigt

Die Liste wird an einer Stelle geführt, Was Pepsi nicht schützt, zusammen mit den verbleibenden Risiken, die jeder Abschnitt über Angreifer benennt: root und ein böswilliger Operator, Verkehrsanalyse und Metadaten, Klartext in der Warteschlange, Signaturen gegenüber einer kompromittierten Pipeline, die eigene Kompromittierung von Korrespondenten und ein Netzwerkangreifer, der zusätzlich im DNS lügen kann.

15.10. Eine Schwachstelle melden

Bitte melden Sie Sicherheitsprobleme vertraulich per E-Mail an Christian Grothoff unter grothoff@gnunet.org, verschlüsselt mit dem unter https://grothoff.org/christian/grothoff.asc veröffentlichten OpenPGP-Schlüssel mit dem Fingerabdruck D842 3BCB 326C 7907 0339 29C7 939E 6BE1 E29F C3CC. Verwenden Sie für eine Schwachstelle nicht den öffentlichen Bugtracker, und lassen Sie vor der Offenlegung Zeit für eine Behebung. SECURITY.md im Quellbaum beschreibt die vollständige Richtlinie.

Gewöhnliche Fehler gehören in den öffentlichen Bugtracker unter https://bugs.gnunet.org/view_all_bug_page.php?project_id=35.

15.11. Siehe auch

Schlüsselverwaltung — Identitäten, Verwahrung, Entdeckung und die Vertrauensleiter. Das Secure-Link-Ausweichportal — das Portal in voller Ausführlichkeit. Die administrative API — die API-Oberfläche und ihre Geltungsbereiche. Interoperabilität mit Clients — was andere Clients tatsächlich lesen können, was wiederum begrenzt, welcher Schutz in der Praxis erreichbar ist. Testsuite — das Fuzzing und die adversarialen Korpora hinter den Parser-Aussagen.