JacobWang
JacobWang
Published on 2025-08-28 / 13 Visits
0
0

从 HTTP 到 HTTPS 的回源链:DMZ_LB(HTTPS)→WAF(HTTP)→LB(HTTPS) 故障转移实践

背景

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


Comment