Abschnitt 15 · Lektion 4
Secrets und Umgebung
Ein Secret ist alles, womit jemand anderes sich als du ausgeben kann. Das Datenbankpasswort, ein API-Schlüssel, der Schlüssel, mit dem du Sitzungen signierst, das Geheimnis, mit dem du eingehende Webhooks prüfst. Der gemeinsame Nenner: Wer es hat, ist für die Gegenstelle du.
Wohin sie gehören
Nicht in den Code. Der Grund ist nicht Ordnungsliebe, sondern dass Code kopiert wird: in jeden Klon des Repositorys, in jedes Backup, in jede Kopie auf dem Rechner eines Kollegen, in jeden CI-Lauf und in jedes Log, das die Datei einmal ausgibt.
Die Umgebung ist der richtige Ort dafür, und process.env kennst du aus 4.2.
// Vier Variablen, und nur zwei davon sind wirklich gesetzt.
process.env.DB_PASSWORT = "aus-der-umgebung";
process.env.DEBUG = "false";
process.env.API_SCHLUESSEL = "";
for (const name of ["DB_PASSWORT", "DEBUG", "API_SCHLUESSEL", "SIGNATUR_GEHEIMNIS"]) {
const wert = process.env[name];
console.log(
`${name.padEnd(20)} typeof ${(typeof wert).padEnd(10)} ` +
`wahr: ${String(Boolean(wert)).padEnd(6)} ${JSON.stringify(wert)}`
);
}
console.log("");
console.log('Der Text "false" ist wahr, und "" ist falsch. Genau deshalb prüft');
console.log("man auf Anwesenheit und wertet danach aus, nie in einem Schritt."); Vier Zeilen, und drei davon sind Stolperstellen. Jeder Wert ist ein String, auch wenn du eine
Zahl oder einen Wahrheitswert gemeint hast. Der Text "false" ist deshalb wahr, und das ist die
Falle aus 4.2. Eine Variable, die auf den leeren Text gesetzt ist, existiert und ist trotzdem
unbrauchbar. Und eine, die es gar nicht gibt, liefert undefined statt eines Fehlers.
Aus den letzten beiden Zeilen folgt die Prüfregel: Ein Secret ist erst da, wenn es da und nicht
leer ist. !process.env.NAME fängt beide Fälle, und mehr braucht es nicht.
Drei Dateien, drei Aufgaben
Im Alltag arbeiten drei Dateien zusammen, und ihre Rollen zu verwechseln ist der häufigste Fehler:
.envliegt auf deinem Rechner und enthält die echten Werte. Sie gehört niemandem außer dir, und sie darf niemals in die Versionskontrolle..gitignoresorgt dafür, dass das auch so bleibt. Ein Eintrag.envreicht, und er muss da sein, bevor du das erste Mal committest..env.exampleenthält dieselben Namen ohne Werte und wird eingecheckt. Sie ist die Antwort auf die Frage „was braucht dieses Projekt, damit es startet”, und ohne sie fragt jeder Neue im Team dieselbe Sache noch einmal.
Auf dem Server gibt es dann meist gar keine .env mehr. Die Werte kommen aus der Konfiguration des
Anbieters, aus einem Secret Manager oder aus der CI. Für deinen Code ändert sich dadurch nichts,
denn process.env liest an allen drei Orten dasselbe. Genau das ist der Vorteil.
Sofort abbrechen statt später raten
Wenn eine Pflichtvariable fehlt, hast du zwei Möglichkeiten. Entweder das Programm startet und scheitert drei Stunden später an einer Stelle, die mit der Ursache nichts zu tun hat, mit einer Fehlermeldung wie „cannot read properties of undefined”. Oder es sagt beim Start, was fehlt.
// Nur zwei der drei Pflichtvariablen sind gesetzt. Der Rest der
// Anwendung startet trotzdem, solange niemand hinschaut.
process.env.DB_PASSWORT = "aus-der-umgebung";
process.env.API_SCHLUESSEL = "sk_test_0815";
const PFLICHT = ["DB_PASSWORT", "API_SCHLUESSEL", "SIGNATUR_GEHEIMNIS"];
const fehlend = PFLICHT.filter((name) => !process.env[name]);
if (fehlend.length > 0) {
console.error(`Start abgebrochen. Diese Umgebungsvariablen fehlen: ${fehlend.join(", ")}`);
console.error("Trag sie in deine .env ein, die Namen stehen in .env.example.");
process.exit(1);
}
console.log("Alle Pflichtvariablen sind da, der Server kann starten."); Zwölf Zeilen, und sie ersparen dir eine Fehlersuche, die sonst regelmäßig einen halben Tag kostet. Drei Details lohnen den Blick: Die Meldung geht auf stderr, weil sie eine Fehlermeldung ist und kein Ergebnis. Sie nennt alle fehlenden Variablen auf einmal statt eine nach der anderen. Und sie sagt, wo die Namen stehen. Der Exit-Code ist 1, und daran erkennt jedes Startsystem, dass der Prozess nicht laufen wollte, sondern nicht konnte.
Und wenn doch eins im Repository gelandet ist
Das passiert, und es passiert regelmäßig. Wichtig ist, was man dann tut, und die verbreitete Antwort ist die falsche.
// So sähe die Git-Historie dieser einen Datei aus. Drei Fassungen, und
// in der letzten steht das Secret nicht mehr.
const historie = [
{ commit: "a1b2c3d", text: 'const apiSchluessel = "sk_live_4711";' },
{ commit: "e4f5a6b", text: 'const apiSchluessel = "sk_live_4711"; // TODO: raus hier' },
{ commit: "c7d8e9f", text: "const apiSchluessel = process.env.API_SCHLUESSEL;" },
];
const gesucht = "sk_live_4711";
for (const { commit, text } of historie) {
console.log(`${commit} ${text.includes(gesucht) ? "enthält das Secret" : "sauber"}`);
}
const treffer = historie.filter((f) => f.text.includes(gesucht)).map((f) => f.commit);
console.log("");
console.log(`Aktueller Stand: sauber`);
console.log(`Abrufbar in: ${treffer.join(", ")}`);
console.log("Wer das Repository klont, bekommt die ganze Liste mit."); Das Beispiel schaut auf drei Fassungen derselben Datei. In der aktuellen steht der Schlüssel nicht mehr, in den beiden davor schon, und beide sind weiterhin abrufbar. „Ich habe es im nächsten Commit rausgenommen” ändert daran nichts. Selbst die Historie umzuschreiben ändert wenig, wenn das Repository schon irgendwo geklont oder gespiegelt wurde.
Dazu kommt der Zeitfaktor: Ein öffentliches Repository wird nicht in Tagen abgegrast, sondern in Minuten. Es gibt fertige Werkzeuge, die neue Commits nach Mustern durchsuchen, die wie API-Schlüssel aussehen, und die laufen rund um die Uhr.
Deshalb lautet der erste Schritt immer gleich, und er hat nichts mit Git zu tun: Wechsle das Secret. Neuen Schlüssel erzeugen, alten ungültig machen, Anwendung umstellen. Erst danach ist es sinnvoll, aufzuräumen, die Historie zu bereinigen und zu prüfen, ob mit dem alten Schlüssel etwas passiert ist. Ein Secret, das einmal öffentlich war, ist verbrannt, und das Einzige, was hilft, ist ein neues.
Zum Mitnehmen
Ein Secret, das einmal im Repository lag, ist verbrannt. Der erste richtige Schritt ist nicht, den Commit zu löschen, sondern das Secret zu wechseln.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, auf dem Server geprüft
Öffnet sich mit dem Basis Konto.