信任库不止一个
一句话总结
隔离网络中的内部镜像,几乎都使用由私有 CA 签发的证书。但是,信任库不止一个。 操作系统、JVM、Python 的 certifi、Node 的内置列表,各有各的信任库。放进一个地方并不意味着全都信任,所以必须用一张表清楚地知道,哪个工具看哪个信任库。
为什么需要它
用 HTTPS 搭建了内部镜像,并把根证书放进了服务器。curl 可以用。但是 Python 批处理以 SSLError: CERTIFICATE_VERIFY_FAILED 失败,前端构建以 UNABLE_TO_VERIFY_LEAF_SIGNATURE 失败,Java 构建以 PKIX path building failed 失败。常见的反应是用 verify=False、strict-ssl=false、-k 把验证关掉。这样一来,内网里有人冒充镜像,也没有人会知道。隔离网络只是外部攻击少,并不是没有内部攻击的地方。
工作原理
CA 与服务器证书。 根 CA 证书必须有 basicConstraints = CA:TRUE,才有资格为其他证书签名(OpenSSL x509v3_config 文档)。服务器证书中,要访问的名称必须以 DNS: 的形式写在 subjectAltName 中。RFC 9525 写明,用 commonName 做名称确认已不再有效,Go 从 1.15 起也不再把 CN 当作名称。只在 CN 中写名称的证书,在如今的工具中会因名称不匹配而被拒绝。
操作系统信任库。 Debian 和 Ubuntu 要放进 /usr/local/share/ca-certificates/,并运行 update-ca-certificates。手册写明扩展名必须是 .crt,结果是连接成一个文件的 /etc/ssl/certs/ca-certificates.crt。这条命令还会运行 /etc/ca-certificates/update.d/ 中的钩子,Ubuntu 的 ca-certificates-java 在这里挂了钩子,连 JVM 信任库(/etc/ssl/certs/java/cacerts)也会一并更新。RHEL 系列则要放进 /etc/pki/ca-trust/source/anchors/,并运行 update-ca-trust extract。
各个工具查看的位置。
curl 빌드할 때 정해진 파일 저장소(우분투는 운영체제 묶음). --cacert·CURL_CA_BUNDLE 로 바꾼다
파이썬 ssl OpenSSL 기본 경로 = 운영체제 묶음. SSL_CERT_FILE·SSL_CERT_DIR 로 바꾼다
requests certifi 묶음. REQUESTS_CA_BUNDLE(없으면 CURL_CA_BUNDLE) 로 바꾼다. SSL_CERT_FILE 은 읽지 않는다
httpx certifi 묶음. SSL_CERT_FILE·SSL_CERT_DIR 를 따른다
Node 빌드에 들어간 모질라 목록. NODE_EXTRA_CA_CERTS 로 더한다(프로세스 시작 때 한 번 읽는다)
Go 리눅스에서는 운영체제 묶음. SSL_CERT_FILE·SSL_CERT_DIR 로 바꾼다
JVM cacerts(우분투는 운영체제 훅이 채운다). keytool 이나 javax.net.ssl.trustStore
该代码块中的韩文注释依次说明:curl 使用构建时确定的文件型证书库(Ubuntu 上是操作系统证书包),可用 --cacert 或 CURL_CA_BUNDLE 更换;Python ssl 使用 OpenSSL 默认路径,也就是操作系统证书包,可用 SSL_CERT_FILE 或 SSL_CERT_DIR 更换;requests 使用 certifi 证书包,可用 REQUESTS_CA_BUNDLE(没有则用 CURL_CA_BUNDLE)更换,并且不读取 SSL_CERT_FILE;httpx 使用 certifi 证书包,遵循 SSL_CERT_FILE 和 SSL_CERT_DIR;Node 使用构建时内置的 Mozilla 列表,可用 NODE_EXTRA_CA_CERTS 追加,该变量在进程启动时读取一次;Go 在 Linux 上使用操作系统证书包,可用 SSL_CERT_FILE 和 SSL_CERT_DIR 更换;JVM 使用 cacerts,Ubuntu 上由操作系统钩子填充,可通过 keytool 或 javax.net.ssl.trustStore 调整。
对于 Node,文档还补充了几点。NODE_EXTRA_CA_CERTS 在以 setuid 启动的进程中会被忽略,文件不存在或格式有误时,只会发出一次警告就继续。让它使用操作系统信任库的 --use-system-ca 出现在 v23.8.0(Linux 从 v23.9.0 起),起同样作用的环境变量 NODE_USE_SYSTEM_CA=1 则在 v22.19.0 和 v24.6.0 中加入。本实验的 node 是 22.11,所以两者都没有——这就是要先确认版本的原因。
证书链。 现场的私有 PKI 常常是在根之下设置中间 CA,服务器证书由中间 CA 签发。客户端信任库中只有根,所以服务器必须把自己的证书和中间 CA 证书一起发送,路径才能连通。如果服务器上只配置了叶证书,就会出现 unable to get local issuer certificate。这不是客户端的问题,而是服务器配置的错误。
在现场相遇的样子
这是在本实验 Pod 中实测的结果。在把根证书放进操作系统信任库之前,curl、Python urllib、requests、httpx、Node 和 pip 在证书验证时全都失败。执行 update-ca-certificates 之后,curl、urllib、Go 和 pip 通过了,而 requests 和 httpx 以 CERTIFICATE_VERIFY_FAILED、Node 以 UNABLE_TO_VERIFY_LEAF_SIGNATURE 仍然失败。设置了 REQUESTS_CA_BUNDLE、SSL_CERT_FILE 和 NODE_EXTRA_CA_CERTS 之后,全部通过。pip 之所以通过,是因为这个镜像中的 pip 是 Ubuntu 修改过的版本,会读取操作系统证书包。
所以在现场,会把环境变量集中放在一处(/etc/profile.d/ 中的文件,如果是服务,则放在单元的 Environment 中),并在拿到新服务器时,留下一张让每个工具各连接一次的表。“放进操作系统里就行了”只说对了一半。
下一项实验要做什么
制作私有根 CA 和带有 SAN 的服务器证书,用 HTTPS 启动 repo.airgap.internal:8443。放入操作系统信任库,并把环境变量集中到 /etc/profile.d/airgap-ca.sh 中,让 Python 和 Node 都信任它。把每个工具“仅靠操作系统信任库是否可行”写成一张表,评分器会在同样的条件下重新测量并核对。最后,连同证书链一起输出由中间 CA 签发的证书。
参考文档:OpenSSL x509v3_config、RFC 9525、update-ca-certificates(8)、RHEL: Using shared system certificates、requests: CA Certificates、httpx SSL、Node.js CLI(NODE_EXTRA_CA_CERTS)、Go crypto/x509、curl SSL CA certificates