Setting up Cockpit with Certbot and a private CA with an ACME endpoint

This is an excerpt from my step-ca post that I felt is useful enough to have its own post.

I have a server running Cockpit that I’d like to use a valid SSL certificate from my CA. I’d like it to renew its own certificate. Let’s do that.

First, be sure to install the root certificate on the system. Since this is a machine running AlmaLinux 10, that looks something like this:

sudo mv root-ca-a.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

Install Certbot:

sudo dnf install -y certbot

Get a certificate. This will stand up a webserver serving the challenge on port 80. If you’re using the machine as a webserver (and it’s listening on 80) you will probably need to use --webroot rather than --standalone for your HTTP challenge. Be sure to open port 80 in your system’s firewall.

sudo certbot certonly \
  --standalone \
  --preferred-challenges http \
  --email noc@wporter.org \
  --server https://intermediate-ca.lab.wporter.org/acme/acme/directory \
  --no-eff-email \
  -d 3060t0.lab.wporter.org \
  --agree-tos \
  --non-interactive

This will drop a certificate and key at /etc/letsencrypt/live/fqdn/fullchain.pem and /etc/letsencrypt/live/fqdn/privkey.pem. Symlink them to /etc/cockpit/ws-certs.d:

fqdn="3060t0.lab.wporter.org"

sudo ln -s /etc/letsencrypt/live/"$fqdn"/fullchain.pem /etc/cockpit/ws-certs.d/0-"$fqdn".crt
sudo chmod 0644 /etc/cockpit/ws-certs.d/0-"$fqdn".crt

sudo ln -s /etc/letsencrypt/live/"$fqdn"/privkey.pem /etc/cockpit/ws-certs.d/0-"$fqdn".key
sudo chmod 0600 /etc/cockpit/ws-certs.d/0-"$fqdn".key

Then, remove the preexisting self-signed CA, cert, and key:

sudo rm /etc/cockpit/ws-certs.d/0-self-signed*

Once you’ve restarted Cockpit:

sudo systemctl restart cockpit

Try to connect to the web UI. It should automagically pick up the new certificate, and now be trusted (unless you’re using Microsoft Edge.. Edge gives mine the stink-eye):

A screenshot of the certificate dialog in Google Chrome, showing a trusted internal certificate’s chain of signatures

The AlmaLinux 10 package for Certbot includes a service (/usr/lib/systemd/system/certbot-renew.service) and associated timer (/usr/lib/systemd/system/certbot-renew.timer).

The certbot-renew service has an EnvironmentFile (/etc/sysconfig/certbot) defined by default. This just contains a few variables that are called in the unit file as arguments to certbot in the unit file, so we don’t need to modify the unit file (and deal with package changes breaking it).

Instead, we’ll set our parameters in the environment file. We won’t reinvent the wheel - just edit the preexisting DEPLOY_HOOK variable in the environment file:

sudo sed -i "s|^DEPLOY_HOOK=\".*|DEPLOY_HOOK=\"--deploy-hook 'systemctl restart cockpit'\"|" /etc/sysconfig/certbot

This sed call replaces the line(s) in our EnvironmentFile beginning with DEPLOY_HOOK=" with DEPLOY_HOOK="--deploy-hook 'systemctl restart cockpit'".

I’m using the deploy hook rather than the post hook because there’s no reason to restart Cockpit if we haven’t successfully updated the certificate.

The package creates and enables a timer, but doesn’t start it:

[wporter@3060t0 ~]$ systemctl status certbot-renew.timer
○ certbot-renew.timer - This is the timer to set the schedule for automated renewals
     Loaded: loaded (/usr/lib/systemd/system/certbot-renew.timer; enabled; preset: enabled)
     Active: inactive (dead)
    Trigger: n/a
   Triggers: ● certbot-renew.service

If you want the systemd timer to run before you reboot, you’ll have to start it:

sudo systemctl start certbot-renew.timer

Once you’ve done this, you can examine the timer with a normal systemctl status command:

[wporter@3060t0 ~]$ systemctl status certbot-renew.timer
● certbot-renew.timer - This is the timer to set the schedule for automated renewals
     Loaded: loaded (/usr/lib/systemd/system/certbot-renew.timer; enabled; preset: enabled)
     Active: active (waiting) since Sun 2025-07-06 21:55:57 EDT; 1s ago
 Invocation: 04e091bd06dd4ccdafddf67170c669de
    Trigger: Mon 2025-07-07 01:18:10 EDT; 3h 22min left
   Triggers: ● certbot-renew.service

Jul 06 21:55:57 3060t0 systemd[1]: Started certbot-renew.timer - This is the timer to set the schedule for automated re>

Isn’t that nice? No more line in a text file.

To test Certbot’s renewal, do a --dry-run:

sudo certbot renew \
  --server https://intermediate-ca.lab.wporter.org/acme/acme/directory \
  --dry-run \
  --deploy-hook "echo '[HOOK] Would restart cockpit now'" \
  --verbose

That command’s output should look something like this:

[wporter@3060t0 ~]$ sudo certbot renew --server https://intermediate-ca.lab.wporter.org/acme/acme/directory --dry-run --deploy-hook "echo '[HOOK] Would restart cockpit now'" --verbose
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/3060t0.lab.wporter.org.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cannot extract OCSP URI from /etc/letsencrypt/archive/3060t0.lab.wporter.org/cert1.pem
Certificate is due for renewal, auto-renewing...
Plugins selected: Authenticator standalone, Installer None
Simulating renewal of an existing certificate for 3060t0.lab.wporter.org
Performing the following challenges:
http-01 challenge for 3060t0.lab.wporter.org
Waiting for verification...
Cleaning up challenges
Dry run: skipping deploy hook command: echo '[HOOK] Would restart cockpit now'

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/3060t0.lab.wporter.org/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Be sure you specified your internal --server - by default, Certbot will try to hit Let’s Encrypt (which will fail if the ACME client is not reachable over the Internet).