Automated Local TLS Certificates With Step-CA

Mar 26, 2024 · 14 mins read
Automated Local TLS Certificates With Step-CA

In the video below, we show you how to install and configure step-ca to automate TLS certificate provisioning for local computers

As useful as OpenSSL has been for letting me manage TLS certificates for internal IT devices, after a while this becomes time consuming and tedious

For one thing, there’s a few a steps to go through to create a certificate and just before a certificate expires you have to go through more steps to revoke the certificate and then create a new one

Now while Let’s Encrypt allows you to automate certificate provisioning, you’d need a public domain name and be willing to leak details about your internal devices to the Public Internet. In other words, it’s a bad idea

So wouldn’t it be good if you could have the security benefit of OpenSSL and the automation benefit of Let’s Encrypt

Well you can with an open source certificate authority called step-ca

Not only can you install this on an internal computer, it supports ACME provisioning

And in this video we’ll go over how to install and configure step-ca as well as demonstrate how to configure Proxmox VE to use it

Useful links:

Because this video is specifically about step-ca, I’m going to assume that you already have a container platform like Docker or Podman installed

If not, then I do have a video which shows you how to install Podman, which is what we’ll be using in this video

Initial Setup:
The first thing to do is to initialise the setup of step-ca

Now although we’ll be doing this on Podman, the process is the same for Docker. You just need to replace the podman commands with docker ones

We’ll pull down the container image to begin with

podman pull

As this is a new installation and Podman is generating warnings, we’ll jump to another session where I have sudo rights and take care of that as suggested

sudo loginctl enable-linger 1002

Although you’ll probably have to change the user ID to one which is different to 1002

Now back on Podman, we’ll initilise step-ca

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

This will create a volume for a step-ca container we’ll run and present us with a wizard asking a series of questions

Notice we’re using the –rm option to remove the container once it completes

This is very useful for Podman in particular which has issues if you try to run a container and the name is already used

For this example we’re choosing the following options:

Deployment type - Standalone
Name for PKI - HomeLabCA
DNS name or IP address - ca.homelab.lan
IP and port - :8443
Name of first provisioner - ca@homelab.lan
Password for CA keys and first provisioner - <your password here>

By default, the deployment mode is standalone, which is what we want

The name for this CA is entirely up to you, but I’ve opted to call it HomeLabCA

And I’m going to be connecting to this using the FQDN of ca.homelab.lan

Because this is a container, I didn’t specify an IP address, only a port. This means it will listen on port 8443 on all IP addresses, which deosn’t really matter as there is only one. In addition, it also won’t matter if the container IP address changes

I’ve opted for port 8443 as this is a non-root port and it’s slightly less obvious than the typical 443. However, you might want to pick something else to be more safe

The provisioner name is basically the admin user account, so you can name this what you like

The wizard can generate a complex password for you if you hit enter, but some special characters may cause problems later

What I tend to do is to let it create a password and replace any single or double quotes with something else, in other words, you can edit the password before you accept it

Once the process completes, copy the password and Root fingerprint and keep these somewhere safe and secure

Password File:
When the CA needs to do anything, it will require the password that was created during the setup process, but we want this to be an automated process

In which case, we’ll create a password file

To do that, we’ll run the container again but connect to it with a shell session

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

And then we’ll create a password file using the echo command

echo -n "<your password here>" > secrets/password

I’ve used double quotes to try and deal with special characters, althouth you could use single quotes

Sometimes though you’ll find the command drops you to a > prompt. In other words, it couldn’t complete what was asked

If that’s the case then hit Ctrl-C to return to the terminal prompt

Nano isn’t installed, so one option is to use the vi text editor to create the password file instead

vi secrets/password

Press the Ins key, to go into insert mode

Now copy the password into your clipboard

Select the terminal session, right-click and select paste and you should see your password appear

Press the Ins key, to exit insert mode

Press the Esc key, press the : key, type wq in the prompt then hit return

TIP: To exit out without change, press the Esc key, then the : key, type q! in the prompt then hit return

An alternative option is to go through the initialisation again, and this time try to avoid any characters that may have caused the problem, and then allow the config files to be overwritten

In any case, check the password in the file matches the one being used

cat secrets/password

Now exit the session


Bootstrapping and Testing:
The CA should now be configured, so we’ll do some initial testing

First we’ll start the container

podman run --rm -d --name step-ca -p 8443:8443 -v step-ca:/home/step smallstep/step-ca

Just make sure to use the port you chose during the initialisation, if it’s not 8443

Then check the container is running

podman ps

TIP: If you find the container isn’t staying up, chances are there’s a problem with the password in the file

You can find out more information by running the container in the foregound

podman run --rm --name step-ca -p 8443:8443 -v step-ca:/home/step smallstep/step-ca

If you’re using a software firewall on the host computer, add a rule to allow access to the CA, in this example we’ll need to allow access to TCP port 8443

In addition, you’ll want to update your DNS server to resolve the FQDN for the CA. In this example I’ll have to add a host entry for ca.homelab.lan

On another computer, we’ll install the step client software

sudo dpkg -i step-cli_amd64.deb

Then we’ll use this to bootstrap the computer so that it trusts the CA certificate

step ca bootstrap --ca-url https://ca.homelab.lan:8443 --fingerprint <your fingerprint here> --install

And now we’ll run a health check

curl https://ca.homelab.lan:8443/health

The expected output is


Again, make sure to use the port you choose during the initialisation, if it’s not 8443

Most, if not all, web browsers these days though have their own trust store for certificates, so you’ll need to configure them separately to trust the CA’s certificate

Now there are various ways to obtain the root certificate, but since we bootstrapped this computer, we already have easy access to it

cat .step/certs/root_ca.crt

Make a copy of that and update your web browser(s) as appropriate

Now we’ll stop the container, as there’s one more thing to do

podman stop step-ca

ACME Server Provisoner:
The main reason for wanting to use step-ca is so that we have a CA that provides automated certificate provisioning with ACME

This requires us to add an ACME provisioner to step-ca and we can do that by running the container again and issuing a command

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

The expectation is you’ll get a postive response back saying the config was updated

With the configuraton now updated, we’ll start the container back up again for actual use

podman run --rm -d --name step-ca -p 8443:8443 -v step-ca:/home/step smallstep/step-ca

At this point we’re good to go

If you plan to use the HTTP challenge option, make sure the CA can resolve the FQDNs for all of the computers that will be configured to use ACME

In addition, the CA will need access to each web server as it will perform checks

The CA will try and connect to port 80 and 443, which may require some creativity on your part if the server is listening on a different port

Proxmox VE ACME Client Configuration:
Some computer systems already support ACME whereas others will need an ACME client like Certbot installing

In this example, we’ll configure a Proxmox VE server to obtain a certificate from the CA

Now unfortunately, the GUI only allows you to register a Let’s Encrypt account so we’ll have to use the CLI to register our CA

Hopefully, Proxmox will make this possible from the GUI in a future update

Either SSH into the server or login via the GUI and open a shell session

At this stage, the server won’t trust the CA certificate so we’ll install the step client software

dpkg -i step-cli_amd64.deb

Then we’ll use this to bootstrap the computer so that it trusts the CA certificate

step ca bootstrap --ca-url https://ca.homelab.lan:8443 --fingerprint <your fingerprint here> --install

Remember to use the relevant URL for your CA

Now we’ll register an account for our CA

pvenode acme account register default testpve@homelab.lan --directory https://ca.homelab.lan:8443/acme/acme/directory

Again, set the appropriate URL and also a suitable contact name

Now, because we’re dealing with internal servers, we’ll keep things simple and use the HTTP challenge option

In other words, the CA will test HTTP(S) connectivity to our server to make sure it’s genuine before issuing a certificate

The problem is, PVE is listening on port 8006 which is not a standard HTTP/HTTPS port that the CA will try to connect on

In which case, we’ll redirect traffic from the CA using iptables

iptables -t nat -I PREROUTING --src --dst -p tcp --dport 443 -j REDIRECT --to-ports 8006

In this example, I want to limit access to the hypevisor, and so not only do I set the destination IP as the hypervisor, but I also set the source IP as the CA

TIP: You can check what rules like this are present with this command

iptables -t nat -L

Unfortunately rules like this don’t survive a reboot by default

In which case, we’ll we’ll edit the network interfaces file to make sure the rule persists

nano /etc/network/interfaces

And insert the following line at the end of the bridge declaration

post-up iptables -t nat -I PREROUTING --src --dst -p tcp --dport 443 -j REDIRECT --to-ports 8006
post-down iptables -t nat -D PREROUTING --src --dst -p tcp --dport 443 -j REDIRECT --to-ports 8006

Now save and exit

Now we’ll go back to the GUI and continue from there

Select the server, then navigate to System | Certificates

Under ACME, click Add

The Challenge Type can remain as HTTP

For Domain, enter the server’s FQDN, in this examples it’s testpve.homelab.lan

Now click Create

Then click Order Certificates Now

Given time, the server should be allocated a certificate and the pveproxy service will then be restarted

To trust the CA, your web browser(s) need to be updated to trust the CA

How you do that varies depending on which browser is being used

One thing to bear in mind, is that a certificate will have an expiration of 24 hours as opposed to the usual 1 year for manual certificate creation

This is deliberate because this automated solution of providing certificates doesn’t offer revocation of certificates

In theory, that’s not too bad, because if a server’s key and certificate are leaked, the certificate will likely expire before it’s had a chance to be used

And while you can’t revoke a certificate, you can block its renewal

In addition, it would probably need a man-in-the middle attack or maybe a DNS change to direct computers to a malicious server, posing as the real server

In either case, you’d have far more to worry about in those situations

Compose Service Account:
We want this container to be running all the time and for Podman, what we’ve done so far won’t survive a reboot

One option is setup a service to run the container, another is to use compose and run that as a service

Since I’ve already got a service setup to use a compose file, we’ll update that

Unfortunately though, I’ve run into problems using volumes with the version of podman-compose that is in the Debian repository so the first thing to do is to uninstall it

Using an account with sudo rights, we’ll run this command

sudo apt remove podman-compose

Next, we’ll install pipx as this is preferred by Debian for installing 3rd party Python applications

sudo apt install pipx -y

Back in a session for the podman user account we’ll update the path

pipx ensurepath

Then exit out and switch back to the account for the change to take affect

Next, we’ll install podman-compose for this user

pipx install podman-compose

Now we’ll update the compose file

nano compose.yaml

    external: true

    container_name: step-ca
      - '8443:8443'
    restart: unless-stopped
      - step-ca:/home/step

Now save and exit

This is basically the same setup as the run command, but since we’ve already created the volume using the podman command, we set external to true, as podman-compose is not responsible for managing it

We’ll stop the container that’s currently running

podman stop step-ca

Then use podman-compose to spin up another instance

podman-compose up -d step-ca

As before, we’ll run a health check from our other computer

curl https://ca.homelab.lan:8443/health

Because we’ve changed the podman-compose software, I need to update the service using an account with sudo rights

sudo nano /lib/systemd/system/podman-compose.service

And change the path for podman-compose from /usr/bin/ to /home/podmanuser/.local/bin/

Bear in mind, the new path depends on the name of the user account running podman, in my case podmanuser

In other words, pipx installs software into the user’s home directory

Now save and exit

Because we’ve changed the service file details, we’ll have to reload the daemon

sudo systemctl daemon-reload

With the changes made, containers should now survive a reboot, but first we’ll check by stopping the service

sudo systemctl stop podman-compose.service

Then we’ll start it back up

sudo systemctl start podman-compose.service

We’ll check the service is still working

sudo systemctl status podman-compose.service

And check that our containers are up and running

podman ps

Now I must admit this was a bit fiddly to setup using Podman

It should be easier to do on a default installation of Docker though, but I prefer the security of non-root containers

Do bear in mind that servers using non-standard ports add a bit more complexity to the ACME process because this is geared towards web servers using port 443

But once this is all setup, your servers should keep their certificates up to date on their own

Now, not all devices support ACME provisioning, for instance network devices, and fortunately this CA does support manual provisioning as well

So in the grand scheme of things, I expect this CA to save a lot of time going forwards

Sharing is caring!