Skip to content
Rotația parolei de bază de date fără downtime: lucrul pe care îl tot amâni
← ← Înapoi la Idei Cloud

Rotația parolei de bază de date fără downtime: lucrul pe care îl tot amâni

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.

Utilizatori alternanți · parola în uz nu e niciodată cea schimbată ziua 0ziua 30 · rotațieziua 60 · rotație secret → app · P1 · fiecare consumator folosește asta app_clone · P0 · inactiv app · P1 · încă valid, inactiv, nicio conexiune nu se rupe secret → app_clone · P2 · conexiunile noi folosesc asta 1 · setează parola app_clone pe P2 2 · testează conexiunea 3 · comută secretul Un consumator care ține P1 merge încă 30 de zile. Până atunci a recitit secretul de multe ori. Ziua 60 face același lucru invers: app primește P3, secretul comută înapoi.

Î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.

serviciul App Runnersecret la conectare · cache 5 mineroare auth → recitește o dată funcțiile Lambdaauth IAM la proxynu țin niciodată o parolă Secrets Managerapp / app_clone · AWSCURRENTLambda de rotație la 30 de zile RDS Proxycitește singur secretulurmărește rotațiile · pool de conexiuni Aurorautilizatorii app și app_cloneaceleași grant-uri printr-un rol

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.