Ein Support-Engineer bat um ein Debug-Log auf einer API-Route, „nur für eine Woche“, um einem Bug in der Adressvalidierung nachzujagen. Die Log-Zeile war der gesamte Request-Body. Die Route war der Checkout. Neun Tage lang wurden E-Mail, Telefonnummer, Lieferadresse und die letzten vier Kartenziffern jeder Bestellung in eine CloudWatch-Log-Gruppe mit 30 Tagen Aufbewahrung geschrieben, lesbar für jeden mit logs:GetLogEvents auf dem Konto, also für alle.
Niemand hat mit den Daten etwas Falsches getan. Wir haben es gefunden, weil eine Data Protection Policy, die wir einen Monat zuvor aktiviert hatten, es markierte, mit etwa 1.200 Funden am Tag, und ihr Alarm losging. Dieser Artikel handelt von dieser Policy: was sie ist, was sie kostet, wie sie konfiguriert wird und warum wir sie trotzdem als Sicherheitsnetz und nicht als Lösung betrachten.
Was eine Data Protection Policy tut
CloudWatch Logs kann jedes Ereignis bei der Ingestion gegen einen Satz von Datenidentifikatoren prüfen, AWS-verwaltet (E-Mail-Adressen, Telefonnummern, Kreditkartennummern, AWS-Secret-Keys, IBANs, nationale IDs für eine lange Länderliste) oder eigene (ein Regex), und zwei Arten von Aktionen ausführen. Audit zählt Treffer, gibt eine Metrik aus und schreibt optional einen Fund in eine andere Log-Gruppe, nach S3 oder Firehose. De-identify maskiert die getroffenen Zeichen im gespeicherten Ereignis, sodass Sie in Logs Insights *********** sehen.
Die Policy kann an eine einzelne Log-Gruppe oder an das ganze Konto gehängt werden. Die Maskierung geschieht bei der Ingestion und ist für jeden ohne die Berechtigung logs:Unmask unumkehrbar; wer sie hat, sieht das Original mit einem Flag in der Abfrage. Der Scan kostet $0,12 pro GB gescannter Log-Daten, zusätzlich zur Ingestion.
Die Policy, die wir betreiben
Auf Kontoebene, damit keine neue Log-Gruppe außerhalb davon entstehen kann. In CDK verwaltet wie alles andere:
// 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: [ /* dieselbe Liste */ ],
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,
});
Zwei Statements, weil Audit und De-identify getrennte Operationen sind und Sie beide wollen: die Maskierung schützt die Daten, der Audit-Fund sagt Ihnen, wo das Leck ist, damit Sie den Code reparieren. Der Alarm auf LogEventsWithFindings ist der ganze Grund, warum wir das Checkout-Log nach neun Tagen und nicht nie gefunden haben.
IpAddress steht absichtlich auf der Liste und ist der Eintrag, der Rauschen erzeugt. Unsere Access-Logs enthalten legitimerweise Client-IPs, und sie dort zu maskieren würde den WAF-Untersuchungsablauf zerstören. Die Konto-Policy ist die Basis; für die beiden Log-Gruppen, in denen IPs der Zweck sind, überschreibt eine Policy auf Log-Gruppen-Ebene ohne IpAddress. Log-Gruppen- und Konto-Policies gelten beide, und ein Begriff wird maskiert, wenn eine von beiden trifft, sodass die Überschreibung das Fehlen des Identifikators sein muss, kein erlaubendes Statement.
Wie die Funde aussehen
Die Fund-Log-Gruppe erhält ein JSON-Ereignis pro treffendem Log-Ereignis, mit Log-Gruppe, Stream, den getroffenen Identifikatoren und Zeichen-Offsets. Nicht den Wert. Eine Logs-Insights-Abfrage darüber liefert das Leck-Inventar:
fields @timestamp, resourceArn, dataIdentifiers.0.name as identifier
| stats count(*) as events by resourceArn, identifier
| sort events desc
An dem Tag, an dem wir nachsahen, stand ganz oben die Log-Gruppe der Checkout-Route mit EmailAddress und PhoneNumber-US, gefolgt von einer Lambda, die den Payload eines Drittanbieter-Webhooks wörtlich loggte (EmailAddress), und einem CodeBuild-Log, das während eines Builds, den ein Kollege debuggte, eine Umgebungsvariable mit einem Access Key ausgegeben hatte (AwsSecretKey). Drei Lecks, die Gewohnheiten dreier Teams, eine Abfrage.
Was es kostet und was es gespart hat
Bei unserem Volumen, etwa 5 GB Logs am Tag, kostet der Scan $18 im Monat. Das entspricht der WAF und ist weniger als Route 53. Für eine Plattform, die Zahlungen abwickelt, ist es die billigste Kontrolle auf der Rechnung.
Was sie nicht tut, ist das zugrunde liegende Problem verschwinden zu lassen. Die maskierten Ereignisse belegen weiterhin Speicher und kosten weiterhin Ingestion. Das Debug-Log des Support-Engineers blieb ein Designfehler: einen ganzen Request-Body auf einer Checkout-Route zu loggen, egal mit welcher Maskierung, ist falsch. Data Protection ist der Rauchmelder, nicht der Brandschutz. Der Brandschutz sitzt in der Anwendung: ein Logger mit einer Allowlist von Feldern, sodass standardmäßig nichts aus dem Request ins Log gelangt, und eine Code-Review-Regel, nach der jedes neue log.info(req.body) auf Sicht abgelehnt wird.
Wenn Ihre Logs nicht nach CloudWatch gehen
Wir haben an anderer Stelle argumentiert, dass Anwendungslogs nicht der Cloud gehören sollten. Wenn Ihre über einen OpenTelemetry Collector nach Loki laufen, gehört dieselbe Kontrolle in den Collector, und dort ist sie wohl sogar besser aufgehoben, weil sie läuft, bevor die Daten den Host verlassen:
processors:
redaction:
allow_all_keys: true
blocked_values:
- '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' # E-Mail
- '\b(?:\d[ -]*?){13,16}\b' # Kartennummer
- 'AKIA[0-9A-Z]{16}' # AWS Access Key ID
summary: info
Der redaction-Prozessor maskiert treffende Werte in Log-Attributen und meldet eine Zusammenfassung, wie viele er maskiert hat, auf die Sie alarmieren können. Er hat nicht AWS' Bibliothek nationaler ID-Formate, sodass Sie die Liste für einen regulierten Workload selbst pflegen müssten. Für die drei Leck-Klassen, die wir tatsächlich sehen, E-Mails, Karten und Zugangsdaten, reichen drei Regexe.
Was wir nach dem Fund geändert haben
- Das Checkout-Debug-Log wurde in derselben Stunde entfernt. Die Log-Gruppe bekam 1 Tag Aufbewahrung, bis die maskierten Ereignisse ausgelaufen waren, dann wieder 30.
- Der Anwendungs-Logger bekam eine Allowlist:
orderId,userId,route,status,durationMs. Alles andere muss namentlich hinzugefügt werden, im Code-Review. - Die Webhook-Lambda loggt jetzt die Form des Payloads (Schlüssel und Typen) und einen Hash, nie die Werte.
- CodeBuild bekam
--no-echoim Umgebungsschritt, und der ausgegebene Access Key wurde rotiert, denn eine Log-Zeile ist eine Kopie. - Der Alarm geht jetzt mit verlinkter Logs-Insights-Abfrage in den On-Call-Kanal. Mittlere Zeit von Leck bis Fix bei den zwei Vorfällen seither: unter einer Stunde.
Schalten Sie die Policy ein, bevor Sie sie brauchen. Es ist eine CloudFormation-Ressource, achtzehn Dollar im Monat, und sie wird etwas finden. Unsere hat in der ersten Woche drei Dinge gefunden. Wenn Sie Hilfe beim Entwurf der Allowlist für Ihren eigenen Logger oder beim Aufsetzen der Policy über mehrere Konten möchten, sprechen Sie mit uns.