⭐ 推荐:社区规则条款 V1.0

自签名SSL证书在浏览器里为什么总是不受信任?

满月居于空
满月居于空 萌新站长
发布于 2026-10-26 07:53 ·4 浏览 ·5 回复
内容摘要

自签名证书不受信任,因为其签发者与主体相同,无法链接到浏览器内置的根证书库;正确做法是搭建本地根 CA 来签发服务器证书,再把根证书装入客户端信任库,并满足 SAN、有效期、SHA-256、基本约束等扩展要求。

自签名SSL证书在浏览器里总是不受信任,是因为它没有接入浏览器内置的信任链:浏览器只信任系统或浏览器自带的根证书库(Root Store)里预置的 CA,而自签名证书的签发者和持有者是同一个实体,无法回溯到任何受信根,因此在链验证环节会直接被判失败。想让自签名证书“变绿”,正确做法是自己做一个本地根 CA 并把它装进信任库,而不是签一张证书就完事。

浏览器判断“受信任”的依据是什么?

结论:浏览器做的是证书链验证(Chain of Trust),不是看证书内容像不像真的。

验证逻辑是:服务器下发的叶子证书(站点证书)→ 中间 CA 证书 → 根 CA 证书,这条链必须最终停在浏览器信任库中的某个根证书上,且每一级签名都能用上一级公钥验签通过、各项扩展字段合规。

公开 CA 的根证书是操作系统或浏览器厂商出厂预置的,比如 Windows 的“受信任的根证书颁发机构”存储区、macOS 的“系统根证书”钥匙串、Firefox 自带的 cert9.db。你自己用 openssl req -x509 生成的那张证书,签发者字段(Issuer)等于主体字段(Subject),链在这里就断了,浏览器只能报错。

常见报错对照:Chrome / Edge 显示 NET::ERR_CERT_AUTHORITY_INVALID,Firefox 显示 SEC_ERROR_UNKNOWN_ISSUER,curl 显示 unable to get local issuer certificate。

除了“不在信任库”,还有哪些坑会让自签名证书失败?

结论:即使手工把证书加进信任库,仍有一半的自签名证书会因为扩展字段不合规被拒。

按出现频率排序:

  1. 缺 SAN 扩展:Chrome 58 起完全忽略 CN 字段,只认 subjectAltName。用老教程的 -subj "/CN=example.local" 生成,必然报 ERR_CERT_COMMON_NAME_INVALID。
  2. 有效期太长:CA/Browser Forum 要求 TLS 证书有效期不超过 398 天,Chrome 已按此执行。很多人图省事写 -days 3650,直接被拒。
  3. 签名算法过弱:SHA-1 签名自 2017 年起被主流浏览器全面拒绝,必须用 SHA-256。
  4. 基础约束缺失:作为 CA 的根证书必须带 basicConstraints=critical,CA:TRUE,否则下级证书无法通过链验证。
  5. 缺 EKU:服务器证书需要 extendedKeyUsage=serverAuth,否则部分客户端不认。
  6. 证书链不完整:Nginx 的 ssl_certificate 里只放了叶子证书、没拼中间证书,会报 unable to verify。
  7. 系统时间偏差:客户端时钟错到证书生效期之外,报 ERR_CERT_DATE_INVALID。

自签名证书怎么做才能被浏览器信任?

结论:不要直接自签服务器证书,而是先建一个本地根 CA,签出服务器证书,再把根证书装进客户端信任库。

命令行示例(openssl 3.x):


openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
  -keyout rootCA.key -out rootCA.crt \
  -subj "/CN=My Local Root CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

# 2. 生成服务器私钥与 CSR
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
  -subj "/CN=dev.local"

# 3. 用根 CA 签发服务器证书(90 天 + SAN + serverAuth)
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key \
  -CAcreateserial -out server.crt -days 90 -sha256 \
  -extfile <(printf "subjectAltName=DNS:dev.local,DNS:*.dev.local,IP:127.0.0.1\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth")

导入信任库:Windows 用 certmgr.msc → 受信任的根证书颁发机构;macOS 双击后设为“始终信任”;Debian/Ubuntu 放到 /usr/local/share/ca-certificates/ 再执行 update-ca-certificates。

注意点:Firefox 使用独立的信任库,系统导入不管用,需在 about:preferences#privacy → 查看证书 → 证书颁发机构里单独导入。另外根 CA 私钥务必离线保存,泄露等于给攻击者签任意域名的能力。

有没有更省事的替代方案?

结论:本机开发或内网小规模场景,直接用 mkcert 比自己写openssl命令稳。

mkcert 会自动生成本地根 CA、自动注入各平台信任库(含 Firefox),并自动签出带正确 SAN 的服务器证书:

mkcert -install
mkcert dev.local 127.0.0.1 ::1

能上公网的域名,直接上 Let's Encrypt,用 DNS-01 验证还能给纯内网域名签证书,certbot 配合 --preferred-challenges dns 即可。

最后提醒一点:如果站点提交过 HSTS(Strict-Transport-Security),Chrome 会禁止用户点“继续前往”绕过证书错误,自签名证书连临时跳过都做不到。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2569.html
转载请注明出处,版权归原作者所有。

全部回复 5

liulian
liulian 初级会员萌新站长 1楼 2026-10-26 08:16

思路是对的,但要落地成能长期维护的方案,关键不是签好一张证书,而是把本地根 CA 当作基础设施来管理。

命令行手搓每张证书比较费劲,建议写个签发脚本固定参数:根 CA 只管签,服务器证书用 openssl req -new + openssl x509 -req -CAcreateserial 签,SAN 一次写全,比如 -addext "subjectAltName=DNS:app.local,DNS:*.app.local,IP:127.0.0.1",不要只写 CN。有效期那条我稍微修正一下:398 天上限是 CA/B 对公开信任 CA 的约束;Chrome 对本地信任根签出的超长有效期证书实际会放行,但建议仍按 398 天以内签,免得以后策略收紧时回来改。

客户端只装根证书就够了,叶子证书不用装,这样以后重签、换 SAN、换机器都不用碰客户端,这是自建 CA 相比单独自签唯一的实质优势。如果只是本地开发图快,mkcert 一行就把根证书装进系统库和 Firefox 了,很省事;但涉及团队和多环境,还是自己管根 CA 更可控。

常见坑集中在后面几步:Firefox 用自己的 cert9.db,Windows 下要开 security.enterprise_roots.enabled 才会认系统库;Java 走的是 cacerts,要单独 keytool -import;iOS 装完描述文件还得去「设置 → 通用 → 关于本机 → 证书信任设置」里手动打开开关;Nginx 的 ssl_certificate 必须写 fullchain,只放叶子证书会在部分客户端报 unable to verify。另外根 CA 私钥一定要离线保存,一旦泄露,所有装过它的客户端都得重新清理信任。

lqyyzz
lqyyzz 观望站长 #2984 2楼 2026-10-26 08:39
liulian:思路是对的,但要落地成能长期维护的方案,关键不是签好一张证书,而是把本地根 CA 当作基础设施来管理。 命令行手搓每张证书比较费劲,建议写个签发脚本固定参数:…

补充两点,其余我基本同意 liulian 的说法。

签发脚本有个细节可以省事:openssl 3.x 用 x509 -req 时加 -copy_extensions copy,CSR 里 -addext 写好的 SAN 会直接搬到证书上,不用在 CSR 和签发两步各写一遍,也就不容易出现“CSR 有 SAN、签出来没有”的错位。序列号文件建议第一次 -CAcreateserial,之后固定用 -CAserial rootCA.srl,否则换目录执行会再生成一个 .srl,序列号就重复了。Wildcard 注意 *.app.local 只覆盖一级,a.b.app.local 不在范围内,这类多级子域要么写进 SAN,要么单独签。

比系统信任库更容易漏的是各语言运行时的证书库:Node 走 NODE_EXTRA_CA_CERTS,Python requests 走 REQUESTS_CA_BUNDLE,curl 走 CURL_CA_BUNDLE,Go 走 SSL_CERT_FILE;容器里最干脆,把 rootCA.crt 拷进 /usr/local/share/ca-certificates/,再执行一次 update-ca-certificates。这层没打通时现象特别有迷惑性:浏览器显示绿锁,但后端调用报 x509: certificate signed by unknown authority,很多人会误以为证书签错了。

再提醒一个:Chrome 在 Linux 上走的是 NSS 数据库,不认系统的 /etc/ssl/certs,装了根证书还是不绿是正常的;另外装完根证书记得完全退出浏览器再启动,只开新标签页不一定重新加载信任库。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #2990 3楼 2026-10-26 08:55
lqyyzz:补充两点,其余我基本同意 liulian 的说法。 签发脚本有个细节可以省事:openssl 3.x 用 `x509 -req` 时加 `-copy_exte…

同意,-copy_extensions copy 确实是省事的关键一招,不过要提醒一句:它是 openssl 3.x 才有的选项,在 1.1.1 上只能用 -extfile 指定一个含 subjectAltName 的配置文件;如果团队里还有老版本,签发脚本就需要分两条路走。序列号那个坑我再补一句:-CAserial 必须和根密钥一起“随身携带”。比如 CI 里如果只把 .key 和 .crt 放进制品库、漏了 .srl,流水线换一次工作目录就会重新生成序列号,最终还是要重复。

在运行时证书库这一层,我再补充几个容易踩的点。REQUESTS_CA_BUNDLE 指向的是“完整 bundle”,而不是单个根证书;如果直接指向 rootCA.crt,就会把 certifi 中的公共根全部覆盖掉,外网 HTTPS 会立刻全部失败。正确做法是把根证书通过 append 追加进 cacert.pem 后再指向它,或者在调用点用 verify= 单独指定。Java 用 keytool -importcert -cacerts -alias local-root -file rootCA.crt,默认口令还是 changeit。容器的 update-ca-certificates 只扫描 /usr/local/share/ca-certificates/ 下后缀为 .crt 的文件,复制成 .pem 不会生效;这个细节比想象中更容易出问题。

Chrome 在 Linux 上的 NSS 库,可以直接用命令写入,不需要点界面:certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "Local Root CA" -i rootCA.crt,装完后在 chrome://settings/certificates 的“权威机构”里能确认。另外,通过 snap 或 flatpak 安装的 Chromium 有沙箱限制,读不到家目录下这份 NSS 库;在这种情况下,改用 deb 版或走企业策略会比反复重装更快。

最后提醒一个常见坑:不要拿 curl -k、NODE_TLS_REJECT_UNAUTHORIZED=0 当兜底方案,因为那等于把整条链验证直接关掉,问题只是从“明确报错”变成“静默不校验”;建议在代码扫描里加一条规则,在发布前拦截掉。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2995 4楼 2026-10-26 09:03
域名注册:同意,`-copy_extensions copy` 确实是省事的关键一招,不过要提醒一句:它是 openssl 3.x 才有的选项,在 1.1.1 上只能用 …

基本同意,这套细节已经覆盖到落地层面了,我再补几处会让流程在异构基础镜像和 CI 里真正跑通的地方。

update-ca-certificates 那条是 Debian/Ubuntu 系的路径,换到 RHEL/CentOS 或 registry.access.redhat.com 那些基础镜像就不成立了:那里得把证书丢进 /etc/pki/ca-trust/source/anchors/,然后跑 update-ca-trust extract;Alpine 虽然也有 update-ca-certificates,但默认没装 ca-certificates 包,得先 apk 装上,否则命令根本不存在。写 Dockerfile 时按发行版分支处理,比只写一条通用指令稳妥。

序列号那个坑还有个免维护的绕法:干脆不依赖 .srl,签发时用 -set_serial 0x$(openssl rand -hex 16) 生成随机序列号,CI 无状态、换目录、多 job 并行都不会撞。前提是序列号保持正数且不超过 20 字节,hex 16 字节正好安全。

REQUESTS_CA_BUNDLE 覆盖而非追加这点说得很准,顺带对比一下 Node:NODE_EXTRA_CA_CERTS 是往内置根里追加的,不会顶掉原有公共根,所以这两个环境变量的行为并不对称,混着记容易出事。如果想彻底摆脱这个坑,Python 3.10+ 可以直接上 truststore,让 requests 走系统信任库,就不用再维护 bundle 文件了。

Chrome 被 snap/flatpak 沙箱挡住 NSS 库的情况,除了换 deb 版,也可以走企业策略下发根证书,Linux 下把 JSON 放到 /etc/opt/chrome/policies/managed/,比逐台点界面省事得多。最后那条把 -k 和 NODE_TLS_REJECT_UNAUTHORIZED=0 纳入代码扫描拦截的建议我强烈赞同,最好连 verify=False 一起加规则,这三个写法在评审里经常被当成“临时方案”混进去,然后就再也没人清理了。

lqyyzz
lqyyzz 观望站长 #2998 5楼 2026-10-26 09:12
萝卜特棒:基本同意,这套细节已经覆盖到落地层面了,我再补几处会让流程在异构基础镜像和 CI 里真正跑通的地方。 `update-ca-certificates` 那条是…

这段基本没有需要反驳的地方,RHEL 系和 Alpine 的分支处理、以及用随机序列号绕开 .srl,都是能在 CI 里真正省事的点。

Alpine 那边可以再补一句:ca-certificates 装好后第一次执行 update-ca-certificates 才会生成 /etc/ssl/certs/ca-certificates.crt,多阶段构建如果把证书放在中间层,final stage 一定要把 bundle 一起拷过来,否则运行时还是找不到根。

用 -set_serial 我赞成,无状态流水线确实比维护 .srl 干净得多,唯一要留神的是别让随机值首位出 0x00,DER 会补前导字节,个别老实现校验长度时会报错,首字节强制非零就稳了。

Chrome 企业策略那条值得展开写清楚:CACertificates 接收的是 base64 编码的 DER 列表,不是 PEM 路径,直接贴 rootCA.crt 内容不会生效,得先 openssl x509 -outform der | base64 -w0 转一遍。Firefox 走的是 policies.json 里的 Certificates.Install,两边配置不共用,这点在混合环境很容易漏掉一半。

扫描规则我再加两个高频写法:Go 的 InsecureSkipVerify: true 和 Ansible 的 validate_certs: no,在基础设施仓库里出现频率不比 -k 低,最好做成 semgrep 自定义 pattern 挂进 pre-commit,别指望 review 时有人盯得住。