quarkusio/quarkus · error · RuntimeException
Failed to obtain certificate: <reason>
Error message
Failed to obtain certificate: <reason>
What it means
issueCertificate calls acmeClient.obtainCertificateChain to request a certificate from the ACME server. If the ACME interaction raises an AcmeException (challenge failed, invalid domain, terms not agreed, server rejection), the exception is logged and rethrown as this RuntimeException carrying the ACME reason in the message. It means the certificate was never issued.
Source
Thrown at extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java:193
boolean staging,
String domain,
File certChainPemLoc,
File privateKeyPemLoc,
String acmeServerUrl,
String acmeStagingServerUrl) {
AcmeAccount acmeAccount = getAccount(letsEncryptPath, acmeServerUrl, acmeStagingServerUrl);
AUDIT.info("Requesting certificate - domain: " + domain + ", staging: " + staging + ", server: "
+ (acmeServerUrl != null ? acmeServerUrl : "default"));
X509CertificateChainAndSigningKey certChainAndPrivateKey;
try {
certChainAndPrivateKey = acmeClient.obtainCertificateChain(acmeAccount, staging, domain);
AUDIT.info("Certificate chain obtained successfully - domain: " + domain + ", chain-length: "
+ certChainAndPrivateKey.getCertificateChain().length);
} catch (AcmeException t) {
AUDIT.error("Failed to obtain certificate - domain: " + domain + ", staging: " + staging, t);
throw new RuntimeException("Failed to obtain certificate: " + t.getMessage(), t);
}
LOGGER.info("\uD83D\uDD35 Certificate and private key issued, converting them to PEM files");
AUDIT.info("Writing certificate to: " + certChainPemLoc.getAbsolutePath());
AUDIT.info("Writing private key to: " + privateKeyPemLoc.getAbsolutePath());
try {
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(certChainAndPrivateKey.getSigningKey(),
certChainAndPrivateKey.getCertificateChain(), privateKeyPemLoc, certChainPemLoc);
} catch (Exception ex) {
AUDIT.error("Failed to write certificate files - cert: " + certChainPemLoc + ", key: "
+ privateKeyPemLoc, ex);
throw new RuntimeException("Failure to copy certificate pem: " + ex.getMessage(), ex);
}
}
private static AcmeAccount getAccount(File letsEncryptPath, String acmeServerUrl, String acmeStagingServerUrl) {
LOGGER.debugf("Getting account from %s", letsEncryptPath);View on GitHub (pinned to e1c734241f)
Solutions
- Read the <reason> in the message and the logged AUDIT error for the underlying ACME failure
- Verify DNS for the domain resolves to this machine and the required ports (80/443) are reachable from the internet
- Check rate limits and account status at the ACME server (e.g. Let's Encrypt rate limits page)
- Test against the staging ACME URL first to avoid production rate limits, then switch to production
- Recreate the ACME account if the account is invalid or terms-of-service agreement changed
Example fix
// before acmeClient.obtainCertificateChain(acmeAccount, staging, "typo-domain.example.com"); // challenge fails // after: ensure DNS record exists and points to this host before issuing // dig +short mydomain.example.com -> should return this server's IP
Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight checks before issuing:
// DNS resolves to this host:
boolean dnsOk = !InetAddress.getAllByName(domain)[0].getHostAddress().isEmpty();
// Port 80 reachable (HTTP-01 challenge):
try (Socket s = new Socket()) {
s.connect(new InetSocketAddress(domain, 80), 5000); // throws if unreachable
} Try / catch
try {
LetsEncryptHelpers.issueCertificate(acmeClient, letsEncryptPath, staging, domain, ...);
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Failed to obtain certificate:")) {
AUDIT.error("ACME issuance failed for " + domain + ": " + e.getMessage() + "; verify DNS, ports 80/443, and rate limits before retry", e);
// retry only after fixing validation; consider staging endpoint first
} else {
throw e;
}
} Prevention
- Verify DNS A/AAAA records point to the host before requesting certificates
- Ensure ports 80/443 are open and publicly reachable for ACME challenges
- Use the Let's Encrypt staging endpoint while testing to avoid rate limits
- Schedule renewals well before expiry (e.g. 30 days) and back off on failures
- Recreate/refresh the ACME account if server rejects it
When it happens
Trigger: Calling issueCertificate or renewCertificate when the ACME server rejects or fails the order — domain validation challenge failure, DNS not pointing at the host, ACME account invalid, or the ACME server returning an error.
Common situations: Domain's DNS A/AAAA record not pointing to the machine running the challenge; port 80/443 blocked so the HTTP-01/TLS-ALPN challenge fails; Let's Encrypt rate limits hit; using the staging URL vs production mismatch; expired ACME account.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Rate limit exceeded: too many ACME challenge requests. Wait
- Missing certificate authority challenge
- Invalid certificate authority challenge
- Failed to respond to certificate authority challenge
- Failure to save the account
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/04f0b3e4d585c1af.
Report an issue: GitHub.