quarkusio/quarkus · error · IllegalArgumentException
The private key cannot be null
Error message
The private key cannot be null
What it means
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem persists the ACME-issued private key and certificate chain as PEM files. As a fail-fast precondition it throws IllegalArgumentException when the PrivateKey argument is null. This almost always means the upstream ACME result (X509CertificateChainAndSigningKey.getSigningKey()) produced no signing key, i.e. certificate issuance did not actually complete.
Source
Thrown at extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java:43
import org.wildfly.security.x500.cert.acme.AcmeAccount;
import org.wildfly.security.x500.cert.acme.AcmeException;
import io.smallrye.certs.CertificateUtils;
import io.vertx.core.json.JsonObject;
public class LetsEncryptHelpers {
public static final String DEFAULT_ACME_URL = "https://acme-v02.api.letsencrypt.org/directory";
public static final String DEFAULT_ACME_STAGING_URL = "https://acme-staging-v02.api.letsencrypt.org/directory";
public static final String TLS_AUDIT_LOG = "io.quarkus.tls.audit";
static Logger LOGGER = Logger.getLogger(LetsEncryptHelpers.class);
public static Logger AUDIT = Logger.getLogger(LetsEncryptHelpers.TLS_AUDIT_LOG);
public static void writePrivateKeyAndCertificateChainsAsPem(PrivateKey pk, X509Certificate[] chain, File privateKeyFile,
File certificateChainFile) throws Exception {
if (pk == null) {
throw new IllegalArgumentException("The private key cannot be null");
}
if (chain == null || chain.length == 0) {
throw new IllegalArgumentException("The certificate chain cannot be null or empty");
}
AUDIT.debug("Writing private key to file: " + privateKeyFile.getAbsolutePath());
CertificateUtils.writePrivateKeyToPem(pk, null, privateKeyFile);
AUDIT.debug("Writing certificate chain to file: " + certificateChainFile.getAbsolutePath());
if (chain.length == 1) {
CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile);
return;
}
// For some reason the method from CertificateUtils distinguishes the first certificate and the rest of the chain
X509Certificate[] restOfTheChain = new X509Certificate[chain.length - 1];
System.arraycopy(chain, 1, restOfTheChain, 0, chain.length - 1);View on GitHub (pinned to e1c734241f)
Solutions
- Null-check the key before calling: if certChainAndPrivateKey.getSigningKey() == null, abort — the certificate issuance failed and must be retried.
- Re-run the certificate issuance (lets-encrypt issue/renew) so a complete chain+key is obtained from the ACME server.
- Verify the ACME server actually issued a key; with custom servers, test against the Let's Encrypt staging URL first.
- Check the account.json in the lets-encrypt directory contains a private-key entry if account-key reuse is involved.
Example fix
// before
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(
certChainAndPrivateKey.getSigningKey(),
certChainAndPrivateKey.getCertificateChain(), keyPem, chainPem);
// after
PrivateKey key = certChainAndPrivateKey.getSigningKey();
if (key == null || certChainAndPrivateKey.getCertificateChain() == null
|| certChainAndPrivateKey.getCertificateChain().length == 0) {
throw new IllegalStateException("ACME order did not return a key/certificate; retry issuance");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key,
certChainAndPrivateKey.getCertificateChain(), keyPem, chainPem); Defensive patterns
Strategy: validation
Validate before calling
PrivateKey key = certChainAndPrivateKey.getSigningKey();
if (key == null) {
throw new IllegalStateException("ACME returned no signing key; issuance failed");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key,
certChainAndPrivateKey.getCertificateChain(), keyPem, chainPem); Type guard
static boolean hasUsableSigningKey(X509CertificateChainAndSigningKey r) {
return r != null && r.getSigningKey() != null
&& r.getCertificateChain() != null && r.getCertificateChain().length > 0;
} Try / catch
try {
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(pk, chain, keyPem, chainPem);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("private key cannot be null")) {
// issuance incomplete: log and trigger a fresh obtainCertificateChain
} else {
throw e;
}
} Prevention
- Always guard X509CertificateChainAndSigningKey results from obtainCertificateChain before writing files.
- Re-run issuance rather than persisting partial results.
- Verify account.json contains the private-key field when reusing accounts.
- Test custom ACME integrations against Let's Encrypt staging first.
When it happens
Trigger: Calling writePrivateKeyAndCertificateChainsAsPem(null, chain, keyFile, certFile) directly, or via issueCertificate when obtainCertificateChain returns an X509CertificateChainAndSigningKey whose getSigningKey() is null (failed/partial ACME order, empty renewal result).
Common situations: Tooling/tests invoking the helper with an unset key; a custom or broken ACME server returning a chain without a corresponding signing key; calling issueCertificate on an order that silently failed; deserializing a stored account/key pair that is missing the private-key field.
Related errors
- The certificate chain cannot be null or empty
- Failed to clear challenge content in the Quarkus management
- Failed to notify the Quarkus management endpoint
- Can only create a production application when using NORMAL l
- Extension passed an invalid shutdown handler
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/9d073610b89ef33f.
Report an issue: GitHub.