背景
WAF在使用反向代理接入时,由于WAF监听的是HTTP端口,上游业务监听的是HTTPS端口,导致在配置upstream时如果WAF故障宕机后无法切换到备份链路
对接链路场景
DMZ_Nginx(https/check健康检查)-WAF(http)-内网LB(https)
upstream waf_pool {
server 192.168.31.159:8080 max_fails=1 fail_timeout=2s;
server 192.168.31.159:8081 max_fails=1 fail_timeout=2s;
server 192.168.31.159:9443 backup;
}
....
proxy_pass http://waf_pool;
....
解决方案
在DMZ Nginx的upstream配置中取消backup地址,然后使用error_page兜底到内网LB。
实验操作
创建目录和文件
目录结构
lab/
├─ docker-compose.yml
├─ dmz/
│ └─ nginx.conf
├─ waf1/
│ └─ nginx.conf
├─ waf2/
│ └─ nginx.conf
├─ clb/
│ └─ nginx.conf
└─ certs/
├─ dmz.local.crt
├─ dmz.local.key
├─ lb.example.com.crt
└─ lb.example.com.key
创建实验目录
mkdir -p lab/{dmz,waf1,waf2,clb,certs}
创建实验文件
创建证书文件
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout lab/certs/dmz.local.key -out lab/certs/dmz.local.crt \
-subj "/CN=dmz.local"
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout lab/certs/lb.example.com.key -out lab/certs/lb.example.com.crt \
-subj "/CN=lb.example.com"
创建docker compose配置文件
version: "3.9"
services:
dmz:
image: nginx:1.25-alpine
container_name: dmz
ports:
- "8443:443" # 入口:https://localhost:8443
volumes:
- ./dmz/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs/dmz.local.crt:/etc/nginx/ssl/dmz.local.crt:ro
- ./certs/dmz.local.key:/etc/nginx/ssl/dmz.local.key:ro
restart: unless-stopped
networks:
- labnet
waf1:
image: nginx:1.25-alpine
container_name: waf1
ports:
- "8080:8080" # 便于你直接 curl 验证
volumes:
- ./waf1/nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
networks:
- labnet
waf2:
image: nginx:1.25-alpine
container_name: waf2
ports:
- "8081:8081"
volumes:
- ./waf2/nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
networks:
- labnet
clb:
image: nginx:1.25-alpine
container_name: clb
ports:
- "9443:443" # 直连 CLB:https://localhost:9443
volumes:
- ./clb/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs/lb.example.com.crt:/etc/nginx/ssl/lb.example.com.crt:ro
- ./certs/lb.example.com.key:/etc/nginx/ssl/lb.example.com.key:ro
restart: unless-stopped
networks:
labnet:
aliases:
- lb.example.com # ★ 供 DMZ 回退用的域名(动态解析)
networks:
labnet:
driver: bridge
创建DMZ Nginx配置文件
user nginx;
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
# --- 上游池:两台 WAF 轮询 ---
upstream waf_pool {
server 192.168.31.159:8080 max_fails=1 fail_timeout=2s;
server 192.168.31.159:8081 max_fails=1 fail_timeout=2s;
keepalive 32;
}
server {
listen 443 ssl;
http2 on;
server_name dmz.local;
ssl_certificate /etc/nginx/ssl/dmz.local.crt;
ssl_certificate_key /etc/nginx/ssl/dmz.local.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# ★ 关键:无论上游 50x 还是连接阶段 502/504,统一走 /__fallback__
proxy_intercept_errors on;
error_page 500 502 503 504 521 522 523 524 = /__fallback__;
# 透传与超时(WAF 不可达时尽快失败)
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 300ms;
proxy_read_timeout 3s;
proxy_send_timeout 3s;
# 先在池里重试另一台;两台都失败才触发上面的 error_page
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
location / {
add_header X-DMZ-Path primary always;
proxy_pass http://waf_pool;
}
# ★ 统一兜底(用 URI 而不是 @named,避免偶发绕过)
location = /__fallback__ {
internal;
add_header X-DMZ-Path fallback always;
proxy_ssl_server_name on;
proxy_ssl_name lb.example.com; # SNI
proxy_set_header Host lb.example.com;
proxy_connect_timeout 500ms;
proxy_read_timeout 3s;
proxy_send_timeout 3s;
proxy_pass https://192.168.31.159:9443;
}
}
}
创建WAF1 Nginx配置文件
user nginx;
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 8080;
add_header X-WAF-ID WAF1 always;
location = /healthz { default_type text/plain; return 200 "healthy-WAF1\n"; }
location = /fail { default_type text/plain; return 500 "oops-WAF1\n"; }
location / {
default_type text/plain;
return 200 "OK from WAF1\n";
}
}
}
创建WAF2 Nginx配置文件
user nginx;
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 8081;
add_header X-WAF-ID WAF2 always;
location = /healthz { default_type text/plain; return 200 "healthy-WAF2\n"; }
location = /fail { default_type text/plain; return 500 "oops-WAF2\n"; }
location / {
default_type text/plain;
return 200 "OK from WAF2\n";
}
}
}
创建内网LB Nginx配置文件
user nginx;
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 443 ssl;
http2 on;
server_name lb.example.com;
ssl_certificate /etc/nginx/ssl/lb.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/lb.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
location = /healthz { default_type text/plain; return 200 "healthz-OK\n"; }
location / {
default_type text/plain;
return 200 "OK from CLB (HTTPS)\n";
}
}
}
实验操作
启动试验环境
执行docker compose up -d
启动试验环境
[root@MiWiFi-RA80-srv lab]# docker compose up -d
WARN[0000] /root/lab/docker-compose.yml: `version` is obsolete
[+] Running 5/5
✔ Network lab_labnet Created 0.0s
✔ Container clb Started 0.4s
✔ Container dmz Started 0.4s
✔ Container waf2 Started 0.4s
✔ Container waf1 Started 0.3s
[root@MiWiFi-RA80-srv lab]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ccf9ca79102d nginx:1.25-alpine "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 80/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp waf1
e35f514e0255 nginx:1.25-alpine "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 80/tcp, 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp waf2
84c009fe8449 nginx:1.25-alpine "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp dmz
3a3ea51fc2db nginx:1.25-alpine "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 80/tcp, 0.0.0.0:9443->443/tcp, :::9443->443/tcp clb
[root@MiWiFi-RA80-srv lab]#
访问DMZ Nginx查看返回数据
执行curl命令,可以看到流量经过WAF1
[root@MiWiFi-RA80-srv lab]# curl -skI https://localhost:8443/
HTTP/1.1 200 OK
Server: nginx/1.25.5
Date: Wed, 27 Aug 2025 17:15:01 GMT
Content-Type: text/plain
Content-Length: 13
Connection: keep-alive
X-WAF-ID: WAF1
X-DMZ-Path: primary
现在停止waf1后继续使用curl命令,可以看到流量经过waf2
[root@MiWiFi-RA80-srv lab]# docker stop waf1
waf1
[root@MiWiFi-RA80-srv lab]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e35f514e0255 nginx:1.25-alpine "/docker-entrypoint.…" 3 minutes ago Up 3 minutes 80/tcp, 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp waf2
84c009fe8449 nginx:1.25-alpine "/docker-entrypoint.…" 3 minutes ago Up 3 minutes 80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp dmz
3a3ea51fc2db nginx:1.25-alpine "/docker-entrypoint.…" 3 minutes ago Up 3 minutes 80/tcp, 0.0.0.0:9443->443/tcp, :::9443->443/tcp clb
[root@MiWiFi-RA80-srv lab]# curl -skI https://localhost:8443/
HTTP/1.1 200 OK
Server: nginx/1.25.5
Date: Wed, 27 Aug 2025 17:16:32 GMT
Content-Type: text/plain
Content-Length: 13
Connection: keep-alive
X-WAF-ID: WAF2
X-DMZ-Path: primary
现在停止waf2,继续测试可以看到流量走了兜底策略进入了/__fallback__
ng兜底策略配置
将回退的兜底策略放在server块中
proxy_intercept_errors on;
error_page 500 502 503 504 521 522 523 524 = /__fallback__;
[root@MiWiFi-RA80-srv lab]# docker stop waf2
waf2
[root@MiWiFi-RA80-srv lab]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
84c009fe8449 nginx:1.25-alpine "/docker-entrypoint.…" 6 minutes ago Up 6 minutes 80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp dmz
3a3ea51fc2db nginx:1.25-alpine "/docker-entrypoint.…" 6 minutes ago Up 6 minutes 80/tcp, 0.0.0.0:9443->443/tcp, :::9443->443/tcp clb
[root@MiWiFi-RA80-srv lab]# curl -skI https://localhost:8443/
HTTP/1.1 200 OK
Server: nginx/1.25.5
Date: Wed, 27 Aug 2025 17:19:04 GMT
Content-Type: text/plain
Content-Length: 20
Connection: keep-alive
X-DMZ-Path: fallback