Fixing acme.sh SSL Certificate Stuck on ZeroSSL Processing by Switching to Let’s Encrypt

Limited Time Offer!

For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!

Enroll Now

While renewing or generating an SSL certificate for one of my subdomains, I ran into an issue where acme.sh completed the DNS verification successfully but then remained stuck at the certificate signing stage.

The DNS record was being created correctly, the ACME challenge was validating, and the certificate authority accepted the domain verification. However, the order stayed in the processing state for several minutes.

This tutorial covers the complete troubleshooting process, including the approaches I checked before finally solving the issue by switching acme.sh from ZeroSSL to Let’s Encrypt.


The Setup

The server was running:

  • Ubuntu on AWS
  • XAMPP/Apache under /opt/lampp
  • acme.sh for SSL certificate management
  • GoDaddy DNS
  • A subdomain hosted through an Apache VirtualHost

For security reasons, I will use placeholders throughout this tutorial.

Replace:

example.yourdomain.com

with your actual domain.

And replace:

/opt/lampp/htdocs/your-project

with the actual DocumentRoot of your website.


1. The Original acme.sh Command

I initially generated the certificate using the GoDaddy DNS integration:

cd ~/.acme.sh

./acme.sh --issue \
  --dns dns_gd \
  -d example.yourdomain.com \
  --force

The process started normally.

acme.sh created a DNS TXT record similar to:

_acme-challenge.example.yourdomain.com

Then it waited for DNS propagation.

After that, the validation completed successfully.

The log looked roughly like this:

Adding TXT value...
Added TXT record...
Checking example.yourdomain.com...
Success for domain...
All checks succeeded
Verifying: example.yourdomain.com
Success

At this point, the DNS configuration was clearly working.

That was an important finding because it meant:

  • GoDaddy DNS API access was working.
  • The TXT record was being created.
  • DNS propagation was working.
  • The domain ownership challenge was passing.

So there was no reason to keep debugging DNS.


2. Where the Process Actually Got Stuck

After DNS verification, acme.sh moved to certificate signing.

The output started showing messages like:

Verification finished, beginning signing.
Let's finalize the order.
Order status is 'processing'
Sleeping for 15 seconds then retrying

Then the same messages continued:

Polling order status...
Order status is 'processing'
Sleeping for 15 seconds then retrying

Again and again.

This is the point where it became clear that the problem was not the DNS challenge.

The certificate authority being used was ZeroSSL.

You can usually identify this near the beginning of the output:

Using CA: https://acme.zerossl.com/...

The ACME authorization had already succeeded, but the CA was taking too long or failing to finalize the certificate order.


3. Checking the Website DocumentRoot

Before changing certificate authorities, I also checked the application’s DocumentRoot.

For example:

/opt/lampp/htdocs/your-project

You can verify your Apache configuration with:

grep -Rni "example.yourdomain.com" /opt/lampp/etc/

You may see something similar to:

<VirtualHost *:80>
    ServerName example.yourdomain.com
    DocumentRoot "/opt/lampp/htdocs/your-project"
</VirtualHost>

Knowing the correct DocumentRoot is useful if you later decide to use HTTP/webroot validation.


4. Testing the ACME Challenge Directory Manually

I created a simple test file inside the standard ACME challenge directory:

mkdir -p /opt/lampp/htdocs/your-project/.well-known/acme-challenge

Then:

echo "certbot-test" > \
/opt/lampp/htdocs/your-project/.well-known/acme-challenge/test.txt

And tested it:

curl http://example.yourdomain.com/.well-known/acme-challenge/test.txt

Instead of getting the text directly, Apache returned:

Moved Permanently

and redirected the request to HTTPS.

This happened because the HTTP VirtualHost was configured to redirect all HTTP traffic to HTTPS.


5. Understanding the HTTP to HTTPS Redirect

A configuration like this redirects everything:

Redirect permanent / https://example.yourdomain.com/

That includes:

/.well-known/acme-challenge/

For normal websites that is usually fine, but when using Certbot’s HTTP-01/webroot method it is safer to let the ACME challenge directory remain accessible through HTTP.

A better Apache configuration would be:

<VirtualHost *:80>

    ServerName example.yourdomain.com

    DocumentRoot "/opt/lampp/htdocs/your-project"

    <Directory "/opt/lampp/htdocs/your-project">
        AllowOverride All
        Require all granted
    </Directory>

    RewriteEngine On

    RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
    RewriteRule ^ https://example.yourdomain.com%{REQUEST_URI} [R=301,L]

</VirtualHost>

This means:

Normal HTTP request
        |
        v
Redirect to HTTPS

but:

/.well-known/acme-challenge/*
        |
        v
Remain accessible through HTTP

After changing Apache configuration, always check the syntax first:

/opt/lampp/bin/apachectl -t

Expected:

Syntax OK

Only then restart Apache.

For XAMPP:

/opt/lampp/lampp stopapache
/opt/lampp/lampp startapache

6. I Considered Using Certbot

Since ZeroSSL was stuck, another possible solution was Certbot with Let’s Encrypt.

The command would have been:

certbot certonly --webroot \
  -w /opt/lampp/htdocs/your-project \
  -d example.yourdomain.com

However, Certbot was not installed on the server.

Running:

certbot --version

returned:

Command 'certbot' not found

So I tried:

apt update
apt install certbot -y

But the installation failed.


7. APT Had Broken Dependencies

The system returned an error similar to:

You might want to run 'apt --fix-broken install'

There were several AWS Linux kernel packages that had been unpacked but not completely configured.

Running:

dpkg --audit

showed pending packages related to:

linux-aws
linux-headers-aws
linux-image-aws
linux-tools

This meant that installing Certbot through APT would require first repairing the server’s package state.

On a production server, I did not want to change kernel packages just to generate an SSL certificate.

So instead of fixing APT unnecessarily, I returned to acme.sh.

That turned out to be the simplest solution.


8. The Better Solution: Keep acme.sh and Change the CA

The useful thing about acme.sh is that it isn’t tied to only one certificate authority.

Since:

  • acme.sh was already installed,
  • the GoDaddy DNS integration was working,
  • DNS validation had already succeeded,

there was no real need to install Certbot.

The only problematic part was ZeroSSL.

So I switched the default certificate authority to Let’s Encrypt.

Run:

cd ~/.acme.sh

Then:

./acme.sh --set-default-ca --server letsencrypt

Now issue the certificate again:

./acme.sh --issue \
  --dns dns_gd \
  -d example.yourdomain.com \
  --server letsencrypt \
  --force

This explicitly tells acme.sh:

Do not use ZeroSSL.
Use Let's Encrypt.

9. Confirm That Let’s Encrypt Is Being Used

Pay attention to the first few lines after running the command.

Previously it showed something similar to:

Using CA: https://acme.zerossl.com/...

After switching, it should show the Let’s Encrypt ACME endpoint instead.

That confirms the request is no longer going through ZeroSSL.


10. What Happens During DNS Validation

With:

--dns dns_gd

acme.sh automatically performs the required DNS challenge.

The process is approximately:

acme.sh
   |
   v
Requests certificate from Let's Encrypt
   |
   v
Receives DNS challenge
   |
   v
Creates TXT record using GoDaddy DNS API
   |
   v
_acme-challenge.example.yourdomain.com
   |
   v
Waits for DNS propagation
   |
   v
Let's Encrypt verifies TXT record
   |
   v
Domain ownership verified
   |
   v
Certificate signed

Because the DNS API integration was already working, there was no need to manually create TXT records.


11. Certificate Files

After successful issuance, acme.sh normally stores the certificate inside its own directory.

For an ECC certificate, this may look similar to:

/root/.acme.sh/example.yourdomain.com_ecc/

Check it:

ls -lah ~/.acme.sh/example.yourdomain.com_ecc/

Typical files include:

example.yourdomain.com.cer
example.yourdomain.com.key
ca.cer
fullchain.cer

The important files for Apache are generally:

fullchain.cer
example.yourdomain.com.key

12. Do Not Point Production Apache Directly at Temporary Files

Although technically Apache can read certificates directly from the .acme.sh directory, I prefer installing them into a dedicated XAMPP certificate directory.

For example:

mkdir -p /opt/lampp/etc/certs/example.yourdomain.com

Then use acme.sh --install-cert.

Example:

~/.acme.sh/acme.sh --install-cert \
  -d example.yourdomain.com \
  --ecc \
  --key-file /opt/lampp/etc/certs/example.yourdomain.com/privkey.pem \
  --fullchain-file /opt/lampp/etc/certs/example.yourdomain.com/fullchain.pem

This gives Apache stable certificate paths.


13. Configure Apache SSL

Your SSL VirtualHost can then use:

<VirtualHost *:443>

    ServerName example.yourdomain.com

    DocumentRoot "/opt/lampp/htdocs/your-project"

    SSLEngine on

    SSLCertificateFile "/opt/lampp/etc/certs/example.yourdomain.com/fullchain.pem"

    SSLCertificateKeyFile "/opt/lampp/etc/certs/example.yourdomain.com/privkey.pem"

    <Directory "/opt/lampp/htdocs/your-project">
        AllowOverride All
        Require all granted
    </Directory>

</VirtualHost>

Do not restart Apache immediately.

Always validate first:

/opt/lampp/bin/apachectl -t

If the result is:

Syntax OK

restart Apache:

/opt/lampp/lampp stopapache
/opt/lampp/lampp startapache

14. Verify the Certificate From the Server

First test the website:

curl -I https://example.yourdomain.com

Then inspect the certificate actually being served:

echo | openssl s_client \
  -connect example.yourdomain.com:443 \
  -servername example.yourdomain.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

This is better than simply checking the certificate file on disk.

Why?

Because the certificate file may be correct while Apache could still be using an old certificate from another path.

The openssl s_client command checks what a real visitor actually receives.


15. Check the Certificate Stored on Disk

You can also inspect the local certificate:

openssl x509 \
  -in /opt/lampp/etc/certs/example.yourdomain.com/fullchain.pem \
  -noout \
  -subject \
  -issuer \
  -dates

You should verify:

subject
issuer
notBefore
notAfter

Make sure the domain and expiry dates are correct.


16. The Important Lesson From This Issue

The most important part of troubleshooting SSL problems is identifying exactly which stage is failing.

In this case, the process looked like:

DNS API                WORKING
        ↓
TXT record creation    WORKING
        ↓
DNS propagation        WORKING
        ↓
ACME validation        WORKING
        ↓
CA order finalization  STUCK

Because validation was successful, repeatedly changing DNS settings would not have solved anything.

The issue was further down the certificate issuance process.

Switching from:

acme.sh → ZeroSSL

to:

acme.sh → Let's Encrypt

allowed me to keep the configuration that was already working instead of introducing Certbot, changing Apache unnecessarily, or repairing unrelated operating-system packages.


17. Commands Used in the Final Solution

If acme.sh and the DNS provider integration are already configured, the main fix is very small.

Set Let’s Encrypt as the default CA:

cd ~/.acme.sh

./acme.sh --set-default-ca --server letsencrypt

Generate the certificate:

./acme.sh --issue \
  --dns dns_gd \
  -d example.yourdomain.com \
  --server letsencrypt \
  --force

Install the certificate:

mkdir -p /opt/lampp/etc/certs/example.yourdomain.com
./acme.sh --install-cert \
  -d example.yourdomain.com \
  --ecc \
  --key-file /opt/lampp/etc/certs/example.yourdomain.com/privkey.pem \
  --fullchain-file /opt/lampp/etc/certs/example.yourdomain.com/fullchain.pem

Test Apache:

/opt/lampp/bin/apachectl -t

Restart:

/opt/lampp/lampp stopapache
/opt/lampp/lampp startapache

Finally verify:

echo | openssl s_client \
  -connect example.yourdomain.com:443 \
  -servername example.yourdomain.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

18. A Note About Credentials

When sharing logs or writing documentation about ACME/DNS certificate generation, never publish:

  • DNS API keys
  • DNS API secrets
  • AWS credentials
  • server passwords
  • SSH private keys
  • ACME account private keys
  • database credentials
  • SMTP passwords
  • access tokens
  • .env file contents
  • private certificate keys

A command such as:

--dns dns_gd

is safe to document because it only identifies the DNS provider integration.

However, environment variables containing the actual API key and secret should never be included in a public tutorial.


Conclusion

What initially looked like a DNS or SSL configuration problem turned out to be much simpler.

The DNS challenge was successful. Apache was running. The domain was reachable. The problem appeared after domain verification, while ZeroSSL was finalizing the certificate order.

I briefly explored Certbot, but installing another ACME client would have required repairing unrelated package dependencies on the server.

The cleaner solution was to keep the existing acme.sh setup and simply change the certificate authority from ZeroSSL to Let’s Encrypt.

In situations like this, avoid changing several things at once.

Follow the certificate process step by step:

DNS resolution
→ TXT challenge
→ ACME validation
→ certificate signing
→ certificate installation
→ Apache configuration
→ live SSL verification

Once you know which stage is actually failing, the solution is usually much easier to find.

Senior Software Development Engineer at Cotocus

Related Posts

Free SSL Certificate Generation Tutorial for Any Website Using Certbot and Apache

Limited Time Offer! For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly. Master DevOps, SRE, DevSecOps Skills! Enroll Now This…

Read More
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments