Skip to content
Secrete în CDK: Secrets Manager, Parameter Store și niciodată nimic în template
← ← Înapoi la Idei Cloud

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 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ă:

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

o valoare de care are nevoie aplicațiase rotește? e generată?e citită din alt cont? da nu Secrets Managersecret JSON · $0,40 / lună · rotație SSM Parameter StoreSecureString · gratuit · versionat template: doar nume sau ARNconsumatorul ia valoarea la runtime · grant IAM environment: { KEY: 'literal' } → în template → scurgere

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.

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.

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:

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:

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.

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 după ce una a apărut oricum printr-un echo.

Secrets Manager · Parameter Storesingurul loc unde există valorile App RunnerruntimeEnvironmentSecretsluate la pornirea instanței Lambdaextensia Parameters & Secretslocalhost, cache, TTL CodeBuildbuildspec env.secrets-managermascat în loguri fiecare primește GetSecretValue pe ARN-uri specifice · acordat în același template care numește secretul · valoarea niciodată în template

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 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; e cam o zi per cont.