内网 HTTPS 部署实战:私有 CA + Nginx 反向代理全流程复盘

文章来源声明: 原文作者:春天花会开131; 来源站点:掘金; 原文链接:https://juejin.cn/post/7688689006075576356; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

实战性强,把私有 CA 到客户端的完整链路和踩坑表都讲清了,适合内网测试、企业系统或 IoT 平台运维人员直接照搬落地。

内网 HTTPS 部署实战:私有 CA + Nginx 反向代理全流程复盘 -------------------------------------

前言

在内网环境中,很多服务只提供 HTTP 访问,存在明文传输、无法使用浏览器安全特性等问题。本文将完整复盘一次内网 HTTPS 改造过程:自建私有 CA 签发证书 → Nginx 容器加载证书 → 反向代理后端服务 → 客户端信任根证书。

全程使用 Docker Compose 部署,配置可复用,适合内网测试环境、企业内部系统、IoT 管理平台等场景。

注:文中出现的 IP、域名均为示例,请替换为你自己的实际地址。

一、为什么不用自签名证书,而要用私有 CA

很多人第一次配 HTTPS 都是直接 openssl req -x509 生成一张自签名证书,但这样会有一个致命问题:每台服务器都要单独导入一次证书,客户端无法统一信任。

正确的做法是:建一个根 CA,用它签出服务器证书。客户端只需要导入一次根 CA 证书,之后这个 CA 签发的所有服务器证书都会被自动信任。

方案客户端操作扩展性
直接自签名服务器证书每台服务器都要导入一次差
私有 CA 签发只导入一次根 CA好

二、整体架构

text

客户端浏览器 <span>--HTTPS--</span>> Nginx 容器 <span>--HTTP--</span>> 后端服务(app<span>.internal</span>:<span>8000</span>)

Nginx 容器挂载 server.crt / server.key

三、环境信息

项目示例值
Nginx 宿主机192.168.1.100
项目目录~/nginx-ssl/
配置目录~/nginx-ssl/conf.d/
证书目录~/nginx-ssl/ssl/
访问域名myapp.local
访问 IP192.168.1.100
后端服务[app.internal:8000/](https://link.juejin.cn?target=http%3A%2F%2Fapp.internal%3A8000%2F "http://app.internal:8000/")

四、目录结构

text

nginx-ssl/
├── docker-compose.yml
├── conf.d/
│   └── <span>default</span>.conf
└── ssl/
    ├── ca.key
    ├── ca.crt
    ├── ca.srl
    ├── <span>server</span>.key
    ├── <span>server</span>.csr
    ├── <span>server</span>.crt
    └── san.ext

五、核心步骤复盘

1. 创建根 CA

bash

openssl genrsa -out ca.<span>key</span> <span>4096</span>
openssl req -x509 -<span>new</span> -<span>key</span> ca.<span>key</span> -days <span>3650</span> -out ca.crt -subj <span>"/CN=MyLAN-CA"</span>

ca.key 是信任链的根基,一旦泄露,所有签发的证书全部失效,务必离线保管。

2. 生成服务器私钥和 CSR

bash

openssl genrsa -out server.<span>key</span> <span>2048</span>
openssl req -<span>new</span> -<span>key</span> server.<span>key</span> -out server.csr -subj <span>"/CN=myapp.local"</span>

3. 编写 SAN 扩展文件(最容易踩坑的一步)

现代浏览器(尤其 Chrome)已经不再识别 CN 字段,只认 SAN(Subject Alternative Name)。如果 SAN 没写对,即使证书被信任,浏览器依然会报 ERR_CERT_COMMON_NAME_INVALID。

san.ext 内容:

text

<span>authorityKeyIdentifier</span>=keyid,issuer
<span>basicConstraints</span>=CA:<span>FALSE</span>
<span>keyUsage</span> = digitalSignature, keyEncipherment
<span>extendedKeyUsage</span> = serverAuth
<span>subjectAltName</span> = @alt_names
<span>[alt_names]</span>
<span>DNS.1</span> = myapp.local
<span>IP.1</span>  = <span>192.168</span>.<span>1.100</span>

经验总结:IP.1 必须是你实际访问用的 IP,而不是随便填。如果你既用域名又用 IP 访问,两个都要列上。

4. 用 CA 签发服务器证书

bash

openssl x509 -req -<span>in</span> server.csr -CA ca.crt -<span>CAkey</span> ca.key \
  -<span>CAcreateserial</span> -days <span>825</span> -sha256 -extfile san.ext -<span>out</span> server.crt

-days 825 是 iOS/macOS 对服务器证书有效期的上限,超过这个天数在这些设备上会直接报错。

5. 验证证书

bash

openssl x509 -<span>in</span> <span>server</span>.crt -noout -text | grep -A1 <span>"Subject Alternative Name"</span>
openssl verify -CAfile ca.crt <span>server</span>.crt

预期输出:server.crt: OK

六、Nginx 容器化部署

1. docker-compose.yml

yaml

<span>services:</span>
  <span>nginx:</span>
    <span>image:</span> <span>nginx:latest</span>
    <span>container_name:</span> <span>nginx-ssl</span>
    <span>restart:</span> <span>unless-stopped</span>
    <span>ports:</span>
      <span>-</span> <span>"80:80"</span>
      <span>-</span> <span>"443:443"</span>
    <span>volumes:</span>
      <span>-</span> <span>./ssl:/etc/nginx/ssl:ro</span>
      <span>-</span> <span>./conf.d:/etc/nginx/conf.d:ro</span>
    <span>networks:</span>
      <span>-</span> <span>webnet</span>

<span>networks:</span>
  <span>webnet:</span>
    <span>driver:</span> <span>bridge</span>

2. conf.d/default.conf

HTTP 自动跳转 HTTPS:

nginx

server {
    <span>listen</span> <span>80</span>;
    server_name myapp.local <span>192.168</span>.<span>1.100</span>;
    <span>return</span> <span>301</span> https:<span>//</span>$host$request_uri;
}

HTTPS 主服务:

nginx

server {
    listen 443 ssl<span>;</span>
    server_name myapp.local 192.168.1.100<span>;</span>

    ssl_certificate     /etc/nginx/ssl/server.crt<span>;</span>
    ssl_certificate_key /etc/nginx/ssl/server.key<span>;</span>

    ssl_protocols       TLSv1.2 TLSv1.3<span>;</span>
    ssl_ciphers         HIGH:!aNULL:!MD5<span>;</span>
    ssl_session_cache   shared:SSL:10m<span>;</span>
    ssl_session_timeout 10m<span>;</span>

    location / {
        proxy_pass http://app.internal:8000/<span>;</span>

        proxy_set_header Host              $host<span>;</span>
        proxy_set_header X-Real-IP         $remote_addr<span>;</span>
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for<span>;</span>
        proxy_set_header X-Forwarded-Proto $scheme<span>;</span>

        proxy_connect_timeout 30s<span>;</span>
        proxy_read_timeout    60s<span>;</span>
        proxy_send_timeout    60s<span>;</span>
    }
}

3. 关于 proxy_pass 末尾斜杠的坑

配置访问 /foo 实际请求
`proxy_pass http://app.internal:8000/;``/foo`(原样转发,推荐)
`proxy_pass http://app.internal:8000;``/foo`(行为相同但语义差)

如果后端服务挂在子路径(如 /vna),则要写成:

nginx

location /vna/ {
    proxy_pass http://app.internal:8000/vna/;
}

七、启动与验证

bash

<span>cd</span> ~/nginx-ssl
docker compose up -d
docker compose <span>exec</span> nginx nginx -t
docker compose <span>exec</span> nginx nginx -s reload
docker compose logs -f nginx

验证命令:

bash

curl -k <span>https</span>:<span>//192.168.1.100/</span>
curl --cacert ~<span>/nginx-ssl/</span>ssl/ca.<span>crt</span> <span>https</span>:<span>//192.168.1.100/</span>
curl <span>http</span>:<span>//app.internal:8000/</span>

八、客户端信任根 CA

关键点:客户端只需导入 ca.crt(根 CA 证书),不需要导入 server.crt。

Windows(Chrome / Edge)

  1. 下载 ca.crt 到本地。
  2. 双击,选择“安装证书”。
  3. 存储位置选“本地计算机”(管理员)或“当前用户”。
  4. 选择“将所有的证书都放入下列存储”,浏览,选择“受信任的根证书颁发机构”。
  5. 完成导入,彻底重启浏览器。

macOS(Safari / Chrome)

  1. 双击 ca.crt,加入“系统”钥匙串。
  2. 钥匙串访问,找到 MyLAN-CA,右键“显示简介”。
  3. “信任”里“使用此证书时”选“始终信任”。
  4. 输入密码确认。

Firefox(独立证书库)

  1. about:preferences#privacy → 证书 → “查看证书”。
  2. “证书颁发机构” → “导入” → 选 ca.crt。
  3. 勾选“信任由此证书颁发机构来标识网站”。

九、踩坑记录

现象原因解决
`ERR_CERT_AUTHORITY_INVALID`根 CA 未导入或导入位置错导入到“受信任的根证书颁发机构”
`ERR_CERT_COMMON_NAME_INVALID`SAN 缺少访问用的域名/IP重新生成 `san.ext`,重签 `server.crt`
用 IP 访问报错但域名正常SAN 只写了 DNS补充 `IP.1`
`502 Bad Gateway`容器访问不到后端宿主机先 `curl` 后端确认可达
`nginx -t` 报证书路径错误挂载路径不对确认容器内路径为 `/etc/nginx/ssl/server.crt`
页面样式/JS 404后端返回绝对路径资源为对应前缀补充 `location` 代理
Firefox 仍报错Firefox 未导入 CA单独导入到 Firefox 证书库

十、经验总结

  1. 私有 CA 比单张自签名证书更优雅,客户端只导一次根证书,后续所有服务器证书自动被信任。
  2. SAN 是必须的,CN 已过时。Chrome 从某个版本开始完全忽略 CN,只看 SAN。
  3. ca.key 要离线保管,不要和 Nginx 部署在同一台机器上。
  4. 有效期不要超过 825 天,否则 iOS/macOS 会拒绝。
  5. 证书续期无需重新导入 CA,只需重签服务器证书并 nginx -s reload。
  6. 生产环境建议用 Let's Encrypt,Nginx 配置方式完全一致,仅替换证书文件即可。

十一、速查清单

服务端:

bash

cd ~/nginx-ssl/ssl
openssl genrsa -out ca.<span>key</span> <span>4096</span>
openssl req -x509 -<span>new</span> -<span>key</span> ca.<span>key</span> -days <span>3650</span> -out ca.crt -subj <span>"/CN=MyLAN-CA"</span>
openssl genrsa -out server.<span>key</span> <span>2048</span>
openssl req -<span>new</span> -<span>key</span> server.<span>key</span> -out server.csr -subj <span>"/CN=myapp.local"</span>
# 写 san.ext(含 DNS.<span>1</span> 和 IP.<span>1</span>)
openssl x509 -req -<span>in</span> server.csr -CA ca.crt -CAkey ca.<span>key</span> \
  -CAcreateserial -days <span>825</span> -sha256 -extfile san.ext -out server.crt

部署:

bash

<span>cd</span> ~/nginx-ssl
docker compose up -d
docker compose <span>exec</span> nginx nginx -t
docker compose <span>exec</span> nginx nginx -s reload

验证:

bash

curl --cacert ~<span>/nginx-ssl/</span>ssl/ca.<span>crt</span> <span>https</span>:<span>//192.168.1.100/</span>
openssl verify -<span>CAfile</span> ca.<span>crt</span> server.<span>crt</span>

客户端:导入 ca.crt 到“受信任的根证书颁发机构”,重启浏览器。

结语

本文完整复盘了内网 HTTPS 改造的全流程,从私有 CA 建设到 Nginx 容器化部署,再到客户端信任配置。相比直接使用自签名证书,私有 CA 方案在可维护性和扩展性上都有明显优势。

关键词:私有 CA、Nginx、HTTPS、反向代理、Docker、自签名证书、SAN