Skip to content
Das Datenbankpasswort ohne Ausfall rotieren: das, was Sie schon lange aufschieben
← ← Zurück zu Gedanken Cloud

Das Datenbankpasswort ohne Ausfall rotieren: das, was Sie schon lange aufschieben

Alle sind sich einig, dass das Datenbankpasswort rotieren sollte. Bei fast niemandem tut es das, und der Grund ist nicht Faulheit. Es ist, dass beim ersten Versuch etwas kaputtgeht: ein Connection Pool, der das alte Passwort hält, eine Lambda, die es beim Kaltstart gecacht hat, ein Hintergrundjob, der es aus einer beim Deploy gesetzten Umgebungsvariable gelesen hat. Also wird die Rotation zurückgerollt, ein Ticket geöffnet, und das Passwort bleibt zwei Jahre gleich.

Unseres rotiert alle dreißig Tage, automatisch, und die Anwendung merkt nichts. Das hier ist der Mechanismus, der größtenteils von AWS stammt, und die drei Änderungen an der Anwendung, die ihn sicher gemacht haben, die von uns stammen.

Warum die naive Rotation Dinge zerstört

Rotation mit einem einzelnen Datenbanknutzer läuft so: neues Passwort erzeugen, ALTER USER app PASSWORD 'new', Secret aktualisieren. Zwischen dem ALTER und dem Moment, in dem jeder Konsument das Secret neu gelesen hat, wird jede neue Verbindung mit dem alten Passwort abgelehnt. Bestehende Verbindungen überleben, weil Postgres beim Verbindungsaufbau authentifiziert, aber ein Pool, der in diesem Fenster eine neue Verbindung öffnet, scheitert, und ein Dienst, der das Secret nur beim Start liest, scheitert bei jeder Neuverbindung, bis er neu gestartet wird.

Das Fenster kann Sekunden dauern, wenn alles prompt neu liest. Es kann Stunden dauern, wenn etwas cacht. In der Praxis ist es „bis zum nächsten Deploy“, weil dann die Umgebungsvariablen aufgefrischt werden, und das ist der Ausfall.

Alternierende Nutzer: die Rotation, die nie ein genutztes Passwort ungültig macht

Die Multi-User-Rotationsstrategie des Secrets Manager verwendet zwei Datenbanknutzer, app und app_clone, mit identischen Rechten. Zu jedem Zeitpunkt ist einer der aktuelle Nutzer im Secret und der andere ruht. Die Rotation ändert das Passwort des ruhenden Nutzers, testet es und schwenkt dann das Secret auf ihn um. Der bisher aktuelle Nutzer, dessen Passwort jeder Konsument halten könnte, bleibt bis zur nächsten Rotation dreißig Tage später unangetastet, und bis dahin hat jeder Konsument das Secret viele Male neu gelesen.

Alternierende Nutzer · das genutzte Passwort ist nie das geänderte Tag 0Tag 30 · RotationTag 60 · Rotation Secret → app · P1 · jeder Konsument nutzt das app_clone · P0 · ruhend app · P1 · weiter gültig, ruhend, keine Verbindung bricht Secret → app_clone · P2 · neue Verbindungen nutzen das 1 · Passwort von app_clone auf P2 setzen 2 · Verbindung testen 3 · Secret umschwenken Ein Konsument mit P1 arbeitet noch 30 Tage weiter. Bis dahin hat er das Secret viele Male neu gelesen. Tag 60 macht dasselbe umgekehrt: app bekommt P3, das Secret schwenkt zurück.

In CDK, auf einem Cluster, dessen Zugangsdaten aus einem generierten Secret stammen, sind es ein paar Zeilen:

cluster.addRotationMultiUser('Rotation', {
  secret: appUserSecret,                 // das Secret des Nutzers 'app', mit gesetztem masterarn
  automaticallyAfter: Duration.days(30),
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});

CDK deployt AWS' Rotations-Lambda aus dem Serverless Application Repository ins VPC, verbindet sie mit dem Secret und plant sie ein. Die Lambda muss sowohl die Datenbank erreichen (sie liegt im VPC) als auch den Secrets Manager (ein Interface-Endpunkt oder ein NAT; wir nutzen den Endpunkt). Der Nutzer app_clone wird von der Lambda bei der ersten Rotation angelegt, falls er nicht existiert, mit denselben Rechten wie app, und das ist der eine Punkt, den man von Hand prüfen sollte: bekommt app später durch eine Migration weitere Rechte, braucht app_clone sie ebenfalls. Wir lösen das, indem wir Rechte einer Rolle erteilen, in der beide Nutzer Mitglied sind, sodass Grants einmal gemacht werden.

Die drei Anwendungsänderungen

Der Rotationsmechanismus ist per Konstruktion sicher. Die Anwendung muss das neue Secret trotzdem irgendwann lesen, und wie sie das tut, entscheidet, ob die Rotation unsichtbar ist oder ein Ausfall in Zeitlupe.

1. Das Secret beim Verbinden lesen, nicht beim Start. Die Verbindungsfabrik des Pools ruft bei jedem Öffnen einer Verbindung den Secrets Manager auf (über einen Cache mit fünf Minuten TTL). Ein langlebiger Dienst übernimmt ein neues Secret innerhalb von fünf Minuten nach dem Umschwenken, ohne Neustart. App Runners runtimeEnvironmentSecrets injiziert den Wert beim Instanzstart, was für einen Dienst, der wöchentlich neu deployt wird, in Ordnung ist und für einen, der einen Monat läuft, falsch. Speziell für die Datenbank-Zugangsdaten lesen wir deshalb im Code aus dem Secrets Manager statt aus der Umgebung.

2. Bei einem Authentifizierungsfehler einmal neu holen und wiederholen. Doppelt genäht: Scheitert ein Verbindungsversuch mit 28P01 (ungültiges Passwort), wird der Cache invalidiert, das Secret neu geholt und einmal wiederholt. Das deckt den Fall ab, dass die Cache-TTL genau im Moment des Umschwenkens noch nicht abgelaufen ist. Es sind zehn Zeilen, und sie haben in Produktion genau so oft gegriffen, wie wir rotiert haben: etwa einmal im Monat, still, in den Logs.

async function connect(): Promise<Client> {
  try {
    return await open(await creds.get());
  } catch (e) {
    if (isAuthError(e)) { creds.invalidate(); return open(await creds.get()); }
    throw e;
  }
}

3. Für die Lambdas RDS Proxy die Arbeit machen lassen. Die Funktionen im VPC verbinden sich über RDS Proxy, und der Proxy authentifiziert sich bei der Datenbank mit dem Secret selbst: er beobachtet den Secrets Manager und übernimmt Rotationen eigenständig. Die Funktionen authentifizieren sich beim Proxy mit IAM, sodass sie nie ein Datenbankpasswort halten. Rotation ist für sie per Design ein Nicht-Ereignis. Es behebt außerdem das Verbindungssturm-Problem, das Lambdas mit Postgres haben, was der Grund ist, warum wir den Proxy ohnehin empfehlen würden.

App-Runner-DienstSecret beim Verbinden · 5-Min-CacheAuth-Fehler → einmal neu holen Lambda-FunktionenIAM-Auth beim Proxyhalten nie ein Passwort Secrets Managerapp / app_clone · AWSCURRENTRotations-Lambda alle 30 Tage RDS Proxyliest das Secret selbstfolgt Rotationen · poolt Verbindungen AuroraNutzer app und app_clonegleiche Rechte über eine Rolle

Die erste Rotation, in Staging, mit Stoppuhr

Wir haben sie nicht zuerst in Produktion eingeschaltet. In Staging haben wir manuell eine Rotation ausgelöst (aws secretsmanager rotate-secret), das Log der Lambda beobachtet und währenddessen einen Lastgenerator gegen die API laufen lassen. Ergebnisse des ersten Versuchs:

Moment Was passierte
T+0 s Rotation startet; Lambda setzt Passwort von app_clone, testet es, schwenkt das Secret um
T+4 s Umschwenken abgeschlossen. Jede bestehende Verbindung weiter in Ordnung (sie halten app)
T+0 bis T+300 s Neue Verbindungen aus dem App-Runner-Pool nutzen noch die gecachten app-Zugangsdaten. Weiter in Ordnung, weil app noch gültig ist
T+300 s Cache läuft ab; die nächste neue Verbindung holt app_clone. Funktioniert
T+5 Min bis T+30 Tage Beide Nutzer gültig. Kein Konsument kann überrascht werden

Null Fehler im Lastgenerator. Das Einzige, was scheiterte, war ein Cron-Job in einem anderen Konto, der das Passwort in einer Umgebungsvariable hatte: er lief dreißig Tage weiter (sein Nutzer war noch gültig) und brach dann bei der zweiten Rotation, was genau der verzögerte Fehler ist, den die alternierende Strategie für Konsumenten erzeugt, die nicht neu lesen. Genau dafür macht man es zuerst in Staging. Der Cron-Job liest jetzt das Secret.

Die Checkliste

  • Multi-User-Rotation, nie Single-User. Der zusätzliche Datenbanknutzer ist kostenlos und macht den ganzen Unterschied.
  • Beide Nutzer bekommen Rechte über eine gemeinsame Rolle, sodass eine Migration, die der Rolle Rechte erteilt, beide abdeckt.
  • Die Rotations-Lambda lebt im VPC und braucht einen Pfad zum Secrets Manager: Interface-Endpunkt oder NAT.
  • Jeder Konsument liest das Secret beim Verbinden über einen Cache mit kurzer TTL oder geht per IAM über RDS Proxy.
  • Bei 28P01 einmal neu holen.
  • Erste Rotation in Staging, manuell ausgelöst, unter Last, mit offenem Log.
  • Dann jede Umgebungsvariable, .env, jeden Cron und jedes Skript nach dem alten Passwort greppen. Was es noch hat, bricht in dreißig bis sechzig Tagen. Besser jetzt finden.

Dreißig-Tage-Rotation, sechs Monate, sechs Rotationen, null Vorfälle. Das Passwort, das niemand gelesen hat, hat sich sechsmal geändert, und niemand hat es gemerkt, und genau so sollte sich ein Secret anfühlen.

Wenn Ihr Datenbankpasswort einen Geburtstag hat, bringen wir es an einem Tag zum Rotieren, zuerst in Staging.