apache/pulsar · critical · KeyStoreException

Configured keystore 'keyStorePath' holds no usable key entry

Error message

Configured keystore 'keyStorePath' 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.

What it means

TlsMaterialSource.load extracts key entries from the configured keystore and validateKeyStoreIdentity checks that at least one entry is a private key with an X.509 certificate chain. If none is found, the keystore presents no TLS identity and KeyStoreException is thrown with remediation guidance naming the keystore path.

Source

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

            TlsMaterial.KeyEntry first = entries.get(0);
            return new TlsMaterial(first.privateKey(), first.chain(), trustCerts, entries);
        }
        validatePemIdentity();
        return new TlsMaterial(loadPemPrivateKey(), loadPemCertificateChain(), trustCerts);
    }

    /**
     * Reject a keystore that holds no usable key entry. v4 handed such a store to
     * {@code KeyManagerFactory.init}, which initialised fine but produced a key manager with no aliases — a
     * certain, undiagnosed handshake failure for a server and a silently identity-less client. Failing the
     * load is a deliberate tightening: the only deployments it can break already presented no identity.
     *
     * @param entries the key entries extracted from the configured keystore
     * @throws KeyStoreException if the keystore carries no usable key entry
     */
    private void validateKeyStoreIdentity(List<TlsMaterial.KeyEntry> entries) throws KeyStoreException {
        if (entries.isEmpty()) {
            throw new KeyStoreException("Configured keystore '" + policy.keyStorePath()
                    + "' 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) {

View on GitHub (pinned to 820761864e)

Solutions

  1. Verify keyStorePath points to a keystore containing a PrivateKeyEntry with certificate chain, not a truststore
  2. Inspect with keytool -list -v -keystore broker.keystore.jks and confirm a key entry exists
  3. Correct keyStorePassword; regenerate or re-import the key pair if the store is empty
  4. If only trust is intended, unset keyStorePath so it is not treated as identity material

Example fix

// before
TlsPolicy policy = TlsPolicy.builder().keyStorePath("/etc/pulsar/truststore.jks").build(); // trust-only
// after
TlsPolicy policy = TlsPolicy.builder()
    .keyStorePath("/etc/pulsar/broker.keystore.jks") // contains PrivateKeyEntry
    .keyStorePassword("********")
    .build();
Defensive patterns

Strategy: validation

Validate before calling

static boolean keystoreHasIdentity(String path, char[] password) throws Exception {
    KeyStore ks = KeyStore.getInstance("JKS");
    try (InputStream in = Files.newInputStream(Path.of(path))) { ks.load(in, password); }
    for (Enumeration<String> e = ks.aliases(); e.hasMoreElements(); ) {
        String a = e.nextElement();
        if (ks.isKeyEntry(a)) {
            java.security.cert.Certificate[] c = ks.getCertificateChain(a);
            if (c != null && c.length > 0 && ks.getKey(a, password) instanceof java.security.PrivateKey) return true;
        }
    }
    return false;
}

Try / catch

try {
    TlsMaterialSource.load(policy);
} catch (KeyStoreException e) {
    log.error("Keystore '{}' has no usable key entry: {}", policy.keyStorePath(), e.getMessage());
}

Prevention

When it happens

Trigger: Calling load on a TlsMaterialSource whose policy.keyStorePath() points to a keystore with no usable key entries — e.g. a trust-only keystore (trusted certs only), an empty/corrupt store, or a wrong password causing an unintended load.

Common situations: Pointing keyStorePath at the truststore instead of the identity keystore; keystore created without a key pair; wrong keyStorePassword loading an unintended store; alias selection filtering out all key entries.

Understand the failure class

Related errors


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