Limited Time Offer!
For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!
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.shfor 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.shwas 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
.envfile 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.