← Übersicht  ·  Skripte & Dateien  ·  cloud-host · Betrieb

Secrets-Management

Passwörter, Tokens und Keys raus aus .env und $env:verschlüsselt in Git, entschlüsselt nur dort, wo sie gebraucht werden. Mit SOPS (Secrets OPerationS) + age: Schlüssel erzeugen, .sops.yaml, ein Secret verschlüsseln, auf dem Server / in CI (Continuous Integration) / in Compose entschlüsseln, rotieren und Zugriff entziehen. Plus: wann Vaultwarden, Infisical oder Vault die bessere Wahl sind.

Stand: 30. August 2026 SOPS + age 0 €

Platzhalter einsetzen – nur im Browser

Voraussetzung: ein eingerichteter Server – einmaliger Aufbau in Server-Basis, wegwerfen & identisch neu in Snapshot & Restore. Was die Bausteine hier kosten: Kosten & Budget.

Das Problem & die Regel

Ein Secret ist alles, was jemandem Zugang gibt: DB-Passwort, API-Token, TLS-Key, SSH-Key, OAuth-Client-Secret, das Restic-Repo-Passwort.

Ein Wert, acht Stationen – sechs sichtbare und zwei, an die man nicht denkt – und an jeder eine andere Art, ihn zu verlieren.

Zeichnung aus Diagramme: Hetzner & CRC (38).

dein Rechner Umgebungsvariable GitHub-Secret verschlüsselt gelagert Runner im Klartext im Speicher helm --set als Argument K8s-Secret Base64 in etcd Prozess env des Containers Leckstelle Shell-Historie .env versehentlich committet Gegenmittel: .gitignore, und nie als Argument tippen Hier ist er sicher. Nicht lesbar, auch nicht für dich. Aber: jeder mit Schreibrecht am Repo kann ihn ausgeben lassen. Protokollausgabe GitHub ersetzt ihn durch Sterne – aber nur den Wert selbst, nicht Teile davon. Deshalb nie umformen oder zerschneiden. ps aux Argumente sind für jeden auf der Maschine sichtbar. Besser: --set-file oder ein Secret, das schon im Cluster liegt. Base64 ist keine Verschlüsselung Wer das Secret lesen darf, liest den Wert. RBAC begrenzen, etcd verschlüsseln, oder externer Tresor. /proc/<pid>/environ Absturzberichte nehmen die Umgebung gern mit auf. Als Datei einhängen statt als Variable – dann steht er nirgends in der Umgebung. Die zwei Stationen, die man nicht sieht Die Image-Schicht Ein Passwort, das beim Bauen in eine Schicht Gerät, bleibt darin – auch wenn eine spätere Schicht die Datei löscht. Schichten werden gestapelt, nie überschrieben. Wer das Image hat, hat den Wert. Gegenmittel: mehrstufiger Build, oder BuildKit-Secrets. Der Helm-Release-Verlauf Helm legt die verwendeten Werte je Revision als Secret ab – siehe Zeichnung 26. Ein einmal per --set übergebenes Passwort steht damit im Verlauf, auch nachdem man es längst geändert hat. Gegenmittel: auf ein bestehendes Secret verweisen statt Werte zu übergeben. Der rote Faden: ein Passwort ist an jeder Station so sicher wie der Zugriff auf diese Station – und es hinterlaesst an fast jeder eine Spur, die laenger lebt als es selbst. Deshalb ist „Passwort geändert“ nicht dasselbe wie „altes Passwort weg“. Was in dieser Sammlung deshalb überall auftaucht: --password-stdin statt -p, einfache Anführungszeichen um Variablen, die erst im Container aufgelöst werden sollen, und Umgebungsvariablen statt fest eingetragener Werte. Drei Gewohnheiten, die je eine Station absichern.
Acht Stationen, und nur eine davon ist wirklich sicher. Interessanter als der Weg selbst sind die beiden Stationen unten: Image-Schicht und Helm-Verlauf halten einen Wert fest, nachdem man ihn geändert hat. Beide sieht man nicht, wenn man nur den Weg von links nach rechts betrachtet.
bisherProblem
.env in .gitignoreliegt unverschlüsselt auf Laptop und Server, nicht in Git – also nicht reproduzierbar, kein „wer hat wann was geändert“, weg bei Server-Neuaufbau
$env:DB_PW = '...' im Terminalsteht in der PowerShell-History, verschwindet beim Neustart, nicht teilbar
Secret als GitHub-Repo-Secretok für CI – aber du kannst es nicht mehr lesen, keine Historie, an GitHub gebunden
Secret im Chat / in der Doku
ZielSecret-Dateien liegen verschlüsselt im Repo – committen ist ok
Empfängerentschlüsseln kann nur, wessen age-Schlüssel in der Datei steht: Laptop, Server, CI
Nutzungbeim Deploy: sops -d secrets/prod.env → die App liest die Klartext-Datei (chmod 600, am besten tmpfs)
RotierenEmpfänger raus → sops updatekeys, und den Wert selbst neu setzen

SOPS (Secrets OPerationS, von Mozilla) verschlüsselt die Werte einer .env/.yaml/.json, lässt die Schlüsselnamen lesbar – Git-Diffs bleiben also aussagekräftig. age ist die Verschlüsselung darunter (modern, ein kleines Binary, ein einzeiliger Public Key).

1 age-Schlüssel erzeugen

Ein Schlüsselpaar pro Ort, der entschlüsseln können soll: dein Laptop, jeder Server, die CI.

Laptop · PowerShell
winget install FiloSottile.age
age-keygen -o "$env:APPDATA\sops\age\keys.txt"
# Ausgabe: "Public key: age1ql3z...".  Die private Haelfte steht IN der Datei.
  • Die keys.txt (privat) verlässt den Rechner nie und wird nie committet. Zusatz-Kopie in den Passwortmanager – geht sie verloren, entschlüsselst du nichts mehr.
  • Der Public Key (age1…) ist harmlos – er kommt gleich in .sops.yaml.
  • Server: dieselbe Zeile als root, Datei nach /root/.config/sops/age/keys.txt (chmod 600). Bootstrap: einmal per SSH (Secure Shell) hinlegen oder in den Snapshot backen.

2 SOPS & .sops.yaml

Laptop · PowerShell
winget install Mozilla.SOPS
Datei · .sops.yaml (im Repo-Root, wird committet)
creation_rules:
  # prod-Secrets: Laptop + Server + CI duerfen entschluesseln
  - path_regex: secrets/prod/.*\.(env|ya?ml|json)$
    age: >-
      age1ql3z...laptop,
      age1abc4...server,
      age1def7...ci

  # alles andere: nur Laptop
  - path_regex: .*
    age: age1ql3z...laptop

Die Regeln greifen von oben nach unten, erste passende gewinnt. Mehr Umgebungen → mehr path_regex-Blöcke (secrets/staging/… mit anderen Empfängern).

3 Ein Secret verschlüsseln

Laptop · PowerShell
# neue Datei anlegen/bearbeiten: EDITOR oeffnet Klartext, beim Speichern wird verschluesselt
$env:EDITOR = 'code --wait'
sops secrets/prod/app.env

# oder eine bestehende .env in-place verschluesseln
sops -e -i secrets/prod/app.env

git add .sops.yaml secrets/prod/app.env
git commit -m "app-secrets (verschluesselt)"
secrets/prod/app.env – so sieht es im Repo aus
DB_PASSWORD=ENC[AES256_GCM,data:9f2K...,iv:...,tag:...,type:str]
JWT_SECRET=ENC[AES256_GCM,data:pQ7...,iv:...,tag:...,type:str]
sops_age__list_0__map_recipient=age1abc4...server
sops_mac=ENC[AES256_GCM,data:...]
sops_version=3.9.4
Was committen ist

Die ENC[…]-Datei und .sops.yaml. Nie die keys.txt. Der Diff zeigt welcher Schlüssel sich geändert hat (Name lesbar), nicht den Wert.

4 Formate

DateiSOPS verschlüsselt
.env / dotenvalle Werte, Keys lesbar – direkt als env_file nach dem Entschlüsseln nutzbar
.yaml / .jsonalle Blätter, mit encrypted_regex: '^(password|secret|token|key)$' nur bestimmte
.txt / binarykomplett (--input-type binary) – z. B. ein TLS-Key oder die keys.txt eines anderen Servers
ohne Datei auf der Platte
sops exec-env secrets/prod/app.env -- ./mein-tool          # Vars nur im Prozess
sops exec-file secrets/prod/config.yaml 'app --config {}'  # Klartext in tmpfs, Pfad als {}
sops -d --extract '["database"]["password"]' secrets/prod/app.yaml   # nur ein Wert

5 Auf dem Server entschlüsseln

Der Server hat seinen eigenen age-Schlüssel (Schritt 1). Der Deploy holt das Repo und schreibt die Klartext-Datei dorthin, wo Compose sie erwartet.

Datei · /opt/stacks/app/deploy.sh
#!/bin/sh
set -eu
export SOPS_AGE_KEY_FILE=/root/.config/sops/age/keys.txt
cd /opt/repo && git pull --ff-only

install -m 600 /dev/null /opt/stacks/app/app.env
sops -d /opt/repo/secrets/prod/app.env > /opt/stacks/app/app.env

cd /opt/stacks/app && docker compose up -d
Klartext-Datei kurz halten

Am saubersten liegt app.env in einem tmpfs (RAM, nie auf Platte): in compose.yaml tmpfs: /run/secrets plus ein Init-Container, der sops -d dorthin schreibt. Oder sops exec-env … -- docker compose up und die App liest die Vars aus der Umgebung. Minimal reicht: Datei chmod 600, gehört root.

systemd-Alternative: systemd-creds encrypt + LoadCredentialEncrypted= bindet an den Host-Schlüssel (TPM) – sehr dicht, aber nicht portabel und nicht in Git. Gut für „nur auf dieser einen Maschine“, schlecht für „reproduzierbar aus dem Repo“.

6 In CI

Ein einziges echtes GitHub-Secret – den age-Schlüssel der CI. Alles andere liegt verschlüsselt im Repo.

  1. CI-age-Schlüssel erzeugen, Public Key in .sops.yaml aufnehmen, sops updatekeys secrets/prod/*.
  2. Die private Haelfte als Repo-Secret SOPS_AGE_KEY (Settings Secrets Actions).
im Workflow
      - uses: actions/checkout@v4
      - run: curl -sSL https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64 -o /usr/local/bin/sops && chmod +x /usr/local/bin/sops
      - env:
          SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
        run: |
          sops -d secrets/prod/app.env > app.env
          # ... deployen (siehe CI/CD-Anleitung); app.env danach nicht als Artefakt hochladen!

Noch besser: die CI entschlüsselt gar nicht, sondern der Server macht es beim Pull (Abschnitt 5). Dann kennt die CI keine Secrets – Pull-Modell in der CI/CD-Anleitung.

7 In Compose / systemd

compose.yaml
services:
  app:
    image: ghcr.io/nursude/cloud-host/app:latest
    env_file: [ ./app.env ]          # die von sops -d geschriebene Datei
    restart: unless-stopped

Alternativ Docker/Podman-Secrets statt Env-Vars (landen als Datei unter /run/secrets/<name>, tauchen nicht in docker inspect auf): docker compose mit secrets: und die App liest *_FILE-Varianten (POSTGRES_PASSWORD_FILE …).

Ein Muster für den ganzen cloud-host

dienste-stack.ps1 / deploy.ps1 vor dem compose up ein sops -d je Stack ausführen lassen – ein Ort für alle Secrets, eine Datei je Stack, alles im Repo nachvollziehbar.

8 Rotieren & Zugriff entziehen

Empfänger ändern (Gerät verloren, Person raus)

Laptop
# .sops.yaml: den alten age1... entfernen, ggf. neuen dazu, dann:
sops updatekeys secrets/prod/app.env
git commit -am "sops: alten Schluessel entfernt"
Git vergisst nicht

updatekeys verschlüsselt die aktuelle Datei neu – die alte Version in der Git-Historie kann der alte Schlüssel weiter lesen. Ein wirklich kompromittiertes Secret musst du deshalb am Wert selbst rotieren (neues DB-Passwort setzen, Token neu ausstellen), nicht nur die Empfänger tauschen.

Wert rotieren

  • Neuen Wert erzeugen, im Dienst setzen (DB: ALTER USER, Token: neu ausstellen, alten widerrufen).
  • sops secrets/prod/app.env → Wert ersetzen → committen → deployen.
  • Regelmäßig für die wichtigsten (DB-Root, API-Tokens) – z. B. halbjährlich, oder sofort bei Verdacht.

9 Betrieb

Dingwohin
age-keys.txt (Laptop, Server, CI)nie in Git. Laptop: Passwortmanager-Kopie. Server: /root/.config/… + im Snapshot. CI: das eine GitHub-Secret.
verschlüsselte Secret-Dateienins Repo, in secrets/
.sops.yamlins Repo
entschlüsselte app.env auf dem Serverchmod 600, am besten tmpfs, in .gitignore falls im Repo-Ordner
  • Backup der age-Identitäten ist Teil des Katastrophenplans – ohne sie ist jedes Secret im Repo verloren. Gehört in den Passwortmanager und außer Haus (verschlüsselter USB-Stick beim Vertrauensperson).
  • Pre-commit-Hook: gitleaks oder ein einfacher Grep, der unverschlüsselte Secrets im Commit blockiert – SOPS hat auch einen .git/hooks-Helfer.
  • Neue Person / neuer Server: deren Public Key in .sops.yaml, sops updatekeys secrets/**, committen – ab dann können sie entschlüsseln.

+ Alternativen

WerkzeugModellwann
SOPS + age (diese Anleitung)Secrets verschlüsselt im Git-Repo1–5 Personen, GitOps, keine laufende Infrastruktur nötig
Vaultwarden-Organisationzentraler Tresor, manuell abrufenInfra-Secrets, die Menschen (nicht Automaten) brauchen – ihr habt Vaultwarden ja schon
Infisical / Dopplergehosteter Secret-Store + CLI (Command-Line Interface)/SDK (Software Development Kit), injiziert zur Laufzeitmehr Komfort, Weboberfläche, Audit-Log, Free-Tier – aber ein weiterer Dienst / Abhängigkeit
HashiCorp Vault / OpenBaoeigener Secret-Server, dynamische Credentials, Policiesviele Dienste, Teams, dynamische DB-Zugänge – deutlich mehr Betrieb
systemd-credshost-gebunden (TPM)„nur auf dieser Maschine“, keine Portabilität nötig

+ Kubernetes

  • helm-secrets (Plugin): helm secrets install … -f secrets.yaml – die secrets.yaml ist SOPS-verschlüsselt im Chart-Repo. Passt zum Helm-Deploy aus der Bibliothek-Enterprise-Anleitung.
  • External Secrets Operator: der Cluster zieht Secrets aus einem externen Store (Infisical, Vault, AWS SM) und legt sie als Secret an – nichts Sensibles im Manifest-Repo.
  • Sealed Secrets (Bitnami): mit dem Cluster-Public-Key verschlüsseltes SealedSecret ins Repo. Nur dieser Cluster kann es öffnen.
  • Nie ein rohes kind: Secret (nur base64!) ins Git.

+ Kosten

Preise sind Richtwerte. Hetzner hat 2026 zweimal erhöht (zuletzt 15. Juni: CX/CAX +30–40 %, CPX/CCX über 100 %). Aktuelle Server-Preise, alle Nebenposten, ein Rechner und Beispielrechnungen stehen zentral in Kosten & Budget.

SOPS und age sind Open Source, kein laufender Dienst – 0 €. Infisical und Doppler haben brauchbare Free-Tiers (bis ~5 Nutzer / ein Projekt). Vault/OpenBao betreibst du selbst (ein kleiner Container). Der eine GitHub-Actions-Secret für den CI-age-Key ist gratis.

⌂ Cockpit