返回

Keycloak FIPS模式:解决SHA1withRSA证书验证失败 (Java 21)

java

解决 Keycloak FIPS 模式下 SHA1withRSA 证书链验证失败问题 (Bouncy Castle + Java 21)

一、 问题来了:连接数据库,证书验证报错

咱们在使用 Keycloak 26.1.0 版本,并且给它配上了 Bouncy Castle FIPS Provider,让它跑在 FIPS (美国联邦信息处理标准) 模式下,环境是 OpenJDK 21。现在遇到的情况是:Keycloak 需要通过 TLS 连接到 PostgreSQL 数据库 (版本 16.6-alpine3.20)。

数据库这边已经配置好了私钥和证书链 (由 Sectigo 签发,证书本身使用 sha256WithRSAEncryption 签名)。但是,启动 Keycloak 容器,尝试连接数据库时,咣当一下,报了个错:

Caused by: java.security.cert.CertPathValidatorException: Signature algorithm 'SHA1WITHRSA' not permitted with given parameters
    at org.bouncycastle.jsse.provider.ProvAlgorithmChecker.checkIssued(ProvAlgorithmChecker.java:262)
    at org.bouncycastle.jsse.provider.ProvAlgorithmChecker.checkChain(ProvAlgorithmChecker.java:205)
    at org.bouncycastle.jsse.provider.ImportX509TrustManager_5.checkAlgorithmConstraints(ImportX509TrustManager_5.java:106)
    ... 33 more

看这错误信息,Signature algorithm 'SHA1WITHRSA' not permitted,意思是 "SHA1WITHRSA 这个签名算法不让用"。仔细排查发现,虽然咱们自己的服务器证书签名算法没问题 (是 SHA256),但证书链往上追溯,有一张 Sectigo 的根证书或中间证书是老古董了(比如追溯到 2004 年那张),用了 SHA1withRSA 签名。正是这个老旧的签名算法,在严格的 FIPS 模式下被 Bouncy Castle 拦了下来。

尝试把那个老的 SHA1 证书从链里去掉?不行,那样 Keycloak 又不认整个证书链了。这事儿确实有点卡手。

这是出问题的 Sectigo 证书链部分信息 (已脱敏处理,展示结构):

-----BEGIN CERTIFICATE-----
[... intermediate cert using sha256WithRSAEncryption, issued by USERTrust RSA CA ...]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[... USERTrust RSA Certification Authority, issued by AAA Certificate Services ...]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[... AAA Certificate Services, self-signed or signed by an older root, potentially the one using SHA1withRSA ...]
-----END CERTIFICATE-----

可以看到,即使你的服务器证书和直接签发它的中间CA证书都使用了现代算法,但信任链最终会追溯到根CA或某个非常古老的中间CA。如果这个“老祖宗”用了 SHA1,FIPS模式就不答应。

二、 为啥会报错?刨根问底

这事儿得从 FIPS 模式和 SHA1 算法说起。

  1. FIPS 模式是啥?
    FIPS 是一套美国政府制定的安全标准。开启 FIPS 模式,意味着你的应用 (这里是 Keycloak 和它依赖的 Java 环境) 必须遵守更严格的安全规则,只能使用经过 FIPS 批准的加密算法和实现。目标就是提高安全性,禁用那些已知存在弱点或者不够健壮的旧技术。

  2. Bouncy Castle FIPS Provider
    Bouncy Castle 是一个流行的 Java 加密库。它提供了 FIPS 认证的版本 (bc-fips)。当你把它配置为 Java 环境的安全 Provider 并启用 FIPS 模式时,它就会像个严格的保安,检查所有加密操作是否符合 FIPS 规范。

  3. SHA1withRSA 的“原罪”
    SHA1 是一种哈希算法,RSA 是一种非对称加密算法。SHA1withRSA 就是用 SHA1 做哈希,然后用 RSA 私钥签名。问题在于,SHA1 算法本身已经被发现存在严重的碰撞漏洞 (Collision Vulnerabilities),意味着攻击者可能伪造出具有相同 SHA1 哈希值的不同内容。因此,它被认为是不安全的,尤其不能用于数字签名这种需要强抗碰撞性的场景。FIPS 标准早就把 SHA1 拉入了“黑名单”,不允许在签名验证这类操作中使用。

  4. 证书链验证机制
    当你建立 TLS 连接时,客户端 (Keycloak) 需要验证服务器 (PostgreSQL) 提供的证书链。这个验证过程不光是检查你服务器证书本身,而是要一路回溯,检查链上的每一个证书签名,直到一个受信任的根 CA 证书为止。Bouncy Castle FIPS Provider 在执行这个链式验证时,会对链上 每一个 证书的签名算法进行 FIPS 合规性检查。一旦发现链中有任何一个签名(哪怕是很久以前的老证书)使用了被 FIPS 禁止的算法,比如 SHA1withRSA,验证就会立刻失败,抛出 CertPathValidatorException

简单说,就是 FIPS 要求高,Bouncy Castle 执行严,SHA1 太老旧,证书链太长牵扯到了老前辈,最终导致 Keycloak 连接数据库失败。

三、 怎么解决?试试这几招

理想的解决方案当然是让证书提供商 (Sectigo) 更新他们的证书链,彻底移除 SHA1 签名的老旧证书。但这通常不是咱们能控制的,而且过程可能很漫长。

所以,我们得找些“曲线救国”的办法,在现有条件下让 Keycloak 能跑起来。主要思路是调整 Java 环境的安全策略,告诉它:“特殊情况,暂时放行 SHA1withRSA”。

方案一:修改 JVM 安全配置文件 java.security

这是最常见也是相对直接的方法。Java 虚拟机通过一个名为 java.security 的配置文件来管理全局的安全策略,包括哪些算法被禁用。

  • 原理和作用:
    这个文件里有个关键属性叫 jdk.certpath.disabledAlgorithms。它定义了一个禁用算法列表,用于证书路径验证 (CertPath Validation)。默认情况下,尤其是在 FIPS 模式下,或者较新的 JDK 版本中,SHA1 或者包含 SHA1 的更具体的规则(比如 SHA1 denyAfter 2019-01-01)通常会出现在这个列表里。我们的目标就是把与 SHA1 相关的限制从这个列表里移除,或者放宽。

  • 操作步骤:

    1. 定位 java.security 文件:
      这个文件通常位于你的 Java 安装目录下的 conf/security/ 文件夹里 (例如:/opt/java/openjdk/conf/security/java.security)。关键点: 由于 Keycloak 跑在 Docker 容器里,你需要修改的是 容器内 Java 环境的这个文件。

    2. 修改 jdk.certpath.disabledAlgorithms 属性:
      打开 java.security 文件,找到以 jdk.certpath.disabledAlgorithms= 开头的那一行。这一行可能很长,包含了很多被禁用的算法和约束。
      你需要仔细查看,找到所有限制 SHA1 用于签名的条目,并将它们移除或注释掉。
      例如,默认值可能类似(具体内容依 JDK 版本和是否启用 FIPS 而略有不同):

      jdk.certpath.disabledAlgorithms=MD2, MD5, SHA1 jdkCA & usage TLSServer, \
          RSA keySize < 2048, DSA keySize < 2048, EC keySize < 224, \
          SHA1 usage SignedJAR & denyAfter 2019-01-01
      

      你需要移除 SHA1 jdkCA & usage TLSServerSHA1 usage SignedJAR & denyAfter 2019-01-01 这些直接或间接禁用 SHA1 签名的部分。修改后可能看起来像这样(注意:这只是示例,你需要根据你的实际默认值进行修改,务必小心,只移除必要的限制 ):

      # Example: Removed SHA1 related restrictions for CertPath
      jdk.certpath.disabledAlgorithms=MD2, MD5, \
          RSA keySize < 2048, DSA keySize < 2048, EC keySize < 224
      

      重点: 具体要移除哪些关于 SHA1 的规则,取决于你的 java.security 文件的原始内容。目标是让证书路径验证不再因为链中存在 SHA1withRSA 签名而失败。最简单的可能是直接移除 SHA1 相关的,但要评估风险。

    3. 将修改应用到 Keycloak 容器:
      你有两种主要方式把修改后的 java.security 文件应用到 Keycloak 容器:

      • 构建自定义 Docker 镜像:
        创建一个 Dockerfile,基于官方的 Keycloak 镜像,然后把你修改后的 java.security 文件复制到容器内对应的 Java 路径。

        FROM quay.io/keycloak/keycloak:26.1.0 as builder
        
        # ... 其他构建步骤 (如果需要) ...
        
        # 准备你的修改后的 java.security 文件 (假设放在 Docker 构建上下文的 ./config/ 目录下)
        COPY ./config/java.security /opt/keycloak/conf/java/security/ # 路径可能需要根据实际Keycloak镜像中的Java位置调整
        
        # 如果Keycloak镜像里的Java是单独安装的,路径可能是 /opt/java/openjdk/conf/security/
        # 你需要先进入官方镜像 `docker run -it --entrypoint /bin/bash quay.io/keycloak/keycloak:26.1.0`
        # 然后用 `find / -name java.security` 找到准确路径
        
        # 确保文件权限正确
        RUN chmod 644 /opt/keycloak/conf/java/security/java.security # 示例路径
        
        # ... 设置 CMD 或 ENTRYPOINT 启动 Keycloak ...
        # 例如:
        # ENV KC_FEATURES=fips,...
        # ENV JAVA_OPTS_APPEND="-Djava.security.properties==/opt/keycloak/conf/java/security/java.security" # 或者确保Keycloak启动时加载了这个文件
        WORKDIR /opt/keycloak
        ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]
        

        然后构建并使用这个自定义镜像。

      • 使用 Docker Volume 挂载:
        将你修改后的 java.security 文件放在宿主机的一个目录,然后在启动 Keycloak 容器时,使用 -v--mount 参数将这个文件挂载到容器内对应的路径,覆盖掉原来的文件。

        docker run -d --name my-keycloak \
          -p 8443:8443 \
          -e KEYCLOAK_ADMIN=admin \
          -e KEYCLOAK_ADMIN_PASSWORD=admin \
          -e KC_FEATURES=fips,... \
          -v /path/on/host/java.security:/opt/java/openjdk/conf/security/java.security \ # 挂载修改后的文件,注意容器内路径要准确
          quay.io/keycloak/keycloak:26.1.0 \
          start --optimized
        

        这种方式更灵活,修改配置不需要重新构建镜像。

    4. 重启 Keycloak 容器: 应用修改后,重启 Keycloak 服务。

  • 安全建议:

    • 极其重要! 这个操作实质上是降低了你的 Java 环境的安全基线,因为它允许了一个已知不够安全的算法用于证书验证。
    • 这应该被视为一个临时性的缓解措施 ,或者是在进行了充分风险评估后,确认风险可接受的情况下才使用。
    • 最好的长期解决方案仍然是推动你的证书提供商更新其根证书和中间证书链,使用现代、安全的签名算法(如 SHA256 或更高)。
    • 定期审查这个配置。一旦证书链更新,或者有其他更安全的解决方案出现,应尽快恢复 java.security 文件的默认设置,重新禁用 SHA1。
    • 确保 Keycloak 与数据库之间的网络通信本身是安全的(例如,在受信任的网络段内,或者有其他网络层安全措施)。

方案二:尝试通过系统属性配置 Bouncy Castle JSSE (可能更复杂)

Bouncy Castle JSSE Provider 可能也接受一些系统属性来微调其行为,但这通常比直接修改 java.security 更复杂,并且不一定能覆盖 FIPS Provider 的强制性约束。

  • 原理和作用:
    通过设置特定的 Java 系统属性 (使用 -D 参数传递给 JVM),可能可以影响 Bouncy Castle 在 TLS 握手过程中的算法选择或验证策略。

  • 尝试方向 (需要查阅 Bouncy Castle 文档):
    查找 Bouncy Castle JSSE Provider 是否有特定的系统属性允许放宽对签名算法的 FIPS 检查。例如,类似 org.bouncycastle.jsse.provider.Property.XXXX 这样的属性。但这方面的公开文档可能不如 java.security 文件那么明确。

  • 如何应用:
    如果找到了适用的系统属性,可以通过 JAVA_OPTS_APPEND 环境变量将其传递给 Keycloak 的启动脚本。在 Docker 环境中,通常这样设置:

    docker run -d --name my-keycloak \
      ... (其他参数) ... \
      -e JAVA_OPTS_APPEND="-Dsome.bouncycastle.property=some_value" \
      quay.io/keycloak/keycloak:26.1.0 \
      start --optimized
    
  • 注意事项:

    • 这种方法成功率可能不高,因为 FIPS 模式的设计初衷就是强制执行严格规则,绕过它的选项通常很少或不被推荐。
    • java.security 中的 jdk.certpath.disabledAlgorithms 是 JCA/JCE 层面标准的控制机制,通常更可靠。

优先推荐方案一 (修改 java.security),因为它更直接地解决了 CertPath 验证阶段的问题,并且是 Java 标准的安全配置方式。但务必牢记其安全风险。

四、 进阶思考:Keycloak 配置的影响

虽然底层的 Java 安全策略是主因,但也值得检查一下 Keycloak 自身的配置(如 conf/keycloak.conf 或通过环境变量/CLI 参数设置的)中,是否有与 FIPS 或 TLS 相关的设置可能间接影响了这个问题。

  • 检查 Keycloak FIPS 相关配置: 确保 FIPS 特性 (features=fips) 是按预期启用的。检查 Keycloak 文档,看是否有 FIPS 相关的细粒度配置选项允许调整算法集。通常 Keycloak 在 FIPS 方面依赖底层 JRE 和 Provider 的配置。
  • 检查 Keycloak TLS/SPI 相关配置: Keycloak 允许通过 SPI (服务提供者接口) 定制很多功能,包括 Truststore 管理。虽然不太可能直接提供绕过算法检查的配置,但了解 Truststore 的配置方式 (例如,spi-truststore-file-...) 对排查问题总是有帮助的。

大部分情况下,对于这类底层的密码学算法策略问题,调整 JVM/Provider 层面(即 java.security)会是更根本的解决路径。

处理这类 FIPS 和老旧算法冲突的问题,总是在安全性和兼容性之间做权衡。选择放宽限制时,一定要清楚潜在的风险,并制定计划,争取早日回归到更安全的默认配置。