Fast jede Container-Anleitung im Netz legt Zugangsdaten als Umgebungsvariable an. Das funktioniert und ist für ein Testsystem in Ordnung. Für alles andere lohnt es, einmal nachzusehen, wer diese Variablen eigentlich lesen kann.
Wer Umgebungsvariablen sieht#
# Der ganze Umgebungsblock, im Klartext, ohne Root-Rechte im Container:
docker inspect meinapp | jq '.[0].Config.Env'
# Auch aus jedem anderen Prozess im selben Container:
docker compose exec app env | grep -i pass
Damit ist klar, wo die Daten überall auftauchen:
| Ort | Wer liest mit |
|---|---|
docker inspect | jeder mit Docker-Zugriff, also faktisch Root |
| Prozessumgebung | jeder Prozess im Container, auch fremder Code |
| Abstürze und Fehlerberichte | Bibliotheken, die die Umgebung mitschicken |
compose.yaml im Repository | alle, die das Repository sehen, auch später noch |
| Shell-Historie | wer den Server hat |
Der vierte Punkt ist der teuerste: Ein Passwort, das einmal in einem Commit steht, bleibt in der Historie, auch wenn die Datei später geändert wird.
Stufe 1: raus aus der Compose-Datei#
Der kleinste sinnvolle Schritt. Werte in eine .env-Datei neben der Compose-Datei, die nicht ins Repository geht:
# .env
POSTGRES_PASSWORD=…
SMTP_PASSWORT=…
chmod 600 .env
echo ".env" >> .gitignore
git check-ignore -v .env # gegenprüfen, dass es wirklich greift
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?fehlt}
Das :?fehlt ist kein Zierrat: Ohne den Wert bricht Compose mit einer klaren Meldung ab, statt einen Container mit leerem Passwort zu starten.
Stufe 2: Dateien statt Variablen#
Viele Abbilder unterstützen …_FILE-Varianten. Dann liest die Anwendung den Wert aus einer Datei, und er steht nirgends in der Umgebung:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_passwort
secrets:
- db_passwort
secrets:
db_passwort:
file: ./geheim/db_passwort.txt
mkdir -p geheim && chmod 700 geheim
head -c 24 /dev/urandom | base64 > geheim/db_passwort.txt
chmod 600 geheim/db_passwort.txt
echo "geheim/" >> .gitignore
# Gegenprobe: die Umgebung ist jetzt sauber
docker inspect $(docker compose ps -q db) | jq '.[0].Config.Env' | grep -i pass
Der secrets:-Block gilt oft als Swarm-Sache. In Compose funktioniert die Variante mit file: auch ohne Cluster, die Datei wird als /run/secrets/<name> eingehängt, schreibgeschützt und nur für diesen Container. Der Unterschied zum einfachen Volume ist gering, aber die Absicht steht in der Datei, und das ist bei Zugangsdaten die halbe Miete.
Stufe 3: gar nicht auf dem Server#
Für Dinge, die wirklich weh tun, Zahlungsschlüssel, Signaturschlüssel, Zugänge zu fremden Systemen, ist die richtige Antwort ein Speicher außerhalb der Anwendung: ein Tresor, aus dem beim Start geholt wird, mit kurzlebigen Zugangsdaten. Das ist deutlich mehr Aufwand und lohnt erst, wenn mehrere Systeme dieselben Geheimnisse brauchen.
Für einen einzelnen VPS ist Stufe 2 plus ein ordentlicher Passwortmanager für die Menschen (Vaultwarden) der realistische Stand.
Die Regeln, die unabhängig von der Stufe gelten#
Ein Geheimnis pro Zweck. Dasselbe Passwort für Datenbank und Mailversand bedeutet: Ein Fund kompromittiert beides.
Rechte prüfen, nicht annehmen.
find . -name ".env" -o -name "*.env" | xargs ls -l
# 600, und der Eigentümer ist der, der die Container startet — nicht "alle".
Nichts in die Protokolle. Anwendungen, die ihre Konfiguration beim Start ausgeben, schreiben das Passwort ins Journal, wo es Wochen liegen bleibt:
docker compose logs | grep -iE "password|secret|token" | head
Keine Zugangsdaten ins Abbild. Ein COPY .env /app/ im Dockerfile brennt das Geheimnis in eine Schicht, die man auch später noch auslesen kann, selbst wenn eine spätere Schicht die Datei löscht.
docker history --no-trunc meinabbild | grep -i env
Wenn etwas veröffentlicht wurde#
Ein Zugangsdatum, das in einem Repository, einem Screenshot, einer Protokollzeile oder einem Ticket gelandet ist, gilt als kompromittiert, auch wenn der Commit später korrigiert wird. Automatisierte Sucher finden veröffentlichte Schlüssel in Minuten. Der einzige wirksame Schritt ist der Austausch, in dieser Reihenfolge:
- Neu erzeugen bei der ausstellenden Stelle (Datenbank-Benutzer, API-Anbieter, Bot-Portal).
- Überall eintragen, wo der alte Wert steht.
- Alten Wert ungültig machen, nicht nur ersetzen.
- Nachsehen, ob er benutzt wurde, Zugriffsprotokolle des betroffenen Dienstes.
- Erst dann die Historie aufräumen, falls möglich.
Häufige Fragen#
Reicht es, .env in .gitignore zu setzen?#
Für die Zukunft ja, für die Vergangenheit nein. git log -p -- .env zeigt, ob der Wert schon einmal drin war.
Sind Umgebungsvariablen immer falsch?#
Nein, für Einstellungen ohne Geheimniswert sind sie genau richtig. Die Trennlinie verläuft zwischen Konfiguration und Zugangsdaten.
Was ist mit docker run -e PASS=…?#
Landet zusätzlich in der Shell-Historie und in der Prozessliste des Hosts. Für einen einmaligen Test hinnehmbar, sonst nicht.
Wie oft sollte man Zugangsdaten wechseln?#
Nach jedem Verdacht sofort, und beim Ausscheiden von Personen, die sie kannten. Ein starres Ablaufdatum ohne Anlass bringt wenig und führt meist zu schwächeren Passwörtern.
Und in Kubernetes?#
Dort heißt der Mechanismus ähnlich, ist aber standardmäßig nur base64-kodiert, nicht verschlüsselt. Die Denkweise aus Stufe 3 gilt dort früher.
Kurz gesagt#
docker inspectzeigt jede Umgebungsvariable, auch die mit dem Passwort.- Stufe 1:
.envmit600, in.gitignore, mit${VAR:?fehlt}. - Stufe 2:
…_FILEplussecrets:, dann steht nichts mehr in der Umgebung. - Nie ins Abbild, nie ins Protokoll, ein Geheimnis pro Zweck.
- Veröffentlicht heißt tauschen, nicht löschen.
Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.