1710 字
9 分钟

签发自定义证书并反向代理 HTTPS 应用:以 nginx 代理 PVE 面板为例

本文讲述了如何用 openssl 签发自定义证书(自签证书),以及如何使用 nginx 反向代理一个 HTTPS 应用。以 Proxmox VE(PVE)面板为例,完整走一遍「签发证书 → 配置反代 → 安装信任 → 兜底站加固」的流程。

背景

内网里很多服务面板(Proxmox VE、NAS、监控平台等)默认监听 HTTPS 端口(如 PVE 的 8006)。直接访问没问题,但端口不好记、证书还是自签的,浏览器同样会警告。于是常规做法是:用一台 nginx 容器/机器做反向代理,统一域名入口,比如 pve.internal

问题来了:后端本身就是 HTTPS,反代时必须妥善处理证书校验、Cookie 的 Secure 标志、WebSocket 升级这三件事,否则就会出现「页面能打开、一登录就 401」的经典坑。

环境
  • 反向代理:LXC 容器内 nginx 1.26(Debian 13),IP 192.168.1.80
  • 后端:PVE 面板 https://192.168.1.101:8006
  • 域名:pve.internal(内网 DNS 由 Pi-hole 提供,解析到 nginx 容器 IP)
  • 客户端:Windows / Linux / 手机

第一部分:签发自定义证书#

自签证书(openssl 一行命令)#

如果你的服务只在内网用,不需要公网 CA 认证,自签证书是最快的方案。关键点是必须加上 SAN(Subject Alternative Name),否则现代浏览器一律报「证书无效」——只填 -subj /CN=xxx 是不够的,Chrome 从 58 版本起就只认 SAN 不认 CN。

Terminal window
# 生成自签证书: 有效期 10 年, SAN 包含 pve.internal 和 *.internal
mkdir -p /etc/nginx/ssl
openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
-keyout /etc/nginx/ssl/pve.internal.key \
-out /etc/nginx/ssl/pve.internal.crt \
-subj "/CN=pve.internal" \
-addext "subjectAltName=DNS:pve.internal,DNS:*.internal"

参数说明:

参数作用
-x509直接生成自签证书而不是证书请求(CSR)
-nodes私钥不加密(nginx 启动无需输入密码)
-newkey rsa:2048同时生成新的 RSA 2048 私钥
-days 3650有效期 10 年
-addext关键:写入 SAN,浏览器信任的依据

验证证书内容:

Terminal window
openssl x509 -in /etc/nginx/ssl/pve.internal.crt -noout -subject -ext subjectAltName
# subject=CN=pve.internal
# X509v3 Subject Alternative Name:
# DNS:pve.internal, DNS:*.internal
IMPORTANT

SAN 里建议同时包含主域名和泛域名pve.internal + *.internal),这样以后加 nas.internalgrafana.internal 时证书不用重签。

安装到客户端信任库#

自签证书的浏览器警告无法完全消除,但把证书安装到系统信任库后,客户端访问就会像正规证书一样静默通过(内网自用足够):

Terminal window
# Linux (Debian/Ubuntu): 放入系统 CA 目录后更新
sudo cp pve.internal.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# Windows: 双击 .crt -> 安装证书 -> 本地计算机 -> 受信任的根证书颁发机构
# macOS: 双击 .crt -> 钥匙串 -> 登录 -> 双击证书 -> 信任 -> 始终信任
# Android: 设置 -> 安全 -> 加密与凭据 -> 安装证书 -> CA 证书
NOTE

与 Let’s Encrypt 的区别:公网服务器(如博客中 headscale 部署的阿里云)应优先用 certbot certonly --standalone -d 域名 申请免费可信证书,因为客户端是外部用户,不可能逐台安装信任库。内网自用服务则反过来——自签 + 客户端装信任库更省事,还能避开 80 端口占用、自动续期等麻烦。

第二部分:nginx 反向代理 HTTPS 应用#

基本配置#

nginx 反代 HTTPS 后端时,proxy_passhttps:// 即可,但默认会校验后端证书(自签会报 502),所以需要关闭校验:

/etc/nginx/conf.d/pve.conf
# 80 -> 443 强制跳转
server {
listen 80;
server_name pve.internal;
return 301 https://$host$request_uri;
}
# HTTPS 入口
server {
listen 443 ssl;
server_name pve.internal;
ssl_certificate /etc/nginx/ssl/pve.internal.crt;
ssl_certificate_key /etc/nginx/ssl/pve.internal.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
charset utf-8;
keepalive_timeout 70;
# 允许上传大文件 (ISO/备份/模板), 0 = 不限制
client_max_body_size 0;
location / {
proxy_redirect off;
proxy_pass https://192.168.1.101:8006; # PVE 面板
proxy_ssl_verify off; # 后端是自签证书, 关闭校验
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_server_name on; # 向 HTTPS 后端发送 SNI
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket 升级
proxy_set_header Connection "upgrade";
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 $scheme;
}
}

测试并重载:

Terminal window
nginx -t && nginx -s reload

四个关键点(缺一不可)#

配置作用缺了会怎样
proxy_ssl_verify off不校验后端自签证书502 Bad Gateway
proxy_set_header Upgrade/Connection "upgrade"透传 WebSocket 升级头noVNC 控制台/SSH 终端打不开
client_max_body_size 0不限上传体积上传 ISO/备份 >1M 报 413
proxy_set_header Host $host透传原始域名后端按 IP 判定虚拟主机出错
CAUTION

最大的坑:Cookie 的 Secure 标志。

现象:http://pve.internal 打开登录页正常,输入账号密码后却一直弹 连接错误 401: No ticket

根因:后端 PVE 走的是 HTTPS,它给 PVEAuthCookie 打上了 Secure 标志——浏览器只会把这种 Cookie 在 https 请求中回传。如果 nginx 只暴露 80(HTTP),浏览器登录后每次请求都带不上 Cookie → 每次都被判定未认证 → 401。

解法:让 nginx 自己也走 HTTPS(80 一律 301 到 443)。前端 https → 后端 https,Cookie 正常回传,问题消失。这就是本配置强制全 HTTPS 的原因。

兜底站:拒绝一切未匹配请求(加固)#

反代配好后还有个隐患:nginx 默认会有一个 default_server(Debian 自带欢迎页),裸 IP 访问或伪造 Host 头会命中它,暴露 nginx 版本信息。替换成「拒绝一切」的兜底站:

//file: /etc/nginx/sites-available/default
# HTTP 兜底: 裸 IP / 其他 Host -> 直接断开
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
# HTTPS 兜底: 防止裸 IP 访问 443 误撞进 PVE 反代
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/pve.internal.crt;
ssl_certificate_key /etc/nginx/ssl/pve.internal.key;
return 444;
}
NOTE

444 是 nginx 特有的状态码:直接断开连接,不返回任何内容。用来拒绝无意义请求(扫描、裸 IP、伪造 Host)非常干净——攻击者看到的是一个黑洞,连版本号都探测不到。

验证部署效果#

Terminal window
# 1. 裸 IP 访问 -> 应被断开 (444)
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.80/ || echo "连接被断开"
# 2. http 访问域名 -> 应 301 到 https
curl -s -o /dev/null -w "HTTP %{http_code} -> %{redirect_url}\n" http://pve.internal/
# 3. https 访问域名 -> 应 200
curl -sk -o /dev/null -w "HTTP %{http_code}\n" https://pve.internal/
# 4. 伪造 Host 头 -> 应被断开
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.80/ -H "Host: evil.example.com"
# 5. 证书 SAN
echo | openssl s_client -connect 192.168.1.80:443 -servername pve.internal 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName

我实测的完整结果:

测试结果
裸 IP http / https✅ 连接被断开(444)
http://pve.internal✅ 301 → https://pve.internal
https://pve.internal 主页✅ 200,正常渲染登录页
API /api2/json/version✅ 401(未认证,与直连一致)
伪造 Host evil.example.com✅ 连接被断开
WebSocket 升级头✅ 正常透传

经验教训#

  1. 自签证书必须带 SAN,只写 CN 在 Chrome/Edge 里等于无效证书
  2. HTTPS 后端 + HTTP 前端 = Cookie Secure 标志失效,登录必弹 401;反代 HTTPS 应用时前端也必须是 HTTPS
  3. 反代自签证书的后端要 proxy_ssl_verify off,否则 502 而不是「配置有问题」这种直观错误
  4. WebSocket 升级头要单独透传,控制台/终端类功能全靠它
  5. 兜底站用 444 断连,比返回 404 更安全——不泄露任何信息
  6. 配置源头文件建议保存在宿主机(如 /root/pve-nginx.conf),容器内改动易丢失
签发自定义证书并反向代理 HTTPS 应用:以 nginx 代理 PVE 面板为例
https://www.mintlab.top/posts/learn/nginx-https-reverseproxy/
作者
Mint
发布于
2026-08-08
许可协议
CC BY-NC-SA 4.0
发表评论

输入用户名和邮箱后自动检查登录状态。登录后用户名和邮箱将被绑定, 只可以修改头像和主页链接。

未登录
昵称
邮箱
填写头像链接与主页链接

头像链接为空默认使用gravatar头像

头像
主页
人机验证
评论列表

以下是可爱的评论们:

暂无评论, 呜呜, 快来评论喵!