AWS WAF vor einer Webanwendung einzuschalten ist ein CDK-Construct und eine Handvoll Häkchen bei Managed Rule Groups. Es ist außerdem, nach unserer Erfahrung, garantiert, dass es in der ersten Woche etwas Echtes blockiert: eine Formularübermittlung, einen Webhook, einen Bild-Upload, ein Rich-Text-Feld. Die Regeln sind nicht falsch. Sie sind generisch, und Ihre Anwendung ist es nicht.
Das hier ist die WAF-Konfiguration, die wir vor einer öffentlichen Next.js-Anwendung auf App Runner betreiben, die fünf Managed Rules, die legitimen Traffic blockiert haben, der Grund, warum jede ausgelöst hat, und der Override, der es behoben hat, ohne die Regel für alles abzuschalten.
Das Setup
CloudFront davor, App Runner dahinter, WAF an die CloudFront-Distribution gehängt, sodass sie jeden Request sieht, bevor die Origin es tut. Drei Managed Rule Groups plus ein Rate-Limit:
const acl = new wafv2.CfnWebACL(this, 'WebAcl', {
scope: 'CLOUDFRONT',
defaultAction: { allow: {} },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'web-acl' },
rules: [
managedGroup('AWSManagedRulesAmazonIpReputationList', 10),
managedGroup('AWSManagedRulesKnownBadInputsRuleSet', 20),
managedGroup('AWSManagedRulesCommonRuleSet', 30, /* Overrides unten */),
{
name: 'rate-limit', priority: 40,
statement: { rateBasedStatement: { limit: 2000, aggregateKeyType: 'IP' } },
action: { block: {} },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'rate-limit' },
},
],
});
Kosten: $5 für die Web ACL, $1 pro Rule Group, $0,60 pro Million Requests. Etwa $16 im Monat bei uns, die billigste Sicherheitskontrolle auf der Rechnung nach der Data Protection Policy.
Erst zählen, später blockieren
Wir haben nicht im Block-Modus angefangen. Jede Managed Group lief zwei Wochen lang mit auf COUNT überschriebener Aktion, das Sampled-Request-Log ging nach S3 und ein CloudWatch-Dashboard zeigte Treffer pro Regel. Diese Phase hat die Liste unten hervorgebracht. Ohne sie hätten wir jeden dieser Punkte gefunden, indem ein Nutzer ein kaputtes Formular meldet, und so finden es die meisten Teams.
Die Dashboard-Abfrage ist einfach: Anzahl passender Requests pro terminatingRuleId und ruleGroupList[].ruleId, gefiltert auf die mit Aktion COUNT. Alles mit echtem Volumen, das nicht offensichtlich ein Angriff ist, wird von Hand geprüft: welcher Pfad, welcher Body, welcher Client.
Die fünf Regeln, die echten Traffic blockiert haben
Alle fünf stehen im AWSManagedRulesCommonRuleSet, der Gruppe, die alle einschalten und die die meisten Meinungen darüber hat, wie ein Request auszusehen hat.
1. SizeRestrictions_BODY: jeder Request-Body über 8 KB. Die Regel existiert, weil übergroße Bodies ein häufiger Angriffsvektor sind und weil WAF ohnehin nur die ersten 8 KB inspiziert (16 KB auf CloudFront in neueren Konfigurationen). Unser Profil-Bearbeitungsformular sendet einen JSON-Payload mit einem Avatar als Base64-Data-URL. Das sind 40 KB. Blockiert. Jedes Speichern eines Profils mit Foto, weg, für die zwei Wochen, die es gedauert hätte, bis jemand es meldet.
Fix: Die Regel bleibt global im Block-Modus und wird per Scope-Down-Statement auf dem URI-Pfad für die zwei Routen, die legitim große Bodies annehmen, auf COUNT überschrieben. Bessere Lösung, die wir ebenfalls umgesetzt haben: Uploads gehen per Presigned URL nach S3, und das Formular sendet einen Schlüssel, nicht die Bytes, sodass der Body wieder 2 KB hat.
2. CrossSiteScripting_BODY: HTML in einem Request-Body. Ein Rich-Text-Editor für Produktbeschreibungen sendet bereinigtes HTML. Für die XSS-Regel sind <p> und <a href> in einem Body ein Angriff. Beim Speichern blockiert.
Fix: Override auf COUNT für die zwei Admin-Routen, die HTML annehmen, überall sonst weiter blockieren. Die Anwendung bereinigt bereits serverseitig mit einer Allowlist; die WAF-Regel hat diese Prüfung mit einem stumpferen Werkzeug dupliziert.
3. GenericRFI_BODY: eine URL in einem Request-Body. Die Remote-File-Inclusion-Erkennung feuert auf http://- oder https://-Strings im Body. Ein Webhook unseres Zahlungsanbieters enthält die URL des Belegs. Ein Nutzer, der einen Link in ein Support-Formular einfügt, enthält eine URL. Beides blockiert.
Fix: COUNT auf /api/webhooks/* und der Support-Formular-Route. Das ist die Regel mit der höchsten False-Positive-Rate auf jeder App, die Freitext annimmt, und die erste, die man sich ansieht, wenn Formulare rätselhaft scheitern.
4. NoUserAgent_HEADER: Request ohne User-Agent. Browser senden immer einen. Manche Webhook-Sender nicht, und einer von unseren tat es nicht. Jede Zahlungsbestätigung dieses Anbieters wurde blockiert, was wir innerhalb eines Tages fanden, weil es den Checkout brach, nicht innerhalb von zwei Wochen.
Fix: COUNT, eingeschränkt auf /api/webhooks/*. Webhook-Routen werden ohnehin per Signatur authentifiziert; die User-Agent-Prüfung bringt dort nichts.
5. EC2MetaDataSSRF_BODY: der String 169.254.169.254 in einem Body. Ein Feld, in das ein Operator Log-Auszüge für ein Support-Ticket einfügt, enthielt eine Instance-Metadata-URL aus einer Debug-Sitzung. Blockiert. Einmal. Wir nehmen es auf, weil es ein gutes Beispiel für eine Regel ist, die richtig liegt, was der String ist, und falsch, ob es eine Rolle spielt.
Fix: keiner. Ein Block in sechs Monaten auf einer internen Route ist in Ordnung. Nicht jedes False Positive braucht einen Override; der Override ist ein dauerhaftes Loch, und der Block war ein Einzelfall.
Wie ein Override geschrieben wird
Die wichtige Eigenschaft: Die Regel wird nicht deaktiviert. Sie wird nur auf COUNT geändert, wenn ein Scope-Down-Statement passt, und blockiert überall sonst. In CDK, im Statement der Managed Rule Group:
managedRuleGroupStatement: {
vendorName: 'AWS', name: 'AWSManagedRulesCommonRuleSet',
ruleActionOverrides: [
{ name: 'GenericRFI_BODY', actionToUse: { count: {} } },
{ name: 'NoUserAgent_HEADER', actionToUse: { count: {} } },
],
scopeDownStatement: {
byteMatchStatement: {
fieldToMatch: { uriPath: {} }, positionalConstraint: 'STARTS_WITH',
searchString: '/api/webhooks/', textTransformations: [{ priority: 0, type: 'LOWERCASE' }],
},
},
},
Das ist eine Instanz der Gruppe für die Webhook-Pfade mit zwei Overrides und eine zweite Instanz derselben Gruppe mit niedrigerer Priorität und ohne Overrides für alles andere. Zwei Kopien der Gruppe, $2 im Monat, und die Regeln gelten für 99 % des Traffics vollständig.
Das Rate-Limit ist die Regel, die sich verdient macht
Die Managed Groups haben stetig ein paar hundert Requests am Tag an Scanner-Rauschen blockiert, das die Anwendung ohnehin verkraftet hätte. Die ratenbasierte Regel, 2.000 Requests pro fünf Minuten pro IP, hat drei Credential-Stuffing-Versuche gegen die Login-Route und einen Scraper blockiert, der jede Produktseite in einer Schleife abzog. Das sind die Vorfälle, die etwas gekostet hätten. Wenn Sie eine Regel einschalten, dann diese, und setzen Sie eine engere (100 pro fünf Minuten) speziell auf /api/auth/*.
Was wir einem Team sagen würden, das morgen WAF einschaltet
- Zwei Wochen in
COUNT, mit Logging, bevor eine einzige Regel blockiert. Das Log nach Regel und Pfad lesen. - Erwarten Sie die fünf Regeln oben, ungefähr in dieser Wahrscheinlichkeitsreihenfolge, auf jeder App mit Formularen, Uploads oder Webhooks.
- Override per Pfad, nie global. Muss eine Regel überall aus, hat die Anwendung ein größeres Problem als die Regel.
- Große Payloads aus Request-Bodies herausnehmen (Presigned Uploads), statt das Body-Limit zu erweitern. Das behebt das WAF-Problem und die Bandbreitenrechnung zugleich.
- Ein enges Rate-Limit auf Authentifizierungsrouten. Es ist die eine Kontrolle hier, die einen echten Angriff gestoppt hat.
- Die Count-Metriken monatlich lesen. Managed Groups aktualisieren ihre Regeln, ohne es zu sagen, und ein neues False Positive zeigt sich als neue Spitze.
Sechzehn Dollar im Monat und ein Nachmittag Log-Lesen. Wenn Sie die zwei Wochen lieber überspringen und mit einer Konfiguration starten möchten, die die fünf Regeln bereits kennt, sprechen Sie mit uns.