本文中的域名、主机名、IP 地址、端口、文件路径、脚本名称和服务名称均为虚构示例,已与实际环境完全分离。

一、需求背景

在家庭服务器、小型实验室或自托管环境中,常见需求包括:

  • 只有一个公网 IP;
  • 多个域名需要提供 HTTPS 服务;
  • 局域网中存在多台服务器;
  • 每台服务器运行多个 Web 应用;
  • 不希望每个应用都直接开放一个公网端口;
  • 不同域名可能使用不同的 TLS 证书;
  • 后续还需要持续增加新的子域名和服务。

例如,存在两套独立域名:

primary-lab.example
cloud.primary-lab.example
panel.primary-lab.example

secondary-lab.example
www.secondary-lab.example
admin.secondary-lab.example
api.secondary-lab.example
portal.secondary-lab.example

这些域名可以全部使用同一个公网 IP,并共同通过标准 HTTPS 端口 443 访问。

核心技术包括:

  • DNS;
  • TLS SNI;
  • Nginx 虚拟主机;
  • 反向代理;
  • 多证书管理;
  • Certbot 自动续期;
  • 路由器端口转发。

二、整体架构

示例网络结构如下:

互联网
   │
   │ 公网 IPv4
   ▼
边界路由器
   │
   │ TCP 443
   ▼
Nginx 反向代理节点
   │
   ├── www.secondary-lab.example
   │      └── 10.88.0.21:18081
   │
   ├── admin.secondary-lab.example
   │      └── 10.88.0.21:18082
   │
   ├── api.secondary-lab.example
   │      └── 10.88.0.21:18083
   │
   └── portal.secondary-lab.example
          └── 10.88.0.21:18084

路由器只需保留一条主要的 HTTPS 转发:

公网 TCP 443
    → 反向代理节点 TCP 443

外部访问地址保持为标准形式:

https://www.secondary-lab.example
https://admin.secondary-lab.example
https://api.secondary-lab.example
https://portal.secondary-lab.example

无需在 URL 后附加内部应用端口。


三、为什么多个域名能够共用 443

HTTPS 客户端在 TLS 握手阶段通常会携带目标域名,这项机制称为:

SNI
Server Name Indication

Nginx 接收到连接后,可以根据客户端请求的域名选择对应的:

  • server_name
  • TLS 证书;
  • 反向代理目标;
  • 日志文件;
  • 上传限制;
  • WebSocket 设置;
  • 安全策略。

因此,以下请求虽然全部进入同一个地址:

198.51.100.20:443

仍可被 Nginx 分别处理:

www.secondary-lab.example
    → 10.88.0.21:18081

admin.secondary-lab.example
    → 10.88.0.21:18082

api.secondary-lab.example
    → 10.88.0.21:18083

portal.secondary-lab.example
    → 10.88.0.21:18084

从公网看,所有服务都使用标准 HTTPS;从局域网看,每个域名可以对应不同的主机、端口和应用。


四、Nginx 配置示例

假设域名和后端关系如下:

secondary-lab.example
www.secondary-lab.example
    → 10.88.0.21:18081

admin.secondary-lab.example
    → 10.88.0.21:18082

api.secondary-lab.example
    → 10.88.0.21:18083

portal.secondary-lab.example
    → 10.88.0.21:18084

可以配置四个独立的 server 块。

主域名和 www 子域名

server {
    listen 443 ssl;
    http2 on;

    server_name secondary-lab.example www.secondary-lab.example;

    client_max_body_size 200M;

    access_log /var/log/nginx/secondary-main.access.log;
    error_log  /var/log/nginx/secondary-main.error.log;

    ssl_certificate     /srv/tls/secondary/fullchain.pem;
    ssl_certificate_key /srv/tls/secondary/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://10.88.0.21:18081;
        proxy_http_version 1.1;

        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 https;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  443;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_redirect off;
    }
}

管理子域名

server {
    listen 443 ssl;
    http2 on;

    server_name admin.secondary-lab.example;

    client_max_body_size 200M;

    access_log /var/log/nginx/secondary-admin.access.log;
    error_log  /var/log/nginx/secondary-admin.error.log;

    ssl_certificate     /srv/tls/secondary/fullchain.pem;
    ssl_certificate_key /srv/tls/secondary/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://10.88.0.21:18082;
        proxy_http_version 1.1;

        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 https;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  443;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_redirect off;
    }
}

API 子域名

server {
    listen 443 ssl;
    http2 on;

    server_name api.secondary-lab.example;

    client_max_body_size 200M;

    access_log /var/log/nginx/secondary-api.access.log;
    error_log  /var/log/nginx/secondary-api.error.log;

    ssl_certificate     /srv/tls/secondary/fullchain.pem;
    ssl_certificate_key /srv/tls/secondary/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://10.88.0.21:18083;
        proxy_http_version 1.1;

        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 https;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  443;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_redirect off;
    }
}

其他应用子域名

server {
    listen 443 ssl;
    http2 on;

    server_name portal.secondary-lab.example;

    client_max_body_size 200M;

    access_log /var/log/nginx/secondary-portal.access.log;
    error_log  /var/log/nginx/secondary-portal.error.log;

    ssl_certificate     /srv/tls/secondary/fullchain.pem;
    ssl_certificate_key /srv/tls/secondary/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://10.88.0.21:18084;
        proxy_http_version 1.1;

        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 https;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  443;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";

        proxy_redirect off;
    }
}

五、不同域名可以使用不同证书

多个域名共用同一个 443 端口,不代表必须使用同一张证书。

例如,第一套域名使用:

/srv/tls/primary/

第二套域名使用:

/srv/tls/secondary/

第一套 Nginx 配置引用:

ssl_certificate     /srv/tls/primary/fullchain.pem;
ssl_certificate_key /srv/tls/primary/privkey.pem;

第二套配置引用:

ssl_certificate     /srv/tls/secondary/fullchain.pem;
ssl_certificate_key /srv/tls/secondary/privkey.pem;

Nginx 会根据客户端发送的 SNI 域名返回相应证书。

因此可以实现:

同一个公网 IP
同一个 TCP 443
不同域名
不同证书
不同后端

六、申请包含多个子域名的证书

可以一次申请覆盖主域名和多个子域名的证书:

certbot certonly --standalone \
  -d secondary-lab.example \
  -d www.secondary-lab.example \
  -d admin.secondary-lab.example \
  -d api.secondary-lab.example \
  -d portal.secondary-lab.example \
  --agree-tos \
  -m administrator@example.invalid \
  --cert-name secondary-lab.example

签发后,Certbot 通常会建立:

/etc/letsencrypt/live/secondary-lab.example/fullchain.pem
/etc/letsencrypt/live/secondary-lab.example/privkey.pem

这些路径通常是符号链接。证书续期后,链接会指向新版本文件,因此 Nginx 配置中的路径无需随续期修改。

使用 standalone 模式时需要注意:

  • 公网 TCP 80 必须能够到达证书申请主机;
  • 本机 TCP 80 在验证期间必须可用;
  • 每个申请域名都必须正确解析;
  • 错误的 IPv6 AAAA 记录可能导致验证失败。

七、多张证书的自动部署

同一台服务器上存在多张证书时,部署钩子不能把所有续期事件都当作同一张证书处理。

Certbot 在 deploy hook 中通常会提供:

RENEWED_LINEAGE
RENEWED_DOMAINS

例如:

RENEWED_LINEAGE=/etc/letsencrypt/live/secondary-lab.example

可以根据证书名称执行不同分支。

以下脚本名称、目录和服务均为虚构示例:

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

RENEWED_LINEAGE="${RENEWED_LINEAGE:-}"

if [[ -z "$RENEWED_LINEAGE" ]]; then
    echo "缺少 RENEWED_LINEAGE" >&2
    exit 1
fi

CERT_NAME="$(basename -- "$RENEWED_LINEAGE")"
FULLCHAIN="$RENEWED_LINEAGE/fullchain.pem"
PRIVKEY="$RENEWED_LINEAGE/privkey.pem"

case "$CERT_NAME" in
    primary-lab.example)
        install -d -m 0755 /srv/tls/primary

        install -m 0644 \
            "$FULLCHAIN" \
            /srv/tls/primary/fullchain.pem

        install -m 0600 \
            "$PRIVKEY" \
            /srv/tls/primary/privkey.pem
        ;;

    secondary-lab.example)
        install -d -m 0755 /srv/tls/secondary

        install -m 0644 \
            "$FULLCHAIN" \
            /srv/tls/secondary/fullchain.pem

        install -m 0600 \
            "$PRIVKEY" \
            /srv/tls/secondary/privkey.pem
        ;;

    *)
        echo "未配置部署规则,跳过:$CERT_NAME"
        exit 0
        ;;
esac

nginx -t
systemctl reload nginx

示例脚本可命名为:

tls-routing-hook.sh

这个名称只是演示,并不对应任何实际服务器文件。

分流部署可以避免:

  • 第二张证书覆盖第一张证书;
  • 某张证书续期时错误重启无关服务;
  • 多套域名共用同一部署目录;
  • 新增证书后被旧逻辑错误处理。

八、从公网多端口暴露迁移到统一 443

在没有反向代理时,应用可能使用多个独立的公网端口。

以下端口均为虚构示例:

公网 31081 → 内网应用端口 18081
公网 32742 → 内网应用端口 18082
公网 34693 → 内网应用端口 18083
公网 35918 → 内网应用端口 18084

这种结构存在几个问题:

  • 每新增一个应用,都需要增加一条 NAT 规则;
  • URL 中需要附加端口;
  • 内部应用可能直接暴露到公网;
  • 可以绕过反向代理的 TLS、安全头和访问限制;
  • 防火墙规则会不断增加;
  • 公网暴露面难以统一管理。

迁移到 Nginx 后,Web 应用统一通过:

公网 TCP 443
    → Nginx 反向代理

Nginx 在局域网内部访问:

10.88.0.21:18081
10.88.0.21:18082
10.88.0.21:18083
10.88.0.21:18084

这些应用端口不再需要直接映射到公网。

最终访问形式变为:

https://www.secondary-lab.example
https://admin.secondary-lab.example
https://api.secondary-lab.example
https://portal.secondary-lab.example

九、后续增加子域名的方法

以后增加一个新应用,例如:

notes.secondary-lab.example

后端运行在:

10.88.0.21:18120

只需完成以下步骤:

  1. 新增 DNS 记录;
  2. 确保证书包含新域名;
  3. 增加一个 Nginx server 块;
  4. proxy_pass 指向新的内网端口;
  5. 执行配置检查;
  6. 平滑加载 Nginx。

示例:

server {
    listen 443 ssl;
    http2 on;

    server_name notes.secondary-lab.example;

    ssl_certificate     /srv/tls/secondary/fullchain.pem;
    ssl_certificate_key /srv/tls/secondary/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://10.88.0.21:18120;
        proxy_http_version 1.1;

        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 https;

        proxy_redirect off;
    }
}

整个过程不需要新增公网端口转发。


十、SSH 仍需单独处理

SSH 不是普通 HTTP 流量,不能直接使用 Nginx HTTP 虚拟主机的 server_name 进行分流。

因此,Web 应用统一通过 443 后,仍可单独保留一个高位 SSH 映射。

以下端口为虚构示例:

公网 TCP 45222
    → 内网服务器 TCP 22

建议同时使用:

  • SSH 公钥认证;
  • 禁用密码登录;
  • 限制允许登录的用户;
  • 防火墙来源限制;
  • 登录失败防护;
  • 日志审计。

SSH 端口与 Web 反向代理属于两类不同入口,不应混为一谈。


十一、同一个 443 不能混用 PROXY Protocol

同一个监听地址和端口上的 Nginx 虚拟主机,必须对 PROXY Protocol 保持一致。

不应采用:

server {
    listen 443 ssl;
    server_name site-a.example;
}

server {
    listen 443 ssl proxy_protocol;
    server_name site-b.example;
}

原因在于:

Nginx 必须先判断连接开头是否存在 PROXY Protocol 头,之后才能继续读取 TLS 握手和 SNI 域名。

因此,不能先根据域名决定是否启用 PROXY Protocol。

正确选择只有三种:

全部不使用

listen 443 ssl;

全部统一使用

listen 443 ssl proxy_protocol;

前提是所有进入该监听端口的连接都带有合法的 PROXY Protocol 头。

使用不同监听端口

443  → 普通 HTTPS
另一个内部端口 → 带 PROXY Protocol 的上游

但普通 NAT 无法根据 TLS 内部的域名,将同一个公网 443 自动分流到两个不同内部端口。


十二、WebSocket 连接头的处理

很多配置会直接使用:

proxy_set_header Upgrade    $http_upgrade;
proxy_set_header Connection "upgrade";

这对 WebSocket 应用通常有效,但更规范的方式是在 http 配置范围中定义:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

然后在反向代理中使用:

proxy_set_header Upgrade    $http_upgrade;
proxy_set_header Connection $connection_upgrade;

这样普通 HTTP 请求不会无条件携带:

Connection: upgrade

是否需要配置 WebSocket,应根据后端应用的实际需求决定。


十三、配置检查与平滑加载

每次修改 Nginx 配置后,都应先执行:

nginx -t

只有在出现类似结果时才继续:

syntax is ok
test is successful

随后使用:

systemctl reload nginx

通常不需要直接重启 Nginx。

reload 会让新工作进程加载新配置,同时允许旧连接继续完成,更适合生产环境中的普通配置变更。


十四、后端端口连通性检查

在反向代理节点上,可以检查内部应用端口。

以下地址和端口均为虚构示例:

for port in 18081 18082 18083 18084; do
    printf '10.88.0.21:%s -> ' "$port"

    timeout 3 bash -c "</dev/tcp/10.88.0.21/$port" \
        && echo OK \
        || echo FAILED
done

结果含义如下。

OK

说明反向代理节点可以建立 TCP 连接。

Connection refused

通常表示:

  • 目标主机可以到达;
  • 但对应端口没有应用监听;
  • 或应用只监听其他地址。

超时

可能表示:

  • 防火墙丢弃;
  • 路由错误;
  • 目标主机离线;
  • 网络不可达。

后端服务器还可以检查监听状态:

ss -lntp | grep -E ':(18081|18082|18083|18084)\b'

若应用只绑定:

127.0.0.1:18081

则反向代理节点无法连接。

通常需要绑定:

0.0.0.0:18081

或者后端服务器的局域网地址:

10.88.0.21:18081

然后通过防火墙仅允许反向代理节点访问。


十五、推荐的最终公网入口

经过整理后,公网入口可以简化为:

公网 TCP 80
    → ACME HTTP 验证或 HTTP 跳转

公网 TCP 443
    → Nginx 反向代理

公网一个高位 TCP 端口
    → SSH

其他 Web 应用端口
    → 不直接暴露公网

Web 请求统一经过:

客户端
  → 公网 443
  → 边界路由器
  → Nginx
  → 内部应用端口

这种架构具有以下优势:

  • 多个域名共用一个公网 IP;
  • 所有网站使用标准 443;
  • URL 无需附加应用端口;
  • TLS 证书集中管理;
  • 后端应用端口不直接暴露;
  • 新增应用不需要新增公网 NAT;
  • 日志和访问控制集中;
  • WebSocket、SSE、上传限制可以统一配置;
  • 不同域名仍可使用不同证书;
  • 不同域名仍可转发到不同服务器。

十六、总结

多个域名共用同一台反向代理服务器的 443 端口,是 Nginx 的标准应用方式。

核心条件包括:

  1. 所有域名解析到同一个公网入口;
  2. 公网 443 转发到 Nginx;
  3. 每组域名配置正确的 server_name
  4. 每个虚拟主机引用正确证书;
  5. proxy_pass 指向正确的局域网后端;
  6. 同一个监听端口的 PROXY Protocol 设置保持一致;
  7. 后端 Web 应用端口不再直接映射到公网;
  8. 多张证书通过 deploy hook 分流部署;
  9. 修改配置后先执行 nginx -t
  10. 配置通过后使用 systemctl reload nginx

完成迁移后,公网结构会从“多个端口分别暴露”变为“统一 HTTPS 入口、按域名分流”,更容易维护,也更适合持续增加新的子域名和内部应用。

Leave a Reply

Your email address will not be published. Required fields are marked *