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

<div class="article-figure">
<svg viewBox="0 0 900 250" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Cronologia rotației cu utilizatori alternanți. Ziua 0: secretul arată spre utilizatorul app cu parola P1; app_clone are parola P0, inactiv. Ziua 30, rotație: Lambda de rotație setează parola lui app_clone pe P2, testează o conexiune, apoi comută secretul spre app_clone. Consumatorii care țin app P1 continuă să meargă; conexiunile noi folosesc app_clone P2. Ziua 60: rotația setează parola lui app pe P3 și comută înapoi. În niciun moment nu e invalidată o parolă pe care ar putea-o ține un consumator.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">Utilizatori alternanți · parola în uz nu e niciodată cea schimbată</text>
<line x1="60" y1="120" x2="860" y2="120" stroke="#2a3150" stroke-width="2"/>
<text x="60" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">ziua 0</text><text x="460" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">ziua 30 · rotație</text><text x="860" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">ziua 60 · rotație</text>
<rect x="60" y="60" width="400" height="22" rx="4" fill="#4fffb0" opacity="0.7"/><text x="260" y="75" text-anchor="middle" fill="#0d1120" font-size="11" font-weight="700">secret → app · P1 · fiecare consumator folosește asta</text>
<rect x="60" y="88" width="400" height="22" rx="4" fill="#2a3150"/><text x="260" y="103" text-anchor="middle" fill="#9aa3c7" font-size="11">app_clone · P0 · inactiv</text>
<rect x="460" y="60" width="400" height="22" rx="4" fill="#2a3150"/><text x="660" y="75" text-anchor="middle" fill="#9aa3c7" font-size="11">app · P1 · încă valid, inactiv, nicio conexiune nu se rupe</text>
<rect x="460" y="88" width="400" height="22" rx="4" fill="#4fffb0" opacity="0.7"/><text x="660" y="103" text-anchor="middle" fill="#0d1120" font-size="11" font-weight="700">secret → app_clone · P2 · conexiunile noi folosesc asta</text>
<line x1="460" y1="50" x2="460" y2="125" stroke="#ffd166" stroke-width="2" stroke-dasharray="5,3"/>
<text x="460" y="176" text-anchor="middle" fill="#ffd166" font-size="11">1 · setează parola app_clone pe P2   2 · testează conexiunea   3 · comută secretul</text>
<text x="450" y="206" text-anchor="middle" fill="#9aa3c7">Un consumator care ține P1 merge încă 30 de zile. Până atunci a recitit secretul de multe ori.</text>
<text x="450" y="228" text-anchor="middle" fill="#9aa3c7">Ziua 60 face același lucru invers: app primește P3, secretul comută înapoi.</text>
</g>
</svg>
</div>

În CDK, pe un cluster ale cărui credențiale au venit dintr-un secret generat, sunt câteva linii:

```ts
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](/ro/blog/nat-gateway-the-most-expensive-line-you-do-not-see); 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.

```ts
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](/ro/blog/aurora-serverless-v2-review).

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Două căi de consumator. Serviciul App Runner: pool-ul citește secretul din Secrets Manager printr-un cache de 5 minute la fiecare conexiune nouă, iar la o eroare de autentificare invalidează cache-ul și recitește o dată. Funcțiile Lambda: se autentifică la RDS Proxy cu IAM și nu țin nicio parolă; proxy-ul citește singur secretul și urmărește rotațiile. Ambele căi ajung la Aurora. Lambda de rotație schimbă parola utilizatorului inactiv la fiecare 30 de zile.">
<defs><marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="15" y="30" width="200" height="70" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="115" y="54" text-anchor="middle" fill="#f1f3ff" font-weight="700">serviciul App Runner</text><text x="115" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">secret la conectare · cache 5 min</text><text x="115" y="90" text-anchor="middle" fill="#9aa3c7" font-size="11">eroare auth → recitește o dată</text>
<rect x="15" y="130" width="200" height="70" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="115" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">funcțiile Lambda</text><text x="115" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">auth IAM la proxy</text><text x="115" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">nu țin niciodată o parolă</text>
<rect x="330" y="30" width="200" height="70" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="430" y="54" text-anchor="middle" fill="#4fffb0" font-weight="700">Secrets Manager</text><text x="430" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">app / app_clone · AWSCURRENT</text><text x="430" y="90" text-anchor="middle" fill="#9aa3c7" font-size="11">Lambda de rotație la 30 de zile</text>
<rect x="330" y="130" width="200" height="70" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="430" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">RDS Proxy</text><text x="430" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">citește singur secretul</text><text x="430" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">urmărește rotațiile · pool de conexiuni</text>
<rect x="680" y="80" width="200" height="70" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="780" y="104" text-anchor="middle" fill="#f1f3ff" font-weight="700">Aurora</text><text x="780" y="122" text-anchor="middle" fill="#9aa3c7" font-size="11">utilizatorii app și app_clone</text><text x="780" y="140" text-anchor="middle" fill="#9aa3c7" font-size="11">aceleași grant-uri printr-un rol</text>
<line x1="217" y1="65" x2="328" y2="65" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="217" y1="165" x2="328" y2="165" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="430" y1="102" x2="430" y2="128" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<path d="M217,80 C300,115 560,115 678,110" fill="none" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="532" y1="165" x2="678" y2="125" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
</g>
</svg>
</div>

## 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](/contact), întâi în staging.
