Artikel

Geheimnisse gehören nicht dauerhaft in die .env

Eine .env-Datei löst das Problem „Zugangsdaten im Code“. Sie löst nicht das Problem „Zugangsdaten im Klartext auf der Platte“. Wie ich Runtime-Zugangsdaten verschlüsselt in die Datenbank verlegt habe.

BlackZackBlackzack

1022 Wörter · 5 Min. Lesezeit

  • sicherheit
  • architektur
  • betrieb

2019 habe ich meinen Bot-Token auf GitHub veröffentlicht und danach gelernt, dass Zugangsdaten nicht in den Code gehören. Die Lösung war eine .env-Datei — und die habe ich fünf Jahre lang für ausreichend gehalten.

Ist sie nicht. Sie ist eine Verbesserung, kein Ziel.

Was eine .env löst und was nicht

Gelöst: Das Geheimnis landet nicht in der Versionsverwaltung, wird nicht mit dem Code weitergegeben und taucht nicht in einem öffentlichen Repository auf.

Nicht gelöst:

  • Sie liegt im Klartext auf der Platte. Wer Dateizugriff hat, hat alles.
  • Sie landet in Sicherungen. Ein Archiv des Anwendungsordners enthält sämtliche Zugangsdaten.
  • Sie steht in der Prozessumgebung. Unter bestimmten Umständen können andere Prozesse die Umgebung lesen.
  • Sie taucht in Fehlerberichten auf, wenn jemand ein Konfigurationsobjekt vollständig ausgibt.
  • Sie ändert sich nur beim Neustart. Einen Schlüssel zu tauschen bedeutet Ausfallzeit.
  • Es gibt keine Historie. Wer hat wann was geändert?

Der letzte Punkt war es, der mich zum Umbau gebracht hat. Als ich einen Token austauschen musste, konnte ich nicht sagen, wann der alte gesetzt wurde und ob er anderswo noch benutzt wird.

Der Aufbau, für den ich mich entschieden habe

Die .env bleibt — als Startkonfiguration. Sie enthält genau die Werte, die nötig sind, damit die Anwendung überhaupt hochkommt: den Pfad zur Datenbank, den Hauptschlüssel für die Verschlüsselung, und beim allerersten Start die Zugangsdaten, mit denen der Betrieb beginnt.

Alles Weitere liegt verschlüsselt in der Datenbank. Bot-Token, Client-Geheimnis, Zugangsdaten für externe Dienste. Verschlüsselt mit einem Verfahren, das Manipulation erkennt.

Klartext ──► AES-256-GCM (Schlüssel aus .env) ──► "enc:v1:<nonce>:<geheimtext>:<prüfwert>"

Drei Details daran sind wichtig:

Das Präfix mit Versionsangabe. Damit kann ich später das Verfahren wechseln und alte Werte weiter entschlüsseln, statt alles auf einmal umstellen zu müssen.

Ein Verfahren mit Authentifizierung. Verschlüsselung allein schützt gegen Mitlesen, nicht gegen Verändern. Ein Verfahren mit Prüfwert merkt, wenn jemand am Geheimtext manipuliert hat.

Eine eigene Nonce pro Wert. Sie darf nie wiederverwendet werden.

Die Reihenfolge beim Lesen

async function ermittleZugangsdaten() {
  const ausDb = await ladeAusDatenbank();     // entschlüsselt
  if (ausDb?.token) return ausDb;             // Vorrang
  return { token: process.env.TOKEN };        // Startkonfiguration
}

Datenbank schlägt Umgebung. Das ist die entscheidende Regel: Sobald ein Wert über die Verwaltung gesetzt wurde, gilt er — auch wenn in der .env noch etwas anderes steht. Sonst hat man wieder zwei Wahrheiten, und die .env gewinnt ausgerechnet nach einem Neustart.

Was dabei zusätzlich entstanden ist

Maskierung an einer Stelle. Eine Funktion, die bekannte Geheimnisfelder erkennt und ihren Inhalt ersetzt, bevor irgendetwas ausgegeben wird. Sie hängt in der Protokollierung und in jeder Antwort der Verwaltungsschnittstelle.

function maskiere(wert) {
  if (!wert || wert.length < 8) return "***";
  return `${wert.slice(0, 4)}${wert.slice(-2)}`;
}

Der Anfang bleibt sichtbar, damit man erkennen kann, ob der richtige Wert gesetzt ist, ohne ihn zu kennen. Das ist im Support Gold wert.

Eine Änderungshistorie. Wer hat wann welches Feld geändert. Nicht der Wert selbst, nur die Tatsache und der Zeitpunkt.

Tausch im laufenden Betrieb. Ein neuer Wert wird gesetzt, die betroffene Komponente lädt neu. Kein Neustart, keine Ausfallzeit.

Warnung

Der Hauptschlüssel bleibt in der .env und ist damit weiterhin im Klartext auf der Platte. Das ist kein Widerspruch, sondern eine Verschiebung: Ein Geheimnis statt fünfzehn. Wer den Hauptschlüssel hat und Zugriff auf die Datenbank, kommt an alles — aber die Datenbanksicherung allein nützt nichts mehr, und eine versehentlich weitergegebene Konfigurationsdatei ebenfalls nicht. Für echte Isolation bräuchte es einen Schlüsselspeicher oder Hardware, und das ist für dieses Projekt die falsche Größenordnung.

Der Ablauf, mit dem ich ein Geheimnis tausche

Damit der Tausch tatsächlich stattfindet, muss er billig sein. Bei mir ist es dieser Ablauf, und er dauert wenige Minuten.

  1. Neuen Wert beim Anbieter erzeugen. Der alte bleibt zunächst gültig.
  2. Neuen Wert über die Verwaltung setzen. Er wird verschlüsselt gespeichert.
  3. Die betroffene Komponente neu laden, nicht den ganzen Dienst.
  4. Prüfen, ob der neue Wert wirkt — an einer beobachtbaren Wirkung, nicht daran, dass kein Fehler kommt.
  5. Alten Wert beim Anbieter zurückziehen.
  6. Im Protokoll vermerken, wann getauscht wurde.

Der entscheidende Punkt ist Schritt 3. Solange ein Tausch einen vollständigen Neustart verlangt, ist er ein Vorgang mit Ausfallzeit — und Vorgänge mit Ausfallzeit werden aufgeschoben.

Die Frage, die man sich einmal stellen sollte

Beim Aufräumen dieser Themen habe ich mir eine Liste gemacht, die ich seitdem für jedes System führe:

Welche Geheimnisse gibt es? Nur die Namen, nicht die Werte.

Wozu gehört jedes? Welcher Dienst, welche Berechtigung.

Wo liegt es? Umgebung, Datenbank, Datei.

Wann wurde es zuletzt getauscht?

Was wäre der Schaden, wenn es öffentlich wird?

Diese Liste zu erstellen war unangenehm — sie hatte mehr Einträge als erwartet, und bei mehreren konnte ich die letzte Frage nicht auf Anhieb beantworten.

Genau darin liegt ihr Wert. Im Ernstfall ist die erste Frage immer: Was ist betroffen? Wer diese Liste hat, antwortet in Minuten. Wer sie nicht hat, sucht stundenlang — und findet erfahrungsgemäß nicht alles.

Was mit dem Hauptschlüssel passiert

Die naheliegende Rückfrage zu diesem Aufbau lautet: Wenn alles mit einem Schlüssel verschlüsselt ist und der im Klartext daneben liegt — was ist gewonnen?

Die Antwort ist eine Verschiebung des Angriffswegs, und die ist konkret:

Eine Datenbanksicherung allein nützt nichts. Sicherungen werden kopiert, verschickt, auf anderen Systemen abgelegt. Das ist der realistischste Weg, auf dem Daten abhandenkommen.

Eine versehentlich weitergegebene Konfigurationsdatei nützt nichts — sie enthält nur den Schlüssel, nicht die Werte.

Ein Zugriff auf das Dateisystem des laufenden Systems nützt alles. Dagegen hilft dieses Verfahren nicht.

Wer auch das ausschließen will, braucht einen getrennten Schlüsselspeicher oder Hardware. Für ein Projekt dieser Größe ist das die falsche Größenordnung — wichtig ist, dass ich es benennen kann, statt zu glauben, alles sei abgesichert.

Was ich mir dabei angewöhnt habe

Beim Anlegen jedes neuen Geheimnisses die Frage: Wie tausche ich es aus? Wenn die Antwort „Neustart aller Dienste" lautet, ist der Tausch teuer — und ein teurer Tausch wird nicht gemacht, auch wenn er nötig wäre.

Werte erzeugen, nicht ausdenken. openssl rand -hex 32 statt eines selbst gewählten Wertes. Jedes Mal, ohne Ausnahme.

Bei jedem Verdacht tauschen. Nicht abwägen, ob es wahrscheinlich ist. Ein Tausch, der zehn Minuten dauert, ist billiger als jede Analyse.

Diese Haltung kommt aus 2019, als Discord meinen veröffentlichten Token automatisch für ungültig erklärt hat. Ich hatte damals Glück, dass jemand anderes die Entscheidung für mich getroffen hat. Fünf Jahre später ist es eine Sache von Minuten — und das war der eigentliche Zweck des ganzen Umbaus.