☰
OpenSSL证书生成与Chrome信任策略:从SAN到Nginx部署全指南
2026/10/20 15:48:47 网站建设 项目流程

做内网服务或者自研工具的人,这两年应该没少被 HTTPS 证书折腾。明明用 OpenSSL 生成了证书,浏览器一打开就是大红叉,终端里openssl命令看起来也正常,最后查来查去,问题往往不在证书本身,而在浏览器对证书的“信任规则”变了。尤其是 Chrome,这几年一直在调整自家对证书的判定方式,搞不清楚这些变化,你辛辛苦苦生成的证书就是“废纸”。

这篇东西我按完整实操来写:从 OpenSSL 生成证书、配置 SAN,到证书链组装、Nginx 部署,再到 Chrome 信任策略和常见报错排查,一条龙拉通。适合三类人看:一是在内网搭过服务、被证书弹窗折磨过的人;二是刚开始接触 TLS、想搞明白证书签发原理的新手;三是维护线上 HTTPS 服务、时不时要换证书的运维。看完不敢说你能成为密码学专家,但至少再碰到证书问题,不会一头雾水。

1. 服务和浏览器到底在验证什么

1.1 证书不是单个文件,而是一条信任链

很多人刚接触证书时会有一个误区,觉得只要给服务器配上.crt文件和.key文件,HTTPS 就成了。实际上浏览器拿到证书后,做的是一个“向上追溯”的动作:它看你服务端出示的证书是谁签发的,如果签发者不是浏览器内置的根证书,那就继续看签发者的证书是谁签发的,直到找到浏览器认可的那个根,整条链路才算闭合。

这个链路就叫证书链。你拿 OpenSSL 自签的证书,如果没被浏览器信任,本质上是因为浏览器不认你那个“根”。自签名的根证书就像你自己刻了一枚公章,内部文件自己盖自己认,但外面的人不认。内网环境里想让浏览器认,要么把自签根证书手动装进系统的信任区,要么用内网搭的私有 CA 统一签发并分发根证书。

这条链上还有两个容易忽略的点。第一,服务端发给浏览器的证书链必须完整,也就是说服务器要把“自己的证书 + 中间 CA 证书”一起发给客户端,不能只发最下面那一张。很多人自己签发的证书只有一层,问题不大;一旦用了两级 CA 结构,漏了中间证书是最常见的“浏览器报错但 OpenSSL 验不出来”的原因。第二,证书链上每一级的签名算法都得是浏览器能接受的,现在 Chrome 已经基本拒绝 SHA-1 签名的证书,而 OpenSSL 1.1.1 和 3.x 默认生成的摘要算法通常没问题,但老脚本或者老版本生成的证书就不好说。

1.2 Chrome 的信任策略已经和系统信任区“分道扬镳”了

以前大家会习惯性地认为,只要把证书装进 Windows 或 macOS 的系统信任区,Chrome 就会认。这几年 Chrome 已经逐步切换到自己的根证书库来判断公共 CA 签发的证书是否可信,不再完全跟随操作系统。它内置了一套“Chrome Root Store”,公共互联网上用的证书到底信不信,先看这套内置库,系统信任区只能作为补充,不能覆盖它的判定。

对内网私有 CA 来说,Chrome 还是会参考操作系统的信任区设置,但前提是你的证书本身“长得合规”。什么算合规?至少得是 X.509 v3 格式、包含 Subject Alternative Name(SAN)扩展、证书链完整、有效期在可接受范围内。其中 SAN 这一条最坑,因为很多教程教你生成证书时只填 Common Name(CN),而近年的 Chrome 已经不再把 CN 当域名匹配的唯一依据,它只看 SAN 里的域名和 IP。所以你在 Chrome 里访问https://192.168.1.100,证书里 SAN 没写这个 IP,不管 CN 写了啥,一律给你ERR_CERT_COMMON_NAME_INVALID。

我个人的经验是,2025 年再自己做证书,就别想着“能用就行”,直接把 Chrome 的要求当成最低标准来满足。原因很简单:你手工维护的证书可能同时被手机浏览器、企业代理、内部爬虫等五花八门的客户端访问,它们各自的信任策略不完全一致,但 SAN、证书链、有效期、签名算法这些是大家都看的硬杠杠。

2. 用 OpenSSL 生成证书:配置文件比命令更关键

2.1 为什么不能只靠一行命令

网上最常见的快速生成命令是openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365。这条命令拿来测试没问题,但正式用一定踩坑,因为它没有把 SAN 写进证书。Chrome 看到这种证书,第一反应就是不可信。

OpenSSL 的-x509参数虽然能直接产出自签证书,但它不会自动从命令行参数里推断你要哪些域名。要生成一个真正能被现代浏览器认可的证书,最好的做法是写一个配置文件,把 DN(Distinguished Name)和 SAN 都固化下来。配置文件的好处不只是“可复用”,更重要的是它把证书参数变成了代码,你可以在团队内评审、走版本管理,下次签发证书不用再去翻历史命令。

配置文件还可以避免中文乱码和交互式提问的问题。openssl req默认会问你“国家、省份、城市”等等一串问题,人一多就容易填错。用配置文件里加prompt = no,直接跳过提问,所有字段都从文件读,稳定可复现。这个习惯一旦养成,你就再也不会想回到交互式签发的老路上了。

2.2 一份可直接用的 OpenSSL 配置样例

下面这份配置我实测过,适用于签发自签根证书和服务端证书。先建一个 CA,然后用它给具体域名签证书。把配置拆成两份,一份是 CA 的,一份是服务器的,逻辑更清楚。

先看 CA 的配置,文件名ca.cnf:

[ req ] default_bits = 2048 distinguished_name = req_distinguished_name prompt = no x509_extensions = v3_ca [ req_distinguished_name ] countryName = CN stateOrProvinceName = Beijing localityName = Beijing organizationName = Internal Lab organizationUnitName = Platform Team commonName = Internal Lab Root CA [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:TRUE keyUsage = critical, digitalSignature, keyCertSign, cRLSign

然后是服务器证书配置,文件名server.cnf:

[ req ] default_bits = 2048 distinguished_name = req_distinguished_name prompt = no req_extensions = req_ext [ req_distinguished_name ] countryName = CN stateOrProvinceName = Beijing localityName = Beijing organizationName = Internal Lab organizationUnitName = Platform Team commonName = server.example.local [ req_ext ] subjectAltName = @alt_names [ alt_names ] DNS.1 = server.example.local DNS.2 = www.example.local IP.1 = 192.168.1.100 IP.2 = 10.0.0.5 [ server_cert ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:FALSE keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names

注意alt_names这一段,它决定了 Chrome 认不认这个证书。你要在哪些域名或 IP 上访问,就要在 SAN 里列全,漏一个就报一个错。你现在可能觉得 IP 写死在配置文件里不够灵活,等你真碰到“证书里明明写了域名,却忘了写 IP”的尴尬时刻,就知道这一步有多重要。

2.3 签发步骤与参数背后的含义

生成根证书私钥和自签根证书:

mkdir -p ~/certs && cd ~/certs openssl genrsa -out ca.key 2048 chmod 600 ca.key openssl req -new -x509 -key ca.key -out ca.crt -days 3650 -config ca.cnf

这里值得停下来解释几个参数。genrsa 2048是生成 2048 位的 RSA 私钥,2025 年这个长度还能用,但新部署我建议直接用 RSA 3072 或者切到 ECC,性能也好一些。days 3650给根证书签十年,因为根证书不轻易更换,换一次代价很大。-x509表示直接输出自签证书而不是签发请求,配合-config ca.cnf才会把[ v3_ca ]里的CA:TRUE写进证书。没有这个扩展,这个根证书拿去签名,浏览器也会认为它是普通证书,不能当根。

生成服务器私钥和证书签发请求(CSR):

openssl genrsa -out server.key 2048 chmod 600 server.key openssl req -new -key server.key -out server.csr -config server.cnf

server.csr不是证书,它只是你拿着自己的公钥和身份信息去申请证书的“请求文件”。这个文件中包含 SAN 扩展,因为req_extensions = req_ext把 SAN 带进来了。在线下签发流程里,CSR 可以发邮件给别人,别人用自己的 CA 给你签,这样你的私钥永远不出你自己的机器。

用根证书给服务器证书签名:

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out server.crt -days 825 \ -extfile server.cnf -extensions server_cert

-CAcreateserial会生成一个ca.srl文件,里面记录序列号,防止两个证书序列号重复。-extfile和-extensions的组合是把配置里[ server_cert ]的扩展搬进最终证书。如果省略这一步,签出来的证书可能没有 SAN,前功尽弃。

以下几点是我反复踩过之后总结的:

  • 服务器证书有效期不要跟根证书一样长,825 天是一个常见的默认值。原因在后面的有效期章节展开说。
  • chmod 600这一步别省,私钥如果被其他用户读到,证书再规范也没意义。
  • 如果 OpenSSL 版本是 3.x,签发时还可以加-copy_extensions copy,直接把 CSR 里的扩展带进来。这个参数在老版本上不可用,所以我一般用-extfile的方式,兼容性最好。

2.4 自检:证书生成后至少跑一遍验证命令

生成完后不要急着部署,先在命令行里看一眼关键信息:

openssl x509 -in server.crt -noout -text -dates -issuer -subject openssl x509 -in server.crt -noout -ext subjectAltName openssl verify -CAfile ca.crt server.crt

第一条命令能看到证书的签发者、主体、生效日期和到期日期。第二条专门确认 SAN,检查列表里有没有你打算用的域名和 IP。第三条是最重要的verify,它会把证书链走一遍,告诉你证书是否合法、链路是否闭合。如果verify输出OK,说明证书本身没问题;如果浏览器仍报错,问题多半出在“浏览器不信任根证书”或“服务器没发完整链”上,那就要看后面两章。

我个人习惯把这三条命令固化成一个 shell 函数,每次签完证书自动跑一遍,输出非OK就中止部署。看似多花十秒钟,实际上能挡住一半以上的低级错误。

3. 证书链的组装、校验与格式选型

3.1 全链路证书的三种装法

很多人在部署环节栽跟头,不是证书签错了,而是不知道服务端该放哪个文件。以 Nginx 为例,它的ssl_certificate指令要求的是一个 PEM 格式的文件,里面既可以只有服务器证书,也可以包含服务器证书和中间证书的拼接。区别在于:

  • 只放server.crt:适用于“根证书直接签服务器证书”的情况,比如你自己建的单层 CA。
  • 放server.crt加上中间 CA 证书:适用于“根 CA 签了中间 CA,中间 CA 再签服务器证书”的情况,这是公共 CA 的常见结构。
  • 放server.crt加所有中间证书再加根证书:根证书实际上可加可不加,有些服务器习惯把整条链都放上去。加了不坏事,但客户端通常不需要根证书,因为它自己已经有了。

我推荐的做法是明确区分三个文件:server.crt只放服务器证书;fullchain.pem放服务器证书 + 中间证书;ca.crt放根证书。Nginx 里ssl_certificate指到fullchain.pem,ssl_trusted_certificate指到ca.crt用于 OCSP Stapling 验证,这样逻辑最清晰。

拼接操作很简单,一行命令:

cat server.crt intermediate.crt > fullchain.pem

但要注意顺序不能反。必须先服务器证书、后中间证书,浏览器按顺序解析,放反了照样报NET::ERR_CERT_AUTHORITY_INVALID。

3.2 如何准确判断链是否完整

用浏览器访问页面是最终验证,但调试时不够快。用openssl s_client命令直接模拟 TLS 握手,几秒钟就能看到服务端会下发哪些证书:

openssl s_client -connect server.example.local:443 -servername server.example.local -showcerts -CAfile ca.crt

-showcerts会打印出服务端下发的每一张证书;-CAfile ca.crt让 OpenSSL 用你的根证书去验证。在输出末尾找Verify return code,如果是0 (ok),说明在 OpenSSL 看来链路是好的;如果看到unable to get local issuer certificate或者certificate has expired,就说明链不完整或者时间有问题。

还有一个细节:-servername要和你访问的域名保持一致,否则服务器拿不到对应的证书(涉及 TLS SNI,按域名选证书),验证结果会失真。很多人调试时忘了带这个参数,本地访问正常、远程访问却报错,折腾半天发现是 SNI 没对上。

3.3 PEM、CRT、CER、PFX……有什么区别

证书文件的后缀五花八门,但底层的编码格式就两类:PEM 和 DER。PEM 是 Base64 编码的文本,典型特征就是文件里有-----BEGIN CERTIFICATE-----这样的标记;DER 是二进制格式,直接用文本编辑器打开是乱码。.crt、.cer、.pem这些后缀不一定代表格式,Nginx、Apache 这类 Linux 服务一般用 PEM,Windows 导入证书时往往要求 DER,Java 的 KeyStore 又常用 PKCS#12(后缀.pfx或.p12)。

我见过不少人在内网部署时,把.crt文件当成 DER 格式去导入,结果系统提示“文件损坏”。解决办法就是先确认编码,用file命令看一眼:

file server.crt

输出显示PEM certificate就按文本处理;显示DER的话,可以用 OpenSSL 转换:

openssl x509 -in server.crt -inform DER -out server.pem -outform PEM

反过来,如果 Windows 导入服务商给的证书要求.cer格式,而手头只有 PEM 文本,也可以用类似方式转成 DER。记住一点:无论后缀怎么变,内容只是编码形式的差别,加密原理没有变。

4. 部署环节:Nginx 与服务端配置细节

4.1 ssl_certificate 到底该放哪个文件

Nginx 配置 HTTPS 的核心段落大家都熟,但最容易出错的就是文件指向。一个最小可用的配置长这样:

server { listen 443 ssl; server_name server.example.local; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/server.key; ssl_trusted_certificate /etc/nginx/certs/ca.crt; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1h; location / { proxy_pass http://127.0.0.1:8080; } }

ssl_certificate放fullchain.pem,ssl_certificate_key放私钥,这两者必须配对。ssl_trusted_certificate放根证书,主要是让 Nginx 在做 OCSP Stapling 验证时使用。如果你只有一个自签根证书,没有中间证书,那么fullchain.pem实际上就等于server.crt,但仍建议单独建一个fullchain.pem,方便以后升级成多级 CA 结构时不用大改配置。

有个容易忽略的地方:listen 443 ssl;和ssl on;是老配置里的写法,Nginx 1.25 之后ssl指令已经废弃。新版直接用listen 443 ssl;即可,不要再在配置里写ssl on;,否则有些版本会直接启动失败或告警。

4.2 配置 HTTPS 跳转与 OCSP 的最大化利用

配好 443 之后,通常还要把 80 端口跳转到 HTTPS,否则用户访问http://server.example.local还是裸奔。跳转时我建议只在 server 块中做return 301 https://$host$request_uri;,不要用rewrite写复杂规则。301 会永久重定向,浏览器会缓存结果,如果后续你要回退到 HTTP,那可就麻烦了。测试环境直接 302,确认无误再改 301。

OCSP Stapling 不少人听过但不清楚作用。它的大意是:服务器主动向 CA 的 OCSP 服务器查询这张证书有没有被吊销,然后把查询结果缓存下来,随 TLS 握手一起发给浏览器。这样浏览器就不用每次都自己去查询吊销状态,握手更快,隐私也更好。配置里两行就够了:

ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 valid=300s;

内网环境可能访问不了公网的 resolver,那就要用内网 DNS 地址,或者干脆关闭 Stapling。关闭也不影响证书使用,只是浏览器每次可能都要自己去查吊销状态。自签 CA 如果没有架设 OCSP 服务,Nginx 日志里会报错,这种情况下建议直接关掉,省得日志里刷屏。

4.3 校验证书与私钥是否匹配

部署中最隐蔽的坑是:手头有多个证书、多个私钥,改配置时复制错文件。证书和私钥不匹配,Nginx 也能正常启动,但浏览器握手会失败,报SSL_ERROR_BAD_CERT_DOMAIN或ERR_SSL_PROTOCOL_ERROR。排查方法是对比两个文件的公钥指纹:

openssl x509 -in server.crt -noout -pubkey | openssl md5 openssl pkey -in server.key -pubout | openssl md5

两条命令输出的 MD5 值一致,说明证书和私钥是一对。这个检查比任何“看文件名”都靠谱,我强烈建议把它写进部署脚本里。另外,systemctl reload nginx之前跑一下nginx -t检查配置语法,这是基本操作,别省。

5. 浏览器端错误与 Chrome 信任策略实操

5.1 三个高频报错对应的根因

内网部署证书,十个报错里九个是这三种:

浏览器提示通常根因处理方向
NET::ERR_CERT_AUTHORITY_INVALID根证书不被信任,或证书链不完整安装根证书,或者把中间证书补进 fullchain.pem
ERR_CERT_COMMON_NAME_INVALID证书 SAN 里没有你访问的域名或 IP在配置文件的 alt_names 里补上对应记录并重新签发
ERR_CERT_DATE_INVALID证书不在有效期内,或系统时间不对检查服务器和客户端的时间,确认证书未过期

NET::ERR_CERT_AUTHORITY_INVALID是最常见的。它有两层含义:第一层,操作系统信任区里没有这个根证书;第二层,即使有,Chrome 也可能因为链不完整而找不到根。我遇到过好几次,Windows 系统里明明已经把根证书装进去了,Chrome 仍然报这个错,最后发现是服务器只发了叶子证书,没发中间证书。所以排查时优先看openssl s_client输出的证书链,而不是先动信任区。

ERR_CERT_COMMON_NAME_INVALID在 Chrome 出现得越来越多,因为这个浏览器已经不把 CN 当重要匹配字段了。你在地址栏输入什么,浏览器就去证书的 SAN 里找什么。输入 IP 就是找 IP 条目,输入域名就是找 DNS 条目,找不到就报错。不要试图靠改 hosts 文件骗过去,证书里没写就是没写。

5.2 内网私有 CA 如何让 Chrome 信任

私有 CA 想要被 Chrome 信任,路线其实只有一条:把根证书安装到操作系统或浏览器的信任区里。Chrome 对私有根没有单独提供像 Firefox 那样的“内置例外列表”,它最终还是看系统信任区,但安装位置很讲究。

Windows 上,根证书要安装到“受信任的根证书颁发机构”,不是“个人”或“中间证书颁发机构”。双击.crt文件,选“安装证书”,然后选“本地计算机”,再一步步到“将所有证书都放入下列存储”并手动选到“受信任的根证书颁发机构”。macOS 上则是用“钥匙串访问”导入到“系统”钥匙串,并手动把信任设置为“始终信任”。

装完之后别急着欢呼,还有一个经典坑:Chrome 不会立刻刷新信任状态。要重启浏览器,最好把chrome://restart跑一遍。有些情况下还要检查是否有“企业策略”覆盖了根证书的设置。Chrome 对企业环境提供了策略来管理根证书,如果你在受管理的电脑上,管理员策略可能会强制禁止非授权根,这类策略优先级很高,手动装根证书也会被拦。

部署私有 CA 时还有个建议:根证书密钥一定要离线保存,平时签服务器证书用独立的中间 CA 来做。内网规模小的时候一上来就用根证书直接签也行,但一旦根私钥泄露,整个内网信任体系都要重建。如果将来要建设正规的私有 PKI,先熟悉一下“离线根 + 在线中间”的玩法。

5.3 localhost 和 IP 证书的特殊处理

Chrome 对localhost有特殊对待:它把localhost视为“安全来源”,即使你访问http://localhost,也不会被标记成不安全。但如果你把服务部署成https://localhost:8443,浏览器依然会要求证书本身有效,不会因为地址是 localhost 就豁免证书校验。

这里就有一个常见的“开发环境简单处理法”:如果只是为了本地调试,Chrome 支持用启动参数临时忽略证书错误。Windows 或 Linux 上可以给 Chrome 快捷方式添加--ignore-certificate-errors,macOS 上则要用命令行方式启动。这个方法只建议在临时调试用,别长期开着,因为它等于关闭了整站证书校验,连访问公网站点都不过滤了。

涉及 IP 地址的 HTTPS 访问,规则比域名更严格。证书的 SAN 里必须包含与地址栏完全一致的 IP,不能用一个“通配 IP”去覆盖多台机器。比如你访问https://10.0.0.5,证书里 SAN 必须有IP.2 = 10.0.0.5。Chrome 不认IP.1 = 10.0.0.0/24这种网段写法,也不认 wildcard IP。这一点和域名不同,域名可以配*.example.com通配符,IP 不行。

6. 有效期管理与轮换

6.1 有效期不是越长越好

很多人喜欢把证书有效期设成十年八年的,觉得省事。但浏览器厂商对公共证书有效期的容忍度一直在收缩,行业当前的共识是公共 CA 签发的 TLS 证书最长不超过 398 天。为什么要卡这个数?因为证书有效期越短,密钥轮换越频繁,即使私钥泄露,影响面也能尽快收敛。同时短周期也倒逼大家自动化续期,降低人为遗忘的概率。

自己内网签的私有证书虽然不受 398 天硬性限制,但也不建议跟根证书一样签到十年。理由很简单:服务器证书涉及的域名、IP、组织信息可能会变,签太长反而成了负担,而且一旦私钥有风险,要撤销也非常麻烦。我一般给服务器证书签 825 天,也就是两年出头。这个数字不是随便选的,旧版的行业上限是 825 天,沿用这个值意味着你签出来的证书在任何严格校验的客户端面前都不会因为“时长”被卡。

根证书则相反,可以签到十年。根证书的更换成本极高,每次换根都意味着所有客户端要信任新根。所以,根证书追求稳定,服务器证书追求短活,这个观念要分开。

6.2 到期前如何平滑切换证书

证书到期前一个月就要开始准备,我最怕看到的是“同事请假了,证书没人换”的场面。建立一个小脚本,把所有证书的到期时间集中列出来:

for f in /etc/nginx/certs/*.crt; do enddate=$(openssl x509 -enddate -noout -in "$f" | cut -d= -f2) echo "$f : $enddate" done

输出里看到快到期的,直接用前文配置生成新证书。切换流程很简单:生成新证书和私钥,更新 Nginx 对应的文件路径,nginx -t && systemctl reload nginx。因为 Nginx reload 是平滑的,不会中断已有连接,可以在白天操作。

但有几个细节值得提醒。第一,私钥文件和证书文件要一起换,不要让旧私钥继续和新证书配对。第二,更新前把旧证书备份到一个只读目录,万一新证书有问题可以秒回滚。第三,如果你开启了 OCSP Stapling,切换后最好清一下 Nginx 的缓存或者等它自动过期,否则 Stapling 里可能还带着旧证书的查询结果,虽然不影响主流程,但会让调试困惑。

还有一个小技巧,可以做成 systemd timer 或者 cron,每月自动跑一次到期扫描,输出到日志。这样没有人盯着证书到期时间,系统也会提前报警。等到证书真的过期了,用户开始刷“网页走丢了”的弹窗,再想去补救,那就太被动了。

就我个人最近两年的实践体会,证书部署最大的难点从来不是命令本身,而是“你以为浏览器会认,但它不认”的那一瞬间。OpenSSL 命令背得再熟,不如老老实实把 SAN、证书链、有效期、信任区这四件事理顺。你只要把这些环节做扎实,无论在 Chrome 还是其他浏览器上,证书问题基本可以一次性解决。最后再分享一个小习惯:每次签发完证书,把配置文件、签发命令和验证输出一起存档,下次换证书直接翻记录,能省掉大量看起来“莫名其妙”的排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询