· DevOps · 8 minuten Lesezeit
Redis ohne Auth und Qdrant mit offenem Admin-Port vor dem Launch
Vor dem Launch habe ich meine Architektur nicht nur auf Skalierung geprüft, sondern auch auf stillschweigende Sicherheitsannahmen. Dabei sind mir zwei kleine Lücken aufgefallen, die in Produktion sehr schnell teuer werden können.

Inhalt
- Der Ausgangspunkt einer Architekturprüfung
- Redis ohne Passwort
- requirepass mit env-var-Default
- Qdrant mit offenem Admin-Port
- Was bleibt
- Warum das vor dem Launch wichtig ist
- Das vollständige Bild
- Alle Artikel der Serie
Der Ausgangspunkt einer Architekturprüfung
Bevor ich ein System in Produktion schiebe, stelle ich mir eine einfache Frage: Würde diese Architektur auch dann noch tragen, wenn morgen nicht hundert, sondern hunderttausend Nutzer auftauchen und ich an der Infrastruktur nichts Grundlegendes ändere?
Diese Frage ist absichtlich unbequem. Sie zwingt mich dazu, nicht nur auf CPU, RAM und horizontale Skalierung zu schauen, sondern auch auf Annahmen, die heute harmlos wirken und morgen riskant werden.
Genau in so einer Prüfung sind mir zwei Punkte aufgefallen. Beide waren klein. Beide liefen lokal sauber. Beide hatten auf den ersten Blick nichts mit Performance zu tun. Aber beide hatten das Potenzial, in Produktion zu einem echten Sicherheitsproblem zu werden.
Redis ohne Passwort
In meinem aktuellen docker-compose.yml startete Redis so:
redis:
command: redis-server --appendonly yesInnerhalb eines einzelnen Docker-Netzwerks auf einer einzelnen VM ist das technisch erst einmal vertretbar. Der Container ist nicht direkt aus dem Internet erreichbar. Docker kapselt das Netz intern. Solange alle beteiligten Services auf genau diesem Host laufen, fühlt sich das sicher genug an.
Genau hier beginnt aber die gefährliche Denkweise. Sicher, weil isoliert, ist keine Eigenschaft des Dienstes selbst. Es ist nur eine Eigenschaft der aktuellen Topologie.
Sobald die Architektur wächst, wird diese Annahme brüchig. In einer späteren Phase mit mehreren VMs, etwa wenn Qdrant auf einen eigenen Host zieht und Redis vielleicht ebenfalls ausgelagert wird, sprechen die Services nicht mehr über ein rein internes Docker-Netzwerk. Dann läuft der Verkehr über das private Netzwerk des Infrastruktur-Anbieters.
Ab diesem Punkt kann jeder Prozess auf diesem privaten Netz theoretisch versuchen, sich mit Redis zu verbinden. Ohne Authentifizierung bedeutet das: Queue lesen, Queue manipulieren, Queue leeren.
Bei BullMQ ist das nicht nur ein Betriebsproblem, sondern auch ein Datenschutzproblem. Der Job-Payload in Redis enthält Metadaten, die ich nicht offen herumliegen lassen will:
// Payload stored in Redis queue per job
{
captureId: string,
userId: string,
blobName: string,
// ... full QdrantPayload
}Je nach Job liegen dort zusätzlich weitere Teile des QdrantPayload. Wer Zugriff auf Redis hat, kann also sehen, welcher Nutzer welche Datei gespeichert hat, welche S3 Keys verwendet werden und welche Jobs gerade in der Pipeline stehen. Im schlimmsten Fall leert ein Angreifer die Queue oder verändert Jobs gezielt.
Für mich ist das keine harmlose Fehlkonfiguration mehr. Eine offengelegte Redis-Queue ist ein Datenleck.
requirepass mit env-var-Default
Die saubere Lösung war überraschend klein. Ich wollte dieselbe Compose-Datei lokal und in Produktion behalten und nur über Umgebungsvariablen steuern, ob Redis Auth verlangt oder nicht.
So sieht die Compose-Änderung aus:
# docker-compose.yml
redis:
# REDIS_PASSWORD unset -> requirepass "" -> Redis accepts connections without auth (local dev).
# REDIS_PASSWORD=<secret> -> Redis requires AUTH on every connection (production).
command: redis-server --appendonly yes --appendfsync everysec --requirepass "${REDIS_PASSWORD:-}"
healthcheck:
# Pass password to redis-cli; works whether or not requirepass is set
test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-}', 'ping']Im Backend nutze ich dieselbe Variable direkt in der Verbindungs-URL:
# docker-compose.yml
backend:
environment:
- REDIS_URL=redis://:${REDIS_PASSWORD:-}@redis:6379Der wichtige Teil ist :-. Das ist der Default-Operator in Compose-Interpolation.
- Lokal ist
REDIS_PASSWORDnicht gesetzt. - Compose ersetzt den Ausdruck dann durch einen leeren String.
- Redis bekommt effektiv
requirepass ""und verlangt damit kein Passwort. - Die URL wird zu
redis://:@redis:6379. - Der Redis-Client sieht ein leeres Passwort und sendet kein AUTH.
- Alles funktioniert wie bisher.
In Produktion läuft es genauso, nur mit gesetzter Variable:
REDIS_PASSWORDenthält ein echtes Secret.- Redis startet mit
--requirepassund erzwingt Authentifizierung. - Die Backend-URL enthält das Passwort.
- Der Client authentifiziert sich automatisch.
Für mich ist genau das die elegante Variante. Eine Compose-Datei, ein Codepfad, zwei Umgebungen. Das Verhalten ändert sich nur über die Variable.
In .env.example würde ich das explizit dokumentieren:
# REDIS_PASSWORD is optional in local dev (empty = no auth required).
# In production, always set a strong password.
# REDIS_PASSWORD=<generate with: openssl rand -hex 32>Damit ist die unsichtbare Annahme aus der Infrastruktur herausgezogen und in eine bewusste Sicherheitskonfiguration verwandelt.
Qdrant mit offenem Admin-Port
Das zweite Problem war noch unscheinbarer. In der Compose-Datei stand bei Qdrant:
qdrant:
ports:
- '6333:6333'
- '6334:6334'Port 6333 ist die REST API. Port 6334 ist die gRPC-Schnittstelle von Qdrant. Sie wird vor allem für Cluster-Kommunikation zwischen Qdrant-Knoten genutzt. In einem Single-Node-Setup stellt sie funktional dieselben Datenoperationen bereit, nur eben über gRPC statt HTTP.
Genau deshalb ist dieser Port in meinem Setup unnötig. Ich betreibe keinen Qdrant-Cluster. Ich habe keinen zweiten Node, der über diesen Port koordinieren müsste.
Wenn ich 6334:6334 in Compose angebe, bindet Docker den Port auf 0.0.0.0 des Hosts. Damit hängt er auf allen Netzwerkschnittstellen der VM, also auch auf der öffentlichen IP.
Natürlich kann eine Firewall diesen Port blockieren. Aber genau darauf allein will ich mich nicht verlassen. Firewalls können falsch konfiguriert werden. Regeln können versehentlich erweitert werden. Infrastruktur ändert sich. Defense in depth bedeutet für mich, unnötige Angriffsfläche gar nicht erst zu veröffentlichen.
Hinzu kommt ein strukturelles Problem: Qdrant OSS bringt keine eingebaute Authentifizierung mit. Wer Port 6333 oder 6334 erreicht, kann lesen, schreiben und löschen. Collections, Vektoren, Payloads mit userId, Metadaten, alles liegt offen.
Die unmittelbare Korrektur ist deshalb banal:
qdrant:
ports:
- '6333:6333'
# Port 6334 intentionally omitted. It is only needed for Qdrant cluster mode.
# Exposing it on the host adds unnecessary attack surface.Der Backend-Container spricht intern ohnehin mit http://qdrant:6333. Dafür braucht er keinen Host-Port 6334. Die Host-Freigabe war nur für lokale Diagnose praktisch. Genau solche Bequemlichkeiten will ich aber nicht versehentlich in Produktion mitschleppen.
Was bleibt
Auch nach dieser Korrektur bleibt eine unbequeme Wahrheit bestehen: Qdrant OSS hat keine native Auth.
Wenn Port 6333 in Produktion offen im Internet läge, wäre das ein ernstes Problem. In meinem Setup entsteht die Sicherheit deshalb aus mehreren Schichten.
- Die Hetzner Firewall blockiert direkte Zugriffe aus dem Internet auf Port
6333. - Qdrant läuft im privaten Netzwerk und ist damit nicht für beliebige externe Clients erreichbar.
- Nur das Backend spricht mit Qdrant und erzwingt die Trennung pro Nutzer über eigene Collections.
Diese Schichten sind gut, aber sie sind nicht perfekt. Sie ersetzen keine echte Auth auf Datenbankebene. Wenn das Thema mit wachsender Nutzerzahl kritischer wird, sind für mich zwei Wege naheliegend: Qdrant Cloud mit API-Key im EU-Standort oder ein zusätzlicher Reverse Proxy, der nur Requests aus dem Backend-Kontext annimmt.
Mir ist wichtig, das offen zu benennen. Port 6334 zu entfernen löst nicht jedes Problem. Es entfernt nur unnötige Angriffsfläche und macht die bestehende Lage deutlich besser.
Warum das vor dem Launch wichtig ist
Solche Änderungen wirken im Vorfeld fast lächerlich klein. Es sind ein paar Zeilen YAML und eine zusätzliche Umgebungsvariable.
Vor dem Launch bedeutet das vielleicht zwanzig Minuten Arbeit. Nach dem Launch kann derselbe Fehler plötzlich eine ganz andere Größenordnung haben.
Dann geht es nicht mehr nur um Konfiguration. Dann geht es um Log-Auswertung, Rekonstruktion des Vorfalls, interne Dokumentation, mögliche Benachrichtigung von Betroffenen und im Ernstfall um eine Meldung nach DSGVO Art. 33 innerhalb von 72 Stunden an die zuständige Aufsichtsbehörde.
Genau deshalb nehme ich solche Dinge vor dem Go-Live ernst. DSGVO Art. 25 spricht von Datenschutz durch Technikgestaltung und durch datenschutzfreundliche Voreinstellungen. Für mich heißt das sehr praktisch: Die Default-Konfiguration darf nicht nur bequem sein. Sie muss in der Zielumgebung sicher sein.
Das vollständige Bild
Nach beiden Korrekturen sieht der relevante Ausschnitt so aus:
# docker-compose.yml
redis:
command: redis-server --appendonly yes --appendfsync everysec --requirepass "${REDIS_PASSWORD:-}"
healthcheck:
test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-}', 'ping']
backend:
environment:
- REDIS_URL=redis://:${REDIS_PASSWORD:-}@redis:6379
qdrant:
ports:
- '6333:6333'
# 6334 removedLokal bleibt alles reibungslos. Ich setze keine zusätzliche Variable und der Stack verhält sich wie bisher.
In Produktion setze ich REDIS_PASSWORD, starte Redis neu und habe sofort eine deutlich robustere Ausgangslage. Gleichzeitig verschwindet mit dem offenen gRPC-Port ein unnötiger Eintrittspunkt.
Für mich ist das genau die Art von Sicherheitsarbeit, die vor dem Launch passieren muss. Nicht spektakulär. Nicht marketingtauglich. Aber im Zweifel der Unterschied zwischen einem sauberen Start und einem sehr unangenehmen ersten Incident.

Die Grafik zeigt zwei kleine Konfigurationsänderungen mit großer Wirkung: Redis verlangt in Produktion ein Passwort, und Qdrant veröffentlicht nur noch den wirklich benötigten Port. Lokal bleibt der Stack bequem, in Produktion fällt unnötige Angriffsfläche weg.
Alle Artikel der Serie
- Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
- RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
- AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
- Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
- Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
- Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
- Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
- tsconfig und Vite:
Node16vs.bundler, warum Vite eigene Regeln hat: Artikel lesen - Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
- Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
- Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
- Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
- PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
- Instagram Karussell vollständig erfassen mit MutationObserver: Lazy-Loading, Observer-before-click, Timeout-Fallback: Artikel lesen
- Notiz und Tags beim Screenshot-Speichern: Artikel lesen
- Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
- Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
- Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
- PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
- React Frontend mit react-oidc-context und Zitadel: Artikel lesen
- Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen
- Event-Driven Ingestion mit BullMQ und Redis: Artikel lesen
- MinIO statt Azurite: S3-kompatible Objektspeicherung lokal und auf Hetzner: Artikel lesen
- access_token, id_token und der Userinfo-Endpoint: was wohin gehört: Artikel lesen
- Qdrant Multi-Tenancy: Pro Nutzer eine eigene Collection: Artikel lesen
- Wenn Backend und Frontend unterschiedliche Typen kennen: Artikel lesen
- Zitadel Bootstrap entfernt: Host-Header-Bug und manuelles Setup: Artikel lesen
- Backend Code Review: sechs Probleme vor dem Launch behoben: Artikel lesen
- Traefik statt NGINX: Reverse Proxy für einen wachsenden Docker-Compose-Stack: Artikel lesen
- Zweischichtiges Rate Limiting: Traefik und express-rate-limit mit Redis: Artikel lesen
- DSGVO Art. 17 korrekt implementieren: Promise.allSettled und Export-Batching: Artikel lesen
- Embedding-Modell-Lock-in: Warum mxbai-embed-large eine Produktionsentscheidung für immer ist: Artikel lesen
- Docker Volumes in Produktion: Named Volumes, Bind Mounts und der Hetzner-Volume-Trick: Artikel lesen
- Zwei Sicherheitslücken vor dem Launch: Redis ohne Auth und ein offener Qdrant-Admin-Port: (dieser Artikel)
Du baust gerade ein ähnliches System und überlegst, welche Entscheidungen für dein Projekt passen? Lass uns das gemeinsam einschätzen.



