Toată lumea e de acord că parola bazei de date ar trebui rotită. Aproape nimeni n-o face, iar motivul nu e lenea. E că prima dată când încerci, ceva se strică: un pool de conexiuni care ține parola veche, o Lambda care a cache-uit-o la cold start, un job de fundal care a citit-o dintr-o variabilă de mediu setată la deploy. Așa că rotația e dată înapoi, se deschide un tichet, iar parola rămâne aceeași doi ani.
Noi o rotim la treizeci de zile, automat, iar aplicația nu observă. Ăsta e mecanismul, care e în mare parte al AWS, și cele trei schimbări în aplicație care l-au făcut sigur, care sunt ale noastre.
De ce rotația naivă strică lucruri
Rotația cu un singur utilizator de bază de date merge așa: generezi o parolă nouă, ALTER USER app PASSWORD 'new', actualizezi secretul. Între ALTER și momentul în care fiecare consumator a recitit secretul, orice conexiune nouă cu parola veche e refuzată. Conexiunile existente supraviețuiesc, pentru că Postgres autentifică la conectare, dar un pool care deschide o conexiune nouă în fereastra aia pică, iar un serviciu care citește secretul doar la pornire pică la fiecare reconectare până e repornit.
Fereastra poate fi de secunde dacă totul recitește prompt. Poate fi de ore dacă ceva cache-uiește. În practică e „până la următorul deploy”, pentru că atunci se reîmprospătează variabilele de mediu, și asta e pana.
Utilizatori alternanți: rotația care nu invalidează niciodată o parolă în uz
Strategia de rotație multi-user din Secrets Manager folosește doi utilizatori de bază de date, app și app_clone, cu grant-uri identice. În orice moment, unul e utilizatorul curent din secret, iar celălalt e inactiv. Rotația schimbă parola celui inactiv, o testează și apoi comută secretul spre el. Utilizatorul care era curent, a cărui parolă o poate ține orice consumator, rămâne neatins până la următoarea rotație, treizeci de zile mai târziu, moment în care fiecare consumator a recitit secretul de multe ori.
În CDK, pe un cluster ale cărui credențiale au venit dintr-un secret generat, sunt câteva linii:
cluster.addRotationMultiUser('Rotation', {
secret: appUserSecret, // secretul utilizatorului 'app', cu masterarn setat
automaticallyAfter: Duration.days(30),
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
CDK desfășoară Lambda de rotație a AWS din Serverless Application Repository în VPC, o leagă de secret și o programează. Lambda trebuie să ajungă atât la baza de date (e în VPC), cât și la Secrets Manager (un interface endpoint sau un NAT; noi folosim endpoint-ul). Utilizatorul app_clone e creat de Lambda la prima rotație, dacă nu există, cu aceleași grant-uri ca app, și asta e partea care merită verificată de mână: dacă app primește grant-uri adăugate mai târziu de o migrare, app_clone are nevoie și el de ele. Noi rezolvăm acordând drepturi unui rol din care fac parte ambii utilizatori, deci grant-urile se fac o singură dată.
Cele trei schimbări în aplicație
Mecanismul de rotație e sigur prin construcție. Aplicația tot trebuie să citească noul secret la un moment dat, iar felul în care o face decide dacă rotația e invizibilă sau o pană cu încetinitorul.
1. Citește secretul la conectare, nu la pornire. Fabrica de conexiuni a pool-ului apelează Secrets Manager (printr-un cache cu TTL de cinci minute) de fiecare dată când deschide o conexiune. Un serviciu cu viață lungă preia un secret nou în cinci minute de la o comutare, fără repornire. runtimeEnvironmentSecrets din App Runner injectează valoarea la pornirea instanței, ceea ce e în regulă pentru un serviciu care se redesfășoară săptămânal și greșit pentru unul care rulează o lună, așa că, specific pentru credențialele bazei de date, citim din Secrets Manager în cod, nu din mediu.
2. La o eroare de autentificare, recitește o dată și reîncearcă. Curea și bretele: dacă o încercare de conectare pică cu 28P01 (parolă invalidă), invalidezi cache-ul, iei secretul din nou și reîncerci o dată. Asta acoperă cazul în care TTL-ul cache-ului n-a expirat exact în momentul comutării. Sunt zece linii și s-au declanșat în producție exact de câte ori am rotit: cam o dată pe lună, în liniște, în loguri.
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. Lasă RDS Proxy s-o facă pentru Lambda-uri. Funcțiile din VPC se conectează prin RDS Proxy, iar proxy-ul se autentifică la baza de date cu secretul în sine: urmărește Secrets Manager și preia rotațiile singur. Funcțiile se autentifică la proxy cu IAM, deci nu țin niciodată o parolă de bază de date. Rotația, pentru ele, e un non-eveniment prin design. Rezolvă și problema furtunilor de conexiuni pe care le au Lambda-urile cu Postgres, care e motivul pentru care am recomanda oricum proxy-ul.
Prima rotație, în staging, cu cronometrul
N-am pornit-o întâi în producție. În staging, am declanșat manual o rotație (aws secretsmanager rotate-secret), am urmărit logul Lambda-ei și am rulat un generator de încărcare pe API tot timpul. Rezultatele primei încercări:
| Moment | Ce s-a întâmplat |
|---|---|
| T+0 s | Începe rotația; Lambda setează parola lui app_clone, o testează, comută secretul |
| T+4 s | Comutare completă. Fiecare conexiune existentă e în regulă (țin app) |
| T+0 până la T+300 s | Conexiunile noi din pool-ul App Runner folosesc încă credențialele app din cache. Tot în regulă, pentru că app e încă valid |
| T+300 s | Cache-ul expiră; următoarea conexiune nouă ia app_clone. Merge |
| T+5 min până la T+30 zile | Ambii utilizatori valizi. Niciun consumator nu poate fi prins pe picior greșit |
Zero erori în generatorul de încărcare. Singurul lucru care a picat a fost un cron job dintr-un alt cont care avea parola într-o variabilă de mediu: a continuat să meargă treizeci de zile (utilizatorul lui era încă valid) și apoi s-a rupt la a doua rotație, exact eșecul întârziat pe care îl produce strategia alternantă pentru consumatorii care nu recitesc. Ăsta e rostul de a face întâi în staging. Cron job-ul citește acum secretul.
Checklist-ul
- Rotație multi-user, niciodată single-user. Utilizatorul suplimentar de bază de date e gratuit și e toată diferența.
- Ambii utilizatori primesc grant-uri printr-un rol comun, ca o migrare care acordă drepturi rolului să-i acopere pe amândoi.
- Lambda de rotație trăiește în VPC și are nevoie de o cale spre Secrets Manager: interface endpoint sau NAT.
- Fiecare consumator citește secretul la conectare printr-un cache cu TTL scurt, sau trece prin RDS Proxy cu IAM.
- Recitește o dată la
28P01. - Prima rotație în staging, declanșată manual, sub încărcare, cu logul deschis.
- Apoi caută cu grep în fiecare variabilă de mediu,
.env, cron și script parola veche. Ce o mai are se va rupe în treizeci până la șaizeci de zile. Mai bine o găsești acum.
Rotație la treizeci de zile, șase luni, șase rotații, zero incidente. Parola pe care n-a citit-o nimeni s-a schimbat de șase ori și nimeni n-a observat, adică exact cum ar trebui să se simtă un secret.
Dacă parola bazei tale de date are o zi de naștere, o putem pune să se rotească într-o zi, întâi în staging.