Skip to content
CloudWatch Data Protection Policies: PII maskieren, bevor sie in Ihren Logs landen
← ← Zurück zu Gedanken Cloud

CloudWatch Data Protection Policies: PII maskieren, bevor sie in Ihren Logs landen

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.

Anwendung{"email":"a@b.com","phone":…} Data Protection Policy · bei IngestionIdentifikatoren: EmailAddress,PhoneNumber, CreditCardNumber, …$0,12 pro gescanntem GB AuditFund → Log-Gruppe · Metrik +1 De-identifygespeichert als {"email":"*******"} AlarmLogEventsWithFindings > 0 Logs InsightsDemaskieren nur mit logs:Unmask Der Rohwert erreicht den Speicher nie. Der Fund nennt Log-Gruppe und Identifikator, nicht den Wert.

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.

1 · in der Anwendung Allowlist-Logger standardmäßig nichts aus req.body Review-Regel: keine rohen Payloads die Lösung 2 · im Agenten OTel-Collector-Redaction-Prozessor bevor Daten den Host verlassen für jedes Backend, Ihre Regexe der Gürtel 3 · in CloudWatch Data Protection Policy bei Ingestion · verwaltete Identifikatoren Funde sagen, wo man hinsehen muss der Rauchmelder Alle drei tun. Nur das erste reduziert, was Sie speichern; nur das dritte sagt Ihnen, dass die ersten beiden versagt haben.

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-echo im 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.