前言
在内网环境中,很多服务只提供 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 |
| 访问 IP | 192.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)
- 下载
ca.crt到本地。 - 双击,选择“安装证书”。
- 存储位置选“本地计算机”(管理员)或“当前用户”。
- 选择“将所有的证书都放入下列存储”,浏览,选择“受信任的根证书颁发机构”。
- 完成导入,彻底重启浏览器。
macOS(Safari / Chrome)
- 双击
ca.crt,加入“系统”钥匙串。 - 钥匙串访问,找到
MyLAN-CA,右键“显示简介”。 - “信任”里“使用此证书时”选“始终信任”。
- 输入密码确认。
Firefox(独立证书库)
about:preferences#privacy→ 证书 → “查看证书”。- “证书颁发机构” → “导入” → 选
ca.crt。 - 勾选“信任由此证书颁发机构来标识网站”。
九、踩坑记录
| 现象 | 原因 | 解决 |
|---|---|---|
| `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 证书库 |
十、经验总结
- 私有 CA 比单张自签名证书更优雅,客户端只导一次根证书,后续所有服务器证书自动被信任。
- SAN 是必须的,CN 已过时。Chrome 从某个版本开始完全忽略 CN,只看 SAN。
ca.key要离线保管,不要和 Nginx 部署在同一台机器上。- 有效期不要超过 825 天,否则 iOS/macOS 会拒绝。
- 证书续期无需重新导入 CA,只需重签服务器证书并
nginx -s reload。 - 生产环境建议用 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
实战性强,把私有 CA 到客户端的完整链路和踩坑表都讲清了,适合内网测试、企业系统或 IoT 平台运维人员直接照搬落地。