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-Ebene | Control-Ebene | |
|---|---|---|
| Zweck | Messenger 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 Server | keines nötig – Zero-Knowledge, nur Chiffretext | voll – der Controller sieht die eigenen Metriken im Klartext |
| Zugang | Einladung, ohne VPN nutzbar | WireGuard-Peer + Biometrie + Berechtigung |
Datenfluss
Öffentlicher Pfad – vom Handy bis in die Datenbank kommt nur Chiffretext an:
relay:8080, nicht öffentlich publiziertVPN-Pfad – unverändert abgeschottet:
10.10.2.1:8000 · FastAPI + mobile Web-UI10.10.2.1:8428 · gespeist von node_exporter und cAdvisorDie 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
| Komponente | Technik | Erreichbarkeit |
|---|---|---|
| Android-App | Kotlin, klassische Views/XML, Material3 DayNight, libsignal, SQLCipher, OkHttp, WireGuard-GoBackend, UnifiedPush | Client |
| Relay | Python 3.13, FastAPI, Starlette-WebSockets, uvicorn, asyncpg, Pydantic v2 | intern relay:8080 im Docker-Netz aegis-edge |
| Datenbank | PostgreSQL 16 | nur privates Compose-Netz |
| Edge | Caddy 2, automatisches Let's Encrypt | öffentlich 80/443 |
| Controller | Python 3.13, FastAPI, Casbin, mobile Web-UI | 10.10.2.1:8000 – VPN-only |
| Metrik-Speicher | VictoriaMetrics, Aufbewahrung 12 Monate | 10.10.2.1:8428 – VPN-only |
| Metrik-Erfassung | node_exporter (Host), cAdvisor (Container) | kein veröffentlichter Port, nur internes Netz |
| Agent | Python/FastAPI-Daemon je Zielserver, Docker-SDK statt Shell | nur vom Controller aus, Token-Auth |
Repo-Aufbau
| Verzeichnis | Inhalt |
|---|---|
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.
| Endpunkt | Zweck |
|---|---|
POST /v1/register | Konto anlegen – nur mit gültigem, einmaligem Invite-Token |
POST /v1/invites | Invite-Token erzeugen (Admin-Token oder bestehender Nutzer) |
POST /v1/auth/challenge | Nonce 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 · /ack | Envelope 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/register | Push-Endpunkt für den inhaltslosen Wake-up melden |
PUT /v1/backup, GET /v1/backup/… | opaken, passphrase-verschlüsselten Identitäts-Blob ablegen/holen |
GET /health | Lebenszeichen inkl. Datenbank-Prüfung (unauthentifiziert) |
WSS /v1/ws | Live-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
| Datum | Server sieht | Server sieht nicht |
|---|---|---|
| Nachrichtentext | opaken Ciphertext | Klartext; Länge nur grob, weil auf Bucket-Größen gepolstert wird |
| Öffentliche Schlüssel, Prekey-Bundles | ja – das ist ihr Zweck | die privaten Gegenstücke |
| Konten | welcher Benutzername existiert | Klarname, Telefonnummer, E-Mail (gibt es nicht) |
| Zustell-Metadaten | Empfänger-ID, Ziel-Gerät, Zeitstempel | den Absender im Sealed-Sender-Modus |
| Push | den Push-Endpunkt | Inhalt – der Push ist leer |
| Identitäts-Backup | opaken Blob + Salt/KDF-Parameter | Passphrase und Inhalt |
| Medien geplant | nur den AES-GCM-Ciphertext-Blob und seine Größe | Inhalt, Dateiname, Typ – alles steckt im E2EE-Umschlag |
| Anrufe geplant | über coturn nur SRTP-Chiffretext | Audio/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ät | Nachrichtentext |
| Zeitstempel, Nachrichtentyp, Zuordnung zur Unterhaltung | Medien-Inhalte – ihr AES-Schlüssel liegt in Schicht 2 |
| Zustell- und Lesestatus, Tipp-Anzeige, Anruf-Signale | Zitate, 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
- 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).
- Invite-gated – ohne gültigen, einmaligen Token wird kein Konto angelegt.
- Identity-Key-Auth – jeder authentifizierte Aufruf verlangt eine Signatur mit einem Geräteschlüssel, den der Server bereits kennt. Unbekannte Schlüssel werden abgewiesen.
- 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 allemINVITE_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.jsonstimmt 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/tcpzwingend,80/tcpfü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 inufw. - 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
relaywird aus./relaygebaut, lauscht intern auf0.0.0.0:8080ohne TLS (RELAY_TLS=off) und hat keinenports:-Eintrag – er ist damit nie direkt aus dem Internet erreichbar, sondern nur überexpose: 8080im Netzaegis-edge. - Dienst
postgres(PostgreSQL 16) hängt nur im privatendefault-Netz, hat ebenfalls keinen veröffentlichten Port und einenpg_isready-Healthcheck, auf den der Relay wartet. - Volumes:
pgdatafür die Datenbank,backupsfü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
/healthund/v1/*anrelay:8080. Das WebSocket-Upgrade auf/v1/wserkennt 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, keinServer-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.
| Port | Protokoll | Wofür | Ebene |
|---|---|---|---|
| 22 | tcp | SSH (nur mit Schlüssel, Passwort-Login aus) | lokal |
| 80 | tcp | HTTP-Redirect und ACME HTTP-01 | Anbieter + lokal |
| 443 | tcp | REST, WebSocket, Landing-Page | Anbieter + lokal |
| 51820 | udp | WireGuard – nur wenn der Controller-Teil betrieben wird | Anbieter + lokal |
| 3478 / 5349 / 49160–49200 geplant | udp+tcp | coturn 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
Registrierungsbildschirm 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.jsoneintragen – 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 nachsrv/download/und schreibtversion.jsonundversion.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.
- WireGuard einrichten, z. B. Subnetz
10.10.2.0/24mit dem Server auf10.10.2.1und je einem Peer pro Gerät. Ein Subnetz wählen, das nicht mit dem heimischen Netz kollidiert. - Monitoring-Stack aus
deploy/monitoring/starten: VictoriaMetrics veröffentlicht ausschließlich10.10.2.1:8428; node_exporter und cAdvisor bekommen keinen Port und werden über ihre Compose-Servicenamen gescrapt. Aufbewahrung 12 Monate, Daten im Volumevmdata. - Controller aus
controller/starten: veröffentlicht10.10.2.1:8000, hängt im Netz des Monitoring-Stacks und erreicht VictoriaMetrics über den Servicenamen. Die mobile Web-UI wird read-only ausapp/hineingemountet und same-origin ausgeliefert. - 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 relaybzw.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