apache/pulsar · critical · IllegalArgumentException

TlsPolicy sets certificateFilePath='certificateFilePath' but

Error message

TlsPolicy sets certificateFilePath='certificateFilePath' but leaves keyFilePath unset; a certificate without its private key yields no usable TLS identity. Set keyFilePath, or unset certificateFilePath.

What it means

TlsMaterialSource.validatePemIdentity enforces PEM config consistency: certificateFilePath set without keyFilePath cannot yield a usable TLS identity (a certificate alone is public material), so IllegalArgumentException is thrown. The inverse (key without cert) is only logged as a warning elsewhere in the same method.

Source

Thrown at pulsar-common/src/main/java/org/apache/pulsar/common/tls/impl/TlsMaterialSource.java:198

                    + "' holds no usable key entry (a private key with an X.509 certificate chain); no TLS "
                    + "identity would be presented. Fix the keystore or its password, or unset keyStorePath.");
        }
    }

    /**
     * Reject a half-configured PEM identity that would be silently dropped. A certificate without its key
     * yields {@link TlsMaterial#hasKeyMaterial()} {@code == false}, so the identity is omitted from the built
     * context and the misconfiguration only surfaces as a handshake/authentication failure much later. The
     * check is deliberately <em>asymmetric</em>: a key without a certificate is what v4 silently tolerated, so
     * it stays a WARN rather than a new startup failure. Enforced here rather than in {@code TlsPolicy.Builder}
     * so custom {@code PulsarTlsFactory} implementations that build their own policies are not constrained by
     * this default factory's requirement.
     */
    private void validatePemIdentity() {
        boolean hasCert = StringUtils.isNotBlank(policy.certificateFilePath());
        boolean hasKey = StringUtils.isNotBlank(policy.keyFilePath());
        if (hasCert && !hasKey) {
            throw new IllegalArgumentException("TlsPolicy sets certificateFilePath='" + policy.certificateFilePath()
                    + "' but leaves keyFilePath unset; a certificate without its private key yields no usable TLS "
                    + "identity. Set keyFilePath, or unset certificateFilePath.");
        }
        if (hasKey && !hasCert) {
            log.warn().attr("keyFilePath", policy.keyFilePath())
                    .log("TlsPolicy sets keyFilePath but no certificateFilePath; no TLS identity will be presented");
        }
    }

    private List<X509Certificate> loadTrustCerts() throws Exception {
        if (StringUtils.isNotBlank(policy.trustStorePath())) {
            List<X509Certificate> trustCerts = TlsKeyStoreLoader.extractTrustCerts(
                    TlsKeyStoreLoader.loadKeyStore(policy.trustStoreType(), policy.trustStorePath(),
                            policy.trustStorePassword(), jcaProvider));
            if (trustCerts.isEmpty()) {
                // An empty trust list is indistinguishable from "no truststore configured" downstream, and both
                // context builders then install the platform default trust manager — silently trusting every
                // public CA. v4 initialised the TrustManagerFactory with the explicit store and rejected every peer.

View on GitHub (pinned to 820761864e)

Solutions

  1. Set keyFilePath to the private key matching certificateFilePath
  2. If only trust material was intended, unset certificateFilePath and configure trust material instead
  3. Check the config file so both certificate and key path properties are present
  4. After fixing paths, verify the PEM pair matches (cert and key belong together)

Example fix

// before
TlsPolicy policy = TlsPolicy.builder()
    .certificateFilePath("/etc/pulsar/broker-cert.pem")
    .build(); // keyFilePath missing
// after
TlsPolicy policy = TlsPolicy.builder()
    .certificateFilePath("/etc/pulsar/broker-cert.pem")
    .keyFilePath("/etc/pulsar/broker-key.pem")
    .build();
Defensive patterns

Strategy: validation

Validate before calling

static void checkPemPair(String certPath, String keyPath) {
    boolean hasCert = certPath != null && !certPath.isBlank();
    boolean hasKey = keyPath != null && !keyPath.isBlank();
    if (hasCert && !hasKey) throw new IllegalArgumentException("certificateFilePath set without keyFilePath");
    if (!hasCert && hasKey) log.warn("keyFilePath set without certificateFilePath");
}

Try / catch

try {
    TlsMaterialSource.load(policy);
} catch (IllegalArgumentException e) {
    log.error("PEM identity config invalid: {}", e.getMessage());
}

Prevention

When it happens

Trigger: Calling load on a TlsMaterialSource where policy.certificateFilePath() is non-blank but policy.keyFilePath() is blank or unset, detected before any material loading.

Common situations: Partial PEM config where only the certificate path property was set in broker.conf/client config; typo'd key-file property name; a config template that omitted keyFilePath; incorrectly split server/client TLS configs.

Understand the failure class

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/9b32e6030f76f9bc. Report an issue: GitHub.