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.
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:
// 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. 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:
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ă.
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-echope 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.