# Secrete în CDK: Secrets Manager, Parameter Store și niciodată nimic în template

Un template CloudFormation e un fișier text. E stocat de CloudFormation, e în `cdk.out` pe discul fiecărui developer, e în logurile de CI de la ultimul synth, iar dacă cineva comite `cdk.out` din greșeală, e în git pentru totdeauna. Orice apare în template ca valoare literală e, practic, public în interiorul organizației. Asta include parola de bază de date pe care ai setat-o cu `environment: { DB_PASSWORD: '...' }` pentru că era mai rapid decât s-o faci cum trebuie.

Rulăm [trei conturi dintr-un singur cod CDK](/ro/blog/aws-three-accounts-one-cdk-codebase) cu vreo douăzeci și cinci de secrete per cont, și niciunul n-a fost vreodată într-un template. Ăsta e setul de reguli care face asta adevărat, cele două servicii AWS implicate, când îl folosești pe care, și pattern-urile prin care o valoare ajunge într-un container, într-o Lambda sau într-un build fără ca un om s-o lipească vreodată undeva.

## Regula

**Template-ul poartă referințe, niciodată valori.** O referință e un ARN, un nume sau o cale de parametru. Valoarea trăiește într-unul din două locuri, Secrets Manager sau SSM Parameter Store, și e luată la runtime de lucrul care are nevoie de ea, folosind o permisiune IAM acordată în același template. Dacă poți scoate un secret cu `grep` din `cdk.out`, e o scurgere, iar CDK face verificarea ușoară:

```bash
npx cdk synth --quiet && grep -rniE 'password|secret|token|api[_-]?key' cdk.out/*.template.json | grep -v 'arn:aws:\|Ref\|Fn::' || echo clean
```

Rulăm asta în CI. A picat de două ori, ambele pe o intrare `environment:` bine intenționată dintr-o Lambda.

## Cele două servicii, și când îl folosești pe care

| | Secrets Manager | SSM Parameter Store (SecureString) |
|---|---|---|
| Preț | $0,40 per secret pe lună + $0,05 per 10.000 de apeluri | Gratuit la nivelul standard (4 KB, 10.000 de parametri); $0,05 per parametru avansat |
| Rotație | Încorporată, cu funcții Lambda de rotație pentru RDS, Aurora, Redshift și custom | Niciuna; rotești scriind o versiune nouă |
| Generare | Poate genera singur valoarea (`generateSecretString`) | Nu |
| Între conturi | Politica de resursă permite altui cont să citească | Nu direct |
| Integrare nativă | App Runner `runtimeEnvironmentSecrets`, ECS `secrets`, extensia Lambda, RDS Proxy | ECS `secrets`, extensia Lambda, CodeBuild `parameter-store` |
| Versionare | Etichete de staging (AWSCURRENT / AWSPREVIOUS) | Versiuni numerotate |

Regula la care am ajuns: **Secrets Manager pentru orice se rotește, e generat sau e citit din alt cont. Parameter Store pentru tot restul.** În practică, asta pune credențialele bazei de date, cheia de semnare pentru web push și token-urile terțe pe care le rotim după un program în Secrets Manager, iar lista lungă de secrete de tip configurație (feature flag-uri cu valori sensibile, un token de preview partajat, URL-uri de API per mediu cu chei încorporate) în Parameter Store.

Diferența de cost contează mai mult decât pare. La $0,40 per secret, douăzeci și cinci de secrete în trei conturi înseamnă $30 pe lună, adică [4 % din factura noastră](/ro/blog/aws-bill-of-a-three-person-startup). Două mutări au tăiat-o la jumătate: gruparea valorilor înrudite într-un singur secret JSON în loc de un secret per valoare, și mutarea celor de tip configurație în Parameter Store, care e gratuit.

<div class="article-figure">
<svg viewBox="0 0 900 250" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Flux de decizie pentru unde trăiește un secret. Se rotește, trebuie generat sau e citit din alt cont? Da: Secrets Manager, ca secret JSON care grupează chei înrudite. Nu: SSM Parameter Store SecureString, gratuit. Ambele sunt referențiate din template-ul CDK doar prin nume, iar serviciul consumator ia valoarea la runtime cu un grant IAM. O casetă roșie marchează calea interzisă: un literal în variabilele de mediu din template.">
<defs><marker id="arrS" 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="#7b8cff"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="20" y="70" width="220" height="80" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="130" y="96" text-anchor="middle" fill="#f1f3ff" font-weight="700">o valoare de care are nevoie aplicația</text><text x="130" y="116" text-anchor="middle" fill="#9aa3c7" font-size="11">se rotește? e generată?</text><text x="130" y="132" text-anchor="middle" fill="#9aa3c7" font-size="11">e citită din alt cont?</text>
<line x1="242" y1="90" x2="318" y2="60" stroke="#7b8cff" stroke-width="1.5" marker-end="url(#arrS)"/><text x="280" y="66" text-anchor="middle" fill="#9aa3c7" font-size="10">da</text>
<line x1="242" y1="130" x2="318" y2="160" stroke="#7b8cff" stroke-width="1.5" marker-end="url(#arrS)"/><text x="280" y="160" text-anchor="middle" fill="#9aa3c7" font-size="10">nu</text>
<rect x="320" y="30" width="240" height="64" rx="12" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="440" y="54" text-anchor="middle" fill="#4fffb0" font-weight="700">Secrets Manager</text><text x="440" y="74" text-anchor="middle" fill="#9aa3c7" font-size="11">secret JSON · $0,40 / lună · rotație</text>
<rect x="320" y="130" width="240" height="64" rx="12" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="440" y="154" text-anchor="middle" fill="#ffd166" font-weight="700">SSM Parameter Store</text><text x="440" y="174" text-anchor="middle" fill="#9aa3c7" font-size="11">SecureString · gratuit · versionat</text>
<line x1="562" y1="62" x2="638" y2="100" stroke="#7b8cff" stroke-width="1.5" marker-end="url(#arrS)"/>
<line x1="562" y1="162" x2="638" y2="124" stroke="#7b8cff" stroke-width="1.5" marker-end="url(#arrS)"/>
<rect x="640" y="80" width="240" height="64" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="760" y="104" text-anchor="middle" fill="#f1f3ff" font-weight="700">template: doar nume sau ARN</text><text x="760" y="124" text-anchor="middle" fill="#9aa3c7" font-size="11">consumatorul ia valoarea la runtime · grant IAM</text>
<rect x="320" y="210" width="240" height="32" rx="8" fill="#2b1522" stroke="#ff6b8a" stroke-width="1.5"/><text x="440" y="231" text-anchor="middle" fill="#ff6b8a" font-size="11">environment: { KEY: 'literal' } → în template → scurgere</text>
</g>
</svg>
</div>

## Pattern-ul 1: secretul pe care îl creează CloudFormation și pe care nu-l vede nimeni

Cel mai bun secret e unul pe care niciun om nu l-a citit vreodată. Parola bazei de date e cazul canonic: CDK îi cere lui Secrets Manager s-o genereze, lui Aurora i se spune s-o folosească, iar valoarea există doar în Secrets Manager și în baza de date.

```ts
const dbSecret = new secretsmanager.Secret(this, 'DbSecret', {
  secretName: `/platform/${env}/db`,
  generateSecretString: {
    secretStringTemplate: JSON.stringify({ username: 'app' }),
    generateStringKey: 'password',
    excludeCharacters: '"@/\\\'',
    passwordLength: 40,
  },
});

const cluster = new rds.DatabaseCluster(this, 'Db', {
  credentials: rds.Credentials.fromSecret(dbSecret),
  // ...
});
```

Template-ul conține *resursa* secretului, cu instrucțiuni de generare a unei valori. Nu conține valoarea. CloudFormation o creează, i-o pasează lui RDS printr-o referință dinamică (`{{resolve:secretsmanager:...}}`) rezolvată în interiorul CloudFormation și care nu apare niciodată în template-ul stocat, și asta e ultima dată când ceva din afara Secrets Manager și Aurora o atinge. Rotația, când o pornești, funcționează la fel; [o tratăm separat](/ro/blog/rotating-the-database-password-without-downtime).

## Pattern-ul 2: secretul pe care un om îl introduce o dată, prin referință

Cheile de API terțe vin din dashboard-ul unui vendor și un om trebuie să le pună undeva. Undeva-ul ăla e CLI-ul, o dată per cont, și niciodată codul:

```bash
aws secretsmanager create-secret --name /platform/prod/payments \
  --secret-string '{"apiKey":"...","webhookSecret":"..."}' --profile prod
```

Partea de CDK îl referențiază după nume și acordă drept de citire:

```ts
const payments = secretsmanager.Secret.fromSecretNameV2(this, 'Payments', `/platform/${env}/payments`);
payments.grantRead(apiService.instanceRole);
```

Template-ul conține numele. Nu valoarea, nici măcar un placeholder. Dacă secretul nu există într-un cont, serviciul nu pornește, cu o eroare clară, ceea ce e comportamentul corect pentru „cineva a uitat să configureze contul nou”. Ținem un `secrets.md` în repo care listează fiecare nume de secret și ce formă are JSON-ul lui, ca persoana care configurează un cont nou să aibă un checklist, iar codul să aibă o singură sursă de adevăr pentru chei.

## Pattern-ul 3: cum ajunge valoarea în proces

Trei consumatori, trei mecanisme, niciunul nu implică template-ul.

**App Runner** are `runtimeEnvironmentSecrets`: un map de la numele variabilei de mediu la ARN-ul secretului plus cheia JSON. Serviciul ia valoarea la pornirea instanței și o injectează ca variabilă de mediu obișnuită în container. Rolul instanței are nevoie de `secretsmanager:GetSecretValue` exact pe acele ARN-uri, ceea ce îi dă `grantRead`.

```ts
runtimeEnvironmentSecrets: {
  DB_PASSWORD: apprunner.Secret.fromSecretsManager(dbSecret, 'password'),
  PAYMENTS_API_KEY: apprunner.Secret.fromSecretsManager(payments, 'apiKey'),
  PREVIEW_TOKEN: apprunner.Secret.fromSsmParameter(previewToken),
},
```

**Lambda** n-are injecție echivalentă, așa că ia valoarea la cold start. Extensia AWS Parameters and Secrets pentru Lambda rulează ca layer, servește un endpoint HTTP local, cache-uiește pentru un TTL configurabil și înseamnă că codul funcției face un apel pe localhost în loc de un apel de SDK. Zece linii în init-ul handler-ului, iar o rotație se propagă în interiorul TTL-ului de cache fără redeploy.

**CodeBuild** citește Parameter Store și Secrets Manager direct în blocurile `env.secrets-manager` și `env.parameter-store` din buildspec, deci un build poate avea token-ul de registry fără ca token-ul să fie în definiția proiectului. Rolul de build primește grant-ul. Valoarea e mascată în logul de build, lucru pe care [am învățat să-l verificăm de două ori](/ro/blog/cloudwatch-data-protection-policies-pii) după ce una a apărut oricum printr-un `echo`.

<div class="article-figure">
<svg viewBox="0 0 900 230" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Trei consumatori care iau secrete la runtime. App Runner: runtimeEnvironmentSecrets mapează numele variabilelor la ARN-uri de secrete, luate la pornirea instanței. Lambda: extensia Parameters and Secrets servește un endpoint local cache-uit la cold start. CodeBuild: blocul env secrets-manager din buildspec, mascat în loguri. Toți trei primesc GetSecretValue pe ARN-uri specifice în același template CDK care referențiază secretele după nume.">
<defs><marker id="arrS2" 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="330" y="20" width="240" height="56" rx="12" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="450" y="44" text-anchor="middle" fill="#4fffb0" font-weight="700">Secrets Manager · Parameter Store</text><text x="450" y="64" text-anchor="middle" fill="#9aa3c7" font-size="11">singurul loc unde există valorile</text>
<line x1="380" y1="78" x2="150" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrS2)"/>
<line x1="450" y1="78" x2="450" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrS2)"/>
<line x1="520" y1="78" x2="750" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrS2)"/>
<rect x="30" y="132" width="240" height="70" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="150" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">App Runner</text><text x="150" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">runtimeEnvironmentSecrets</text><text x="150" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">luate la pornirea instanței</text>
<rect x="330" y="132" width="240" height="70" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="450" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">Lambda</text><text x="450" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">extensia Parameters &amp; Secrets</text><text x="450" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">localhost, cache, TTL</text>
<rect x="630" y="132" width="240" height="70" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="750" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">CodeBuild</text><text x="750" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">buildspec env.secrets-manager</text><text x="750" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">mascat în loguri</text>
<text x="450" y="224" text-anchor="middle" fill="#9aa3c7">fiecare primește GetSecretValue pe ARN-uri specifice · acordat în același template care numește secretul · valoarea niciodată în template</text>
</g>
</svg>
</div>

## Cele trei moduri în care merge prost oricum

**`SecretValue.unsafePlainText`.** CDK te obligă să tastezi cuvântul „unsafe” ca să pui un secret literal într-un template, și oamenii tot o fac, de obicei într-un stack de test care devine mai târziu unul real. Grep-ul nostru din CI îl prinde. Interzice-l și în linter.

**Secrete în `cdk.context.json`.** Valorile de context sunt comise în git prin design, deci sunt locul greșit pentru orice sensibil. Am văzut o cheie de API ajungând acolo prin `--context apiKey=...` din linia de comandă. Contextul e pentru ID-uri de cont, lookup-uri de VPC și toggle-uri de funcții, nimic altceva.

**Citirea unui secret în aplicația CDK în sine.** `secretsmanager.Secret.fromSecretNameV2(...).secretValue.unsafeUnwrap()` la synth rezolvă valoarea pe mașina developerului și o scrie în template. E aceeași scurgere cu mai mulți pași. Singura folosire corectă a lui `secretValue` e pasarea către un construct care știe s-o transforme într-o referință dinamică, adică exact ce fac `Credentials.fromSecret` și `runtimeEnvironmentSecrets`.

## Cum arată pe trei conturi

Același cod, trei conturi, trei seturi de valori, zero valori în repository:

| Secret | Trăiește în | Creat de | Citit de |
|---|---|---|---|
| Credențialele bazei de date | Secrets Manager | CloudFormation (generat) | App Runner, Lambda de migrare, RDS Proxy |
| Chei de API terțe (3) | Secrets Manager, câte un JSON fiecare | Un om, o dată per cont, prin CLI | App Runner, Lambda-uri de webhook |
| Cheia de semnare pentru web push | Secrets Manager | Un om, o dată | Lambda de notificări |
| Token de preview, feature flag-uri cu valori, URL-uri interne | Parameter Store | Un om, o dată, sau CDK pentru cele nesensibile | App Runner, Lambda-uri |
| Token de registry pentru build-uri | Secrets Manager | CloudFormation (generat) | CodeBuild |

Nimic din tabelul ăsta nu e într-un template, într-un fișier `.env` din git, într-un secret GitHub sau în istoricul de shell al unui laptop, iar [rolul de deploy](/ro/blog/github-oidc-deploy-roles-per-aws-account) nu poate citi nimic din el, pentru că n-are nevoie: CloudFormation și serviciile consumatoare fac citirea, cu propriile roluri.

Dacă template-urile sau fișierele tale `.env` au valori în ele și ai vrea să nu mai aibă, [am mai făcut migrarea asta](/contact); e cam o zi per cont.
