Be the LetsEncrypt in your homelab with step-ca

12 min read Original article ↗

Posted:

15 minute read

So you have a Cute Homelab and you want to use it to secure your services and containers with x509 certificates? But your homelab isn’t on the internet, so you can’t simply use LetsEncrypt? Well. You can become your own LetsEncrypt and hand out certificates with certbot. You “just” need to run your own CA (Certificate Authority). Sounds frightening and complicated? It kinda is, but not really when you use step-ca, an open source solution that you can run in a container.

Why another step-ca setup blog post when there are already so many out there?

Mainly because there are not many posts out there that specifically focus on podman and using RHEL (Red Hat Enterprise Linux). And also because I wanted to make sure this setup is as self-contained as possible, meaning it will run even when my internet connection is down. My homelab stays at home :)

The goal

What we want to have at the end of this blog post:

  • A CA (Certificate Authority) that runs as a service in a container (more precise: a pod in podman) on RHEL 10 (Red Hat Enterprise Linux)
  • Add this CA to the trust store on other machines
  • use certbot to create and renew certificates, fully automated

In this blog post we will get all of that done. This is the setup I actually use at home for my homelab.

And yes, my homelab completely runs on RHEL 10 (Red Hat Enterprise Linux). You can get it for free by registering at https://developers.redhat.com. The Red Hat Developer Subscription allows you to run RHEL on up to 16 machines, including in production. You get all updates, all products, access to the knowledge base and much more.

The steps

  • Get the CA up and running as container with podman
  • Create a web server certificate for another machine in my homelab, using certbot and nginx

So. Let’s go :)

Get the container

On a RHEL 10 machine with podman installed, we first pull the latest step-ca container, in my case the machine is hl03.fritz.box and reachable on the local network under that name, as it is registered on my Fritz Box DSL router.

podman pull docker.io/smallstep/step-ca

Initial configuration

So now that we have the container, we start it up for the first time to go through the initial configuration. We create persistent storage called hlca for this container to store the config, data etc.

podman run --rm -it -v hlca:/home/step smallstep/step-ca step ca init

This starts the interactive setup, I used:

Question Answer
Deployment type Standalone
Name for PKI JHW Homelab CA
DNS name or IP address hl03.fritz.box
IP and port :4443
Name of first provisioner Jan Wildeboer <aca@nerdcert.eu>
Password for CA keys and first provisioner <PASSWORD>

Most of this is self-explanatory, the only thing worth explaining is the :4443 IP and port setting. As we run this CA in a container, we avoid the extra steps needed to use the default privileged port :443 for https:// connections and simply put it above the limit for privileged ports, which is 1024. Also, by not adding an IP address, we leave it to podman and the container host to get the CA connected to the local network.

The container will now generate the root and intermediate certificate.

After that the container exits. Now we persist the admin password and check if its saved.

podman run --rm -it -v hlca:/home/step smallstep/step-ca sh
echo -n "<PASSWORD>" > secrets/password
cat secrets/password
exit

Enable ACME for certbot

We want to be our own little LetsEncrypt, meaning we want to use certbot to create and renew certificates. This means we need step-ca to act just like LetsEncrypt, which is a done by activating the ACME provider. This is included in step-ca, so we just need to activate that option.

podman run --rm -v hlca:/home/step smallstep/step-ca step ca provisioner add acme --type ACME

Adjust certificate duration

To make our CA behave even more like LetsEncrypt, we change the default duration period for certificates from 1 day (the step-ca default setting) to 90 days. And we will limit the CA to only create certificates for our local domain fritz.box

This is done by editing the ca.json config file outside of the container and adding a "claims" and "policy" block.

So from the shell on the machine with the container, edit the config file, which is at /var/lib/containers/storage/volumes/hlca/_data/config/ca.json and find the "authority" block.

[...]
  "db": {
    "type": "badgerv2",
    "dataSource": "/home/step/db",
    "badgerFileLoadingMode": ""
  },
  "authority": {
    "provisioners": [
      {
        "type": "JWK",
        "name": "JHW Homelab CA",
        "key": {
          "use": "sig",
          "kty": "EC",
[...]

And now add a "policy" block to limit the certificates to *.fritz.box and a "claims" block to set the correct duration. I allow a maximum of 1 year and set the default to 90 days:

[...]
  "db": {
    "type": "badgerv2",
    "dataSource": "/home/step/db",
    "badgerFileLoadingMode": ""
  },
  "authority": {
    "policy": {
      "x509": {
        "allow": {
          "dns": ["*.fritz.box"]
        },
        "allowWildcardNames": false
      },
      "host": {
        "allow": {
          "dns": ["*.fritz.box"]
        }
      }
    },
    "claims": {
      "minTLSCertDuration": "5m",
      "maxTLSCertDuration": "8760h",
      "defaultTLSCertDuration": "2160h"
    },
    "provisioners": [
      {
        "type": "JWK",
        "name": "JHW Homelab CA",
        "key": {
          "use": "sig",
          "kty": "EC",
[...]

With that, the initial setup is done and we can finish everything up by creating a pod called hlcapod and putting our container in there under the name hlca.

Create the pod

Now that our CA container is configured, we will put in a pod, so we can add more containers to it, if needed. Note that now we add the port mapping to the pod, not to the container anymore.

podman pod create -p 4443:4443 -n hlcapod

And we run our hlca container inside the pod, again note that we do NOT map the 4443 port anymore, as that is done by the pod:

podman run -d --name hlca --pod hlcapod -v hlca:/home/step smallstep/step-ca

When the container starts, it tells you the fingerprint of the root certificate:

<DATE> X.509 Root Fingerprint: 0afa95ceef4008376964821a7a666c87ef2116e3b97851d250c15a5272c1d078

Copy this fingerprint as you will need it when using the step tool on other machines in your homelab or to verify all is good.

If you want to access this CA from other machines in your LAN or the internet, make sure to open port 4443/TCP on the firewall of the machine that runs this container. So as root on the host machine:

firewall-cmd --zone=public --permanent --add-port=4443/tcp
firewall-cmd --reload

And that should be it! We now have a container inside a pod that offers CA services to our local network and we can use certbot to get and renew certificates once the pod is up and running. So let’s make sure it actually is by turning it into a systemd service.

Running the pod as a systemd service

With the pod up and running, I simply did (deprecated, I should learn about quadlets, I know :)

# cd /etc/systemd/system
# podman generate systemd --files --name hlcapod

DEPRECATED command:
It is recommended to use Quadlets for running containers and pods under systemd.

Please refer to podman-systemd.unit(5) for details.
/etc/systemd/system/container-hlca.service
/etc/systemd/system/pod-hlcapod.service

# systemctl daemon-reload
# systemctl enable pod-hlcapod.service 
Created symlink '/etc/systemd/system/default.target.wants/pod-hlcapod.service''/etc/systemd/system/pod-hlcapod.service'.
# systemctl start pod-hlcapod.service 

And now my homelab CA runs as a service, inside a pod, and is ready to hand out certificates.

The next thing we need to do is to tell other machines in the homelab that they can trust this CA.

Get and install the root cert on another RHEL 10 machine

Example with root certificate from the CA we just created

The root CA certificate that was generated by the initial config can be downloaded on any other machine in teh homelab now. On a RHEL 10 machien we will fetch the root certificate and add it to the trust store:

curl -k https://hl03.fritz.box:4443/roots.pem -o /etc/pki/ca-trust/source/anchors/roots.pem

As we are connecting to the new CA to download the root certificate and the CA itself is secured with a certificate signed by that CA, we have to tell curl to NOT verify the connection with the -k option, which is insecure. So after we get teh certifcate we calculate the SHA256 fingerprint and compare the result with the fingerprint you see in the log of step-ca.

openssl x509 -in /etc/pki/ca-trust/source/anchors/roots.pem -outform DER | sha256sum | cut -d' ' -f1
0afa95ceef4008376964821a7a666c87ef2116e3b97851d250c15a5272c1d078

When both fingerprints are identical, we can add the root certificate to the trust store with:

Now that the root certificate is installed, we can try again without the -k to verify that the root certificate is now working:

curl https://hl03.fritz.box:4443/health
{"status":"ok"}

For full connection details that show the SSL/TLS negotiation, use curl -v

# curl -v https://hl03.fritz.box:4443/health
* Host hl03.fritz.box:4443 was resolved.
* IPv6: (none)
* IPv4: 192.168.1.13
*   Trying 192.168.1.13:4443...
* Connected to hl03.fritz.box (192.168.1.13) port 4443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=JHW Homelab CA
*  start date: Jul 29 02:42:43 2025 GMT
*  expire date: Jul 30 02:43:43 2025 GMT
*  subjectAltName: host "hl03.fritz.box" matched cert's "hl03.fritz.box"
*  issuer: O=JHW Homelab CA; CN=JHW Homelab CA Intermediate CA
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 2: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hl03.fritz.box:4443/health
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hl03.fritz.box:4443]
* [HTTP/2] [1] [:path: /health]
* [HTTP/2] [1] [user-agent: curl/8.9.1]
* [HTTP/2] [1] [accept: */*]
> GET /health HTTP/2
> Host: hl03.fritz.box:4443
> User-Agent: curl/8.9.1
> Accept: */*
> 
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200 
< content-type: application/json
< x-request-id: f1a2d5cb-1b82-4ce7-b985-05e2bbe06b3a
< content-length: 16
< date: Tue, 29 Jul 2025 10:48:04 GMT
< 
{"status":"ok"}
* Connection #0 to host hl03.fritz.box left intact

Getting a certificate from the new CA with certbot on RHEL 10 for nginx

So now we have created the CA, added the root certificate of the new CA to the trust store on another RHEL 10 machine and can now request a certificate for that machine.

Install certbot

To install certbot and the needed nginx plugin, you must first add the EPEL repository. So as root do:

dnf config-manager --set-enabled crb
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm

Now we can install certbot, the nginx plugin for certbot and nginx itself.

dnf install certbot python3-certbot-nginx nginx

Configure nginx

First: open ports 80 and 443 in the firewall, so web traffic can get in and out.

firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

Set the server_name in /etc/nginx/nginx.conf to our hostname, in this example hl01.fritz.box.

After that is done, enable and start nginx so it`s always running.

systemctl enable nginx
systemctl start nginx

Get the certificate

Now the big moment has arrived. We have everything in place. A web server on hl01.fritz.box. Certbot. And our own CA that behaves just like LetsEncrypt on hl03.fritz.box, listening on port 4443. So let’s go :)

root@hl01:~# certbot --nginx -d hl01.fritz.box --server https://hl03.fritz.box:4443/acme/acme/directory
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for hl01.fritz.box

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/hl01.fritz.box/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/hl01.fritz.box/privkey.pem
This certificate expires on 2025-10-27.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for hl01.fritz.box to /etc/nginx/nginx.conf

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

There you have it! The certificate is generated, saved and will be automatically renewed!

root@hl01:~# certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: hl01.fritz.box
    Serial Number: 3fff58e45370d375b8b968a01c97b434
    Key Type: ECDSA
    Domains: hl01.fritz.box
    Expiry Date: 2025-10-27 09:42:25+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/hl01.fritz.box/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/hl01.fritz.box/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Install the root certificate on your laptop or PC

If your laptop or PC runs Linux, you can follow the above guide to install the root certificate to your trust store.

If you use MacOS, it’s almost the same. Download the roots.pem from the CA with curl or wget (using the insecure option). Double-click the downloaded PEM file to start Keychain Access and give the root certificate full trust.

After you did that, you can open your web browser, go to the machine with nginx and open the web page. You should have a secure connection.

Congratulations! You are now your own CA on your homelab :)

Shoutout and credits