Dokumentation

Aegis selbst betreiben

Alles, was hinter dem Messenger steckt – Architektur, Sicherheitsmodell und eine Schritt-für-Schritt-Anleitung, um Relay, öffentlichen Edge und optional den Controller auf einem eigenen Server aufzusetzen. Diese Seiten beschreiben den real laufenden Aufbau, nicht ein Ideal.

1 · Architektur-Überblick

Aegis besteht aus zwei bewusst getrennten Ebenen, die sich nur das App-Gehäuse teilen – nicht die Vertrauensgrenze.

Zwei Ebenen, zwei Vertrauensgrenzen

Chat-EbeneControl-Ebene
ZweckMessenger für Freunde und FamilieÜberwachung und Steuerung der eigenen Infrastruktur
Erreichbarkeitöffentlich über TLS (Port 443)nur über WireGuard-VPN, gebunden an 10.10.2.1
Vertrauen in den Serverkeines nötig – Zero-Knowledge, nur Chiffretextvoll – der Controller sieht die eigenen Metriken im Klartext
ZugangEinladung, ohne VPN nutzbarWireGuard-Peer + Biometrie + Berechtigung

Datenfluss

Öffentlicher Pfad – vom Handy bis in die Datenbank kommt nur Chiffretext an:

Android-Applibsignal · SQLCipher · OkHttp mit SPKI-Pinning
Caddy (Edge)Port 80/443, automatisches Let's Encrypt, terminiert TLS
RelayFastAPI, intern relay:8080, nicht öffentlich publiziert
PostgreSQLnur im privaten Compose-Netz, kein Port nach außen
Push-Dispatchinhaltsloser Wake-up an ntfy / UnifiedPush, nie Inhalt
App holt Mailboxentschlüsselt Schicht 1, zeigt „Neue Nachricht von X“

VPN-Pfad – unverändert abgeschottet:

App (Dashboard-Tab)WireGuard-Tunnel + Biometrie
Controller10.10.2.1:8000 · FastAPI + mobile Web-UI
VictoriaMetrics10.10.2.1:8428 · gespeist von node_exporter und cAdvisor
Agents auf den ZielservernWhitelist-Aktionen, Token-Auth, kein Root-SSH

Die WebSocket-Verbindung der App läuft über denselben Edge: wss://<host>/v1/ws. Caddy erkennt das Upgrade selbst und tunnelt es an den Relay weiter; der Relay läuft mit --proxy-headers und sieht darum weiterhin X-Forwarded-Proto: https.

Komponenten

KomponenteTechnikErreichbarkeit
Android-AppKotlin, klassische Views/XML, Material3 DayNight, libsignal, SQLCipher, OkHttp, WireGuard-GoBackend, UnifiedPushClient
RelayPython 3.13, FastAPI, Starlette-WebSockets, uvicorn, asyncpg, Pydantic v2intern relay:8080 im Docker-Netz aegis-edge
DatenbankPostgreSQL 16nur privates Compose-Netz
EdgeCaddy 2, automatisches Let's Encryptöffentlich 80/443
ControllerPython 3.13, FastAPI, Casbin, mobile Web-UI10.10.2.1:8000 – VPN-only
Metrik-SpeicherVictoriaMetrics, Aufbewahrung 12 Monate10.10.2.1:8428 – VPN-only
Metrik-Erfassungnode_exporter (Host), cAdvisor (Container)kein veröffentlichter Port, nur internes Netz
AgentPython/FastAPI-Daemon je Zielserver, Docker-SDK statt Shellnur vom Controller aus, Token-Auth

Repo-Aufbau

VerzeichnisInhalt
messenger-server/Relay (FastAPI) + Compose + Tests inkl. Echo-Testkontakt
messenger-edge/Caddy-Edge: Caddyfile, Compose, statische Seiten unter srv/
app-android/native App (Chat-Tab + Dashboard-Tab)
controller/Controller-API, Modul-Registry, Rechte, Health-Engine
agent/Least-Privilege-Daemon für die Zielserver
app/Web-UI des Dashboards (im WebView der App)
deploy/monitoring/VictoriaMetrics + node_exporter + cAdvisor
docs/Architektur, Messenger-Architektur, Infrastruktur, Konventionen, Roadmap

Protokoll in Kurzform

REST über HTTPS für Setup und Abholung, eine dauerhafte WebSocket-Verbindung für den Live-Kanal. Bis auf Registrierung und Restore verlangt jeder Aufruf Identity-Key-Auth: Der Client signiert eine Server-Nonce mit dem privaten Geräteschlüssel und schickt das Ergebnis im Header X-Aegis-Auth. Es gibt keine Passwörter.

EndpunktZweck
POST /v1/registerKonto anlegen – nur mit gültigem, einmaligem Invite-Token
POST /v1/invitesInvite-Token erzeugen (Admin-Token oder bestehender Nutzer)
POST /v1/auth/challengeNonce für die Challenge-Response holen
POST /v1/keys · GET /v1/keys/{name}eigene Prekeys nachfüllen · Bundles aller Geräte eines Kontakts holen
POST /v1/messages · GET · /ackEnvelope einliefern · Mailbox abholen · bestätigen (löscht serverseitig)
GET /v1/devices, /devices/provision*Geräteliste und QR-Provisioning für weitere Geräte
POST /v1/push/registerPush-Endpunkt für den inhaltslosen Wake-up melden
PUT /v1/backup, GET /v1/backup/…opaken, passphrase-verschlüsselten Identitäts-Blob ablegen/holen
GET /healthLebenszeichen inkl. Datenbank-Prüfung (unauthentifiziert)
WSS /v1/wsLive-Kanal: challenge · auth · deliver · ack · send · ping

Was der Server tatsächlich speichert, ist ein Envelope ohne jeden lesbaren Inhalt:

{ "to": "bob", "toDeviceId": 1,
  "type": "PREKEY | CIPHERTEXT | UNIDENTIFIED",
  "ciphertext": "<base64 libsignal-CiphertextMessage, gepolstert>" }

envelopeId und serverTs vergibt der Server. Im Sealed-Sender-Modus gibt es kein Absenderfeld – der Absender steckt authentifiziert im Chiffretext.

2 · Sicherheitsmodell

Kern-Annahme: Der Server ist nicht vertrauenswürdig. Betrachtet werden ein passiver Netz-Beobachter, ein aktiver MITM, ein vollständig übernommener Relay (Root, Live-Mitschnitt, Datenbank-Dump), Geräteverlust und der Versuch fremder Clients oder Server, unsere Nutzer zu erreichen.

Garantie: Wer Root auf dem Relay hat und den kompletten Datenbestand abzieht, erhält keinen Nachrichtenklartext und keine privaten Schlüssel – nur opake Blobs und grobe Routing-Metadaten.

Was der Server sieht – und was nicht

DatumServer siehtServer sieht nicht
Nachrichtentextopaken CiphertextKlartext; Länge nur grob, weil auf Bucket-Größen gepolstert wird
Öffentliche Schlüssel, Prekey-Bundlesja – das ist ihr Zweckdie privaten Gegenstücke
Kontenwelcher Benutzername existiertKlarname, Telefonnummer, E-Mail (gibt es nicht)
Zustell-MetadatenEmpfänger-ID, Ziel-Gerät, Zeitstempelden Absender im Sealed-Sender-Modus
Pushden Push-EndpunktInhalt – der Push ist leer
Identitäts-Backupopaken Blob + Salt/KDF-ParameterPassphrase und Inhalt
Medien geplantnur den AES-GCM-Ciphertext-Blob und seine GrößeInhalt, Dateiname, Typ – alles steckt im E2EE-Umschlag
Anrufe geplantüber coturn nur SRTP-ChiffretextAudio/Video, SRTP-Schlüssel, das komplette Signaling

Schlüsselverwaltung

  • Account-Identity-Key – die stabile Identität des Kontos. Aus ihr und der Identität des Gegenübers wird die Sicherheitsnummer berechnet.
  • Geräteschlüssel – jedes Gerät hat ein eigenes Schlüsselpaar. Das zugehörige Gerätezertifikat ist eine Signatur des Account-Schlüssels über den Geräteschlüssel; der Relay prüft sie bei Registrierung und Geräte-Verknüpfung.
  • Signed-Prekey, One-Time-Prekeys, Kyber-Prekeys – ermöglichen den Sessionaufbau, während der Empfänger offline ist. Ein Kyber-Prekey ist als last resort markiert und wird nie verbraucht, damit ein Gerät nie unerreichbar wird.
  • Content-Key – pro Gerät ein Curve25519-Schlüsselpaar für die Inhalts-Schicht. Der öffentliche Teil liegt im Prekey-Bundle, der private ausschließlich im Android-Keystore mit erzwungener Nutzer-Authentifizierung (StrongBox/TEE, nicht exportierbar).
  • Alle privaten Schlüssel und der Session-Zustand liegen ausschließlich in der SQLCipher-Datenbank des Geräts – nie im Klartext auf der Platte, nie auf dem Server.

Signaturen sind Curve25519/XEdDSA, wie libsignal sie erzeugt. Der Relay verifiziert sie, ohne je privates Material zu sehen – die Zero-Knowledge-Eigenschaft bleibt unberührt.

Zwei Schichten – warum die Benachrichtigung funktioniert

Im Hintergrund lesbar (nicht biometrisch)Erst nach Entsperrung (biometrisch)
Absender-IdentitätNachrichtentext
Zeitstempel, Nachrichtentyp, Zuordnung zur UnterhaltungMedien-Inhalte – ihr AES-Schlüssel liegt in Schicht 2
Zustell- und Lesestatus, Tipp-Anzeige, Anruf-SignaleZitate, Vorschauen, Vorschaubilder
→ genug für „Neue Nachricht von X“→ bewusst keine Inhaltsvorschau im gesperrten Zustand

Trade-off, ehrlich benannt: Der Content-Key lebt länger als die Ratchet-Schlüssel. Abgefedert wird das durch einen eigenen, ephemeren Schlüssel pro Nachricht und durch Rotation des Content-Keys. Die Forward Secrecy des Transports bleibt davon unberührt – Schicht 1 ratcheted weiter.

Warum kein Fremder hereinkommt

  1. Standardmäßig keine Föderation – im Auslieferungszustand existiert keine Server-zu-Server-Schnittstelle; ein fremder Server hat nichts, womit er sprechen könnte. Föderation ist opt-in und wird erst durch den Betreiber im Dashboard zugeschaltet, dann gegenseitig gepinnt und fail-closed (siehe Föderation).
  2. Invite-gated – ohne gültigen, einmaligen Token wird kein Konto angelegt.
  3. Identity-Key-Auth – jeder authentifizierte Aufruf verlangt eine Signatur mit einem Geräteschlüssel, den der Server bereits kennt. Unbekannte Schlüssel werden abgewiesen.
  4. Rate-Limits und Lockout auf Registrierung, Prekey-Abruf, Versand und Backup-Download.

Ehrliche Grenzen

  • Ein einzelner, authentifizierter Relay kann Metadaten nicht vollständig eliminieren. Solange ein Gerät seine Verbindung mit dem eigenen Konto authentifiziert, ist auf Verbindungsebene ein zeitliches Muster beobachtbar. Sealed Sender entkoppelt den Envelope-Inhalt, nicht die physische Verbindung.
  • Ein in der App mitgeliefertes Client-Geheimnis ist grundsätzlich extrahierbar. Die echte Schließung sind der geschlossene Default, Invite-Zwang und Schlüssel-Auth – nicht das App-Geheimnis. Auch bei zugeschalteter Föderation bleibt der Inhalt Ende-zu-Ende verschlüsselt (Zero-Knowledge), und Peer-Relays werden gegenseitig gepinnt.
  • Ein kompromittiertes Endgerät mit laufender Schadsoftware ist außerhalb des Modells: Dort liegt der Klartext ohnehin offen.
  • Langfristige Traffic-Analyse auf Timing-Ebene wird minimiert, aber nicht ausgeschlossen.

Backup-Hinweise

Für Nutzer

Das Identitäts-Backup ist client-seitig mit einer Passphrase verschlüsselt (Argon2id als Schlüsselableitung, AES-256-GCM); der Server hält nur den opaken Blob samt Salt und KDF-Parametern. Ohne Passphrase gibt es keine Wiederherstellung – niemand kann sie zurücksetzen. Bis der Restore-Fluss in der App verdrahtet ist, gilt: Geht das Gerät verloren, sind Identität und Verlauf weg, und Kontakte müssen die neue Sicherheitsnummer bestätigen.

Für Betreiber

  • PostgreSQL – Konten, öffentliche Schlüssel, Prekeys, wartende Envelopes: docker compose exec -T postgres pg_dump -U aegis aegis_relay | gzip > relay-$(date +%F).sql.gz
  • Volumes – die opaken Backup-Blobs (backups) und der ACME-Zustand von Caddy (caddy_data: Konto und Zertifikate). Letzterer spart bei einem Umzug neue Ausstellungen.
  • .env-Dateien gehören nie ins Repo, aber unbedingt in den Passwort-Manager – vor allem INVITE_SIGNING_SECRET, sonst werden ausgegebene Invites ungültig.
  • Release-Keystore der App offline sichern: Ohne ihn gibt es für installierte Apps keinen Update-Pfad mehr, und der Fingerabdruck in assetlinks.json stimmt nicht mehr.
  • Ein Server-Backup enthält keinen Nachrichtenklartext. Es rettet Konten und Zustellbarkeit, nicht den Gesprächsverlauf – der liegt ausschließlich auf den Geräten.

3 · Selbst hosten

Der komplette Chat-Stack besteht aus zwei Docker-Compose-Stacks, die sich ein Netz teilen. Der Controller-Teil ist optional und völlig unabhängig davon.

Föderation (opt-in, standardmäßig aus)

Ein Relay ist standardmäßig eine in sich geschlossene Welt. Ohne Betreiber-Aktion föderiert es nicht: Ein Konto lebt auf genau einem Relay, seine Sicherheitsidentität hängt daran, und es erreicht zunächst nur Kontakte auf demselben Relay – exakt wie bisher.

Neu: Föderation lässt sich pro Relay zuschalten. Wer selbst hostet, kann sie im Dashboard aktivieren (Whitelist bekannter Peer-Relays oder – für ein offenes Relay – Blacklist). Der Handshake pinnt beide Relay-Identitäten gegenseitig; scheitert die Prüfung, wird nicht zugestellt (fail-closed). Wichtig: föderierte Relays replizieren nichts. Eine Nachricht von alice@relayA an bob@relayB läuft ausschließlich über genau diese zwei Server (E-Mail-artige Zwei-Relay-Kette), nie über einen dritten; der Inhalt bleibt Ende-zu-Ende verschlüsselt, ein föderierendes Relay bleibt Zero-Knowledge.

Der öffentliche Relay chat.personaplay.de ist in der App die vorbelegte Standardeinstellung und bleibt vorerst geschlossen (offene Föderation folgt erst mit den Missbrauchskontrollen des Blacklist-Modus). Man muss das Relay-Feld beim Erststart aktiv überschreiben, um einen eigenen Relay zu nutzen. Detailmodell und Invarianten: docs/FEDERATION.md im Repo.

Voraussetzungen

  • Server: ein kleiner Linux-Server mit Docker und Compose-Plugin. Debian 13 (trixie) ist die Referenz. Für einen Freundeskreis genügen 1–2 vCPU und 2 GB RAM; Speicherbedarf entsteht praktisch nur durch wartende Envelopes und Blobs.
  • Domain: ein A-Record auf die öffentliche IP des Servers (DNS-only, kein Proxy davor – die ACME-Validierung muss den Server direkt erreichen).
  • Ports: eingehend 443/tcp zwingend, 80/tcp für den HTTP-Redirect und die HTTP-01-Challenge. Bei Anbietern mit vorgelagerter Cloud-Firewall (z. B. IONOS) müssen die Ports zweimal geöffnet werden: im Anbieter-Panel und lokal in ufw.
  • Android-Seite: zum eigenen Ausliefern der App JDK 17, Android SDK 34 und Gradle 8.7 sowie ein eigener Release-Keystore.

1 Relay aufsetzen

Beide Stacks hängen an einem gemeinsamen, externen Docker-Netz. Das wird einmalig angelegt, bevor irgendetwas startet:

docker network create aegis-edge

Danach das Repo klonen und den Relay-Teil nach dem Projekt-Muster „ein Verzeichnis pro Dienst“ ablegen, also z. B. nach /opt/aegis/messenger. Die Geheimnisse kommen in eine .env daneben – niemals ins Repo:

DB_PASSWORD=<openssl rand -base64 36>
ADMIN_TOKEN=<openssl rand -base64 36>
INVITE_SIGNING_SECRET=<openssl rand -base64 36>

Was die docker-compose.yml des Relays macht:

  • Dienst relay wird aus ./relay gebaut, lauscht intern auf 0.0.0.0:8080 ohne TLS (RELAY_TLS=off) und hat keinen ports:-Eintrag – er ist damit nie direkt aus dem Internet erreichbar, sondern nur über expose: 8080 im Netz aegis-edge.
  • Dienst postgres (PostgreSQL 16) hängt nur im privaten default-Netz, hat ebenfalls keinen veröffentlichten Port und einen pg_isready-Healthcheck, auf den der Relay wartet.
  • Volumes: pgdata für die Datenbank, backups für die opaken Identitäts-Backup-Blobs.
  • Alles mit restart: unless-stopped, damit der Stack einen Reboot übersteht.
cd /opt/aegis/messenger
docker compose up -d --build
docker compose logs -f relay

2 Edge mit TLS aufsetzen

Der Edge liegt unter /opt/aegis/edge und besteht aus einem einzigen Caddy-Container, der am selben aegis-edge-Netz hängt. Er veröffentlicht 80 und 443, mountet das Caddyfile und das statische Verzeichnis srv/ read-only und hält den ACME-Zustand im Volume caddy_data – das Volume ist wichtig, sonst wird bei jedem Neustart ein neues Zertifikat beantragt.

Das Caddyfile macht vier Dinge:

  • TLS automatisch – Let's Encrypt mit Ausstellung und Erneuerung ohne Zutun. HTTP-01 (Port 80) und TLS-ALPN-01 (Port 443) sind beide aktiv; blockiert der Anbieter Port 80, greift der ALPN-Weg auf 443 von allein.
  • Reverse-Proxy für /health und /v1/* an relay:8080. Das WebSocket-Upgrade auf /v1/ws erkennt Caddy selbst.
  • Statisches Hosting für diese Seite, den Docs-Bereich, das Download-Verzeichnis und /.well-known/assetlinks.json.
  • Header-Härtung: Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff, kein Server-Header.
cd /opt/aegis/edge
docker compose up -d
docker compose logs -f caddy      # ACME-Fortschritt beobachten

Der Landing-Bereich nutzt Caddys templates-Direktive: Der Download-Button erscheint nur, wenn srv/download/aegis.apk wirklich existiert, und die angezeigte Version stammt aus einem beim Deployment erzeugten HTML-Schnipsel – sie kann also nicht veralten.

3 Firewall

Grundregel des Projekts: eingehend alles verbieten, ausgehend erlauben, und nur gezielt öffnen. Öffentlich ist ausschließlich der Chat-Pfad.

PortProtokollWofürEbene
22tcpSSH (nur mit Schlüssel, Passwort-Login aus)lokal
80tcpHTTP-Redirect und ACME HTTP-01Anbieter + lokal
443tcpREST, WebSocket, Landing-PageAnbieter + lokal
51820udpWireGuard – nur wenn der Controller-Teil betrieben wirdAnbieter + lokal
3478 / 5349 / 49160–49200 geplantudp+tcpcoturn für Anrufe (Stufe 3)Anbieter + lokal
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Alles andere – Datenbank, Metrik-Speicher, Git-Server, Controller – bindet an die VPN-Adresse und bekommt nie eine Freigabe nach außen.

4 Einladung erzeugen und registrieren

Die Registrierung ist geschlossen: Ohne einmaligen Invite-Token legt der Relay kein Konto an. Tokens erzeugt entweder der Betreiber mit dem Admin-Token oder ein bereits registrierter Nutzer aus der App heraus.

curl -s -X POST https://<host>/v1/invites \
  -H "X-Admin-Token: $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"count":1}'

Daraus wird ein Einladungslink der Form

https://<host>/i/<token>#u=<einladender>&aik=<dessen Identitätsschlüssel>

Ist die App installiert, öffnet Android diesen Link dank verifizierter App-Links direkt in Aegis; der Einladende wird gepinnt und die Freundschaft automatisch angelegt. Ist sie es nicht, landet man auf dieser Website mit Download und Kopier-Button. Der Teil hinter dem # wird nie an den Server gesendet – er bleibt im Browser, weshalb der Link wortwörtlich kopiert werden muss.

5 Eigenen Relay und ntfy in der App wählen

Das Wichtigste zuerst: für einen eigenen Relay muss die App nicht neu gebaut werden. Beim ersten Start nach der Installation steht im Registrierungs­bildschirm ein Feld Relay-Server, vorbelegt mit chat.personaplay.de. Wer selbst hostet, überschreibt es aktiv mit der eigenen Domain (nur der Hostname genügt, HTTPS wird vorausgesetzt). Die gewählte Adresse wird gespeichert und ab dann durchgängig verwendet – REST, WebSocket, Blob-Store und die Push-Registrierung.

Die Relay-Adresse ist nach der Registrierung fest: Die Identität hängt am Relay, und ein Wechsel wäre ein anderes Konto in einer anderen Welt. Wer den Relay wechseln will, registriert sich neu. Bestehende Installationen, die es schon vor dieser Option gab, bleiben unverändert auf chat.personaplay.de.

  • Zertifikat: Der eigene Relay braucht ein von einer normalen CA (z. B. Let's Encrypt über den Edge) ausgestelltes Zertifikat – die App prüft es gegen den System-Speicher und merkt sich zusätzlich per TOFU den beim ersten Verbinden gesehenen Schlüssel (siehe Zertifikat & Pinning). Ein Tippfehler oder ein nicht erreichbarer Host führt zu einer klaren Fehlermeldung, nicht zu einem stillen Hänger.
  • Eigener ntfy-Server: Der inhaltslose Weckruf läuft über UnifiedPush/ntfy. Unter Einstellungen → Push-Verteiler lässt sich die ntfy-Serveradresse frei eintragen (Standard: unser push.personaplay.de). Genau diese Adresse gehört in der ntfy-App als Standard-Server hinterlegt; die App meldet den daraus entstehenden Weck-Endpunkt wie gehabt beim Relay an. Der Relay darf an einen fremden, öffentlichen ntfy-Server senden – interne Ziele (WireGuard, RFC1918, Loopback, Docker-Bridges) bleiben hart gesperrt, und der Zugangs-Token unseres eigenen Brokers wird niemals an einen fremden Server geschickt. Der Weckruf bleibt inhaltslos (kein Absender, kein Inhalt).

6 App bauen und ausliefern (nur bei eigener App-Signatur)

Für den reinen Relay-Betrieb ist dieser Schritt nicht nötig – der Relay wird in der App zur Laufzeit gewählt (Schritt 5). Ein eigener App-Build lohnt sich, wenn man die App unter eigenem Namen/Signatur ausliefern oder die verifizierten App-Links auf die eigene Domain legen will.

  • Für die Invite-App-Links die eigene Domain im App-Modul eintragen (Host und Basis-URL der /i/…-Links) und mit einem eigenen Release-Keystore signieren. Die Relay-Basis-URL muss hier nicht mehr angefasst werden – sie ist zur Laufzeit einstellbar. Der Keystore gehört nie ins Repo; der Build löst ihn über Umgebungsvariablen oder eine Properties-Datei auf und baut andernfalls einfach unsigniert weiter.
  • Den SHA-256-Fingerabdruck des eigenen Signaturzertifikats in srv/.well-known/assetlinks.json eintragen – nur dann verifiziert Android die App-Links und öffnet /i/… direkt in der App.
  • APK ausrollen: messenger-edge/deploy-apk.sh <pfad/zur/app-release.apk>. Das Skript kopiert die Datei atomar nach srv/download/ und schreibt version.json und version.html – Version, Größe, SHA-256 und Datum werden aus der APK selbst gelesen, nichts ist fest verdrahtet.

Prüfen lässt sich die Verknüpfung auf echter Hardware mit adb shell pm get-app-links <package> – erwartet wird „verified“.

7 Controller und Monitoring (optional)

Der Control-Teil ist vom Messenger unabhängig und bleibt strikt im VPN.

  1. WireGuard einrichten, z. B. Subnetz 10.10.2.0/24 mit dem Server auf 10.10.2.1 und je einem Peer pro Gerät. Ein Subnetz wählen, das nicht mit dem heimischen Netz kollidiert.
  2. Monitoring-Stack aus deploy/monitoring/ starten: VictoriaMetrics veröffentlicht ausschließlich 10.10.2.1:8428; node_exporter und cAdvisor bekommen keinen Port und werden über ihre Compose-Servicenamen gescrapt. Aufbewahrung 12 Monate, Daten im Volume vmdata.
  3. Controller aus controller/ starten: veröffentlicht 10.10.2.1:8000, hängt im Netz des Monitoring-Stacks und erreicht VictoriaMetrics über den Servicenamen. Die mobile Web-UI wird read-only aus app/ hineingemountet und same-origin ausgeliefert.
  4. Agents auf den Zielservern ausrollen – ein Daemon mit fester Whitelist, Token-Auth und Docker-SDK statt Shell. Kein Root-SSH vom Controller.

Danach zeigt der Dashboard-Tab der App Ampel-Status und Verlaufscharts – erreichbar nur mit aktivem Tunnel und nach Biometrie.

Admin-Governance (kapazitätsbasiert, kein Register). Wer welche Admin-Aktion ausführen darf, hängt an einer operator-signierten Capability, die der Controller stateless prüft – es gibt kein Zugangs-Register. Ein Root-Gate reserviert die Admin-Einstellungen dem Erst-Einrichter; jede Schreibaktion wird ins Audit-Log (nur Root) geschrieben und legt einen Pre-Snapshot an, sodass sich Admin-Änderungen zurückrollen lassen. Die granulare Delegation einzelner Rechte an Kontakte ist im Aufbau.

Föderation und VPN steuert man ebenfalls hier. Das Dashboard bietet einen Bereich, um die (opt-in) Föderation ein-/auszuschalten und Peer-Relays zu verwalten, sowie die Verwaltung mehrerer log-freier VPNs (Name, Endpunkt, Subnetz, optionale Pi-hole-DNS, Preis/Laufzeit) mit Auto-Ablauf-Sperre. Der App-seitige VPN-Tab bezieht daraus seine VPN-Liste; der Kauf-Fluss folgt.

Zertifikat und Pinning

Die App kennt hier bewusst zwei Vertrauensregime, je nachdem, welcher Relay eingestellt ist:

  • Öffentlicher Relay chat.personaplay.de (Standard): festes SPKI-Pinning, fest in die App gebaut. Gepinnt wird die stabile obere Kette (Let's-Encrypt-Intermediate und Root) samt Backup-Pin, nicht der bei jeder Erneuerung wechselnde Leaf – eine Leaf-Rotation sperrt also niemanden aus, ein fremdes gültiges Zertifikat kommt trotzdem nicht durch. Diese stärkste Absicherung bleibt unangetastet.
  • Selbst eingetragener Relay: normale System-CA-Prüfung (das Let's-Encrypt-Zertifikat vom eigenen Edge wird gegen den Geräte-Trust-Store validiert) plus TOFU (Trust On First Use). Der beim ersten erfolgreichen Verbinden gesehene SPKI wird pro Host gespeichert; ändert er sich später unerwartet, wird die Verbindung blockiert und gewarnt. Das ist strikt stärker als reine CA-Prüfung – es fängt auch ein später mis-issued, aber gültiges Zertifikat einer anderen CA – und weicht den Standardpfad an keiner Stelle auf. Voraussetzung ist deshalb ein CA-gültiges Zertifikat; ein rein selbst-signiertes reicht für einen zur Laufzeit gewählten Relay nicht.

Aktuellen Leaf-Pin jederzeit selbst ziehen:

echo | openssl s_client -connect <host>:443 -servername <host> 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary | openssl enc -base64

Die App-Links-Verifikation nutzt ohnehin den System-Trust-Store; das Pinning betrifft nur die Relay-Verbindung und kann parallel bestehen. Wer einen eigenen App-Build mit eigenem Pin für den eigenen Relay ausliefern will, kann den festen Pin dort setzen; die Laufzeit-Option nutzt System-CA + TOFU.

Betrieb und Prüfen

  • curl -s https://<host>/health – Lebenszeichen des Relays inklusive Datenbank-Prüfung.
  • docker compose logs -f relay bzw. caddy – ACME-Ausstellung, Verbindungen, Rate-Limit-Treffer.
  • Zustellung wird laufend geräumt: bestätigte Envelopes verschwinden sofort, unbestätigte nach 30 Tagen. Der Speicherbedarf bleibt dadurch von selbst klein.
  • Zum Testen ohne zweite Person gibt es einen dauerhaft laufenden Echo-Testkontakt: Er nimmt Nachrichten an, entschlüsselt sie durch beide Schichten und antwortet verschlüsselt zurück. Er ist ausdrücklich eine Testeinrichtung – ein unbeaufsichtigter Prozess auf dem Server liest, was man ihm schickt. Der Relay bleibt davon unberührt zero-knowledge.

4 · Stand und Grenzen

Fertig live

  • Zero-Knowledge-Relay mit Store-and-Forward, WebSocket-Live-Kanal und Fan-out je Gerät
  • Invite-gated Registrierung, Identity-Key-Auth, Rate-Limits
  • 1:1-Textchat Ende-zu-Ende verschlüsselt, inklusive Zwei-Schicht-Verschlüsselung
  • Echtzeit-Zustellung über einen Vordergrunddienst plus inhaltslosem Push als Rückfallweg
  • Sicherheitsnummer im Chat, TOFU-Pinning beim Hinzufügen
  • Öffentlicher Edge mit automatischem TLS, App-Links und APK-Auslieferung
  • Controller: Health-Ampel, Verlaufscharts, Modul-Registry, Casbin-Rechte, Audit-Log
  • Opt-in-Föderation: Relay-Kern (Whitelist/Blacklist, S2S-Pinning, Zwei-Relay-Kette, AIK-signierte Prekeys) + Dashboard-Steuerung – standardmäßig aus
  • Admin-Governance: kapazitätsbasierter Zugang (kein Register), Root-Gate, Audit-Log und Rollback (nur Erst-Admin)
  • VPN: eigener App-Tab mit Always-on + Umschalter, Dashboard-Override, admin-verwaltete, log-freie VPNs (opt. Pi-hole-DNS), Auto-Ablauf-Sperre

Offen geplant

  • Medien und Dateien – client-seitig verschlüsselt vor dem Upload; der Server bekäme nur einen opaken Blob (Stufe 2)
  • Sprach- und Videoanrufe – WebRTC mit eigenem coturn, Signaling als E2EE-Nachricht (Stufe 3)
  • Multi-Device – die Relay-Endpunkte für QR-Provisioning stehen, der App-Flow fehlt
  • Verschlüsseltes Identitäts-Backup – serverseitig vorhanden, in der App noch nicht verdrahtet
  • Gruppenchats – geplant
  • Status-Feature – geplant
  • Offene Föderation (Blacklist-Modus) mit Missbrauchskontrollen; erst danach föderiert das öffentliche Relay
  • VPN-Kauf-Fluss (bezahlter Zugang) und granulare Admin-Delegation
  • Controller: Login und 2FA über Authentik, selbstheilender Watchdog, Updates mit Rollback, restic-Backups, Module für UniFi-Gateway und Plex