如何将自建 CA 添加到 Ubuntu 信任存储
使用 OpenSSL 创建私有 CA、签发服务器证书,并将根证书复制到 /usr/local/share/ca-certificates,运行 update-ca-certificates 后让 Ubuntu 信任内部 HTTPS。
将您自己的 CA 添加到 Ubuntu 的信任存储
要将您自己的 CA 添加到 Ubuntu 的信任存储,请将根证书复制到 /usr/local/share/ca-certificates/,并将文件名设置为以 .crt 结尾,然后运行 sudo update-ca-certificates。CA(证书颁发机构)是一对密钥,其中的证书有权为其他证书签名。计算机信任您的根证书后,该根证书签发的所有证书都会被接受,因此您自己的服务之间的 HTTPS 连接将不再验证失败。
本指南使用 openssl 离线构建完整证书链。您将创建根密钥和根证书,为服务器签发一个叶证书,然后安装根证书,并观察同一个验证命令的结果如何变化。顺序很重要:在安装前后分别进行验证,才能确认是安装操作改变了结果。
Ubuntu 24.04 的默认镜像已包含 OpenSSL 3 和 ca-certificates 软件包,因此无需先安装任何内容(检查于 2026 年 8 月)。
何时应运行自己的 CA?
Let's Encrypt 等公共 CA 需要公共 DNS 中的名称,以及一个它能够访问的服务器。内部名称不符合条件。私有网络中的数据库或绑定到隧道的管理面板无法获取公共证书,也不应仅为了获取证书而将其暴露到互联网。
Ubuntu 上的自签名证书只能解决一个主机的问题。每个客户端都必须信任这张证书,下一个主机又要重复同样的工作。私有 CA 将信任决策提升了一层。客户端只需信任一次根证书,此后由该根证书签发的所有证书都会被信任,包括尚不存在的主机的证书。
这确实会带来风险。根密钥可以签发约束允许的任何证书,因此读取 ca.key 的人可以签发您的机器会接受的证书。应像保护 SSH 密钥管理中的私钥一样保护它。如果服务具有公共 DNS 名称,请跳过这一切并使用公共 CA:使用 nginx 和 Let's Encrypt 的 Certbot 工作量更小,也无需在客户端安装任何软件。
创建 CA 密钥和根证书
请在只有您本人可以访问的目录中操作。根密钥不得离开该目录。
install -d -m 700 ~/ca
cd ~/ca
openssl genrsa -aes256 -out ca.key 4096
chmod 600 ca.key-aes256 会使用您选择的密码短语加密密钥。之后每条使用此密钥签名的命令都会要求输入该密码短语。不要省略 -aes256,否则密钥会以明文形式存储在磁盘上。这样,只要有一份备份或第二个管理员账户,就足以让他人获得签发受您机器信任的证书的权限。
现在创建根证书。根证书由 CA 密钥为自身签名。
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
-subj "/O=Example Internal/CN=Example Internal Root CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" \
-addext "nameConstraints=critical,permitted;DNS:internal.example" \
-out ca.crt将 internal.example 替换为您实际使用的名称后缀。保留最后一个扩展项之前,请先阅读下一节。
每个扩展项只负责一项功能。
basicConstraints配合CA:TRUE将该证书标记为 CA 证书。没有它,即使签名本身正确,客户端也会拒绝该密钥签发的所有证书。pathlen:0表示该 CA 可以签发叶证书,但不能签发下级 CA 证书。keyUsage将密钥限制为签发证书和吊销列表,因此不会误将同一密钥用作 TLS 服务器密钥。subjectKeyIdentifier为根证书提供标识符,叶证书会指向该标识符。客户端据此可以在包含数百个证书的存储中找到正确的签发者。nameConstraints限制该 CA 可以为哪些名称提供信任担保。
请读取您创建的内容,不要假定命令一定执行了您想要的操作。
openssl x509 -noout -subject -issuer -serial -dates -in ca.crt
openssl x509 -noout -text -in ca.crtSubject 和 issuer 会显示相同的字符串,因为根证书会为自身签名。序列号和两个日期来自刚刚创建的文件,因此应从该输出中获取这些值,而不是照抄任何指南中的内容。
限制 CA 允许签发的名称
系统信任库中的 root 默认信任 Internet 上的所有名称,除非另行限制。将如此大的权限集中在一台服务器上的一个文件中,风险很高。nameConstraints 可以缩小此权限范围。在 root 中加入 permitted;DNS:internal.example 后,即使签名有效,来自此 CA 且名称位于 internal.example 之外的证书链也会被拒绝。
请先测试限制是否生效,不要直接信任该证书。
openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/outside.key -out /tmp/outside.csr \
-subj "/CN=www.example.com"
openssl x509 -req -in /tmp/outside.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 30 -sha256 \
-extfile <(printf 'subjectAltName=DNS:www.example.com\n') -out /tmp/outside.crt
openssl verify -CAfile ca.crt /tmp/outside.crt
echo $?证书仍会成功签发,因为您的 CA 会签发您要求它签发的任何证书。失败发生在验证阶段:退出状态为非零,OpenSSL 会指出触发的限制。这正是该扩展的作用。即使 CA 密钥被窃取,攻击者仍无法为子树之外的名称生成可用证书。完成后,使用 rm /tmp/outside.* 删除残留文件。
在确定使用此限制前,需要了解以下 4 点。该限制标记为 critical,因此不理解此扩展的客户端必须拒绝证书链,而不能忽略该扩展。这符合安全要求,但可能导致旧版 TLS 库出现意外问题。针对 DNS 名称设置的允许子树不会限制 IP 地址 SAN,因为未列出子树的名称类型仍不受限制。因此,如果证书包含 IP 地址,请在同一扩展中加入 permitted;IP:10.0.0.0/255.255.0.0。子树必须覆盖您今后可能签发的所有名称,包括短主机名。因此,针对裸名称 app 的证书将无法通过上述示例中的验证。此限制会固化在 root 中,因此如果之后需要更改,就必须生成新的 root 证书,并在每个客户端上重新安装。
签发由 CA 签名的叶证书
叶证书是服务器提供给客户端的证书。首先生成该证书自己的密钥和 CSR(证书签名请求)。CSR 包含公钥和请求的名称,并由叶证书密钥签名,以证明请求方持有对应的私钥。
openssl req -new -newkey rsa:2048 -nodes \
-keyout app.key -out app.csr \
-subj "/CN=app.internal.example"
chmod 600 app.key需要使用的名称应写入扩展文件,而不是 CSR。客户端会将主机名与 subjectAltName(SAN)进行匹配,并完全忽略通用名称。因此,在 CN 中设置名称但不包含 SAN 的证书,在所有当前客户端上都会因主机名验证失败,无论 CN 的值是什么。
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:app.internal.example, DNS:api.internal.example
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always将其保存为 app.ext,然后使用 CA 签名该请求。
openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 397 -sha256 -extfile app.ext -out app.crt-CAcreateserial 会在 CA 旁边写入 ca.srl,其中保存下一个序列号,确保该 CA 签发的证书不会使用相同的序列号。请将此文件保存在 CA 目录中。-days 397 是有效期设置,不是工具的限制。这里的短有效期比使用公共 CA 时更重要,因为私有 CA 没有吊销基础设施:除非自行构建,否则不会有 CRL 或 OCSP 响应程序。因此,泄露的叶密钥在证书过期前仍可使用。
在接触信任存储之前,先检查结果。
openssl x509 -noout -subject -issuer -serial -dates -in app.crt
openssl x509 -noout -ext subjectAltName -in app.crt现在,签发者行显示的是 CA,而不是叶证书自身。SAN 行列出了该证书有效的名称,客户端只会根据此列表进行匹配。
使用显式的 -CAfile 验证,然后再安装任何内容
openssl verify -CAfile ca.crt app.crt
echo $?这只会检查一个问题:app.crt 是否链接到 ca.crt 中的证书?它不会说明这台机器信任什么,因为您在命令行中将根证书直接传给了 OpenSSL。此处验证失败表示证书本身存在问题,因此请先修复证书,再继续操作。
现在让机器自行验证。
openssl verify app.crt
echo $?未指定 -CAfile 时,OpenSSL 会回退到其内置的证书目录。openssl version -d 会输出当前构建所使用的基础目录;在 Ubuntu 上,该目录下的 certs 会解析为 /etc/ssl/certs。您的根证书尚未位于该位置,因此验证失败:证书链到达了证书存储中没有的颁发者,并且没有其他位置可继续查找。请注意退出状态。两步之后,退出状态会发生变化。
与 openssl verify 相比,真实客户端更适合作为测试工具,因为它会同时检查主机名和证书链。提供证书并获取它。
openssl s_server -accept 8443 -cert app.crt -key app.key -www &
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/--resolve 会将连接发送到 127.0.0.1,同时仍请求 app.internal.example,因此 SAN 可以匹配,唯一待确认的问题就是信任关系。curl 会失败,并输出无法验证证书链的原因。添加 -v 可获取更多详细信息。让测试服务器继续运行。
将根证书安装到 /usr/local/share/ca-certificates
sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-root.crt
sudo chmod 644 /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates以下细节决定此操作是否能够生效:
- 文件名必须以
.crt结尾。update-ca-certificates手册页说明,/usr/local/share/ca-certificates下方找到的、扩展名为.crt的证书会被纳入并默认信任。名为root.pem或root.cer的文件会被直接跳过,且不会显示任何提示。 - 内容必须是 PEM 格式,也就是包裹在
BEGIN CERTIFICATE和END CERTIFICATE行之间的 base64 数据块。将 DER 文件重命名为.crt后,它仍然是二进制文件,不会被读取。请使用openssl x509 -inform DER -in ca.der -out ca.crt转换它。 - 此处只能放置根证书。CA 私钥和叶证书不应放入信任存储。
update-ca-certificates 会显示添加和移除的证书数量。如果一个证书都没有添加,原因就是文件扩展名或文件格式不正确。
请从系统侧确认更改,而不要只依赖该命令输出的信息。
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in /usr/local/share/ca-certificates/example-internal-root.crt).0
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt第一条命令根据证书自身的 subject hash 生成文件名,并列出该文件。update-ca-certificates 创建了这个符号链接,它指向刚刚安装的文件。第二条命令统计单文件 bundle 中的证书数量。安装前也运行一次,便可以看到数量增加 1。
将此根证书复制到其他机器时,请先确认副本完整无误,再进行安装。根证书一旦出错,影响最严重,因此使用前应像处理其他下载文件一样,先使用校验和验证。
再次根据系统信任库进行验证
openssl verify app.crt
echo $?
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/命令相同,证书文件相同,但结果不同。app.crt 没有任何变化,服务器仍是您之前启动的服务器。唯一的区别是,根证书现在位于这些客户端读取的信任库中,因此证书链可以完成验证。需要记住的机制是:验证过程会查找客户端已信任的签发者,而安装 CA 就是将该签发者放入客户端搜索位置的方式。
使用 kill %1 停止测试服务器。
为什么 /etc/ssl/certs 不是放置文件的位置
/etc/ssl/certs 是生成的输出。update-ca-certificates 会在其中创建指向实际证书文件的符号链接,并在这些文件旁边写入合并后的证书包 /etc/ssl/certs/ca-certificates.crt。
手动复制到该目录中的证书不会被任何组件找到。OpenSSL 的目录查找只会打开以证书主题哈希命名的文件,因此名为 myca.crt 的文件对它不可见。Ubuntu 上的 curl 读取证书包文件,而该证书包由已注册的源重新生成,因此你的副本也不会出现在该路径中。运行 update-ca-certificates --fresh 时,目录中的符号链接会被删除并重新创建,手动创建的链接也会随之消失。
这一区分的另一部分是 /usr/share/ca-certificates,它属于 ca-certificates 软件包,并记录在 /etc/ca-certificates.conf 中。软件包更新会重写它。/usr/local/share/ca-certificates 是为本地管理员保留的目录,因此负责管理其余内容的软件包每次升级时,你的 CA 都会保留。
哪些程序会忽略系统信任存储
安装根证书可以修复所有调用 OpenSSL 或读取 /etc/ssl/certs 的程序。这包括 curl、wget、git、Python 的标准 ssl 模块,以及在 Linux 上读取系统文件的 Go 程序。自带证书列表的运行时不受影响。成功安装后仍然出现问题,大多是因为这些运行时使用了自己的证书列表。
- Node.js 使用编译内置的证书列表。请通过
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/example-internal-root.crt指定根证书,并在进程启动前设置该环境变量,因为 Node 只会在启动时读取一次。当前 Node 版本还提供了读取系统存储的选项。运行node --help | grep -i system-ca,确认您的版本是否支持该选项。 - Python 的
requests库使用certifi证书包。请为该进程设置REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt,或在调用时传入verify="/etc/ssl/certs/ca-certificates.crt"。pip出于同样原因接受--cert。 - Java 读取密钥库。在 Ubuntu 上,
ca-certificates-java软件包会在/etc/ca-certificates/update.d/下安装一个钩子。因此,只要该软件包已安装,update-ca-certificates也会刷新 Java 密钥库。否则,请使用keytool -importcert导入根证书。 - Firefox 使用自己的证书存储,完全不会读取
/etc/ssl/certs。请通过其证书设置导入证书。Linux 上的 Chromium 使用每个用户独立的 NSS 数据库,您可以通过libnss3-tools软件包中的certutil编辑该数据库。 - 容器拥有独立的文件系统,因此宿主机的证书存储在容器内不起作用。请将根证书复制到镜像中,并在构建期间运行
update-ca-certificates。如果您的服务运行在 VPS 上的 Docker Compose 中,请提前规划这一点。
如果程序在正确安装证书后仍然拒绝该证书,请先确定它打开了哪些文件,再进行其他修改。strace -f -e trace=openat <command> 2>&1 | grep -i cert 的检查范围较宽,一次运行即可回答这个问题。
长期保持 CA 可用
重新签发叶证书时,需要再次执行 CSR 步骤和签名步骤,并继续使用相同的 app.ext 文件。客户端无需执行任何操作,因为它们信任的根证书没有变化。请将 ca.srl 和 CA 目录中的每个 .ext 文件都保留下来,这样下次签发时可以重复执行已验证过的命令,而不必凭记忆重新构建流程。
将 ca.key 和 ca.crt 备份到服务器之外的位置,并继续保持加密状态。如果密钥丢失,您将无法签发任何新证书,必须重新构建第二个 CA,并在第一套 CA 所覆盖的所有位置安装它的根证书。请记录收到该根证书的每台计算机和每个应用程序证书存储区,因为只有这份清单才能支持后续的证书轮换和移除。
根证书接近到期时,请提前生成替代根证书,并将两个根证书并行安装。证书存储区中同时存在两个根证书没有问题,客户端可以接受其中任意一个。使用新根证书重新签发叶证书,确认没有任何组件依赖旧根证书后,再将其移除。
从信任存储中移除 CA
sudo rm /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates --fresh--fresh会移除 /etc/ssl/certs中的符号链接,并根据仍然存在的源文件重新生成这些链接,因此已删除的根证书会同时从该目录和证书包中移除。使用与验证安装相同的方法验证移除结果。
openssl verify app.crt
echo $?
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in ~/ca/ca.crt).0验证再次失败,证书数量恢复到初始值,哈希符号链接也已消失。
该命令只会修改系统存储,不会影响其他位置。请手动撤销其他位置的安装:清空 NODE_EXTRA_CA_CERTS,从所有 Java 密钥库中删除该别名,从每个浏览器配置文件中移除该根证书,并重新构建已将其内置其中的所有容器镜像。移除根证书也不会使它签发的证书失效。任何仍然信任该根证书的计算机都会继续认为这些证书有效。这正是私有 CA 必须记录根证书部署位置的实际原因。无法完全撤回的 CA 会形成永久的安全漏洞。因此,应在部署当天于一台计算机上测试移除流程,此时清单仍然较短。
FAQ
在 Ubuntu 中应将 CA 证书放在哪里?
将证书放在 /usr/local/share/ca-certificates/ 中,文件名以 .crt 结尾,并包含 PEM 内容,然后运行 sudo update-ca-certificates。该目录专供本地管理员使用,因此软件包升级不会修改其中的文件。/usr/share/ca-certificates 属于 ca-certificates 软件包,/etc/ssl/certs 会根据两者生成,因此放入其中任一目录的文件都可能被覆盖或忽略。
运行 update-ca-certificates 后,curl 为什么仍然拒绝证书?
按顺序排查以下原因。文件名可能不是以 .crt 结尾,或者证书格式是 DER 而不是 PEM;此时 update-ca-certificates 会跳过该文件,不会添加任何内容。证书可能没有与主机名匹配的 subjectAltName,这属于主机名验证失败,而不是信任失败;使用 openssl x509 -noout -ext subjectAltName -in app.crt 检查。服务器可能只发送了叶证书,但验证还需要中间证书。curl 可能通过 CURL_CA_BUNDLE 或 --cacert 指向了其他证书包。长时间运行的服务还需要重启,因为大多数程序只会在启动时读取一次信任存储。
系统信任存储是否适用于 Firefox、Chrome、Node 和 Java?
不适用。curl、wget、git、Python 的标准 ssl 模块和 Go 程序会读取系统文件,因此 update-ca-certificates 运行后,它们即可使用新的证书。Firefox 使用自己的证书存储。Linux 上的 Chromium 使用按用户划分的 NSS 数据库,可通过 libnss3-tools 软件包中的 certutil 编辑。Node.js 需要通过 NODE_EXTRA_CA_CERTS 指向您的根证书文件。Java 读取密钥库;只有安装 ca-certificates-java 软件包后,update-ca-certificates 才会刷新该密钥库。Python 的 requests 使用 certifi,并需要 REQUESTS_CA_BUNDLE。
如何从 Ubuntu 的信任存储中删除 CA?
从 /usr/local/share/ca-certificates/ 删除该文件,然后运行 sudo update-ca-certificates --fresh。--fresh 选项会清除 /etc/ssl/certs 中的符号链接并重新构建,因此该证书会同时从哈希符号链接和 ca-certificates.crt 证书包中移除。使用 openssl verify 验证一个由该 CA 签名的证书,并查看退出状态。然后在您添加该证书的其他每个存储中重复删除操作,因为该命令不会修改其他存储。
公共网站可以使用私有 CA 代替 Let's Encrypt 吗?
不可以。访问者的浏览器从未见过您的根证书,因此会显示整页警告;您也无法在不受您控制的计算机上安装该根证书。私有 CA 适用于只有您自己的计算机能够解析的名称,以及由您管理的客户端。对于陌生用户访问的任何网站,都应从公共 CA 获取证书。