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.
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.
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
28P01einmal 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.