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

  1. Read the <reason> in the message and the logged AUDIT error for the underlying ACME failure
  2. Verify DNS for the domain resolves to this machine and the required ports (80/443) are reachable from the internet
  3. Check rate limits and account status at the ACME server (e.g. Let's Encrypt rate limits page)
  4. Test against the staging ACME URL first to avoid production rate limits, then switch to production
  5. 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

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

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/04f0b3e4d585c1af. Report an issue: GitHub.