Nexus 换了 SSL 证书之后,所有客户端瞬间“炸锅”,报错一屏接一屏,这事谁碰谁头疼。别慌,这不是什么玄学,绝大多数情况下都是两个毛病:要么是证书链没给全,要么是客户端的信任列表里根本不认新证书。我让你用平常心看待这件事,先搞懂原理,再照着本文的方法一步步排查和修复,很快就能恢复平静。

一、先搞清楚 SSL 证书和证书链是什么

咱们平时上网,浏览器地址栏那个小锁头,就是 SSL 证书在起作用。它的本质其实是“身份证”:服务器亮出一张身份证,客户端看一眼,觉得发证机关是自己认识的,那双方就开始加密聊天。

但这张身份证不是随便印的。它有一层传递关系:服务器自己的证书,叫“服务器证书”;给它发证的机构,叫“中间 CA”;给中间 CA 发证的,叫“根 CA”。这三者串起来,就叫证书链。就像你的身份证是公安局发的,公安局的授权来自上级部门,一层层往上,最终所有人都信这个体系。

如果服务器只把“服务器证书”发给客户端,而没有附带中间的“中间 CA 证书”,那客户端就傻眼了:我只认识根 CA,但你给我的这张身份证上,签字的那个“中间 CA”我不认识,我又没有它的记录,也没法确认它到底是不是根 CA 手下的人,于是就只能报错。

Nexus 换了新证书后出现一堆报错,最常见的就是这个:新证书是正规 CA 签的,但你的 Nexus 没把中间证书一并配上去。这就像你拿着身份证去办事,身份证是真的,但人家要确认发证机关,可你又没带户口本,人家就只能把你拦下来。

二、最容易踩的坑:证书链不完整

怎么知道证书链全不全?最简单直接的办法,就是亲自去跟 Nexus 服务器握手,把它到底发给了客户端哪些证书,全打印出来看一眼。

这里我统一用 Python 技术栈来做演示。因为 Python 自带的 ssl 模块和 socket 模块就能干这个事,不需要额外安装任何东西,任何会点 Python 的开发者都能直接跑。

import socket
import ssl

def fetch_cert_chain(host, port=8443):
    """
    连接指定主机的 SSL 端口,抓取服务器下发的证书链。
    参数:
        host: Nexus 服务器的域名或 IP
        port: 端口,默认 8443,Nexus 的 HTTPS 端口通常也是 8443
    返回值:
       列表,每个元素是一张证书的 PEM 文本
    """
    context = ssl.create_default_context()
    # 把验证关掉,我们只是想看看服务器到底发了哪些证书
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    with socket.create_connection((host, port), timeout=10) as sock:
        with context.wrap_socket(sock, server_hostname=host) as tls_sock:
            # 获得的是包含证书 DER 字节的列表,顺序是从服务器证书开始
            der_certs = tls_sock.getpeercert(binary_form=True)  # 只拿到一张?注意不是全部
            # 其实 getpeercert 只能拿到服务器证书,拿不到完整链
            # 需要改用 get_unverified_chain()
            # 注意:wrap_socket 之后,能用 _sslobj 拿底层对象
            chain_der = tls_sock._sslobj.get_unverified_chain()  

    # 把 DER 格式转成常见的 PEM 文本格式
    from cryptography.x509 import load_der_x509_certificate
    from cryptography.hazmat.primitives import serialization

    pem_certs = []
    for der in chain_der:
        cert = load_der_x509_certificate(der)
        pem = cert.public_bytes(serialization.Encoding.PEM)
        pem_certs.append(pem.decode('utf-8'))
    return pem_certs

# 直接调用,换成你的 Nexus 地址
host = 'nexus.example.com'
chain = fetch_cert_chain(host)

# 打印每一张证书的简短信息
for i, cert_pem in enumerate(chain):
    print(f"证书 {i+1}:")
    print(cert_pem)

这段代码用到了 cryptography 库,如果你没有装,可以先执行 pip install cryptography。注意这里有一个细节:我用 _sslobj.get_unverified_chain() 才能拿到完整证书链,这是 Python 内部没公开的方法,但是非常好使。

运行之后,你会看到一串证书,正常情况下应该是三张:服务器证书、中间 CA 证书、根 CA 证书。其中根 CA 证书往往不会主动下发,因为客户端本地早就有了。所以你最多看到两张:服务器证书和一张中间证书。如果只看到一张“服务器证书”,没什么其他证书,那就说明中间证书没配上,这就是报错的根源。

三、客户端信任存储没更新,也会让你寸步难行

假设证书链是完整的,或者说你确认服务端已经配好了,但客户端还是报 SSL 错误,那么十有八九是客户端的信任库存量名单里,没有签发新证书的那个根 CA。

打个比方:你换了一个新城市的户口本,但办事大厅的系统里还没有收录你们这个新派出所的印章备案,人家自然不认。客户端也一样。我们用的各种开发工具、编程语言,都内置了一个“信任库”,里面装着一批它认可的根 CA 证书。如果你的新证书是某个偏门的内网 CA 签的,或者用了自己搭建的 CA,那客户端的信任库里可不会有它。

怎么验证?还是用 Python,这次我们不关验证,让它严格去检查一下,到底能不能通过信任校验。

import socket
import ssl

def verify_against_default_trust(host, port=8443):
    """
    用系统默认信任库去校验目标服务器证书是否受信任。
    如果受信任,则正常返回。
    如果不受信任,会抛出 ssl.SSLCertVerificationError 异常。
    """
    context = ssl.create_default_context()
    # 这里保持默认的验证开启
    with socket.create_connection((host, port), timeout=10) as sock:
        try:
            with context.wrap_socket(sock, server_hostname=host) as tls_sock:
                print("连接成功,SSL 握手通过,证书受信任!")
                print("加密协议:", tls_sock.version())
        except ssl.SSLCertVerificationError as e:
            print("证书验证失败:")
            print(e)
        except ssl.SSLError as e:
            print("SSL 通用错误:")
            print(e)

# 调用函数
verify_against_default_trust('nexus.example.com')

如果你运行这段代码,拿到了 CERTIFICATE_VERIFY_FAILED 之类的错,那就说明,当前客户端默认信任库里没有对应的根证书。注意,这里用的是 Python 默认信任库,通常就是指 certifi 这个包里带的 cacert.pem 文件。而你的报错客户端可能是 Maven、npm、Docker,它们各有各的信任库,但道理一样,都是缺根证书。

那如何更新 Python 的信任库?你只需要把那个“根 CA 证书”追加到 cacert.pem 文件末尾。下面这段代码可以帮你自动完成这件事。注意:先备份原文件。

import certifi
import os

def append_ca_to_certifi(ca_pem_path):
    """
    把自定义的根 CA 证书追加到 certifi 的信任库里。
    参数:
        ca_pem_path: 根 CA 证书的 PEM 文件路径,比如 /path/my-ca.crt
    """
    cacert_path = certifi.where()
    print("当前 certifi 信任库位置:", cacert_path)

    # 先备份
    backup_path = cacert_path + ".bak"
    if not os.path.exists(backup_path):
        # 用 shutil 复制,这里用 Python 的简单文件读写也行
        with open(cacert_path, 'rb') as src:
            data = src.read()
        with open(backup_path, 'wb') as dst:
            dst.write(data)
        print("已备份原信任库到:", backup_path)

    # 读取要追加的 CA 证书内容
    with open(ca_pem_path, 'r', encoding='utf-8') as f:
        ca_content = f.read()

    # 读取现有信任库内容
    with open(cacert_path, 'r', encoding='utf-8') as f:
        original_content = f.read()

    # 如果信任库里已经有这个 CA,为了避免重复,先做一个简单检查
    # 提取 CA 里的 CN 或者指纹?这里简化,用证书内容头部的名称做判断
    ca_name_line = [line for line in ca_content.splitlines() if line.startswith("Subject:") or "subject=" in line]
    # 更可靠的:直接查一段特征字符串,比如证书的序列号
    # 简单起见,我们用整个证书块做去重
    if ca_content.strip() in original_content:
        print("这个 CA 证书已经在信任库里了,不用重复添加。")
        return

    # 追加到末尾
    with open(cacert_path, 'a', encoding='utf-8') as f:
        f.write("\n")
        f.write(ca_content)
        f.write("\n")

    print("追加完成!新信任库路径:", cacert_path)

# 这里换成你的根 CA 证书路径
append_ca_to_certifi('/path/to/root-ca.crt')

这样,Python 客户端再跑 requests 或者 urllib 访问 Nexus,就不会报错了。但是其他客户端呢?比如 Java 写的 Maven 构建工具,它的信任库是 JAVA_HOME/lib/security/cacerts。你不可能用 Python 代码去改 Java 的库,但你可以手动用命令操作,不过那已经超出我们本文的 Python 单一技术栈了,所以这里不展开。你只需要记住思路:所有“客户端报错”的修复本质,就是让对应的信任库里包含新证书的根 CA。

四、Nexus 服务端证书配置的正确姿势

说了半天客户端,服务端这边也容易出问题。很多人在 Nexus 后台配置证书时,只上传了服务器证书,没把中间证书一起合并进同一个文件里。Nexus 背后的 Jetty 服务器,在读取证书链时,需要你把服务器证书和中间证书按顺序放在同一个 PEM 文件里,才肯完整地下发给客户端。

你完全可以自己用 Python 把这些文件合并成一个。假设你手头有三个文件:server.crt(服务器证书)、intermediate.crt(中间证书)、root.crt(根证书,可选)。合并顺序很重要:服务器证书在最前面,中间证书排第二,根证书可以放最后。下面这段代码帮你合并。

def concat_certs(server_cert, intermediate_cert, output_path):
    """
    按顺序拼接服务器证书和中间证书,生成一个完整的证书链文件。
    参数:
        server_cert: 服务器证书 PEM 文件路径
        intermediate_cert: 中间证书 PEM 文件路径
        output_path: 输出文件路径
    """
    files = [server_cert, intermediate_cert]
    combined = ""
    for file_path in files:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read().strip()
            combined += content + "\n"

    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(combined)

    print(f"证书链已写入:{output_path}")
    print("请把这个文件配置到 Nexus 的 SSL 证书位置,替代原先的单一证书文件。")

# 调用示例
concat_certs('server.crt', 'intermediate.crt', 'chain.crt')

注意,有些情况下中间证书不止一层,比如有两级中间证书。那你也按顺序排列:服务器证书、一级中间、二级中间,以此类推。合并完成后,把这个包含完整链的 chain.crt 文件配置到 Nexus 的 HTTPS 连接器上。这个动作一般是通过 Nexus 的安装目录下 etc/jetty/jetty-https.xmletc/ssl/keystore 来操作,或者更简单的,在 Nexus 的管理界面里重新选择证书文件。这里的关键是,你放进去的文件必须是包含完整链的,而不是孤零零一张服务器证书。

五、用一段综合诊断脚本,一次定位问题

为了省事,我们可以把上面的检查合并成一个 Python 脚本。这样当你再遇到“所有客户端报错”时,直接跑一下,脚本会告诉你:证书链是否完整,默认信任验证是否通过。你也能省点力气。

import socket
import ssl
from cryptography.x509 import load_der_x509_certificate
from cryptography.hazmat.primitives import serialization

def diagnose_ssl(host, port=8443):
    """
    综合诊断一个 SSL 服务器的证书链。
    1. 打印服务器下发的证书链
    2. 用系统默认信任库尝试验证
    """
    print("===== 开始诊断 =====")
    print(f"目标:{host}:{port}")
    print()

    # 先尝试获取证书链
    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE
    try:
        with socket.create_connection((host, port), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=host) as tls_sock:
                chain_der = tls_sock._sslobj.get_unverified_chain()
    except Exception as e:
        print(f"连接或获取证书链失败:{e}")
        return

    print(f"收到证书的数量:{len(chain_der)}")
    print()
    # 转换并打印每张证书的主题
    for i, der in enumerate(chain_der):
        cert = load_der_x509_certificate(der)
        # 获取证书的 CN 等字段信息,简单展示
        subject = cert.subject.rfc4514_string()
        issuer = cert.issuer.rfc4514_string()
        # 打印一张证书的 PEM 但太长,只打印关键信息
        print(f"证书 {i+1}:")
        print(f"  颁发给(Subject): {subject}")
        print(f"  颁发者(Issuer): {issuer}")
        print()

    # 判断是否有中间证书
    if len(chain_der) <= 1:
        print("警告:只收到 1 张证书,大概率缺少中间证书链!")
        print("请检查 Nexus 服务端证书配置,确保证书链文件完整。")
    else:
        print("信息:证书链数量 >= 2,服务端下发的证书包含中间证书,看起来不错。")
        print("注意:如果客户端仍然报错,继续看下一步。")

    print()
    print("===== 进行默认信任验证 =====")
    default_context = ssl.create_default_context()
    try:
        with socket.create_connection((host, port), timeout=10) as sock:
            with default_context.wrap_socket(sock, server_hostname=host) as tls_sock:
                print("✔ 证书通过系统默认信任库验证,客户端可正常访问。")
    except ssl.SSLCertVerificationError as e:
        print("✘ 证书未通过验证,客户端会报错。")
        print("错误原因:" + str(e))
        print("解决办法:把签发新证书的根 CA 证书追加到客户端的信任库。")
    except Exception as e:
        print("✘ 验证过程中出现其他错误:" + str(e))
    print("===== 诊断结束 =====")

# 运行诊断
diagnose_ssl('nexus.example.com', 8443)

这个脚本已经够日常排查用了。你把它保存成 check_nexus_ssl.py,以后每次换证书,先跑一遍,心里就有底了。

六、这种问题的应用场景与解决办法的优缺点

“Nexus SSL 证书更换导致所有客户端报错”这个场景,主要出现在公司内部的私有仓库环境。比如你负责运维一个 Nexus 服务器,给团队提供 Maven 包、npm 包或者 Docker 镜像。证书过期后,你高高兴兴更新了证书,结果第二天,所有团队成员都在喊“拉不了依赖了”。这个场景下,你面对的客户端五花八门:有人用 IDEA 里的 Maven,有人用命令行 npm,还有人用 docker pull。它们的信任库各不相同,所以修复起来也需要分别处理。

本文给出的两个核心思路,也有各自的优缺点。

先说服务端补全证书链。优点是治本,只要服务端下发完整链,所有支持标准 SSL 验证的客户端理论上都能通过。缺点是,如果客户端不信任你的根 CA,那补全链也没用。所以它只解决“链不完整”的问题。

再说客户端更新信任库。优点是能解决根 CA 不信任的问题,尤其适合内网自建 CA 的场景。缺点是每个客户端类型都得单独改:改 Python 的 certifi,改 Java 的 cacerts,改 Linux 系统的 /etc/ssl/certs,改 Docker 的证书目录……这是一个“面”上的工作,一旦有新人入职,又得教他们配一遍。所以这种方案更适合小团队、有限设备的环境。

如果你同时做了服务端补全链和客户端信任库更新,那基本就能全端通吃。但也要注意,更新信任库本身有一定的安全风险。如果你随意把一个不受信任的根 CA 加进信任库,那你等于也信任了这个 CA 签发的所有证书。所以一定要确认这个 CA 是自己的、安全的,不要乱加别人的。

七、注意事项,别忽视这些小坑

除了证书链和信任库,还有几个细节特别容易让你白忙活一场。

第一,换证书之后,Nexus 服务端一定要重启。很多新手改了配置以为立刻生效,实际上 Jetty 并没有热加载证书。不重启的话,你检查客户端永远还是老证书。

第二,别忘了检查服务器时间。SSL 证书有有效期,你的 Nexus 服务器如果系统时间不对,比如慢了几天,那么新证书会被判定为“尚未生效”,客户端也会报错。所以换证书前,顺手 date 看一下服务器时间。

第三,客户端访问时用的主机名必须和证书上的 CN 或者 SAN 匹配。如果你新证书只签给 nexus.example.com,但客户端用 IP 192.168.1.10 去访问,那么即使证书链完整、信任库没问题,一样会报“Hostname mismatch”的错。这种情况需要把证书里的 SAN 域名和实际访问域名对应起来。

第四,如果你把根 CA 追加到了 certifi 的 cacert.pem 里,下次升级 Python 或 certifi 包,这个文件可能会被覆盖,导致你追加的根证书丢失。不要慌,再执行一次追加脚本就好。或者,更优雅的做法是用环境变量 SSL_CERT_FILE 指向一个自定义的包含完整信任链的 PEM 文件,这样就不会被升级覆盖了。只不过,这招只对 Python 相关程序有效,对于 Java 程序还是得改 cacerts。

第五,Nexus 自己也有一个 JVM 的信任存储。如果你在 Nexus 上配置了使用外部 LDAP 或者代理仓库走 HTTPS,那么 Nexus 本身作为客户端去连接别的服务时,也需要信任对应的证书。别只盯着自己的客户端报错,Nexus 自己连不上远程源也是同一个道理。

八、文章总结

换 Nexus 的 SSL 证书引发“所有客户端报错”这件事,听起来吓人,其实拆开来就两件事:服务端证书链给全了没有,客户端信任库里有没有对应的根 CA。你用本文提供的 Python 诊断脚本跑一遍,就能快速定位是哪一层出了问题。接下来,服务端就把证书链拼接完整,客户端就把根 CA 加进各自的信任库。搞定这两个动作,绝大部分报错都能消失。最后记住,任何一次证书更换都不只是敲几个命令那么简单,前后要把服务端、客户端、时间、域名都检查一遍,稳扎稳打,才能让整个环境继续顺畅运转。