Cài đặt ModSecurity v3 và OWASP CRS trên Nginx Ubuntu: Dựng WAF chuẩn cho Web Server

Ubuntu tutorial - IT technology blog
Ubuntu tutorial - IT technology blog

1. Tình huống thực tế: Server sập dù đã bật UFW và Fail2ban

Nhiều anh em quản trị server Ubuntu thường có thói quen: dựng UFW đóng port thừa, cài thêm Fail2ban chống dò SSH rồi yên tâm đi ngủ. Nhưng thực tế vận hành lại phũ phàng hơn nhiều.

Đợt vừa rồi, một con web app bên mình bất ngờ bị dump sạch bảng users. Mở log ra kiểm tra thì UFW vẫn xanh lè. Fail2ban cũng nằm im bất động. Kẻ tấn công không hề brute-force dồn dập hàng nghìn request để kích hoạt ngưỡng chặn. Hắn chỉ gửi đúng 3 HTTP request chứa payload khai thác lỗ hổng SQL Injection:

' UNION SELECT id, username, password_hash FROM admin_users--

Nginx nhận gói tin qua port 443 như bao lượt truy cập hợp lệ khác. Backend PHP-FPM xử lý ngon lành và trả về mã HTTP 200 kèm toàn bộ credential của ban quản trị.

Tường lửa mạng truyền thống hoàn toàn “mù” trước nội dung bên trong gói tin HTTP. Với chúng, traffic sạch hay payload độc đều giống hệt nhau.

2. Khoảng trống chết người ở tầng ứng dụng (Layer 7)

Muốn chặn đúng, trước hết ta phải xếp từng công cụ vào đúng vị trí của nó:

  • UFW / Iptables / NFTables: Trấn giữ Layer 3 (Network) và Layer 4 (Transport). Chúng kiểm tra IP nguồn, IP đích, port và cờ TCP/UDP. Tuyệt đối không thể đọc hay giải mã nội dung bên trong body HTTP.
  • Fail2ban: Bản chất là công cụ phản ứng sau sự cố (reactive). Nó quét log tìm dấu hiệu bất thường để block IP. Nếu kẻ tấn công đi chậm từng bước (“low and slow”), Fail2ban coi như phế.

Hiểm họa thực tế luôn nằm ở phần dữ liệu: query param trên URL, header tùy biến, hoặc payload JSON ẩn trong POST body. Muốn tóm gọn SQL Injection (SQLi), Cross-Site Scripting (XSS) hay Remote Code Execution (RCE), bạn cần một chốt chặn soi kỹ từng byte dữ liệu trước khi đẩy xuống backend. Đó là vai trò của Web Application Firewall (WAF).

3. So sánh 3 giải pháp WAF phổ biến

Đứng trước bài toán này, anh em DevOps thường cân nhắc ba hướng đi:

  • Dùng Cloud WAF (Cloudflare, AWS WAF): Kích hoạt siêu nhanh qua DNS, mạng lưới CDN rộng. Nhưng chi phí tăng phi mã theo lượng request. Dữ liệu nhạy cảm buộc phải chạy qua hạ tầng bên thứ ba. Ngoài ra, các gói miễn phí hoặc giá rẻ bị bóp tính năng tùy biến rule rất ngặt.
  • Tự viết Regex chặn trong file Nginx config: Ví dụ dùng rule if ($query_string ~* "union.*select") { return 403; }. Cách này rất dễ vỡ trận. Hacker chỉ cần đổi thành uNiOn/**/sElEcT hoặc mã hóa URL %75%6e%69%6f%6e là qua mặt êm ru. Viết regex dày đặc còn khiến Nginx ngốn thêm 15-30% CPU mỗi khi có traffic đột biến.
  • Dựng WAF On-Premise với ModSecurity + OWASP CRS: Kết hợp giữa engine ModSecurity v3 (libmodsecurity) và bộ quy tắc tiêu chuẩn OWASP Core Rule Set. Chi phí bản quyền: 0 đồng. Chạy trực tiếp trong tiến trình Nginx, kiểm soát 100% dữ liệu và tinh chỉnh thoải mái theo đặc thù từng API.

4. Triển khai ModSecurity v3 và OWASP CRS trên Ubuntu

Từ bản v3, ModSecurity đã tách hoàn toàn engine xử lý (libmodsecurity) ra khỏi web server connector. Kiến trúc module động này giúp Nginx chạy ổn định hơn hẳn bản v2 nặng nề trước kia.

Các bước dưới đây được thực hiện trực tiếp trên Ubuntu 22.04 LTS với Nginx đang chạy sẵn.

Bước 1: Cài đặt thư viện biên dịch cần thiết

Chạy lệnh cập nhật repo và tải các package phục vụ build mã nguồn:

sudo apt update
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev \
  libssl-dev libxml2-dev libxslt1-dev libgeoip-dev liblmdb-dev libyajl-dev \
  git automake libtool curl libfuzzy-dev

Bước 2: Biên dịch core engine ModSecurity v3

Clone mã nguồn chính thức của libmodsecurity và tiến hành compile:

cd /usr/local/src
sudo git clone --depth 1 -b v3/master https://github.com/owasp-modsecurity/ModSecurity.git
cd ModSecurity
sudo git submodule init
sudo git submodule update
sudo ./build.sh
sudo ./configure
sudo make -j$(nproc)
sudo make install

Quá trình biên dịch thường tốn từ 3 đến 8 phút tùy số core CPU. Sau khi lệnh kết thúc thành công, thư viện lõi sẽ nằm gọn trong /usr/local/modsecurity/.

Bước 3: Biên dịch connector module khớp bản Nginx

Nginx yêu cầu module động phải được build từ mã nguồn cùng phiên bản tuyệt đối với binary đang chạy. Đầu tiên, hãy check phiên bản hiện tại:

nginx -v

Màn hình trả về kết quả, ví dụ nginx version: nginx/1.24.0. Tải đúng tarball bản đó và clone connector ModSecurity-nginx:

cd /usr/local/src
sudo git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git

wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -xzvf nginx-1.24.0.tar.gz
cd nginx-1.24.0

# Biên dịch module tương thích với binary hiện hành
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules

# Đưa file binary module vào thư mục Nginx
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/

Bước 4: Nạp module vào file cấu hình chính

Mở file /etc/nginx/nginx.conf:

sudo nano /etc/nginx/nginx.conf

Thêm chỉ thị sau lên ngay đầu file, trước cả khối events {}:

load_module modules/ngx_http_modsecurity_module.so;

Bước 5: Tải và thiết lập bộ luật OWASP CRS

Engine ModSecurity giống như khẩu súng, còn OWASP CRS chính là băng đạn. Tạo thư mục làm việc và tải bộ rule về máy:

sudo mkdir -p /etc/nginx/modsec
cd /usr/local/src/ModSecurity
sudo cp modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
sudo cp unicode.mapping /etc/nginx/modsec/

# Tải OWASP CRS v3.3.5 (bản ổn định cao)
cd /etc/nginx/modsec
sudo git clone -b v3.3.5 https://github.com/coreruleset/coreruleset.git owasp-crs
sudo cp owasp-crs/crs-setup.conf.example owasp-crs/crs-setup.conf

Tạo file gom rule /etc/nginx/modsec/main.conf để Nginx gọi vào:

sudo nano /etc/nginx/modsec/main.conf

Dán 3 dòng khai báo cấu hình sau:

Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/owasp-crs/crs-setup.conf
Include /etc/nginx/modsec/owasp-crs/rules/*.conf

Bước 6: Chuyển WAF sang chế độ kích hoạt

Mặc định ModSecurity chạy ở chế độ DetectionOnly (chỉ ghi log, không drop request). Hãy bật chế độ chặn thực tế trong /etc/nginx/modsec/modsecurity.conf:

sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf

Đồng thời thiết lập log audit ra file riêng để theo dõi:

SecAuditLog /var/log/nginx/modsec_audit.log

Bước 7: Kích hoạt ModSecurity trên Virtual Host

Chỉ định site nào cần WAF bảo vệ trong file config server block (ví dụ: /etc/nginx/sites-available/default):

server {
    listen 80;
    server_name example.com;

    # Bật WAF cho toàn bộ virtual host này
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    location / {
        try_files $uri $uri/ =404;
    }
}

Kiểm tra cú pháp và reload dịch vụ:

sudo nginx -t && sudo systemctl reload nginx

Bước 8: Bắn payload test và kiểm tra log

Dùng curl giả lập một request chứa payload XSS kinh điển:

curl -I "http://localhost/?q=<script>alert(1)</script>"

Thay vì mã 200, Nginx lập tức phản hồi ngay:

HTTP/1.1 403 Forbidden
Server: nginx/1.24.0
Content-Type: text/html
Connection: keep-alive

Mở file audit log để xem chi tiết lý do chặn:

sudo tail -n 30 /var/log/nginx/modsec_audit.log

Hệ thống sẽ chỉ rõ Rule ID 941100 (XSS Filter – Category 1) bị kích hoạt. Tổng điểm anomaly score của request vượt quá ngưỡng mặc định (5 điểm), ModSecurity ngắt kết nối ngay tại chỗ.

5. Kinh nghiệm thực chiến: Xử lý False Positive mà không làm vỡ app

Triển khai WAF sợ nhất là chặn nhầm người dùng hợp lệ (False Positive). Một form soạn thảo văn bản chứa thẻ HTML hoặc API gửi payload JSON lồng nhau rất dễ bị rule 942100 (SQLi) gắn cờ oan.

Để tránh sự cố ngắt quãng dịch vụ, bạn nên áp dụng quy trình 2 bước sau:

  1. Chạy rà soát log trước 3-7 ngày: Đặt SecRuleEngine DetectionOnly. Hằng ngày mở log lọc xem những request nào của user thật bị dính đạn.
  2. Whitelist theo từng endpoint: Không nên tắt hoàn toàn rule trên toàn site. Nếu endpoint /api/v1/posts/editor hay bị chặn oan bởi rule 942100, hãy chèn thêm ngoại lệ vào cuối file /etc/nginx/modsec/main.conf:
# Whitelist rule 942100 riêng cho endpoint soạn thảo
SecRule REQUEST_URI "@beginsWith /api/v1/posts/editor" \
    "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

Bằng cách này, cánh cửa vào database vẫn được chốt chặn nghiêm ngặt mà tính năng nghiệp vụ của ứng dụng vẫn vận hành trơn tru.

Share: