The Day My SSL Auto-Renewal Quietly Died

By Joseph Davis Chamdani · August 23, 2026

My VPS renewed its own certificates every night for months. Then one alert told me three sites were about to expire, and the answer I was confident about turned out to be wrong.

Three of my nine sites were 7, 11 and 20 days from expired certificates. I decided CloudPanel had deleted its own renewal command. It hadn't. The command is scoped to a user I wasn't running as, and the rest of my sites were fine the whole time. I still don't know why those three weren't, because the cron job was throwing its own errors away.

I run nine websites on one small VPS. Portfolio projects, class demos, a status page, my personal dashboard, an AI chatbot. Every one of them has an SSL certificate, and every certificate expires after 90 days. The whole point of the setup is that I never have to think about it. CloudPanel, the control panel I use, renews everything at 5:15 every morning while I'm asleep.

At least that's what I thought was happening.

The alert

One morning my cert monitor sent me this:

(class project one): 7 days
(class project two): 11 days
(another project): 20 days

Seven days. That number shouldn't exist on a server that's working. Renewal usually happens around 30 days before expiry, so anything under 30 means it's been failing quietly for weeks. Three of my nine sites were counting down to a browser error page and I had no idea.

Why seven days scared me

An expired certificate doesn't make a site worse. It takes it down. Browsers throw a full screen warning that says your connection is not private, with the word attackers in it. Almost nobody clicks past that. Traffic doesn't drop, it just stops.

The INFO 380 course registration site working normally, showing a course search page with filters and a list of classes
What people were supposed to see
Full screen browser security warning for info380.joechamdani.com reading Your connection is not private, with the error NET::ERR_CERT_DATE_INVALID
What they got instead, for two months

For a class project that's embarrassing and nothing more. But I also build sites for freelance clients, and on a small shop the owner doesn't see an error, they just see a quiet day, while every customer hits a screen that makes a real shop look fake. The only thing standing between that happening and not happening is whether something is checking.

Certificates used to last years. Let's Encrypt issues them for 90 days now, and the industry keeps pushing that shorter, so renewal isn't something you can put on a calendar anymore. It has to be automated, which means you have to trust the automation. If nothing is actively stopping you from forgetting, you'll forget.

What forgetting looks like

I know exactly what that looks like, because it already happened to me once.

Uptime history for a class project site, two months of downtime from an expired certificate
info380.joechamdani.com on my status page, 90 days of history

That's info380.joechamdani.com on my own status page, at 27 percent uptime over 90 days. The certificate expired in early June and the site sat behind a browser warning for two months before I noticed. The fix wasn't hard. I just forgot the site existed. No monitor on it, no alert, and no reason to open a class project I'd already finished. Anyone who clicked it from my portfolio during those two months got a full screen warning telling them to leave, and I had no idea.

That one I can explain. Renewal proves you own a domain by writing a token into the site's standard htdocs path, under .well-known/acme-challenge/ , then fetching it back over HTTP. I'd hand-edited that vhost so nginx served out of /var/www instead, where no token ever appeared. Every validation came back 404 and nothing wrote it down. I repointed the root, reissued, then standardized every other site, wrote a runbook, and built the cert monitor. I felt pretty good about myself.

So when the monitor went off a week later with three more sites counting down, the annoying part was that all three were configured correctly. Whatever I'd fixed wasn't what was broken.

Ruling out the obvious

Renewal works through an HTTP-01 challenge. Let's Encrypt drops a token file on your server, then fetches it back over the web to check you actually control the domain. If that round trip fails, you don't get a certificate.

So I tested it by hand on every site. Wrote a test file into each web root, requested it through the front door the way Let's Encrypt would, followed the redirect to HTTPS, got the file back. All nine passed. The plumbing was fine.

Which meant, I decided, that the renewals weren't failing. They weren't running at all.

The command exists. It belongs to clp.

The schedule lived in a cron file, which is just a list of commands the server runs on a timer:

15 5 * * * clp /usr/bin/bash -c "/usr/bin/clpctl lets-encrypt:renew:certificates" &> /dev/null

Every morning at 5:15, run the renew command. Looks fine. So I ran it myself to watch it work.

There are no commands defined in the "lets-encrypt:renew" namespace.

I took that at face value and decided CloudPanel had dropped the command in an update. That was wrong, and it took a review of this post, after I'd published it, to make me go back and check properly.

The command is still there. It's scoped to the clp user, and I ran it as root:

$ clpctl                    # as root
  lets-encrypt:install:certificate

$ sudo -u clp clpctl
  lets-encrypt:renew:certificates
  lets-encrypt:renew:custom-domain:certificate

Now look at the cron line again. The sixth field is clp. Cron was already running it as the one user that has the command, so it was calling the right thing the right way the whole time.

The renewals weren't dead either. My dashboard, status page, CDN and transfer tool all got fresh certificates around 5:15 in the days before the alert, which is that same cron doing its job. Whatever went wrong was specific to three sites. And I can't tell you what, because of the end of that line:

&> /dev/null

That throws away all output, errors included. Whatever those three were failing on, they failed on it every morning and told nobody. The error that would have explained this went into /dev/null every day for weeks, and the only reason I found out anything was wrong at all was a seven day threshold in a script I'd written the week before.

The fix

The immediate fix was easy. The command that issues a certificate is one root does have, and issuing one is the same as renewing it. Three commands and all three sites had fresh certificates good until November.

The real fix wasn't replacing CloudPanel's automation, because it isn't broken. It's accepting that it can fail on some sites without saying so, so it can't be the only thing between me and an expired certificate. A script of mine runs every morning, checks how many days each certificate has left, and reissues anything under 15:

#!/bin/bash
# cron: 30 5 * * * /root/ssl-cert-autorenew.sh 15 >> /var/log/ssl-cert-autorenew.log 2>&1
shopt -s nullglob   # an empty glob must not survive as a literal path

THRESHOLD=${1:-15}
certs=(/etc/nginx/ssl-certificates/*.crt)
[ ${#certs[@]} -gt 0 ] || { echo "NO CERTIFICATES FOUND"; exit 1; }

for c in "${certs[@]}"; do
  d=$(basename "$c" .crt)
  days=$(( ($(date -d "$(openssl x509 -enddate -noout -in "$c" | cut -d= -f2)" +%s) - $(date +%s)) / 86400 ))
  if [ "$days" -lt "$THRESHOLD" ]; then
    echo "$d: ${days}d left, reissuing"
    clpctl lets-encrypt:install:certificate --domainName="$d"
  fi
done

The redirect is on the cron line, not on the commands inside, so if the loop itself dies the error still lands in the log. That matters more than it sounds: if the certificate path ever moves, the glob matches nothing and a script without that guard sails through doing nothing, quietly, which is the exact failure I'm writing about. I also went back to CloudPanel's cron and took the &> /dev/null off those two lines, so the stock sweep writes to a log now too. One caveat if you want to copy any of this: clpctl is CloudPanel's own CLI, so the reissue line only works on a CloudPanel box. On a normal setup the same job is certbot renew or acme.sh, and the rest of the script is the same.

I left that cron running, so two things renew certificates on my box now. They rarely collide, since mine only acts inside 15 days, but Let's Encrypt caps you at five identical certificates per week. If you copy this and raise the threshold, or wrap it in a retry, that ceiling is where you'll land.

A second script runs 30 minutes later. If any certificate is still under 7 days it pushes an alert to my phone. And yes, I thought about what happens when my own script dies quietly too. That's why the second one knows nothing about how renewal works. It only reads expiry dates off the certificates, so it doesn't care what broke or why. For both to stay quiet, renewal actually has to be working.

The part that still bugs me

What gets me is how normal all of it felt. Certificates renewing and certificates quietly not renewing looked identical from where I was standing. Nothing happened either way, the sites stayed up, I went about my week. If I'd monitored whether the cron job ran, it would've been green every single day. It did run. It ran the right command, as the right user, and three certificates still sat there running out.

The other thing I got wrong is that I was sure I knew why. I ran one command, got an error that looked like an answer, and published it as the cause. The reason I could be that confident is the same reason the problem lasted: the thing that would have corrected me was going into /dev/null . Now anything on my server that matters either writes to a log I'll read or pushes to my phone.

I still don't know why those three didn't renew, and I can live with that because nothing in my setup depends on me knowing. The script reissues on the expiry date, not on a theory about what went wrong. Whatever the cause was, it would have been caught anyway.

My script checks every certificate every night and reissues anything inside 15 days, and there's a second one behind it whose only job is to tell me when the first one stops. That's probably paranoid for nine small websites. But the two months of red are still sitting on my status page and I'm leaving them there.