
我碰过不少开发同学装完 Nginx 之后第一件事就是想把 HTTPS 补上结果卡在了证书这一关。要么是openssl命令生成的证书浏览器不认要么是配好了 Nginx 却总是跳警告要么干脆不知道“让浏览器信任”这一步到底该怎么做。这篇文章就是把这些事从头到尾捋一遍怎么自己生成 SSL 证书、怎么正确配到 Nginx 里、怎么让本机和局域网内的浏览器真正信任这套证书中间哪些坑值得注意我都会写清楚。适合刚接触 Nginx 配置的运维新手也适合需要在本地环境或内网环境里快速启用 HTTPS 的前后端开发。1. 为什么你的证书总不被信任先搞懂证书机制1.1 一个证书里到底装了什么很多人把 SSL 证书理解成“一个加密文件”其实不太准确。证书文件本身是一段经过签名和编码的数据里面包含的核心信息有这么几块主体信息Subject证书是发给谁的常见字段是 CNCommon Name也就是域名或主机名。比如CNexample.com。公钥非对称加密里的公钥部分会和服务器上的私钥配对。公钥可以公开私钥必须保密。有效期从哪天开始生效到哪天失效。证书过期是浏览器报“不安全”的常见原因之一。签名算法比如 SHA-256 RSA表示这个证书是用什么算法做的摘要和签名。签发者信息Issuer这张证书是谁签发的。如果是自签名证书Issuer 和 Subject 是同一个。SANSubject Alternative Name这是非常关键的一个扩展字段用来指定证书适用的域名或 IP 地址。现代浏览器基本只看 SAN不怎么看 CN。看一个证书的实际内容可以用这条命令openssl x509 -in server.crt -text -noout输出里能看到上面提到的所有字段。我建议每一位准备配 HTTPS 的人都先跑一次这条命令看看 SAN 字段里到底有没有你的域名。很多“证书配了但浏览器不认”的问题根因就是 SAN 缺失或者写错了。1.2 系统是怎么验证证书的浏览器信任一个网站走的是一条“信任链”浏览器连接到你的 Nginx获取服务器证书。浏览器用这个证书里的公钥去验证服务器是否持有对应的私钥。这一步通过 SSL 握手完成。浏览器检查证书的签发者Issuer看看签发者是否在系统内置的“受信任根证书颁发机构”列表里。如果签发者不是受信任的根证书浏览器继续向上找直到找到一个受信任的根证书为止。浏览器还会检查证书域名是否匹配、是否过期、是否被吊销。所以“让浏览器信任证书”这件事本质上不是修改 Nginx 配置而是把证书的签发者安装到浏览器的受信任根证书列表里。这就是为什么你给自己服务器生成的证书浏览器会提示“不安全”因为你的服务器证书的签发者不是任何一家被系统信任的 CA而是你自己。1.3 三条证书路线怎么选实际工作中根据场景不同通常有三条路线方案适用场景优点缺点公共 CA 证书如阿里云免费证书、Lets Encrypt公网正式环境所有浏览器直接信任无警告需要域名内网或本地测试不方便自签名证书临时测试、内网工具生成简单零成本浏览器不信任需要手动导入根证书且单张证书难以管理本地自建 CA 签发服务器证书内网多台机器、开发环境只用导入一次根证书局域网内所有机器都能信任所有由该 CA 签发的证书需要理解 CA 签发流程搭建时有几步操作大多数场景下我推荐第三种本地 CA。因为它解决了一个核心问题——你不用在每台机器、每个域名上都单独处理浏览器信任只要把自建 CA 的根证书装一次后续由这个 CA 签发的所有服务器证书都会自动被信任。2. 证书生成实操从零产出一张可用的证书2.1 快速方案一条命令生成自签名证书如果你只是想在本地快速跑起来不管浏览器警告最直接的办法是用一条命令生成自签名证书openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes逐项说一下参数的含义reqOpenSSL 的证书请求与生成子命令。-x509直接输出一张自签名证书而不是生成 CSR 证书请求文件。-newkey rsa:2048同时生成一个 2048 位的 RSA 私钥。2048 位是当前的最低安全标准低于这个位数会触发浏览器的安全警告。-keyout server.key私钥输出文件名。-out server.crt证书输出文件名。-days 365有效期 365 天。-nodes私钥不加密。如果不加这个参数OpenSSL 会要求你输入私钥密码而 Nginx 每次启动都会卡在密码输入上非常麻烦。这条命令会交互式地询问国家、地区、公司、Common Name 等信息。其中 Common Name 是关键必须填你要访问的域名或 IP。如果只是为了本地测试填localhost就行。不过这条命令生成的证书有一个隐患它不带 SAN 扩展。在 Chrome 和 Firefox 里如果证书没有 SAN浏览器会直接拒绝即使 CN 填对了也会报ERR_CERT_COMMON_NAME_INVALID。所以要加一个-addext参数openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes -subj /CNlocalhost -addext subjectAltNameDNS:localhost,DNS:example.com,IP:127.0.0.1这里通过-addext把 SAN 写进去同时兼容域名和 IP。自签名证书一步到位的方案就完成了配到 Nginx 里能跑起来浏览器仍然会提示“不安全”但至少不会报域名不匹配的错误。2.2 本地 CA 方案根证书与服务器证书签发流程自签名证书适合“一次性使用”但如果你有多个域名、多台服务器每张证书都要手动处理信任关系效率太低。更合理的做法是自建一个本地 CA。第一步生成本地 CA 的私钥和根证书openssl genrsa -out ca.key 2048 openssl req -x509 -new -key ca.key -out ca.crt -days 3650 -subj /CNMy Local CAca.key是 CA 的私钥一定要保存好因为后续所有证书都要它来签名。ca.crt是根证书后续需要分发到每台需要信任这些证书的机器上。第二步生成服务器证书的私钥和 CSRopenssl genrsa -out server.key 2048 openssl req -new -key server.key -out server.csr -subj /CNexample.com这里我建议CN填主域名。注意这只是请求文件不能直接用作服务器证书。第三步创建扩展配置文件指定 SANsubjectAltNameDNS:example.com,DNS:localhost,IP:192.168.1.100文件名随意比如server.ext。第四步用 CA 的私钥为服务器证书签名openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -extfile server.ext这条命令会读取server.csr用ca.key对请求内容签名签发server.crt并且把server.ext里的 SAN 扩展写入最终证书。-CAcreateserial参数表示如果不存在序列号文件就自动生成一个。完成之后你手里就有三个关键文件ca.crt根证书、server.crt服务器证书、server.key服务器私钥。Nginx 需要的是后两个浏览器需要的是第一个。2.3 生成证书时的几个关键参数细节证书生成看似命令不多但细节决定了后面能不能用。私钥位数。2048 位是底线4096 位更安全但握手性能略慢。对于内网环境2048 位完全够用。证书有效期。本地 CA 的根证书建议签 10 年避免频繁分发根证书服务器证书建议 1 年方便轮换。公网证书目前主流 CA 也只给 90 天到 1 年的有效期。SAN 覆盖范围。如果有多台内网机器、多个开发域名尽量在 SAN 里一次性列全。漏了一个域名浏览器访问那个域名时就会报错。IP 地址也是可以写进 SAN 的用IP:前缀。签名算法。有同学生成证书时用了默认的 SHA-1 签名这在新版浏览器里已经被列为不安全算法。确保用 SHA-256OpenSSL 1.1.1 之后的默认签名算法已经是 SHA-256老版本需要显式指定openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -sha256 -out server.crt -days 365 -extfile server.ext私钥权限。server.key和ca.key都要设置为只有所有者可读否则 Nginx 会拒绝加载私钥甚至直接报权限错误。chmod 600 server.key ca.key3. Nginx 配置 HTTPS 的完整步骤3.1 存放证书文件与目录规划证书文件不建议散落在/root或/home下面最好集中到一个统一目录里。我习惯放在/etc/nginx/ssl/下按站点名分目录mkdir -p /etc/nginx/ssl/example.com cp server.crt /etc/nginx/ssl/example.com/ cp server.key /etc/nginx/ssl/example.com/ chmod 700 /etc/nginx/ssl/example.com chmod 600 /etc/nginx/ssl/example.com/server.key这里有几个需要注意的点Nginx 的 worker 进程通常以nginx用户运行server.key必须能被该用户读取但目录权限又不能太开放。如果 Nginx 配置里用相对路径引用证书容易在测试和正式环境之间产生混乱建议一律用绝对路径。证书文件名里最好带上站点标识不要所有站点都用server.crt这种通用名识别起来太费劲。3.2 最小可用的 SSL 站点配置在/etc/nginx/conf.d/example-ssl.conf里写一个最简配置server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com/server.crt; ssl_certificate_key /etc/nginx/ssl/example.com/server.key; access_log /var/log/nginx/example-ssl.access.log; error_log /var/log/nginx/example-ssl.error.log; root /var/www/html; index index.html; location / { try_files $uri $uri/ 404; } }关键指令就两句ssl_certificate指定证书文件路径可以包含服务器证书也可以把中间证书和根证书合并写在同一个文件里。ssl_certificate_key指定私钥文件路径。配置文件写完后先做语法检查nginx -t看到syntax is ok和test is successful再重载systemctl reload nginx这里有一个非常常见的错误写错了证书路径或者证书文件格式不对nginx -t会报错并提示无法加载证书。解决办法就是检查路径、确认文件是 PEM 格式纯文本以-----BEGIN CERTIFICATE-----开头。3.3 HTTP 自动跳转 HTTPS配置 HTTPS 之后还要考虑原来用 HTTP 访问的用户。如果不做跳转用户访问http://example.com时仍然会走 80 端口拿到的是明文流量不安全。在同一个配置文件里加一个 80 端口的 server 块server { listen 80; server_name example.com; return 301 https://$host$request_uri; }return 301表示返回 301 永久重定向$host和$request_uri是 Nginx 内置变量可以保留原始域名和路径。这样所有 HTTP 请求都会被导到 HTTPS 上。需要注意如果是本地测试环境Chrome 对localhost有特殊处理不一定走这个跳转逻辑但对外部域名或者局域网 IP 的访问这个配置是有效的。3.4 局域网 IP 访问与多域名证书配置如果是内网服务器很多人会直接用 IP 访问比如https://192.168.1.100。这要求在生成证书时SAN 里必须包含IP:192.168.1.100。如果证书里只有域名没有 IP浏览器会报NET::ERR_CERT_COMMON_NAME_INVALID。多域名的情况比如你有一个主站和一个 API 服务可以复用同一张证书吗可以只要 SAN 里同时包含example.com和api.example.comNginx 配置里写同一个证书路径即可。更简单的做法是给两个 server 块指定不同的证书文件分别对应不同的域名管理起来更清晰。4. 让浏览器信任证书系统级导入与常见报错4.1 浏览器信任链是怎么建立的前面提到过浏览器验证证书时会一路向上找签发者直到找到系统中受信任的根证书。所以要让浏览器信任你的服务器证书核心动作就是把本地 CA 的根证书装进操作系统的受信任根证书列表。这一步做完后浏览器会自动信任由该 CA 签发的所有服务器证书不需要再针对单个域名做任何处理。这就是本地 CA 方案相比单张自签名证书最大的优势。4.2 Windows 系统导入根证书Windows 下导入根证书的步骤如下把ca.crt文件拷贝到目标 Windows 机器。双击ca.crt点击“安装证书”。存储位置选择“本地计算机”如果没有管理员权限则选“当前用户”。选择“将所有证书放入下列存储”点击“浏览”选择“受信任的根证书颁发机构”。一路点击“下一步”和“完成”。导入完成后可以验证一下打开 Chrome 或 Edge访问https://你的服务器域名或IP正常情况下不会再出现红色警告页地址栏里会有一个小锁图标。需要注意Windows 导入根证书时如果选错存储位置比如放进了“个人”存储而不是“受信任的根证书颁发机构”浏览器还是会报错。另外证书颁发机构列表里如果出现同名证书建议先删掉旧的再导入新的避免冲突。4.3 Linux 系统导入根证书Linux 下的导入方式取决于发行版。基于 Debian 和 Ubuntu 的系统推荐使用ca-certificates工具cp ca.crt /usr/local/share/ca-certificates/ update-ca-certificates这样会把根证书加入系统证书库Chrome 等基于系统证书库的浏览器会直接生效。Firefox 不读取系统证书库需要单独导入。基于 RHEL、CentOS、AlmaLinux 的系统做法类似cp ca.crt /etc/pki/ca-trust/source/anchors/ update-ca-trust extract导入之后可以用这条命令确认openssl verify -CAfile ca.crt server.crt如果输出server.crt: OK说明签名验证通过系统的信任列表也已经有了对应的根证书。4.4 macOS 导入根证书macOS 下操作也简单双击ca.crt钥匙串访问工具会自动打开。选择登录钥匙串或系统钥匙串建议选择“系统”可以让所有本地用户共享信任。导入后在钥匙串列表里找到该证书双击打开展开“信任”栏。把“使用此证书时”改为“始终信任”。macOS 有个比较容易踩的坑是导入后如果没有把信任级别改成“始终信任”浏览器还是会报警告。这一步容易遗漏。4.5 开发者工具的替代方案mkcert如果你觉得手动生成 CA 和签名太繁琐这里推荐一个开发者常用的工具叫mkcert。它会自动在本地创建 CA并把这个 CA 加入系统信任库然后一条命令就能生成浏览器信任的本地证书。安装方法各平台不同macOS 上可以用 Homebrewbrew install mkcert初始化并生成证书mkcert -install mkcert example.com localhost 127.0.0.1mkcert -install会把根证书装进系统信任列表mkcert生成证书时也会自动带上 SAN。用生成出来的example.com2.pem和example.com2-key.pem配置 Nginx 即可。它的原理和手工创建本地 CA 完全一样只是把步骤自动化了。如果你只是想让本机浏览器没了警告用mkcert比手敲一长串 OpenSSL 命令省心得多。5. 高频问题排查清单与避坑记录5.1 浏览器还是显示“不安全”先按顺序排查我见过最多的场景就是证书生成了Nginx 配置也写了系统根证书也导入了但浏览器依然大红色警告。这时候别急按下面这个顺序排查是否所有需要的根证书都导入了。自建 CA 的ca.crt必须导入而且必须放进受信任的根证书列表。证书文件是否是最新签发的。如果你重新生成了证书但忘了更新 Nginx 配置里的文件路径浏览器拿到的是旧证书。服务器时间是否准确。证书验证对时间极其敏感服务器时间差几分钟都可能导致“证书无效”。这一点在虚拟机里特别容易碰到。浏览器是否启用了 DNS over HTTPS 或代理解析了不同 IP。访问的域名解析到的服务器和你配置 Nginx 的服务器不是同一台自然验证不过。5.2 证书链不完整导致的问题如果你的服务器证书是某个公共 CA 签发的而且下发的证书文件里只有一张叶子证书Nginx 可能需要配置完整的证书链否则部分客户端会验证失败。判断方法是用openssl检查openssl s_client -connect example.com:443 -showcerts输出里应该能看到两张以上证书服务器证书和中间证书。如果只有一张说明证书链不完整。处理办法是把中间证书和服务器证书合并为一个文件cat server.crt intermediate.crt server-chain.crt然后在 Nginx 里指定合并后的文件ssl_certificate /etc/nginx/ssl/example.com/server-chain.crt;对于本地 CA 签发的证书如果根证书已经装进系统信任列表原则上不需要合并中间证书但如果你希望稳妥也可以把ca.crt和server.crt合并。5.3 Chrome 的 HSTS 与本地测试冲突HSTS 是 HTTP 严格传输安全协议浏览器会把“该站点必须使用 HTTPS”的策略记下来。如果你曾经开发过某个域名开启过 HSTS之后用自签名证书做本地测试时Chrome 会强制要求 HTTPS并且仍然报错因为证书不被信任。此时需要先清理 HSTS 缓存在 Chrome 地址栏输入chrome://net-internals/#hsts。在 “Delete domain security policies” 里输入域名点 Delete。如果是在localhost上测试Chrome 对localhost默认放行 HTTP但如果你特意配置过 HSTS 或者用了特殊端口也可能触发这个机制。遇到这种情况时优先检查 HSTS再检查代理设置。5.4 用命令快速验证证书是否正常配置完 Nginx 后我习惯用命令而不是浏览器去验证证书的完整性和有效性。最常用的是这个openssl s_client -connect 127.0.0.1:443 -servername example.com加上-servername是为了测试 SNI即多站点域名证书。输出里如果能看到Verify return code: 0 (ok)说明证书链验证通过。如果返回21 (unable to verify the first certificate)说明证书链不完整或根证书未被本地信任。另外也可以看证书有效期echo | openssl s_client -servername example.com -connect 127.0.0.1:443 2/dev/null | openssl x509 -noout -dates这条命令会输出证书的生效和过期时间适合快速判断证书是否过期。5.5 常见问题速查表现象可能原因解决办法浏览器提示“不安全”但不提示具体错误根证书未导入或导入到了错误位置重新导入ca.crt到受信任根证书机构报NET::ERR_CERT_COMMON_NAME_INVALIDSAN 中没有当前访问的域名或 IP重新生成证书确保 SAN 包含对应域名 IP报NET::ERR_CERT_AUTHORITY_INVALID签发者不被系统信任导入根证书或检查证书链报NET::ERR_CERT_DATE_INVALID证书过期或系统时间不准重新签发证书校正服务器时间访问时连接被重置或握手失败端口未放行、TLS 协议版本不匹配检查 443 端口监听状态与ssl_protocols配置浏览器提示证书使用弱算法证书使用了 SHA-1 签名用-sha256重新签发证书6. 进阶加固把 HTTPS 从“能用”变成“好用”6.1 TLS 版本与加密套件的合理配置很多默认配置把 TLS 1.0 和 TLS 1.1 也开着这在新浏览器里会被标记为不安全甚至直接拒绝连接。建议在 server 块里显式启用 TLS 1.2 和 TLS 1.3ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on;说明一下ECDHE开头的套件支持前向保密即使私钥泄露历史通信内容也难以被解密。GCM是 AEAD 加密模式性能和安全性都不错。不需要再加一些旧的 CBC 套件来兼容老设备现代浏览器都支持 GCM。配置后同样用nginx -t检查再重载。可以用在线工具或者本地的nmap --script ssl-enum-ciphers -p 443验证服务端支持的协议版本。6.2 会话缓存与 OCSP 装订SSL 握手本身有开销大量短连接场景下尤其明显。开启会话缓存可以减少重复握手ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;10m表示缓存放 10 兆大约可以缓存 2 万多个会话标识。如果站点访问量不大1m 也够用。OCSP 装订是让 Nginx 在握手时主动向浏览器提供证书吊销状态的查询结果省去浏览器自己访问 OCSP 服务器的延迟。配置方式ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 valid300s; resolver_timeout 5s;这里resolver是让 Nginx 自己去解析 OCSP 响应服务器域名用的。如果内网环境没有外部 DNSOCSP 装订可以不开不影响正常 HTTPS 功能。6.3 证书到期提醒与自动化续期思路无论是自签名证书还是本地 CA 签发的证书到期都是绕不开的问题。比较省心的做法是用cron定期检查证书过期时间。写一个简单脚本check_cert.sh#!/bin/bash expire_date$(openssl x509 -enddate -noout -in /etc/nginx/ssl/example.com/server.crt | cut -d -f2) echo 证书到期时间: $expire_date然后放到cron里每周跑一次0 9 * * 1 /usr/local/bin/check_cert.sh如果是公共 CA 的证书比如 Lets Encrypt配合certbot renew可以做到自动续期。很多云厂商的免费证书也支持到期前手动重新申请再覆盖到 Nginx 配置目录里重载服务。我只提醒一句证书续期不是换文件就完事一定记得重载 Nginx不然新证书不会生效。我个人在实际操作中最深的感受是SSL 证书这事难点不在“生成”和“配置”而在“信任链的传递”。公网环境只要跟着 CA 的指引走就行内网或本地环境必须搞明白根证书、服务器证书、私钥这三者之间的关系才能从根本上去排查那些看似玄学的浏览器报错。希望这篇内容能帮你省掉一些排查时间少走几步弯路。