On this page
End-to-End Guide: Automating SSL Certificate Issuance and Renewal in WHMCS with Let’s Encrypt and ACME.sh
If you’re a hosting provider managing dozens—or hundreds—of client domains, manually handling SSL certificates is a time sink and a ticking time bomb. Certificates expire. Clients forget. Renewals slip through cracks. And when a client’s site goes down because of a missing https://, you are the one getting the 2 a.m. call.
The good news? You don’t have to do it manually.
In this guide, I’ll walk you through how to automate Let’s Encrypt SSL certificate issuance and renewal inside WHMCS, using ACME.sh—a lightweight, dependency-free ACME protocol client—paired with WHMCS automation hooks and cron jobs.
No fluff. No theory. Just what works in production, what broke for me, and how I fixed it.
Why Automate SSL in WHMCS?
WHMCS has built-in support for selling SSL certificates—especially through partners like cPanel AutoSSL, Comodo, DigiCert, etc. But those are commercial. Let’s Encrypt is free, trusted, and machine-readable (via ACME). That makes it ideal for automation.
But WHMCS doesn’t natively issue Let’s Encrypt certs via ACME. So we fill the gap.
We use:
- ACME.sh — the ACME client (shell-based, no deps, supports DNS/API challenges)
- WHMCS Automation Hooks — to trigger issuance/renewal on domain provisioning, upgrade, or renewal
- Cron + CLI — to batch-check expiries and re-issue if needed
The result? Clients get HTTPS on new domains within minutes of provisioning, and renewals happen silently—no tickets, no panic.
Prerequisites
Before you begin:
- WHMCS ≥ 8.0 (tested on 8.5 and 8.6)
- Root SSH access to the WHMCS server (or at least the WHM server if using cPanel)
- Let’s Encrypt DNS/API challenge credentials (e.g., Cloudflare API token, Route53 keys, or cPanel API access)
- Basic shell scripting familiarity
⚠️ Important: Never run ACME.sh as
rootunless absolutely necessary. Create a dedicatedacmeuser with sudo privileges.
Step 1: Install ACME.sh on the WHMCS Server
We’ll install ACME.sh in /opt/acme.sh and use a non-root service user.
# Create service user
useradd -r -s /bin/false acme
# Install ACME.sh as the acme user
sudo -u acme mkdir -p /home/acme/.acme.sh
sudo -u acme git clone https://github.com/acmesh-project/acme.sh.git /opt/acme.sh
# Install to user's home (sets up aliases)
sudo -u acme /opt/acme.sh/acme.sh --install \
-m /opt/acme.sh \
-h /home/acme/.acme.sh/acme.sh \
-d /home/acme/.acme.sh \
--home /home/acme/.acme.sh
✅ Pro Tip: Use
--hometo avoid polluting/root/.acme.sh. Keep it clean.
Step 2: Configure ACME.sh Defaults for WHMCS
ACME.sh supports multiple DNS/API challenges. Let’s pick the most common one: Cloudflare.
Set global defaults (as acme user):
sudo -u acme /opt/acme.sh/acme.sh --set-default-ca --server letsencrypt
sudo -u acme /opt/acme.sh/acme.sh --set-default-flags --server-letsencrypt
sudo -u acme /opt/acme.sh/acme.sh --set-default-flags --no-deps
Now store Cloudflare credentials securely:
export CF_Key="your_cloudflare_api_key"
export CF_Email="[email protected]"
🔒 Security note: Store these in
/home/acme/.acme.sh/cloudflare.env(mode600) andsourceit in scripts—not in.bashrc.
Step 3: Build a WHMCS Automation Hook
WHMCS hooks let us run custom PHP code on events like ProductSetup, DomainRegister, or InvoicePaid. We’ll use ProductSetup to trigger SSL issuance when a client purchases a hosting plan and selects an SSL product.
Create: modules/hooks/acme_ssl_automation.php
<?php
use WHMCS\Database\Capsule;
add_hook('ProductSetup', 1, function($vars) {
$pid = $vars['service']['productid'];
$domain = $vars['domain'];
$serviceid = $vars['service']['id'];
// Only trigger for hosting plans that opted for SSL
$service = Capsule::table('tblhosting')
->where('id', $serviceid)
->where('domainstatus', 'Active')
->first();
if (!$service) return;
$sslOption = Capsule::table('tblproductconfiglinks')
->where('pid', $pid)
->where('optionid', $service['configoption1'])
->first();
// Assuming config option ID 1 = "Free Let's Encrypt SSL"
if ($service['configoption1'] != 1) return;
// Trigger ACME.sh issuance
exec("sudo -u acme /opt/acme.sh/acme.sh --issue -d {$domain} --dns dns_cf --home /home/acme/.acme.sh --log /var/log/acme_{$domain}.log 2>&1", $output, $return);
if ($return === 0) {
// Mark SSL as installed in WHMCS
Capsule::table('tblhosting')
->where('id', $serviceid)
->update(['sslinstalled' => 1, 'sslstatus' => 'Valid']);
} else {
logActivity("ACME SSL issuance failed for domain {$domain}: " . implode("\n", $output));
}
});
⚠️ Security reminder:
sudomust be configured to allow theacmeuser to run/opt/acme.sh/acme.shwithout password. Edit/etc/sudoers.d/acme:
acme ALL=(ALL) NOPASSWD: /opt/acme.sh/acme.sh
Step 4: Handle Domain Renewal & Automatic Renewal
Domains auto-renew in WHMCS. But Let’s Encrypt certs only last 90 days—so we need to trigger renewal before expiry.
We use a cron job that runs daily:
Create /usr/local/bin/acme_renewal_check.sh:
#!/bin/bash
set -euo pipefail
# Load Cloudflare credentials
source /home/acme/.acme.sh/cloudflare.env
# Get all domains with SSL enabled in WHMCS
domains=$(mysql -u root -p'YOUR_DB_PASS' -N -e "SELECT domain FROM tblhosting WHERE sslinstalled = 1;")
for domain in $domains; do
# Check expiry (days remaining)
expiry=$(sudo -u acme /opt/acme.sh/acme.sh --list -d "$domain" 2>/dev/null | awk '/Expire/ {print $3}')
if [[ -z "$expiry" ]]; then
logger -t acme-renew "No cert found for $domain"
continue
fi
# Convert expiry date to days left
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [[ $days_left -lt 14 ]]; then
logger -t acme-renew "Renewing $domain ($days_left days left)"
sudo -u acme /opt/acme.sh/acme.sh --renew -d "$domain" --dns dns_cf --home /home/acme/.acme.sh --log /var/log/acme_renewal.log
fi
done
Then add to root’s crontab (sudo crontab -e):
0 2 * * * /usr/local/bin/acme_renewal_check.sh >/dev/null 2>&1
✅ Pro Tip: Use
loggerfor syslog integration—journalctl -t acme-renewhelps debug.
Step 5: Update WHMCS SSL Status Automatically
Let’s say you’ve issued a cert via ACME.sh, but WHMCS still shows “SSL Not Installed.” That breaks billing and client expectations.
We’ll add a hook that syncs real cert status to WHMCS every night.
Create: modules/hooks/sync_ssl_status.php
<?php
use WHMCS\Database\Capsule;
add_hook('DailyCronJob', 1, function() {
$domains = Capsule::table('tblhosting')
->where('sslinstalled', 1)
->where('domainstatus', 'Active')
->pluck('domain');
foreach ($domains as $domain) {
$cmd = "sudo -u acme /opt/acme.sh/acme.sh --list -d {$domain} 2>/dev/null";
exec($cmd, $output, $return);
if ($return !== 0) {
// Cert missing → mark as uninstalled
Capsule::table('tblhosting')
->where('domain', $domain)
->update(['sslinstalled' => 0, 'sslstatus' => 'Expired']);
continue;
}
// Parse output: "Expire: 2025-04-15"
$expire_line = array_filter($output, function($line) { return strpos($line, 'Expire') !== false; });
$expire_date = trim(explode(':', array_shift($expire_line))[1] ?? '');
if (!$expire_date) continue;
$days_left = (int)floor((strtotime($expire_date) - time()) / 86400);
$status = $days_left > 0 ? 'Valid' : 'Expired';
Capsule::table('tblhosting')
->where('domain', $domain)
->update(['sslstatus' => $status, 'sslinstalled' => $days_left > 0 ? 1 : 0]);
}
});
This ensures the WHMCS client portal always shows accurate SSL status—no more “SSL Expired” surprises.
Common Pitfalls & Fixes
❌ “ACME: Invalid domain” or “DNS error”
Most often, it’s a typo in the domain name or misconfigured DNS credentials. ACME.sh logs to /var/log/acme_*.log. Always check those first.
❌ “Permission denied” when running ACME.sh via PHP
PHP (Apache/Nginx) runs as www-data, but ACME.sh is owned by acme. Use sudo -u acme and configure /etc/sudoers.d/acme as above.
❌ WHMCS shows “SSL Installed” but site is still HTTP
You issued the cert, but didn’t configure the web server (Apache/Nginx) to use it. For cPanel hosts, use whmapi1 sslinstall after ACME.sh succeeds.
Add this to your ProductSetup hook:
if ($return === 0) {
$cert_path = "/home/acme/.acme.sh/{$domain}/{$domain}.crt";
$key_path = "/home/acme/.acme.sh/{$domain}/{$domain}.key";
$ca_path = "/home/acme/.acme.sh/{$domain}/ca.cer";
// For cPanel/WHM servers
exec("whmapi1 sslinstall domain={$domain} certificate=$(cat {$cert_path}) key=$(cat {$key_path}) cabundle=$(cat {$ca_path})");
}
🛠️ Test first: Run
whmapi1 sslinstall ...manually before automating.
Let’s Encrypt vs. Commercial SSL in WHMCS: Quick Comparison
| Feature | Let’s Encrypt (via ACME.sh) | Commercial (e.g., Comodo, DigiCert) |
|---|---|---|
| Cost | Free | $5–$100+ per cert |
| Automation | ✅ Full control (CLI, hooks) | ✅ (via WHMCS modules, but vendor-dependent) |
| Validation | DNS/API (fast), HTTP (slower) | DV (fast), OV/EV (manual, slow) |
| Coverage | Single domain + 100 SANs (with -d) |
Single domain or multi-domain |
| Renewal | Auto (90-day cycle) | Auto (1–2 years), but vendor alerts needed |
| Trust | ✅ All modern browsers | ✅ |
| Support | Community-driven | Paid support (if purchased) |
💡 Verdict: Let’s Encrypt + ACME.sh is ideal for most hosting providers. Use commercial certs only for EV or niche requirements.
FAQ: SSL Automation in WHMCS
Q1: Can I use ACME.sh with non-cPanel servers (e.g., Plesk, custom Nginx)?
Yes. ACME.sh handles the cert generation. You just need to install the cert on your web server after issuance. Use --install-cert in ACME.sh to point to your custom paths:
acme.sh --install-cert -d example.com \
--cert-file /etc/ssl/certs/example.crt \
--key-file /etc/ssl/private/example.key \
--fullchain-file /etc/ssl/certs/example-fullchain.crt \
--reloadcmd "systemctl reload nginx"
Q2: What if a domain fails DNS challenge due to propagation delay?
ACME.sh waits up to 60 seconds by default. Increase it:
export CF_Propagation_Timeout=180
Or switch to HTTP challenge if DNS is flaky (but requires port 80 open and no redirects).
Q3: Do I need a separate ACME account for each domain?
No. ACME.sh reuses the same account key unless you specify --accountkeyfile. One acme user + one account key is fine—even for 1,000 domains.
Q4: How do I handle wildcard SSL (*.example.com)?
ACME.sh supports DNS-01 challenges for wildcards. Use:
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
But note: WHMCS doesn’t natively support SAN/wildcard billing—so you’ll need custom product configuration.
Q5: Will this work in a multi-server environment (e.g., load-balanced web or separate DB)?
Yes—but store certs in a shared directory (e.g., NFS, S3 + sync), or run ACME.sh on each web node and sync certs via rsync or ansible. Don’t run ACME.sh from the DB server.
Final Thoughts
Automating SSL in WHMCS isn’t just about saving time—it’s about reliability. Clients trust your hosting when HTTPS just works, without manual tickets or downtime.
ACME.sh + WHMCS hooks is a lightweight, transparent, and controllable stack. You’re not locked into a vendor. You control the logs, the errors, and the rollback.
Start small: automate one domain. Then scale.
And if it breaks? You’ll know exactly where—because the logs are right there, in /var/log/acme_*.log, and the code is in your hooks.
If you’re building a hosting automation workflow, mahbuburriad.com has more deep dives on WHMCS internals, billing automation, and infrastructure-as-code.
Happy automating.