Nginx Sau Cloudflare: Sửa Lỗi IP Whitelist Không Hoạt Động
Bạn đã dành cả buổi chiều để cấu hình Nginx cẩn thận: SSL wildcard certificate, reverse proxy đến dashboard nội bộ, IP whitelist chỉ cho phép đúng hai địa chỉ IP của team, thêm cả basic auth cho chắc ăn. Test trên localhost thấy hoạt động tốt. Deploy lên production, bật Cloudflare — và ngay lập tức nhận về một trang 403 lạnh lùng. IP của bạn rõ ràng nằm trong whitelist, nhưng Nginx vẫn từ chối. Không có log lỗi rõ ràng, không có thông báo nào. Chỉ là 403.
Đây chính xác là tình huống chúng ta gặp phải khi cấu hình Nginx reverse proxy cho OpenClaw dashboard. Và nguyên nhân hoàn toàn không phải do config sai — mà do Cloudflare đã âm thầm thay thế IP thật của bạn trước khi request chạm đến Nginx. Bài viết này sẽ giải thích tại sao điều đó xảy ra, và cách fix dứt điểm bằng ngx_http_realip_module.
Bài Toán: Expose OpenClaw Dashboard Qua Nginx
OpenClaw là một API gateway mã nguồn mở với giao diện quản trị (Gateway Control UI) chạy mặc định trên cổng 18789. Giống như hầu hết các dashboard nội bộ — Grafana, Kibana, Prometheus — OpenClaw không được thiết kế để phơi trực tiếp ra internet. Nó không có TLS termination riêng, không có IP filtering ở tầng network, và cơ chế xác thực mặc định khá tối giản.
Giải pháp tiêu chuẩn là đặt nó sau một Nginx reverse proxy với đầy đủ bảo vệ: SSL, IP whitelist, và basic authentication. Config ban đầu trông như thế này:
server {
listen 443 ssl;
server_name opencua.example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# IP Whitelist
allow 203.0.113.10;
allow 198.51.100.25;
deny all;
location / {
# Basic Auth
auth_basic "OpenClaw Dashboard";
auth_basic_user_file /etc/nginx/.htpasswd_opencua;
# Reverse proxy đến OpenClaw
proxy_pass http://127.0.0.1:18789;
# WebSocket support (bắt buộc cho Control UI)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
}
}
Nhìn vào config này, mọi thứ đều hợp lý. Nhưng khi Cloudflare xuất hiện trong bức tranh, toàn bộ logic IP whitelist sụp đổ.
Tại Sao Cloudflare Phá Vỡ IP-Based Rules
Để hiểu vấn đề, cần nắm rõ Cloudflare hoạt động như thế nào ở tầng TCP. Khi bạn bật proxy (biểu tượng đám mây màu cam trong DNS), mọi kết nối TCP đến server của bạn đều xuất phát từ Cloudflare edge node — không phải từ trình duyệt của người dùng thực.
Luồng request thực tế diễn ra như sau:
Browser (IP: 198.51.100.25) → gửi request → Cloudflare Edge (IP: 172.70.x.x) → thiết lập kết nối TCP mới đến origin → Nginx nhận kết nối từ 172.70.x.x
Biến $remote_addr trong Nginx luôn chứa địa chỉ IP của kết nối TCP trực tiếp — và kết nối đó đến từ Cloudflare, không phải từ người dùng. Cloudflare hiện đang sử dụng các dải IP như 162.158.0.0/15, 172.64.0.0/13, 104.16.0.0/13, và nhiều range khác.
Kết quả: khi Nginx đánh giá rule allow 198.51.100.25, nó so sánh với $remote_addr = 172.70.x.x. Không khớp. Rule deny all kích hoạt. Người dùng nhận 403 — dù IP của họ hoàn toàn hợp lệ.

Cloudflare không bỏ thông tin IP thật. Nó đính kèm IP gốc của visitor vào HTTP header CF-Connecting-IP trước khi forward request đến origin. Vấn đề là Nginx mặc định không biết điều này — nó chỉ nhìn vào kết nối TCP.
Giải Pháp: ngx_http_realip_module + CF-Connecting-IP
Nginx có sẵn module ngx_http_realip_module được thiết kế chính xác cho tình huống này. Module này cho phép Nginx ghi đè $remote_addr bằng giá trị lấy từ một HTTP header — nhưng chỉ khi request đến từ một proxy đáng tin cậy.
Có hai directive chính cần hiểu:
set_real_ip_from: Khai báo dải IP nào được phép cung cấp header IP thật. Nếu request không đến từ các IP này, header sẽ bị bỏ qua hoàn toàn.real_ip_header: Chỉ định header nào chứa IP thật của visitor.
Tại sao nên dùng CF-Connecting-IP thay vì X-Forwarded-For? Đây là điểm quan trọng về bảo mật. Header X-Forwarded-For có thể bị client giả mạo — ai cũng có thể gửi request với header X-Forwarded-For: 203.0.113.10 để bypass whitelist. Ngược lại, CF-Connecting-IP là header do Cloudflare inject, không phải do client gửi. Cloudflare sẽ ghi đè bất kỳ giá trị nào client cố tình đặt vào. Kết hợp với việc chỉ trust các IP range của Cloudflare, đây là cách tiếp cận an toàn nhất.
Tạo File Cấu Hình Cloudflare RealIP
Thay vì đặt config này trong từng virtual host, hãy tạo một file riêng trong /etc/nginx/conf.d/ để áp dụng globally cho toàn bộ server:
# /etc/nginx/conf.d/cloudflare-realip.conf
# Cloudflare IPv4 ranges
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
# Cloudflare IPv6 ranges
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# Dùng CF-Connecting-IP thay vì X-Forwarded-For
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
Việc đặt file này trong conf.d/ có ý nghĩa quan trọng: Nginx tự động include toàn bộ file *.conf trong thư mục này vào http block, nên config sẽ áp dụng cho tất cả virtual host trên server — không cần lặp lại trong từng site config. Đây là cách tiếp cận đúng đắn vì mọi site đều hưởng lợi từ việc có IP thật trong log và access control.
Toàn Bộ Cấu Hình Hoàn Chỉnh
Site Config: opencua.example.com
# /etc/nginx/sites-available/opencua.example.com
server {
listen 80;
server_name opencua.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name opencua.example.com;
# Wildcard SSL certificate cho *.example.com
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# IP Whitelist — sau khi realip module restore $remote_addr,
# các rule này sẽ đánh giá đúng IP thật của visitor
allow 203.0.113.10;
allow 198.51.100.25;
deny all;
location / {
# Basic Authentication
auth_basic "OpenClaw Control Panel";
auth_basic_user_file /etc/nginx/.htpasswd_opencua;
# Reverse proxy đến OpenClaw Gateway UI
proxy_pass http://127.0.0.1:18789;
# WebSocket support — bắt buộc cho OpenClaw Control UI
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Forward headers chuẩn
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout cho WebSocket connections dài
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
}
}
Tạo File .htpasswd
Tạo file basic auth riêng cho site này (không dùng chung với các site khác để dễ quản lý credential):
# Cài htpasswd nếu chưa có
sudo apt install apache2-utils
# Tạo file và thêm user
sudo htpasswd -c /etc/nginx/.htpasswd_opencua opencua
# Nhập password khi được hỏi: mysecretpassword
# Kiểm tra file được tạo
cat /etc/nginx/.htpasswd_opencua
# Output: opencua:$apr1$xxxxx$xxxxxxxxxxxxxxxxxxxxxxxxxx
Kích Hoạt và Reload
# Tạo symlink để enable site
sudo ln -s /etc/nginx/sites-available/opencua.example.com \
/etc/nginx/sites-enabled/
# Kiểm tra syntax trước khi reload
sudo nginx -t
# Expected: nginx: configuration file /etc/nginx/nginx.conf test is successful
# Reload Nginx (không có downtime)
sudo systemctl reload nginx
Sau khi reload, hãy test từ một trong hai IP được whitelist. Lần này Nginx sẽ thấy IP thật của bạn trong $remote_addr, rule allow sẽ match, và bạn sẽ thấy màn hình basic auth thay vì trang 403.

Bảo Trì: Cập Nhật IP Ranges Của Cloudflare
Cloudflare liên tục mở rộng mạng lưới và thêm data center mới. Điều này đồng nghĩa với việc các IP range trong file config của bạn sẽ lỗi thời theo thời gian. Khi Cloudflare thêm một range mới mà bạn chưa khai báo trong set_real_ip_from, các request qua edge node đó sẽ không được restore IP thật — và IP whitelist lại bắt đầu fail.
Cloudflare duy trì danh sách cập nhật tại:
https://www.cloudflare.com/ips-v4https://www.cloudflare.com/ips-v6- API:
https://api.cloudflare.com/client/v4/ips
Dưới đây là script đơn giản để tự động cập nhật và reload:
#!/bin/bash
# /usr/local/bin/update-cloudflare-ips.sh
OUTPUT="/etc/nginx/conf.d/cloudflare-realip.conf"
TEMP=$(mktemp)
echo "# Auto-generated by update-cloudflare-ips.sh" > "$TEMP"
echo "# Last updated: $(date -u)" >> "$TEMP"
echo "" >> "$TEMP"
# Fetch IPv4 ranges
while IFS= read -r range; do
echo "set_real_ip_from $range;" >> "$TEMP"
done < <(curl -sf https://www.cloudflare.com/ips-v4)
# Fetch IPv6 ranges
while IFS= read -r range; do
echo "set_real_ip_from $range;" >> "$TEMP"
done < <(curl -sf https://www.cloudflare.com/ips-v6)
echo "" >> "$TEMP"
echo "real_ip_header CF-Connecting-IP;" >> "$TEMP"
echo "real_ip_recursive on;" >> "$TEMP"
# Chỉ apply nếu nginx -t pass
mv "$TEMP" "$OUTPUT"
nginx -t 2>/dev/null && systemctl reload nginx && \
echo "Cloudflare IPs updated successfully" || \
echo "Nginx config test failed — check $OUTPUT"
Thêm vào crontab để chạy hàng tháng:
# Chạy vào 3 giờ sáng ngày đầu mỗi tháng
0 3 1 * * /usr/local/bin/update-cloudflare-ips.sh >> /var/log/cloudflare-ip-update.log 2>&1
Kết Luận
Vấn đề cốt lõi ở đây rất đơn giản khi bạn đã hiểu rõ: Cloudflare là một proxy layer vô hình đối với Nginx mặc định. Mọi kết nối TCP đến origin đều đến từ Cloudflare edge, khiến $remote_addr luôn là IP của Cloudflare — và mọi rule allow/deny bạn viết đều đang đánh giá sai đối tượng.
Giải pháp — khai báo Cloudflare IP ranges là trusted proxy qua set_real_ip_from, sau đó đọc IP thật từ CF-Connecting-IP qua real_ip_header — chỉ mất vài phút để implement nhưng đòi hỏi phải hiểu rõ tầng nào đang xử lý gì trong chuỗi request.
Điều quan trọng cần nhớ: pattern này không chỉ áp dụng cho OpenClaw. Bất kỳ service nào bạn expose qua Nginx và đặt sau Cloudflare — Grafana, Kibana, Jupyter, hay bất kỳ internal tool nào khác — đều gặp đúng vấn đề này và được fix bằng đúng giải pháp này. File cloudflare-realip.conf trong conf.d/ là thứ bạn chỉ cần viết một lần, và toàn bộ server sẽ hưởng lợi.
Hai điều cần ghi nhớ cho production: thứ nhất, cập nhật Cloudflare IP ranges định kỳ — tốt nhất là tự động hóa bằng cron script như đã trình bày. Thứ hai, nếu bạn cần mức độ bảo mật cao hơn và muốn origin server hoàn toàn không thể tiếp cận từ internet, hãy xem xét Cloudflare Tunnel (cloudflared) — giải pháp này loại bỏ hoàn toàn nhu cầu mở port 443 ra ngoài, và cũng không cần