Kein Klartext-Secret kommt durch — pre-commit, gitleaks und SOPS

Ein GitOps-Repo ist öffentlich, sobald man es vergisst. Drei Hooks sorgen dafür, dass ich gar nicht erst ein unverschlüsseltes Secret committen kann: gitleaks scannt auf Muster, ein SOPS-Hook verweigert Klartext in geschützten Pfaden, und ein lokaler Decrypt-Check stellt sicher, dass jede verschlüsselte Datei sauber entschlüsselt und alle Empfänger trägt.

Mein Lab-Repo ist öffentlich — abrufbar über meinen Radicle -Seed-Node seed.this-is-fine.io. Genau deshalb darf da nie ein Klartext-Secret hineinrutschen. Nicht „sollte nicht", sondern kann nicht: Drei pre-commit -Hooks fangen den Fehler ab, bevor er ein Commit-Objekt wird. Das ist die billigste Versicherung, die ich kenne — ein paar Zeilen YAML gegen einen Leak, den man nie ganz zurückholt.

Drei Schichten gegen denselben Fehler

Die Hooks greifen ineinander, jeder deckt eine andere Lücke ab:

git commit
   ├─ gitleaks ............ findet Secret-MUSTER (Keys, Tokens) per Entropie/Regex
   ├─ sops-pre-commit ..... verweigert KLARTEXT in .sops-Pfaden (.sops.yaml-Regeln)
   └─ check-sops .......... validiert: entschlüsselt sauber? alle Empfänger drin?

1. gitleaks — der Mustererkenner. gitleaks scannt den Diff auf alles, was wie ein Secret aussieht: AWS-Keys, JWTs, hochentropische Strings. Standardregeln plus eine kleine Erweiterung, denn nicht alles, was wie ein Key aussieht, ist eins:

# .gitleaks.toml
[extend]
useDefault = true

# Public Radicle / W3C DID identifiers — keine Secrets.
[[allowlists]]
description = "did:key public DIDs (Radicle assignees, delegates)"
regexTarget = "line"
regexes = ['''did:key:z[1-9A-HJ-NP-Za-km-z]+''']

Öffentliche did:key-Identifier aus meinem Radicle-Workflow sehen für einen Entropie-Scanner verdächtig aus, sind aber per Definition öffentlich. Pattern-Allowlists wandern in die .gitleaks.toml; einzelne, bewusst geduldete Fundstellen in eine .gitleaksignore mit <pfad>:<rule-id>:<zeile>-Fingerprints. Die Trennung ist wichtig: Muster-Ausnahmen sind generell, Fingerprints sind chirurgisch.

2. sops-pre-commit — der Klartext-Wächter. gitleaks erkennt bekannte Muster. Aber ein DB-Passwort ohne Struktur? Das rutscht durch. Hier greift die zweite Schicht: Sie verweigert, dass eine Datei, die laut .sops.yaml-creation_rules verschlüsselt sein müsste, im Klartext eingecheckt wird.

- repo: https://github.com/red-lichtie/sops-pre-commit-hook
  hooks:
    - id: sops-pre-commit-hook
      name: ensure sops-encrypted secrets
      files: \.sops
      exclude: ^(\.sops\.yaml|.*\.sops-k8s\.yaml\.tmpl|.*\.sops\.yaml\.tmpl)$

Der Hook kennt meine Verschlüsselungsregeln und prüft Pfad gegen Inhalt. Templates (.tmpl) und die Regeldatei selbst sind ausgenommen — sie sollen Klartext sein.

3. check-sops — der Empfänger-Check. Die dritte Schicht ist eigener Code und fängt einen subtileren Fehler: Eine Datei ist verschlüsselt, aber gegen die falsche Empfängerliste. Das passiert nach einem schlechten updatekeys oder wenn ich einen neuen Contributor vergessen habe. Der lokale Hook entschlüsselt jede geänderte SOPS-Datei (prüft den MAC) und verifiziert, dass alle Pflicht-Empfänger drin sind:

- repo: local
  hooks:
    - id: sops-decrypt-check
      name: validate SOPS decrypt + recipients
      entry: .taskfiles/core/scripts/check-sops.sh
      language: script
      files: ^.*\.sops(?:(\.|-).*)?$
      exclude: \.tmpl$

Damit ist die Kette geschlossen: gitleaks fängt das Muster, sops-pre-commit den fehlenden Schutz, check-sops den kaputten Schutz.

Der Rest ist Hygiene

Die übrigen Hooks sind unspektakulär, aber sie halten Diffs sauber und Skripte korrekt: check-yaml (mit --allow-multiple-documents für Kustomize-Streams), end-of-file-fixer, trailing-whitespace, remove-crlf/remove-tabs, und shellcheck für die Task-Skripte. fail_fast: false — ich will alle Befunde auf einmal, nicht beim ersten abbrechen.

pre-commit ist eine lokale Bremse, keine Mauer. Wer --no-verify tippt, umgeht alles. Der Schutz lebt davon, dass pre-commit install einmal lief und ich die Hooks nicht aushebele. Eine echte Mauer wäre serverseitig — ein Push-Hook am Seed-Node oder ein CI-Gate. Hier vertraue ich der lokalen Disziplin, weil ich der einzige Committer bin.

Warum das reicht

Es ist nicht perfekt, und das ist okay. Der teuerste Fehler — ein Klartext-Secret in der History eines öffentlichen Repos — braucht genau eine Sekunde Unaufmerksamkeit und ist danach nie wieder ganz weg. Diese drei Hooks kosten mich nichts im Alltag und stehen genau an der Stelle, an der der Fehler passiert: zwischen git add und einem unwiderruflichen Commit-Objekt. Wie die Secrets danach in den Cluster fließen — über SOPS, Vault und den External Secrets Operator —, ist eine eigene Geschichte.