Snippet bảo mật dùng chung trên balancer (bài chuẩn hoá bảo mật Nginx bằng security snippet) chặn tốt các đường dẫn nhạy cảm như .env, .git, wp-login. Nhưng nó không nhìn vào nội dung request: một tham số chứa ' UNION SELECT hay <script> vẫn đi thẳng tới PHP. Để chặn lớp tấn công này ngay ở cổng vào, có thể đưa Coraza WAF cùng bộ luật OWASP CRS 4 vào nginx trên balancer Debian 12. Tài liệu này hướng dẫn build module, cấu hình mẫu, cách tuning chặn nhầm, cách tránh lỗi trùng rule ID và đưa log WAF lên Grafana để cảnh báo.
1. WAF là gì và Coraza WAF hoạt động thế nào?
WAF (Web Application Firewall) là tường lửa ở tầng ứng dụng. Thay vì xét IP và cổng như ufw, nó đọc từng request HTTP và so với một bộ luật. Với Coraza WAF chạy như module nginx, mỗi request đi qua các phase:
- Phase 1: header và URI. Phát hiện path traversal (
../), method lạ, header bất thường. - Phase 2: body. Giải mã form, JSON, XML để tìm SQL injection, XSS, chèn lệnh shell.
- Phase 3, 4: header và body của response. Trên máy RAM nhỏ nên tắt phần đọc body response.
Bộ luật là OWASP Core Rule Set (CRS), chia theo nhóm ID: 920xxx vi phạm giao thức, 930xxx LFI, 931xxx RFI, 932xxx chèn lệnh (RCE), 933xxx chèn mã PHP, 941xxx XSS, 942xxx SQLi.
Anomaly scoring: cộng điểm rồi mới chặn
Với CRS 4, Coraza WAF không chặn ngay khi một rule khớp. Mỗi rule khớp cộng điểm theo mức độ: CRITICAL 5 điểm, ERROR 4, WARNING 3, NOTICE 2. Cuối phase, rule 949110 so tổng điểm với ngưỡng inbound_anomaly_score_threshold, vượt ngưỡng thì trả 403. Ngưỡng càng thấp càng chặt: với ngưỡng 5, chỉ cần một rule CRITICAL khớp là request bị chặn.
Song song đó là paranoia level (PL1 đến PL4): mức càng cao càng nhiều rule, bắt nhiều hơn nhưng chặn nhầm cũng nhiều hơn. PL1 là mức khởi đầu khuyến nghị.
2. Coraza WAF hay ModSecurity?
| ModSecurity (libmodsecurity v3) | Coraza WAF | |
|---|---|---|
| Ngôn ngữ | C++ | Go, có thư viện C libcoraza để nhúng vào nginx |
| Ngôn ngữ luật | SecLang | Tương thích SecLang, chạy được CRS 4 |
| Tích hợp nginx | Connector ModSecurity-nginx | Connector coraza-nginx, dynamic module |
Hai bên dùng chung cú pháp SecRule, nên kinh nghiệm tuning CRS của ModSecurity phần lớn áp dụng được cho Coraza WAF. Tài liệu chính thức ở trang dự án Coraza.
3. Build module Coraza WAF khớp nginx của Debian 12
Debian 12 đóng gói nginx 1.22.1. Dynamic module của nginx phải được build từ đúng phiên bản source và đúng bộ tham số configure của binary đang chạy. Lệch một trong hai, nginx từ chối nạp với lỗi kiểu module ... version 1024000 instead of 1022001 hoặc is not binary compatible. Cách chắc chắn nhất là đọc tham số từ chính nginx -V:
# 1. thư viện libcoraza: lõi Coraza WAF viết bằng Go, cần Go và bộ công cụ build
cd /tmp && git clone --depth 1 https://github.com/corazawaf/libcoraza
cd libcoraza && ./build.sh && ./configure && make && make install && ldconfig
# 2. connector nginx + source nginx đúng phiên bản đang chạy
cd /tmp && git clone --depth 1 https://github.com/corazawaf/coraza-nginx
NGINX_VERSION=$(nginx -v 2>&1 | grep -oP 'nginx/\K[0-9.]+') # 1.22.1
NGINX_CONFIGURE=$(nginx -V 2>&1 | grep -oP 'configure arguments: \K.*')
wget -q "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
tar -xzf "nginx-${NGINX_VERSION}.tar.gz" && cd "nginx-${NGINX_VERSION}"
# 3. chỉ build module, không build lại nginx
eval ./configure $NGINX_CONFIGURE --add-dynamic-module=/tmp/coraza-nginx
make modules
cp objs/ngx_http_coraza_module.so /usr/lib/nginx/modules/
# 4. bộ luật OWASP CRS 4
wget -q https://github.com/coreruleset/coreruleset/archive/v4.0.0.tar.gz
tar -xzf v4.0.0.tar.gz && mv coreruleset-4.0.0 /etc/coraza/crs
cp /etc/coraza/crs/crs-setup.conf.example /etc/coraza/crs/crs-setup.confHệ quả cần nhớ: mỗi khi apt upgrade nâng nginx lên bản vá mới, module phải build lại theo phiên bản mới, nếu không nginx sẽ không khởi động được. Nên đóng các bước trên thành một pipeline nhận tham số là tên balancer (cách tổ chức xem bài Jenkins làm "control plane" cho hàng trăm website), hoặc giữ gói nginx bằng apt-mark hold nginx cho tới khi build lại.
4. Cấu hình Coraza WAF mẫu
# /etc/coraza/coraza.conf
Include /etc/coraza/rules/admin-tuning.conf # luật tuning PHẢI nạp trước CRS
Include /etc/coraza/crs/crs-setup.conf
Include /etc/coraza/crs/rules/*.conf
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off # máy RAM nhỏ: không soi response
SecAuditEngine RelevantOnly # chỉ ghi request đáng chú ý
SecAuditLog /var/log/coraza/audit.log
SecAuditLogFormat JSON # mỗi sự kiện một dòng JSON
SecAuditLogParts ABCHZ
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1"
SecAction "id:900001,phase:1,nolog,pass,t:none,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"Khuyến nghị giữ đúng mặc định của CRS: PL1, ngưỡng 5. Khi chặn nhầm nhiều, rất dễ bị cám dỗ nâng ngưỡng lên 10 hay 15, nhưng khi đó một payload chỉ khớp một rule CRITICAL sẽ lọt qua. Giữ ngưỡng và xử lý chặn nhầm từng trường hợp một.
Nạp module mà không ghi đè nginx.conf
Đừng để script cài Coraza WAF ghi đè toàn bộ /etc/nginx/nginx.conf bằng file mẫu: chỉnh sửa riêng của từng balancer (zone rate limit, worker_connections, log format) sẽ mất, và nếu file mẫu thiếu include /etc/nginx/modules-enabled/*.conf; thì module như headers-more không được nạp. Cách an toàn trên Debian là tách thành hai file nhỏ:
# /etc/nginx/modules-enabled/50-mod-http-coraza.conf (nạp ở main context)
load_module modules/ngx_http_coraza_module.so;
# /etc/nginx/conf.d/00-coraza.conf (conf.d được include trong http {})
coraza on;
coraza_rules_file /etc/coraza/coraza.conf;5. Tuning chặn nhầm Coraza WAF
Mọi ngoại lệ của Coraza WAF nằm trong /etc/coraza/rules/admin-tuning.conf, nạp trước CRS, dùng dải ID riêng (ví dụ từ 800000 tới 899999) để không đụng ID của CRS. Nguyên tắc: gỡ hẹp nhất có thể, ưu tiên bỏ một rule trên một tham số cụ thể (ctl:ruleRemoveTargetById) thay vì tắt cả rule.
Ví dụ: Coraza WAF chặn một từ trùng tên lệnh Unix
Rule 932260 thuộc nhóm phát hiện chèn lệnh Unix. Nếu tên thương hiệu, tên miền email hay mật khẩu của người dùng chứa một từ trùng tên lệnh, chẳng hạn "sudo", form đăng nhập sẽ trả 403. Gỡ hẹp bằng cách chỉ bỏ rule đó trên tham số khi request chứa đúng từ ấy:
# 932260 chặn nhầm khi tham số chứa "sudo" (tên thương hiệu trong email)
SecRule ARGS "@rx (?i:sudo)" \
"id:840900,phase:2,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS"
SecRule REQUEST_BODY "@rx (?i:sudo)" \
"id:840901,phase:2,pass,nolog,ctl:ruleRemoveTargetById=932260;REQUEST_BODY"Cái giá: request có từ đó thì rule 932260 không còn soi tham số. Các rule RCE khác và toàn bộ nhóm SQLi, XSS vẫn hoạt động, nhưng đây vẫn là lỗ hổng có chủ đích cần ghi lý do và theo dõi.
Các trường hợp chặn nhầm hay gặp
| Hiện tượng | Rule | Cách gỡ |
|---|---|---|
| Trang quản trị đăng bài có HTML, script nhúng bị chặn | Nhiều rule XSS | Tắt engine cho /admin/*, riêng trang login vẫn bật |
| JSON tiếng Việt bị coi là né Unicode | 920540 | Bỏ rule toàn cục |
| Cookie chứa JWT, session token khớp mẫu tấn công | Nhiều rule | Bỏ REQUEST_COOKIES khỏi CRS |
API REST dùng PUT, DELETE bị chặn | 911100 | Mở tx.allowed_methods theo Host của API |
Trường JSON tên number_format bị coi là chèn hàm PHP | 933150 | Bỏ rule trên đúng endpoint |
Cách tìm rule gây chặn nhầm: lọc audit log theo domain và IP người dùng báo lỗi, đọc mảng messages để lấy ID rule và chuỗi khớp, rồi mới quyết định gỡ ở mức nào. Không gỡ chỉ vì "có người kêu bị 403": bot quét .env hay khai thác wp-json cũng nhận đúng mã 403 đó.
6. Trùng rule ID: lỗi mà nginx -t không bắt được
Khi file tuning dài dần, rất dễ đặt rule mới trùng ID với rule đã có. nginx -t vẫn báo OK. Chỉ khi reload, Coraza WAF mới báo duplicated rule id, không khởi tạo được, và mọi site trên balancer có thể ngừng phục vụ cho tới khi sửa xong. Hãy chạy kiểm tra này trước mỗi lần sửa:
# ID nào xuất hiện hơn một lần trong tuning + toàn bộ CRS?
grep -ohE 'id:[0-9]+' /etc/coraza/rules/*.conf /etc/coraza/crs/crs-setup.conf \
/etc/coraza/crs/rules/*.conf | sort | uniq -d
# sau reload: xác nhận WAF khởi tạo được
nginx -t && systemctl reload nginx && sleep 2 && \
journalctl -u nginx --since "1 min ago" | grep -i "failed to create WAF"Nên đặt lệnh uniq -d làm bước bắt buộc trước khi reload Coraza WAF: có ID trùng thì dừng.
7. Audit log Coraza WAF → Loki → cảnh báo Grafana
Audit log JSON có thể được chính agent Fluent Bit trên balancer đẩy về Loki với label job="coraza-waf" (cấu hình ở bài gom log Nginx về Loki với Fluent Bit). Nếu bản ghi còn khoá thừa như filename, dòng log bị bọc trong {"log": ...}, và truy vấn phải mở hai lớp JSON:
sum by (ip, host, instance) (
count_over_time(
{job="coraza-waf"}
| json | line_format "{{.log}}"
| json ip="transaction.client_ip", host="transaction.request.headers.host[0]"
[2m]
)
)Trên nền truy vấn này, có thể đặt hai luật Grafana Alerting gửi về kênh chat của đội vận hành (ngưỡng dưới đây là ví dụ, chỉnh theo lưu lượng của bạn):
- IP tấn công tần suất cao: một IP bị ghi nhận hơn 50 lần trong 2 phút, kéo dài 5 phút.
- Website đang bị tấn công: một host có hơn 200 sự kiện WAF trong 5 phút, kéo dài 5 phút.
Dashboard Coraza WAF nên có top IP, top domain và top rule ID (bỏ qua 949110, rule tổng điểm). Một rule bỗng khớp rất nhiều lần trên site bình thường thường là dấu hiệu cần tuning.
8. Lỗi thường gặp với Coraza WAF
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
nginx không start sau apt upgrade | Module build cho phiên bản nginx cũ | Build lại theo nginx -V mới, hoặc apt-mark hold |
| Rule tuning không có tác dụng | File tuning nạp sau CRS | Include tuning trước crs-setup.conf |
| Agent log tốn CPU, ghép dòng sai | Audit log dạng Native nhiều dòng | SecAuditLogFormat JSON |
| Người dùng thật bị 403 rồi bị chặn hẳn IP | Chặn nhầm cộng fail2ban đếm 403 | Tuning rule, gỡ ban IP, xem lại ngưỡng fail2ban |
9. Checklist
- Giữ ngưỡng mặc định, tuning từng ca. Nâng ngưỡng là cách nhanh nhất để hết chặn nhầm, cũng là cách nhanh nhất để WAF thành vô dụng.
- Mỗi ngoại lệ là một khoản nợ. Ghi lý do ngay trên từng
SecRule, gỡ hẹp theo tham số và đường dẫn. nginx -tlà chưa đủ. Với module bên thứ ba như Coraza WAF, kiểm tra ID trùng trước và xem log ngay sau reload.- WAF không đứng một mình. Coraza WAF chỉ chặn từng request. Chặn cả IP sau nhiều lần vi phạm là việc của tầng khác.
Bài tiếp theo
fail2ban và limit_req: chỉ giới hạn request động. Cách giới hạn tốc độ mà không đếm file tĩnh, và để fail2ban tự ban những IP liên tục bị 403 hoặc 429.
