apache/pulsar · critical · PulsarClientException.AuthenticationException

Unable to read private key: ${message}

Error message

Unable to read private key: ${message}

What it means

authenticate() reads the private key file before requesting a token; any IOException from loadPrivateKey (unreadable location, bad URI, malformed wrapper) becomes PulsarClientException.AuthenticationException('Unable to read private key: <cause message>'). It signals the client never got far enough to attempt the token exchange.

Source

Thrown at pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/ClientCredentialsFlow.java:166

    }

    @Override
    public void initialize() throws PulsarClientException {
        super.initialize();
        assert this.metadata != null;

        URL tokenUrl = this.metadata.getTokenEndpoint();
        this.exchanger = new TokenClient(tokenUrl, getHttpClient());
        initialized = true;
    }

    public TokenResult authenticate() throws PulsarClientException {
        // read the private key from storage
        KeyFile keyFile;
        try {
            keyFile = loadPrivateKey(this.privateKey);
        } catch (IOException e) {
            throw new PulsarClientException.AuthenticationException("Unable to read private key: " + e.getMessage());
        }

        // request an access token using client credentials
        ClientCredentialsExchangeRequest req = ClientCredentialsExchangeRequest.builder()
                .clientId(keyFile.getClientId())
                .clientSecret(keyFile.getClientSecret())
                .audience(this.audience)
                .scope(this.scope)
                .authMethod(TokenEndpointAuthMethod.CLIENT_SECRET_POST)
                .build();
        TokenResult tr;
        if (!initialized) {
            initialize();
        }
        try {
            tr = this.exchanger.exchangeClientCredentials(req);
        } catch (TokenExchangeException | IOException e) {
            throw new PulsarClientException.AuthenticationException("Unable to obtain an access token: "

View on GitHub (pinned to 820761864e)

Solutions

  1. Verify the privateKey URL/path exists and is readable by the client process
  2. Validate the key file is well-formed JSON containing clientId/clientSecret
  3. Check file permissions or mount configuration for the secret

Example fix

// before
authParams.put("privateKey", "file:///etc/pulsar/auth/old-key.json"); // file deleted
// after
authParams.put("privateKey", "file:///etc/pulsar/auth/client_credentials.json"); // verified exists + readable
Defensive patterns

Strategy: validation

Validate before calling

String key = authParams.get("privateKey");
if (key != null && key.startsWith("file:")) {
    java.nio.file.Path p = java.nio.file.Paths.get(java.net.URI.create(key));
    if (!java.nio.file.Files.isReadable(p)) {
        throw new IllegalStateException("privateKey file not readable: " + p);
    }
    try (var in = java.nio.file.Files.newInputStream(p)) {
        new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).trim(); // ensure non-empty
    }
}

Try / catch

try {
    flow.initialize();
} catch (PulsarClientException.AuthenticationException e) {
    if (e.getMessage().startsWith("Unable to read private key")) {
        // inspect cause: fix path/permissions/JSON, then recreate the auth
        throw new ConfigException("Check privateKey path, permissions and JSON content: " + e.getMessage(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: privateKey file path does not exist or is not readable; data: URI payload is not valid JSON so KeyFile.fromJson throws and surfaces as an IO failure; permissions deny reading the secret file.

Common situations: Kubernetes secret mounted at a different path than configured; JSON key file corrupted or truncated; typo in privateKey parameter; missing IAM permissions on cloud storage backends.

Related errors


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