Ứng dụng di động của một khách hàng không cập nhật được hồ sơ người dùng và không xoá được giao dịch: mọi request PUT, DELETE đều nhận 403. Đây là một ca WAF chặn nhầm kinh điển trên balancer chạy Coraza WAF. Trong lúc vá, nếu một rule mới bị đặt trùng ID với rule cũ, lần reload nginx sau đó có thể làm toàn bộ website trên balancer ngừng phục vụ, trong khi nginx -t và systemctl reload đều báo thành công. Tài liệu này đi theo đúng thứ tự xử lý: phân loại request bị chặn, gỡ ban nhầm của fail2ban, viết ngoại lệ hẹp, và quy trình reload an toàn để một ca WAF chặn nhầm của một khách không biến thành sự cố của mọi khách.
Kiến thức nền về Coraza WAF, OWASP CRS 4, anomaly scoring và cách tổ chức file ngoại lệ có trong bài Coraza WAF và OWASP CRS 4 trên Nginx.
Triệu chứng của WAF chặn nhầm
- App di động (User-Agent kiểu
okhttp/4.x) báo lỗi ở các thao tác cập nhật hồ sơ, cài đặt thông báo và xoá giao dịch, tức đúng các endpoint dùngPUTvàDELETE. - Lỗi trả về là 403 từ balancer. Request bị chặn ngay ở balancer nên không bao giờ tới backend PHP trên minion.
- Một số IP của người dùng thật nằm trong danh sách ban của fail2ban, tức bị chặn hoàn toàn chứ không chỉ ở vài endpoint.
Ba dấu hiệu này cùng chỉ về tầng balancer: lỗi gắn với một nhóm method, 403 sinh ra trước backend, và IP bị chặn hẳn sau một loạt lỗi. Đó là kiểu WAF chặn nhầm cộng với fail2ban đếm 403.
Quy trình điều tra
Bước 1: tách WAF chặn nhầm khỏi tấn công thật trong audit log
Audit log của Coraza (ví dụ /var/log/coraza/audit.log) ghi mỗi giao dịch một dòng JSON. Lọc theo các domain của khách, rồi gom theo ID rule và theo đường dẫn. Không phải mọi request bị chặn đều là WAF chặn nhầm:
| Nhóm | Rule | Đường dẫn ví dụ | Kết luận |
|---|---|---|---|
| Method không nằm trong policy | 911100 | PUT /api/v1/me, DELETE /api/v1/transactions/{id} | WAF chặn nhầm: REST API dùng PUT/DELETE hợp lệ |
| Tên hàm PHP nguy hiểm | 933150 | PUT /api/v1/me/preferences, body có trường number_format | WAF chặn nhầm: tên trường JSON trùng tên hàm number_format() |
| Quét file nhạy cảm | nhiều rule | /.env.bak, /.git/config | Chặn đúng, giữ nguyên |
| Dò công cụ debug | nhiều rule | /phpinfo.php.bak, /_profiler/phpinfo | Chặn đúng |
| Khai thác WordPress | nhiều rule | /wp-json/batch/v1 | Chặn đúng |
Rule 911100 của CRS chỉ cho phép GET HEAD POST OPTIONS theo mặc định. Rule 933150 so khớp tên các hàm PHP nhạy cảm trong tham số. Với một API REST viết đúng chuẩn, cả hai đều là WAF chặn nhầm có thể đoán trước. Nhưng nếu không lọc kỹ, rất dễ "vá" luôn cả các request quét .env, vì chúng cũng trả về đúng mã 403 đó.
# đếm request bị chặn theo rule ID cho một host
jq -r 'select(.transaction.request.headers.host[0]=="api-app.example.com")
| .transaction.messages[]?.data.id' /var/log/coraza/audit.log | sort | uniq -c | sort -rnBước 2: fail2ban biến WAF chặn nhầm thành lệnh ban IP
Jail kiểu nginx-coraza đếm 403 do WAF trả ra và ban IP. Nếu jail dùng chung cho mọi site trên balancer, danh sách ban sẽ lẫn IP của nhiều site. Đối chiếu từng IP với log 403 của riêng domain khách rồi phân loại:
- IP chỉ dính rule
911100/933150trên endpoint API, User-Agent của app: người dùng thật, gỡ ban. - IP quét
/config/.env,/api/.envhoặc khai thácwp-json: bot, giữ ban. - IP bị ban vì vi phạm ở site khác: không đụng tới.
Mỗi lần WAF chặn nhầm không chỉ làm hỏng một request, mà còn có thể biến người dùng thật thành "kẻ tấn công" trong mắt fail2ban.
Bước 3: vá, reload, và cái bẫy trùng rule ID
Thao tác hay gặp: thêm hai rule ngoại lệ vào cuối file tuning, xem phần cuối file bằng tail, thấy các ID gần nhất rồi đặt ID mới nối tiếp. nginx -t báo OK, systemctl reload nginx báo thành công. Nhưng nếu ID đó đã được dùng ở giữa file, journal sẽ ghi:
systemd[1]: Reloading nginx.service ...
nginx: [notice] coraza: rules collected inline/local: 0/1 (WAFs will be created in worker processes)
systemd[1]: Reloaded nginx.service ...
nginx: [error] coraza: failed to create WAF: invalid WAF config from file:
... failed to compile the directive "secrule": duplicated rule id 10001
nginx: [error] coraza: failed to build loc WAF in worker
nginx: [alert] worker process ... exited with fatal code 2 and cannot be respawnedTừ lúc đó, mọi site trên balancer ngừng phục vụ, và thường chỉ được phát hiện khi người dùng báo.
Nguyên nhân gốc
- WAF chặn nhầm API REST. Policy method mặc định của CRS và danh sách tên hàm PHP không biết gì về một API dùng
PUT/DELETEvà có trường tênnumber_format. fail2ban khuếch đại lỗi: 403 do WAF chặn nhầm được đếm như 403 do tấn công. - Trùng rule ID làm sập balancer. Ba yếu tố cộng lại:
- Coraza không biên dịch rule ở bước kiểm tra cấu hình. Dòng
WAFs will be created in worker processesnói rõ: rule chỉ được dựng khi worker khởi động. Vì vậynginx -tkhông thể bắt được lỗi trùng ID. - Reload là "graceful": master cho worker cũ thoát và sinh worker mới. Worker mới thoát với
fatal code 2và master không sinh lại. Master nginx còn sống nhưng không còn worker nào phục vụ. systemctl reloadvẫn trả về thành công, nên ai chỉ nhìn mã thoát sẽ tưởng mọi thứ ổn.
- Coraza không biên dịch rule ở bước kiểm tra cấu hình. Dòng
Phạm vi ảnh hưởng là toàn bộ balancer, vì mọi site dùng chung một tiến trình nginx và một file ngoại lệ Coraza.
Khắc phục
Khi balancer đã mất worker
# tìm ID trùng trong mọi file rule được nạp
grep -hoE 'id:[0-9]+' /etc/coraza/rules/*.conf /etc/coraza/crs-setup.conf /etc/coraza/crs/rules/*.conf \
| sort | uniq -d
# đổi ID của rule vừa thêm, kiểm tra và reload lại
sed -i 's/id:10001,/id:10101,/; s/id:10002,/id:10102,/' /etc/coraza/rules/local-tuning.conf
nginx -t && systemctl reload nginx
sleep 2; pgrep -c -f "nginx: worker"Sau đó curl trang chủ của các domain khách và vài domain ngẫu nhiên khác trên balancer để xác nhận.
Vá WAF chặn nhầm cho đúng phạm vi
Nguyên tắc là gỡ hẹp nhất có thể: mở method cho đúng các host API, bỏ rule 933150 cho đúng một endpoint:
# WAF chặn nhầm 1: API REST dùng PUT/DELETE hợp lệ (rule 911100)
SecRule REQUEST_HEADERS:Host "@within api-app.example.com,api-admin.example.com" \
"id:10101,phase:1,pass,nolog,setvar:tx.allowed_methods=GET HEAD POST PUT DELETE PATCH OPTIONS"
# WAF chặn nhầm 2: trường JSON "number_format" bị coi là chèn hàm PHP (rule 933150)
SecRule REQUEST_URI "@beginsWith /api/v1/me/preferences" \
"id:10102,phase:2,pass,nolog,ctl:ruleRemoveById=933150"Rule đầu chạy ở phase 1 để biến tx.allowed_methods được đặt lại trước khi rule 911100 kiểm tra method. Thử lại bằng một request PUT /api/v1/me, xác nhận audit log không còn ghi chặn, rồi mới gỡ ban cho IP người dùng thật:
fail2ban-client set nginx-coraza unbanip <ip-nguoi-dung>Khi khách yêu cầu tắt WAF
Đôi khi khách yêu cầu tắt hẳn WAF cho domain của họ, dù các ca WAF chặn nhầm đã được vá. Nếu buộc phải làm, tắt theo host và kiểm chứng bằng một payload SQLi đơn giản:
SecRule REQUEST_HEADERS:Host "@within example.com,www.example.com" \
"id:10201,phase:1,pass,nolog,ctl:ruleEngine=Off"
curl -s -o /dev/null -w '%{http_code}\n' "https://example.com/?id=1'%20UNION%20SELECT%201" # 200: đã tắt
curl -s -o /dev/null -w '%{http_code}\n' "https://site-khac.example/?id=1'%20UNION%20SELECT%201" # 403: vẫn bậtQuyết định này phải đi kèm văn bản ghi rủi ro (domain không còn lớp lọc SQLi, XSS, RCE ở balancer), một ngày hết hạn và lịch nhắc bật lại. Bật lại chỉ cần xoá rule tắt engine, giữ các ngoại lệ hẹp, rồi kiểm tra và reload.
Checklist khi sửa WAF chặn nhầm trên balancer
- Trước khi thêm rule: quét ID trong toàn bộ file tuning,
crs-setup.confvàcrs/rules/*.conf, không chỉ xem cuối file. - Quy ước dải ID theo mục đích (ví dụ mỗi khách hoặc mỗi loại ngoại lệ một dải trăm ID), ghi trong comment đầu file.
- Backup file ngoại lệ có dấu thời gian trước mỗi lần sửa, ghi kèm cách rollback.
- Dùng một script reload duy nhất: kiểm tra ID trùng,
nginx -t, reload, đợi vài giây, đếm worker,curlvài domain mẫu. Bước nào hỏng thì chép lại backup và reload. - Sau khi reload: đọc ngay
journalctl -u nginx, tìmfailed to create WAF. Không tin vàonginx -thay mã thoát củasystemctl reload. - Đặt probe HTTP từ bên ngoài cho từng balancer, để người dùng không phải là hệ thống giám sát đầu tiên.
- fail2ban: không ban ngay khi chỉ có 403 từ một rule đã biết hay chặn nhầm; cân nhắc tách jail theo nhóm site.
- Mọi rule
ruleEngine=Offtheo host phải có ngày hết hạn.
Bài học
- Lỗi cấu hình dùng chung có phạm vi ảnh hưởng của cả balancer. Sửa cho một khách thì phải kiểm tra như đang sửa cho tất cả.
nginx -tchỉ kiểm tra phần nginx hiểu. Module nào dựng cấu hình trong worker thì lỗi chỉ lộ ra lúc worker khởi động.- Phân loại trước khi gỡ. Phần lớn request bị chặn thường là bot. Một ca WAF chặn nhầm thật chỉ cần vài ngoại lệ rất hẹp, không cần tắt WAF.
- Tắt WAF là quyết định có hạn. Khách có quyền yêu cầu, nhưng đội vận hành phải ghi rủi ro và theo dõi cho tới khi bật lại.
Tài liệu tham khảo: tài liệu Coraza WAF và tài liệu OWASP Core Rule Set. Các biện pháp chặn ở balancer khác có trong bài chuẩn hoá bảo mật Nginx.
Bài tiếp theo
Upload ảnh lỗi 419 và 502 trên Kubernetes. Một ảnh dung lượng lớn, nhiều giả thuyết sai và hai giới hạn bộ nhớ chồng lên nhau trong cùng một pod.
