Să pornești AWS WAF în fața unei aplicații web e un construct CDK și o bifă de grupuri de reguli managed. E și, din experiența noastră, garantat să blocheze ceva real în prima săptămână: o trimitere de formular, un webhook, un upload de imagine, un câmp de text formatat. Regulile nu greșesc. Sunt generice, iar aplicația ta nu e.
Asta e configurația de WAF pe care o rulăm în fața unei aplicații Next.js publice pe App Runner, cele cinci reguli managed care au blocat trafic legitim, motivul pentru care s-a declanșat fiecare și override-ul care a rezolvat fără să oprim regula pentru tot.
Configurația
CloudFront în față, App Runner în spate, WAF atașat distribuției CloudFront, ca să vadă fiecare request înaintea originii. Trei grupuri de reguli managed plus o limită de rată:
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, /* override-uri mai jos */),
{
name: 'rate-limit', priority: 40,
statement: { rateBasedStatement: { limit: 2000, aggregateKeyType: 'IP' } },
action: { block: {} },
visibilityConfig: { sampledRequestsEnabled: true, cloudWatchMetricsEnabled: true, metricName: 'rate-limit' },
},
],
});
Cost: $5 pentru web ACL, $1 per grup de reguli, $0,60 per milion de request-uri. Cam $16 pe lună la noi, cel mai ieftin control de securitate de pe factură după politica de protecție a datelor.
Întâi numeri, apoi blochezi
N-am început în modul block. Fiecare grup managed a rulat cu acțiunea suprascrisă pe COUNT timp de două săptămâni, cu logul de request-uri eșantionate mergând în S3 și un dashboard CloudWatch cu numărul de potriviri per regulă. Perioada aia a produs lista de mai jos. Fără ea, am fi găsit fiecare dintre ele printr-un utilizator care raportează un formular stricat, adică felul în care le găsesc majoritatea echipelor.
Query-ul de dashboard e simplu: numărul de request-uri potrivite per terminatingRuleId și ruleGroupList[].ruleId, filtrat pe cele cu acțiunea COUNT. Orice are volum real și nu e evident un atac e examinat de mână: ce cale, ce body, ce client.
Cele cinci reguli care au blocat trafic real
Toate cinci sunt în AWSManagedRulesCommonRuleSet, grupul pe care îl activează toată lumea și cel cu cele mai multe opinii despre cum ar trebui să arate un request.
1. SizeRestrictions_BODY: orice body de request peste 8 KB. Regula există pentru că body-urile supradimensionate sunt un vector de atac comun și pentru că WAF inspectează oricum doar primii 8 KB (16 KB pe CloudFront în configurațiile mai noi). Formularul nostru de editare a profilului trimite un payload JSON cu un avatar ca data URL base64. Adică 40 KB. Blocat. Fiecare salvare de profil de la un utilizator cu poză, dispărută, pentru cele două săptămâni cât i-ar fi luat cuiva s-o raporteze.
Rezolvare: regula rămâne în modul block global și e suprascrisă pe COUNT pentru cele două rute care acceptă legitim body-uri mari, printr-un scope-down statement pe calea URI. Rezolvarea mai bună, pe care am făcut-o și noi: upload-urile merg în S3 printr-un presigned URL, iar formularul trimite o cheie, nu octeții, deci body-ul e din nou 2 KB.
2. CrossSiteScripting_BODY: HTML într-un body de request. Un editor de text formatat pentru descrieri de produse trimite HTML sanitizat. Pentru regula XSS, <p> și <a href> într-un body sunt un atac. Blocat la salvare.
Rezolvare: override pe COUNT pentru cele două rute de admin care acceptă HTML, și blocare în continuare peste tot. Aplicația sanitizează deja cu o listă albă pe server; regula WAF duplica verificarea aia cu un instrument mai grosier.
3. GenericRFI_BODY: un URL într-un body de request. Detecția de remote-file-inclusion se declanșează pe string-uri http:// sau https:// în body. Un webhook de la procesatorul nostru de plăți include URL-ul chitanței. Un utilizator care lipește un link într-un formular de suport include un URL. Ambele blocate.
Rezolvare: COUNT pe /api/webhooks/* și pe ruta formularului de suport. E regula cu cea mai mare rată de fals-pozitive pe orice aplicație care acceptă text liber, și prima la care te uiți dacă formularele pică misterios.
4. NoUserAgent_HEADER: request fără User-Agent. Browserele trimit mereu unul. Unii emițători de webhook nu, și unul de-al nostru nu trimitea. Fiecare confirmare de plată de la procesatorul ăla era blocată, ceea ce am găsit într-o zi, pentru că a stricat checkout-ul, nu în două săptămâni.
Rezolvare: COUNT restrâns la /api/webhooks/*. Rutele de webhook sunt oricum autentificate prin semnătură; verificarea User-Agent nu adaugă nimic acolo.
5. EC2MetaDataSSRF_BODY: string-ul 169.254.169.254 într-un body. Un câmp în care un operator lipește fragmente de log pentru un tichet de suport conținea un URL de instance metadata dintr-o sesiune de depanare. Blocat. O dată. Îl includem pentru că e un exemplu bun de regulă care are dreptate despre ce e string-ul și greșește despre dacă contează.
Rezolvare: niciuna. Un blocaj în șase luni pe o rută internă e în regulă. Nu fiecare fals-pozitiv are nevoie de override; override-ul e o gaură permanentă, iar blocajul a fost o întâmplare.
Cum se scrie un override
Proprietatea importantă: regula nu e dezactivată. E schimbată pe COUNT doar când se potrivește un scope-down statement, și blochează peste tot în rest. În CDK, pe statement-ul grupului de reguli managed:
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' }],
},
},
},
Asta e o instanță a grupului pentru căile de webhook, cu două override-uri, și o a doua instanță a aceluiași grup, cu prioritate mai mică, fără override-uri, pentru tot restul. Două copii ale grupului, $2 pe lună, iar regulile se aplică integral pe 99 % din trafic.
Limita de rată e regula care își câștigă pâinea
Grupurile managed au blocat constant câteva sute de request-uri pe zi de zgomot de scanere, pe care aplicația le-ar fi gestionat oricum. Regula bazată pe rată, 2.000 de request-uri per cinci minute per IP, a blocat trei încercări de credential stuffing pe ruta de login și un scraper care trăgea fiecare pagină de produs în buclă. Alea sunt incidentele care ar fi costat ceva. Dacă activezi o singură regulă, activeaz-o pe aia, și pune una mai strânsă (100 per cinci minute) specific pe /api/auth/*.
Ce i-am spune unei echipe care activează WAF mâine
- Două săptămâni în
COUNT, cu logging pornit, înainte ca o singură regulă să blocheze. Citește logul pe regulă și pe cale. - Așteaptă-te la cele cinci reguli de mai sus, cam în ordinea asta de probabilitate, pe orice aplicație cu formulare, upload-uri sau webhook-uri.
- Override pe cale, niciodată global. Dacă o regulă trebuie oprită peste tot, aplicația are o problemă mai mare decât regula.
- Mută payload-urile mari din body-urile de request (upload-uri presigned) în loc să lărgești limita de body. Rezolvă problema de WAF și factura de bandă deodată.
- Pune o limită de rată strânsă pe rutele de autentificare. E singurul control de aici care a oprit un atac real.
- Recitește metricile de count lunar. Grupurile managed își actualizează regulile fără să-ți spună, iar un fals-pozitiv nou apare ca un vârf nou.
Șaisprezece dolari pe lună și o după-amiază de citit loguri. Dacă preferi să sari peste cele două săptămâni și să pornești de la o configurație care știe deja de cele cinci reguli, hai să vorbim.