# Politici de protecție a datelor în CloudWatch: mascarea PII înainte să ajungă în loguri

Un inginer de suport a cerut un log de debug pe o rută de API, „doar o săptămână”, ca să vâneze un bug în validarea adreselor. Linia de log era tot corpul request-ului. Ruta era checkout-ul. Timp de nouă zile, emailul, numărul de telefon, adresa de livrare și ultimele patru cifre ale cardului din fiecare comandă au fost scrise într-un log group CloudWatch cu retenție de 30 de zile, citibil de oricine avea `logs:GetLogEvents` pe cont, adică toată lumea.

Nimeni n-a făcut nimic rău cu datele. Le-am găsit pentru că o politică de protecție a datelor pe care o activaserăm cu o lună înainte le-a semnalat, în ritm de cam 1.200 de constatări pe zi, iar alarma ei a sunat. Articolul ăsta e despre politica aia: ce e, cât costă, cum se configurează și de ce o considerăm tot o plasă de siguranță, nu o rezolvare.

## Ce face o politică de protecție a datelor

CloudWatch Logs poate scana fiecare eveniment la ingestion contra unui set de identificatori de date, gestionați de AWS (adrese de email, numere de telefon, numere de card, chei secrete AWS, IBAN-uri, ID-uri naționale pentru o listă lungă de țări) sau custom (un regex), și poate lua două feluri de acțiuni. **Audit** numără potrivirile, emite o metrică și, opțional, scrie o constatare în alt log group, în S3 sau în Firehose. **De-identify** maschează caracterele potrivite în evenimentul stocat, așa că ce vezi în Logs Insights e `***********`.

Politica poate fi atașată unui singur log group sau întregului cont. Mascarea se aplică la ingestion și e ireversibilă pentru oricine n-are permisiunea `logs:Unmask`; cei care o au pot vedea originalul cu un flag pe query. Scanarea costă $0,12 per GB de loguri scanate, peste ingestion.

<div class="article-figure">
<svg viewBox="0 0 900 230" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline-ul unui eveniment de log cu o politică de protecție a datelor. Aplicația scrie o linie cu un email și un număr de telefon. CloudWatch Logs o ingerează, politica potrivește identificatorii EmailAddress și PhoneNumber, acțiunea de audit emite o constatare într-un log group de constatări și incrementează metrica LogEventsWithFindings, iar acțiunea de de-identify stochează evenimentul cu potrivirile mascate. Un inginer cu logs:Unmask poate vedea originalul; toți ceilalți văd asteriscuri. O alarmă pe metrică cheamă echipa.">
<defs><marker id="arrDp" 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="60" width="160" height="70" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="95" y="86" text-anchor="middle" fill="#f1f3ff" font-weight="700">aplicația</text><text x="95" y="106" text-anchor="middle" fill="#ff6b8a" font-size="11">{"email":"a@b.com","phone":…}</text>
<line x1="177" y1="95" x2="205" y2="95" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrDp)"/>
<rect x="208" y="40" width="250" height="110" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="333" y="64" text-anchor="middle" fill="#ffd166" font-weight="700">politica de protecție · la ingestion</text><text x="333" y="88" text-anchor="middle" fill="#f1f3ff">identificatori: EmailAddress,</text><text x="333" y="106" text-anchor="middle" fill="#f1f3ff">PhoneNumber, CreditCardNumber, …</text><text x="333" y="132" text-anchor="middle" fill="#9aa3c7">$0,12 per GB scanat</text>
<line x1="460" y1="75" x2="518" y2="60" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrDp)"/>
<line x1="460" y1="115" x2="518" y2="130" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrDp)"/>
<rect x="520" y="30" width="180" height="56" rx="10" fill="#0d1120" stroke="#ff6b8a" stroke-width="1.5"/><text x="610" y="52" text-anchor="middle" fill="#ff6b8a" font-weight="700">audit</text><text x="610" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">constatare → log group · metrică +1</text>
<rect x="520" y="104" width="180" height="56" rx="10" fill="#0d1120" stroke="#4fffb0" stroke-width="1.5"/><text x="610" y="126" text-anchor="middle" fill="#4fffb0" font-weight="700">de-identify</text><text x="610" y="146" text-anchor="middle" fill="#9aa3c7" font-size="11">stocat ca {"email":"*******"}</text>
<line x1="702" y1="58" x2="740" y2="58" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrDp)"/>
<rect x="742" y="30" width="145" height="56" rx="10" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="814" y="52" text-anchor="middle" fill="#f1f3ff" font-weight="700">alarmă</text><text x="814" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">LogEventsWithFindings &gt; 0</text>
<line x1="702" y1="132" x2="740" y2="132" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrDp)"/>
<rect x="742" y="104" width="145" height="56" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="814" y="126" text-anchor="middle" fill="#f1f3ff" font-weight="700">Logs Insights</text><text x="814" y="146" text-anchor="middle" fill="#9aa3c7" font-size="11">demascare doar cu logs:Unmask</text>
<text x="450" y="200" text-anchor="middle" fill="#9aa3c7">Valoarea brută nu ajunge niciodată în stocare. Constatarea îți spune care log group și care identificator, nu valoarea.</text>
</g>
</svg>
</div>

## Politica pe care o rulăm

La nivel de cont, ca un log group nou să nu poată fi creat în afara ei. Gestionată în CDK ca tot restul:

```ts
// infra/lib/log-data-protection.ts
new logs.CfnAccountPolicy(this, 'LogDataProtection', {
  policyName: 'pii-guard',
  policyType: 'DATA_PROTECTION_POLICY',
  scope: 'ALL',
  policyDocument: JSON.stringify({
    Name: 'pii-guard',
    Version: '2021-06-01',
    Statement: [
      {
        Sid: 'audit',
        DataIdentifier: [
          'arn:aws:dataprotection::aws:data-identifier/EmailAddress',
          'arn:aws:dataprotection::aws:data-identifier/PhoneNumber-US',
          'arn:aws:dataprotection::aws:data-identifier/CreditCardNumber',
          'arn:aws:dataprotection::aws:data-identifier/AwsSecretKey',
          'arn:aws:dataprotection::aws:data-identifier/IpAddress',
        ],
        Operation: { Audit: { FindingsDestination: { CloudWatchLogs: { LogGroup: findingsGroup.logGroupName } } } },
      },
      {
        Sid: 'mask',
        DataIdentifier: [ /* aceeași listă */ ],
        Operation: { Deidentify: { MaskConfig: {} } },
      },
    ],
  }),
});

new cloudwatch.Alarm(this, 'PiiInLogs', {
  metric: new cloudwatch.Metric({ namespace: 'AWS/Logs', metricName: 'LogEventsWithFindings', statistic: 'Sum', period: Duration.minutes(5) }),
  threshold: 0,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  evaluationPeriods: 1,
});
```

Două declarații, pentru că audit și de-identify sunt operații separate și le vrei pe amândouă: mascarea protejează datele, constatarea de audit îți spune *unde* e scurgerea ca să repari codul. Alarma pe `LogEventsWithFindings` e tot motivul pentru care am prins logul de checkout în nouă zile, nu niciodată.

`IpAddress` e pe listă intenționat și e cel care generează zgomot. Logurile noastre de acces conțin legitim IP-urile clienților, iar mascarea lor acolo ar strica fluxul de investigație din WAF. Politica de cont e baza; pentru cele două log group-uri unde IP-urile sunt rostul, o politică la nivel de log group fără `IpAddress` o suprascrie. Politicile de log group și cele de cont se aplică amândouă, iar un termen e mascat dacă oricare se potrivește, deci suprascrierea trebuie să fie *absența* identificatorului, nu o declarație permisivă.

## Cum arată constatările

Log group-ul de constatări primește un eveniment JSON per eveniment de log potrivit, cu log group-ul, stream-ul, identificatorii potriviți și offset-urile de caractere. Nu valoarea. Un query Logs Insights peste el dă inventarul scurgerilor:

```
fields @timestamp, resourceArn, dataIdentifiers.0.name as identifier
| stats count(*) as events by resourceArn, identifier
| sort events desc
```

În ziua în care ne-am uitat, primul rând era log group-ul rutei de checkout cu `EmailAddress` și `PhoneNumber-US`, urmat de o Lambda care loga verbatim payload-ul unui webhook terț (`EmailAddress`) și de un log CodeBuild care afișase o variabilă de mediu cu o cheie de acces în timpul unui build pe care un coleg îl depana (`AwsSecretKey`). Trei scurgeri, obiceiurile a trei echipe diferite, un singur query.

## Cât costă și cât a economisit

La volumul nostru, cam 5 GB de loguri pe zi, scanarea e $18 pe lună. Cât WAF-ul și mai puțin decât Route 53. Pentru o platformă care procesează plăți, e cel mai ieftin control de pe factură.

Ce nu face e să facă problema de fond să dispară. Evenimentele mascate ocupă în continuare stocare și costă în continuare ingestion. Logul de debug al inginerului de suport a rămas o greșeală de design: să loghezi tot corpul unui request pe o rută de checkout, la orice nivel de mascare, e greșit. Protecția datelor e detectorul de fum, nu ignifugarea. Ignifugarea e în aplicație: un logger cu o listă albă de câmpuri, ca implicit *nimic* din request să nu ajungă în log, și o regulă de code review ca orice `log.info(req.body)` nou să fie respins pe loc.

## Dacă logurile tale nu merg în CloudWatch

Am argumentat în altă parte că [logurile aplicației n-ar trebui deținute de cloud](/ro/blog/your-logs-should-not-know-which-cloud). Dacă ale tale trec printr-un OpenTelemetry Collector spre Loki, același control ține de collector, și e, poate, mai bine acolo, pentru că rulează înainte ca datele să părăsească host-ul:

```yaml
processors:
  redaction:
    allow_all_keys: true
    blocked_values:
      - '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'     # email
      - '\b(?:\d[ -]*?){13,16}\b'                             # număr de card
      - 'AKIA[0-9A-Z]{16}'                                    # AWS access key id
    summary: info
```

Procesorul `redaction` maschează valorile potrivite din atributele logurilor și raportează un rezumat cu câte a mascat, pe care poți pune alertă. N-are biblioteca AWS de formate de ID-uri naționale, deci pentru un workload reglementat ai întreține lista singur. Pentru cele trei clase de scurgeri pe care le vedem noi de fapt, emailuri, carduri și credențiale, trei regex-uri le acoperă.

<div class="article-figure">
<svg viewBox="0 0 900 200" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Trei locuri unde poți redacta, de la stânga la dreapta, în ordinea preferinței. În aplicație: un logger cu listă albă, nimic din request implicit, singura rezolvare reală. În agent: procesorul de redaction al OpenTelemetry Collector, înainte ca datele să părăsească host-ul, merge cu orice backend. În CloudWatch: politica de protecție a datelor, la ingestion, identificatori gestionați și constatări, plasa de siguranță care prinde ce au ratat primele două.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="20" y="30" width="270" height="140" rx="12" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/>
<text x="155" y="56" text-anchor="middle" fill="#4fffb0" font-size="14" font-weight="700">1 · în aplicație</text>
<text x="155" y="82" text-anchor="middle" fill="#f1f3ff">logger cu listă albă</text>
<text x="155" y="102" text-anchor="middle" fill="#9aa3c7">nimic din req.body implicit</text>
<text x="155" y="122" text-anchor="middle" fill="#9aa3c7">regulă de review: fără payload-uri brute</text>
<text x="155" y="152" text-anchor="middle" fill="#4fffb0" font-weight="700">rezolvarea</text>
<rect x="315" y="30" width="270" height="140" rx="12" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/>
<text x="450" y="56" text-anchor="middle" fill="#ffd166" font-size="14" font-weight="700">2 · în agent</text>
<text x="450" y="82" text-anchor="middle" fill="#f1f3ff">procesorul redaction din OTel Collector</text>
<text x="450" y="102" text-anchor="middle" fill="#9aa3c7">înainte ca datele să părăsească host-ul</text>
<text x="450" y="122" text-anchor="middle" fill="#9aa3c7">merge cu orice backend, regex-urile tale</text>
<text x="450" y="152" text-anchor="middle" fill="#ffd166" font-weight="700">cureaua</text>
<rect x="610" y="30" width="270" height="140" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="745" y="56" text-anchor="middle" fill="#7b8cff" font-size="14" font-weight="700">3 · în CloudWatch</text>
<text x="745" y="82" text-anchor="middle" fill="#f1f3ff">politica de protecție a datelor</text>
<text x="745" y="102" text-anchor="middle" fill="#9aa3c7">la ingestion · identificatori gestionați</text>
<text x="745" y="122" text-anchor="middle" fill="#9aa3c7">constatările îți spun unde să te uiți</text>
<text x="745" y="152" text-anchor="middle" fill="#7b8cff" font-weight="700">detectorul de fum</text>
<text x="450" y="192" text-anchor="middle" fill="#9aa3c7">Fă-le pe toate trei. Prima e singura care reduce ce stochezi; a treia e singura care îți spune că primele două au eșuat.</text>
</g>
</svg>
</div>

## Ce am schimbat după constatare

- Logul de debug de pe checkout a fost scos în aceeași oră. Log group-ul a fost pus pe retenție de o zi până au expirat evenimentele mascate, apoi înapoi la 30.
- Logger-ul aplicației a primit o listă albă: `orderId`, `userId`, `route`, `status`, `durationMs`. Orice altceva trebuie adăugat pe nume, în code review.
- Lambda de webhook loghează acum *forma* payload-ului (chei și tipuri) și un hash, niciodată valorile.
- CodeBuild a primit `--no-echo` pe pasul de mediu, iar cheia de acces afișată a fost rotită, pentru că o linie de log e o copie.
- Alarma merge acum pe canalul de on-call, cu query-ul Logs Insights linkat. Timpul mediu de la scurgere la reparare în cele două ocazii de atunci: sub o oră.

Pornește politica înainte să ai nevoie de ea. E o singură resursă CloudFormation, optsprezece dolari pe lună, și va găsi ceva. A noastră a găsit trei lucruri în prima săptămână. Dacă vrei ajutor să trasezi lista albă pentru propriul logger sau să configurezi politica pe mai multe conturi, [hai să vorbim](/contact).
