MongoDB & X.509#
Hinata speichert alles — Projekte, Vorgänge, Kommentare, Wissensdatenbank-Artikel, Laufzeiteinstellungen — in MongoDB. In der Produktion läuft sie als Replikatset mit TLS-Verschlüsselung und X.509-Client-Authentifizierung, dem MongoDB-„Goldstandard" für ein selbstgehostetes Cluster. Diese Seite erklärt warum, wie die Topologie aufgebaut ist und was die Deploy-Skripte im Server-Repo genau tun, damit du das Setup nachvollziehen und ihm vertrauen kannst.
Info
Alle folgenden Befehle liegen im Server-Repo unter deploy/. Es sind schlichte
openssl/mongosh-Skripte — nichts Magisches — du kannst sie also lesen, bevor
du sie ausführst.
Warum ein Replikatset#
Ein einzelnes mongod reicht, um Daten zu speichern, aber Hinata setzt in der
Produktion bewusst auf ein Replikatset — aus zwei Gründen:
- Mehrdokumenten-Transaktionen. Operationen, die alles-oder-nichts sein müssen — etwa das Abschließen eines Sprints samt Verschieben seiner Vorgänge — nutzen MongoDB-Transaktionen. Die bietet MongoDB nur auf einem Replikatset, nie auf einem Einzelknoten.
- Hochverfügbarkeit. Mit zwei datenhaltenden Knoten und einem Arbiter übersteht das Cluster den Ausfall eines Datenknotens: Der verbleibende Knoten wird zum Primary gewählt und der Server läuft weiter.
SSE wird in der App verarbeitet, nicht von Mongo
Hinatas Live-Anhang-Updates nutzen In-Process-Server-Sent-Events, keine Mongo-Change-Streams — das SSE-Feature selbst hängt also nicht vom Replikatset ab. Beim Replikatset geht es um Transaktionen und Verfügbarkeit.
Produktiv-Topologie#
Die Produktiv-docker-compose.yml bringt drei MongoDB-Container in einem privaten
Docker-Netzwerk hoch:
| Container | Rolle | Daten | Stimme |
|---|---|---|---|
mongo1 |
Datenknoten (Priorität 2 — bevorzugter Primary) | ja (mongo1-data-Volume) |
ja |
mongo2 |
Datenknoten (Priorität 1) | ja (mongo2-data-Volume) |
ja |
mongo-arbiter |
Arbiter — nur Wahl-Zünglein an der Waage | keine | ja |
Der Arbiter hält keine Daten; er existiert nur, damit Wahlen eine ungerade Anzahl Stimmberechtigter haben, ohne für eine dritte volle Kopie zu zahlen. Jeder Knoten läuft mit demselben Befehl:
command: >-
mongod --replSet rs0 --bind_ip_all --keyFile /etc/mongo/keyfile
--tlsMode requireTLS
--tlsCertificateKeyFile /etc/mongo/certs/server.pem
--tlsCAFile /etc/mongo/certs/ca.crt
Hier sind zwei unabhängige Authentifizierungsebenen im Spiel:
--keyFile— ein geteiltes Geheimnis, mit dem sich die Replikatset-Mitglieder gegenseitig authentifizieren (interne Cluster-Auth, SCRAM).--tlsMode requireTLS+--tlsCAFile— jede Client-Verbindung muss TLS verwenden und ein Zertifikat vorlegen, das von der CA des Clusters signiert ist. Genau das ermöglicht die X.509-Client-Authentifizierung.
Das Replikatset wird beim ersten gesunden Start von mongo1 automatisch
initialisiert — sein Docker-Healthcheck ruft rs.initiate(...) mit den drei
Mitgliedern auf, falls das Set noch nicht konfiguriert ist. Du musst das also nie
von Hand tun.
Zwei Wege der App-Authentifizierung: SCRAM-Root vs. App-X.509#
Es gibt zwei verschiedene MongoDB-Identitäten, die man nicht verwechseln sollte:
MONGO_ROOT_USERNAME/MONGO_ROOT_PASSWORD— ein klassisches SCRAM-Root-Konto, das das Mongo-Image erzeugt (MONGO_INITDB_ROOT_*). Es ist rein administrativ: Es initialisiert das Replikatset und registriert den X.509-Benutzer. Der Hinata-Server nutzt es nie.- Der Anwendungs-X.509-Benutzer — der Server authentifiziert sich mit einem
Client-Zertifikat, nicht mit einem Passwort. Sein Benutzername ist der
Subject-DN des Zertifikats und liegt in der speziellen
$external-Authentifizierungsdatenbank.
Deshalb trägt der Produktiv-Verbindungsstring gar kein Passwort:
mongodb://mongo1:27017,mongo2:27017/hinata?replicaSet=rs0&tls=true&authMechanism=MONGODB-X509&authSource=$external
Diese URI wird für dich in der docker-compose.yml gesetzt (als
HINATA_MONGODB_URI am Server-Container). Das Zertifikat, das der Server vorlegt,
stammt aus dem JVM-Keystore aus HINATA_MONGO_TLS_KEYSTORE, und er validiert das
Cluster mit dem Truststore aus HINATA_MONGO_TLS_TRUSTSTORE.
Keyfile und PKI erzeugen#
Drei Skripte erzeugen alles. Führe sie für einen frischen Produktivhost in dieser Reihenfolge aus.
1. Replikatset-Keyfile und Vorschläge für Geheimnisse#
cp .env.example .env
./deploy/generate-secrets.sh
generate-secrets.sh erstellt deploy/mongo-keyfile (openssl rand -base64 756,
Modus 400), falls es noch nicht existiert — ein bestehendes überschreibt es nicht —
und gibt einfügefertige Werte für HINATA_JWT_SECRET, MONGO_ROOT_PASSWORD und
MINIO_ROOT_PASSWORD aus. Kopiere diese in deine .env.
2. Die X.509-Zertifizierungsstelle und die Zertifikate#
./deploy/x509/generate-certs.sh prod
Das baut eine in sich geschlossene PKI unter deploy/x509/prod/:
| Datei | Was es ist |
|---|---|
ca.crt / ca.key |
Die private Zertifizierungsstelle (4096-Bit-RSA, 10 Jahre gültig) |
server.pem |
Das TLS-Cert + Key von mongod; sein SAN deckt mongo1, mongo2, mongo-arbiter ab |
hinata-app.p12 |
JVM-Keystore — Client-Zertifikat + Key der App |
truststore.p12 |
JVM-Truststore — nur die CA |
app-subject-dn.txt |
Der Subject-DN des Client-Certs = der Mongo-$external-Benutzername |
keyfile |
Ein Replikatset-Internal-Auth-Keyfile (nur prod) |
Das Anwendungszertifikat wird bewusst mit einer anderen Organizational Unit
(OU=Hinata Application) ausgestellt als das Server-/Mitglieds-Zertifikat, sodass
mongod es als normalen X.509-Benutzer behandelt und nicht als Cluster-Mitglied.
Die CA auf einem laufenden Cluster nicht neu erzeugen
generate-certs.sh überschreibt eine bestehende CA nur mit --force, denn ein
Austausch der CA würde augenblicklich jedes Zertifikat ungültig machen, dem das
laufende Cluster bereits vertraut. Nutze --force nur bei einem frischen Setup.
3. Den X.509-Benutzer registrieren#
Bring die Datenknoten hoch und erstelle dann den $external-Benutzer, der zum DN des
App-Zertifikats gehört:
docker compose up -d mongo1 mongo2 mongo-arbiter
./deploy/x509/init-prod-user.sh
docker compose up -d hinata-server
init-prod-user.sh verbindet sich als SCRAM-Root-Konto (aus deiner .env) über TLS
und ruft createUser in $external mit dem DN aus app-subject-dn.txt auf, wobei
es readWrite und dbAdmin auf der hinata-Datenbank vergibt. Es ist idempotent —
existiert der Benutzer bereits, sagt es das und macht weiter.
Die Dev-Datenbank (standalone, trotzdem TLS + X.509)#
Die lokale Entwicklung läuft nicht als Replikatset.
docker-compose.dev.yml startet ein einzelnes standalone mongod — behält aber
dieselbe Sicherheitshaltung: requireTLS, --auth und nur X.509-Client-Zugriff. Ein
Befehl richtet alles ein:
./deploy/x509/setup-dev.sh
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun
setup-dev.sh erzeugt die Dev-PKI (deploy/x509/dev/), startet das Dev-Mongo,
erstellt den $external-X.509-Benutzer über die Localhost-Ausnahme und prüft, dass
der X.509-Login funktioniert. application-dev.yml zeigt bereits auf die
TLS/X.509-Verbindung, du setzt HINATA_MONGODB_URI also nicht selbst.
Dev bindet nur an Loopback
Das Dev-Mongo veröffentlicht 127.0.0.1:27017 — niemals 0.0.0.0 — eine
Entwicklungsdatenbank ist also nie aus dem Netzwerk erreichbar.
Keystore- und Truststore-Passwörter#
Der JVM-Keystore und -Truststore sind PKCS#12-Dateien, geschützt durch Passwörter, die du kontrollierst:
| Variable | Schützt | Standard |
|---|---|---|
HINATA_MONGO_TLS_KEYSTORE_PASSWORD |
hinata-app.p12 (Client-Cert + Key) |
changeit |
HINATA_MONGO_TLS_TRUSTSTORE_PASSWORD |
truststore.p12 (die CA) |
changeit |
generate-certs.sh liest diese beiden Variablen beim Bau der .p12-Dateien. Wenn du
also andere Passwörter willst, exportiere sie vor dem Erzeugen der Zertifikate:
export HINATA_MONGO_TLS_KEYSTORE_PASSWORD='ein-langer-zufaelliger-wert'
export HINATA_MONGO_TLS_TRUSTSTORE_PASSWORD='ein-weiterer-langer-zufaelliger-wert'
./deploy/x509/generate-certs.sh prod
Setze dann dieselben Werte in .env, damit der Server die Stores zur Laufzeit öffnen
kann.
Ändere jeden Standard vor dem Livegang
changeit, hinata-dev-secret und das Beispiel-MONGO_ROOT_PASSWORD in
.env.example sind Entwicklungs-Bequemlichkeiten. Erzeuge frische Geheimnisse mit
./deploy/generate-secrets.sh und setze echte Keystore-Passwörter für jedes ans
Internet gerichtete Deployment.
Datenpersistenz und Betriebssicherheit#
- Benannte Volumes. Jeder Datenknoten schreibt in ein benanntes Docker-Volume
(
mongo1-data,mongo2-data), sodass deine Daten Container-Neustarts, Image-Upgrades unddocker compose up-Neuaufbauten überstehen. Das Entfernen dieser Volumes (docker compose down -v) zerstört die Datenbank — tu das nicht. - Mongo nie öffentlich exponieren. Die Replikatset-Ports bleiben im internen
hinata-Docker-Netzwerk. Nichts im Standard-Compose veröffentlicht27017in der Produktion zum Host. Nur der Server (hinter deinem Reverse Proxy) sollte die Datenbank erreichen. - Der Arbiter ist kein Backup. Er speichert keine Daten. Echte Backups kommen aus
mongodump/Volume-Snapshots — siehe Backups & Upgrades.
Zum umgebenden Stack — Objektspeicher, Mail und Reverse Proxy — siehe Objektspeicher (S3/MinIO), E-Mail & SMTP und Reverse Proxy & TLS. Jede Umgebungsvariable ist in der Konfigurationsreferenz katalogisiert.