Certificate Manager | Get your self-service free hosted homelab CA today

Build a Tiny Certificate Authority For Your Homelab

Carl-Tashian.jpg

Carl Tashian

Follow Smallstep

Updated on February 16, 2023

tiny-ca.png

TL;DR In this tutorial, we're going to build a tiny, standalone, online Certificate Authority (CA) that will mint TLS certificates and is secured with a YubiKey. It will be an internal ACME server on our local network (ACME is the same protocol used by Let's Encrypt). The YubiKey will securely store the CA private keys and sign certificates, acting as a cheap alternative to a Hardware Security Module (HSM). We'll also use an open-source True Random Number Generator, called Infinite Noise TRNG, to spice up the Linux entropy pool.

Why would I want a Certificate Authority in my homelab?!

  • Because end-to-end TLS is great and you should easily be able to run TLS wherever you need it. Especially in your homelab. Internal networks are no longer perceived as a safe zone where unencrypted traffic is okay. But you need certificates.
  • Because TLS client authentication is becoming more widely supported in different services, and it's a lot better than passwords. But you need certificates.
  • Because the ACME protocol (used by Let's Encrypt) can easily be deployed internally, so you can automate renewal and never have to think about your certificates.
  • Because maybe you've done the 'self-signed certificate' rigmarole with OpenSSL a dozen times already. Might as well formalize things and get your devices to trust a CA that you can use wherever you need it.
  • Because setting up a simple CA is a great learning experience.
  • Still not convinced? Spin up a free hosted homelab CA using our Certificate Manager offering instead.

Tiny CA Specs

Part 1: System Setup

Basic OS & Networking Setup

  • On your laptop, burn the Ubuntu 22.10 Server 64-bit ARM pre-installed server image onto the microSD card using the Raspberry Pi Imager.
  • Fire up the Raspberry Pi, plug it into your network, and find its initial IP address. You can run arp -na | grep -e "b8:27:eb" -e "dc:a6:32" -e "e4:5f:01" to discover Raspberry Pi devices on the local network.
  • Login via SSH (username and password will be ubuntu), and change the password.
  • Set the hostname via hostnamectl set-hostname tinyca
  • Set the timezone using timedatectl set-timezone America/Los_Angeles (or whatever your timezone is; timedatectl list-timezones will list them all)
  • Be sure NTP is working. Check status with timedatectl— make sure "NTP Service" is "active". If not, you can add some NTP servers to /etc/systemd/timesyncd.conf and run systemctl restart systemd-timesyncd.
  • You'll need the machine to have a DNS name (for me it's tinyca.internal) and/or a static IP on your network.

Now that you have good time synchronization and a stable hostname, we can proceed.

Install prerequisite: ykman

Now, insert your YubiKey. Let's install the yubikey-manager (and dependency pcscd) and make sure you can connect to the YubiKey:

$ sudo apt update
$ sudo apt install -y yubikey-manager
$ ykman info
Device type: YubiKey 5 NFC
Serial number: 13910388
Firmware version: 5.2.7
Form factor: Keychain (USB-A)
Enabled USB interfaces: OTP+FIDO+CCID
NFC interface is enabled.

Install prerequisite: Go

You'll need Go in order to build the step-ca server.

$ cd
$ curl -LO https://go.dev/dl/go1.20.1.linux-arm64.tar.gz
$ sudo tar -C /usr/local -xzf go1.20.1.linux-arm64.tar.gz
$ echo "export PATH=\$PATH:/usr/local/go/bin" >> .profile
$ source .profile
$ go version
go version go1.20.1 linux/arm64

Build and install step-ca and step

You'll need to install both step-ca (the CA server software) and step (the command used to configure and control step-ca).

First, download the source for step-ca and build it with experimental YubiKey support enabled:

$ curl -LO https://github.com/smallstep/certificates/releases/download/v0.23.2/step-ca_0.23.2.tar.gz
$ mkdir step-ca
$ tar -xvzf step-ca_0.23.2.tar.gz -C step-ca
$ cd step-ca

Now build step-ca. This will take some time on a Raspberry Pi, so be patient:

$ sudo apt-get install -y libpcsclite-dev gcc make pkg-config
$ make bootstrap
$ make build GOFLAGS=""
....
Build Complete!
$ sudo cp bin/step-ca /usr/local/bin
$ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
$ step-ca version
Smallstep CA/0.23.2 (linux/arm64)
Release Date: 2023-02-16 22:25 UTC

Now install step from a prebuilt binary:

$ cd
$ curl -LO https://github.com/smallstep/cli/releases/download/v0.23.2/step_linux_0.23.2_arm64.tar.gz
$ tar xvzf step_linux_0.23.2_arm64.tar.gz
$ sudo cp step_0.23.2/bin/step /usr/local/bin
$ step version
Smallstep CLI/0.23.2 (linux/arm64)
Release Date: 2023-02-07T00:53:54Z

Optional, but 🔥: Set up the outboard random number generator

Infinite Noise TRNG is an open-source USB True Random Number Generator. It uses a "modular entropy multiplier" architecture to generate a lot of random data quickly. For this setup, a daemon will continuously feed entropy into Linux's system entropy pool by writing to /dev/random.

But will this lovely new entropy generator actually be used by the CA? I needed to answer two questions here:

  1. How does the CA generate random numbers? I had to dig around a little to confirm this. step-ca uses Go's crypto/rand for all of its key generation, and cyrpto/rand uses /dev/urandom as its random data source on Linux systems.
  2. Does the entropy created via writing to /dev/random actually affects what is read from /dev/urandom? It does—because Linux has only one entropy pool, shared by /dev/random and /dev/urandom.

We also need to confirm that the outboard TRNG is actually generating high quality noise. We'll do that in a minute. You'll need to compile the driver from source, because there's no pre-built arm64 package available.

$ curl -LO https://github.com/leetronics/infnoise/archive/refs/tags/0.3.3.tar.gz
$ tar xvzf 0.3.3.tar.gz
$ cd infnoise-0.3.3/software
$ sudo apt-get install -y libftdi-dev libusb-dev
$ make -f Makefile.linux
$ sudo make -f Makefile.linux install
install -d /usr/local/sbin
install -m 0755 infnoise /usr/local/sbin/
install -d /usr/local/lib/udev/rules.d/
install -m 0644 init_scripts/75-infnoise.rules /usr/local/lib/udev/rules.d/
install -d /usr/local/lib/systemd/system
install -m 0644 init_scripts/infnoise.service /usr/local/lib/systemd/system
$ infnoise --version
GIT VERSION -
GIT COMMIT  -
GIT DATE    -

Now, plug in the TRNG and restart your system.

$ sudo reboot

After a restart, you should see that the driver has started up. It will start and stop based on whether the TRNG is present.

$ systemctl status infnoise
● infnoise.service - Wayward Geek InfNoise TRNG driver
     Loaded: loaded (/usr/local/lib/systemd/system/infnoise.service; disabled; preset: enabled)
     Active: active (running) since Thu 2023-02-16 14:43:02 PST; 1min 44s ago
    Process: 655 ExecStart=/usr/local/sbin/infnoise --dev-random --daemon --pidfile /var/run/infnoise.pid (code=e>
   Main PID: 661 (infnoise)
      Tasks: 1 (limit: 2082)
     Memory: 700.0K
        CPU: 162ms
     CGroup: /system.slice/infnoise.service
             └─661 /usr/local/sbin/infnoise --dev-random --daemon --pidfile /var/run/infnoise.pid

Feb 16 14:43:02 tinyca systemd[1]: Starting Wayward Geek InfNoise TRNG driver...
Feb 16 14:43:02 tinyca systemd[1]: Started Wayward Geek InfNoise TRNG driver.

Finally, let's run a health check to make sure the TRNG is ready for use:

$ infnoise --debug --no-output
Generated 1048576 bits.  OK to use data.  Estimated entropy per bit: 0.878415, estimated K: 1.838354
num1s:50.466260%, even misfires:0.119403%, odd misfires:0.156459%
^C

Entropy is written to /dev/random by infnoise.service every second. You're all set on randomness! Now that you have more than enough entropy, you're ready to generate your CA keys.

Part 2: Creating Your PKI

Now you'll create your root and intermediate CA certificates and keys, and store them securely on the YubiKey.

Ideally, your Raspberry Pi should be kept offline for this section. Disconnect the Ethernet cable, and connect directly to the device via HDMI and a keyboard.

Prepare a USB thumb drive for storing the private keys

You can't just have your CA private keys live only on the YubiKey. You'll want at least one backup of them, in case the YubiKey breaks!

Insert a USB thumb drive. You'll generate the keys directly on this drive, so that they never touch the Pi's microSD card. First, find the device name of your USB drive:

$ sudo fdisk -l
...
Disk /dev/sda: 14.91 GiB, 16005464064 bytes, 31260672 sectors
Disk model: Cruzer Fit
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
...

In this case, the drive is /dev/sda. Let's initialize it with a single ext4 partition:

$ sudo fdisk /dev/sda
Welcome to fdisk (util-linux 2.36).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.
Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1):
First sector (2048-31260671, default 2048):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-31260671, default 31260671):
Created a new partition 1 of type 'Linux' and of size 14.9 GiB.
Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
$ sudo mkfs.ext4 /dev/sda1 -v
mke2fs 1.45.6 (20-Mar-2020)
fs_types for mke2fs.conf resolution: 'ext4'
Filesystem label=
OS type: Linux
...
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

Generate your PKI on the thumb drive

Great, now you're ready to create your Public Key Infrastructure (PKI). Specifically, you'll be creating CA keys and certificates.

Tiny CA PKI Highlights:

  • Tiny CA has a root CA key and certificate, and an intermediate CA key and certificate.
  • The root CA key signs the Intermediate CA certificate.
  • The root CA certificate is self-signed (signed with the root CA key)
  • The intermediate CA key will sign all of your TLS certificates.
  • By default, step-ca issues certificates with a 24-hour lifetime. I hope this default will compel you to set up automated renewal on your clients. And you can always increase the TLS certificate duration in the CA configuration, if you want something a bit more relaxed.
  • If a device is configured to trust your root CA, it will trust certificates you create with step-ca.
  • You can throw away the root CA key if you never need another intermediate.
  • Need a refresher on X.509 certificates? See our post, Everything you should know about certificates and PKI but are too afraid to ask. Use a strong password when prompted, and save your password separately, offline, somewhere super duper safe.
$ sudo mount /dev/sda1 /mnt
$ cd /mnt
$ sudo mkdir ca
$ sudo chown ubuntu:ubuntu ca
$ export STEPPATH=/mnt/ca
$ step ca init --pki --name="Tiny" --deployment-type standalone
✔ What do you want your password to be? [leave empty and we'll generate one]: ...
Generating root certificate...
all done!
Generating intermediate certificate...
all done!
✔ Root certificate: /mnt/ca/certs/root_ca.crt
✔ Root private key: /mnt/ca/secrets/root_ca_key
✔ Root fingerprint: d6b3b9ef79a42aeeabcd5580b2b516458ddb25d1af4ea7ff0845e624ec1bb609
✔ Intermediate certificate: /mnt/ca/certs/intermediate_ca.crt
✔ Intermediate private key: /mnt/ca/secrets/intermediate_ca_key
FEEDBACK 😍 🍻
      The step utility is not instrumented for usage statistics. It does not
      phone home. But your feedback is extremely valuable. Any information you
      can provide regarding how you’re using `step` helps. Please send us a
      sentence or two, good or bad: feedback@smallstep.com or join
      https://github.com/smallstep/certificates/discussions.

Don't forget to give your CA a cute name! It will appear on all of your certificates. Hold onto your root fingerprint, too; you'll need it to bootstrap your clients later.

Import the CA into the YubiKey

Now, let's import our PKI to the YubiKey.

$ sudo systemctl enable pcscd
$ sudo systemctl start pcscd
$ ykman piv certificates import 9a /mnt/ca/certs/root_ca.crt
Successfully imported a new certificate.
$ ykman piv keys import 9a /mnt/ca/secrets/root_ca_key
Enter PEM pass phrase: ...
Successfully imported a new private key.
$ ykman piv certificates import 9c /mnt/ca/certs/intermediate_ca.crt
Successfully imported a new certificate.
$ ykman piv keys import 9c /mnt/ca/secrets/intermediate_ca_key
Enter PEM pass phrase: ...
Successfully imported a new private key.
$ ykman piv info
PIV version: 5.2.7
PIN tries remaining: 3
CHUID:	3019d4e739da739ced39ce739d836858210842108421c84210c3eb34104610300df33f7fd273e44f17361ce7c4350832303330303130313e00fe00
CCC: 	No data available.
Slot 9a:
	Algorithm:	ECCP256
	Subject DN:	CN=Tiny CA Root CA
	Issuer DN:	CN=Tiny CA Root CA
	Serial:		280998571002718115143415195266043025218
	Fingerprint:	d6b3b9ef79a42aeeabcd5580b2b516458ddb25d1af4ea7ff0845e624ec1bb609
	Not before:	2020-12-08 20:12:15
	Not after:	2030-12-08 20:12:15
Slot 9c:
	Algorithm:	ECCP256
	Subject DN:	CN=Tiny CA Intermediate CA
	Issuer DN:	CN=Tiny CA Root CA
	Serial:		38398140468675846143165983044297636289
	Fingerprint:	fa21279c114ef44be899cb41e830b920faa6ce2c0ec5bc4f1c9310194e5837d2
	Not before:	2020-12-08 20:12:15
	Not after:	2030-12-08 20:12:15

OK! Now you'll copy out the CA certificate files, leave the private keys on the USB stick, and continue creating your CA.

$ sudo cp /mnt/ca/certs/intermediate_ca.crt /mnt/ca/certs/root_ca.crt /root
$ cd
$ sudo umount /mnt

Finally, reconnect your CA to your local network to continue the setup.

Part 3: Configuring Your CA

You're going to re-run step ca init now, but you're not going to use the certificates or keys that it generates. You're just doing this to create the configuration files. The password you choose when prompted will be your admin provisioner password. Anyone with this password will be able to administer your CA and get any certificate from it, using the step ca certificate subcommand.

Don't use your root CA password for your provisioner, but pick something strong and store it somewhere safe.

$ sudo useradd step
$ sudo passwd -l step
$ sudo mkdir /etc/step-ca
$ export STEPPATH=/etc/step-ca
$ sudo --preserve-env step ca init --name="Tiny CA" \
    --dns="tinyca.internal,10.20.30.42" --address=":443" \
    --provisioner="you@example.com" \
    --deployment-type standalone \
    --remote-management
Choose a password for your CA keys and first provisioner.
✔ [leave empty and we'll generate one]:

Generating root certificate... done!
Generating intermediate certificate... done!

✔ Root certificate: /etc/step-ca/certs/root_ca.crt
✔ Root private key: /etc/step-ca/secrets/root_ca_key
✔ Root fingerprint: 60440dc6ef5b923810b22f85a907f307badb58314c5fdc2231a3c1a892d6c275
✔ Intermediate certificate: /etc/step-ca/certs/intermediate_ca.crt
✔ Intermediate private key: /etc/step-ca/secrets/intermediate_ca_key
✔ Database folder: /etc/step-ca/db
✔ Default configuration: /etc/step-ca/config/defaults.json
✔ Certificate Authority configuration: /etc/step-ca/config/ca.json
✔ Admin provisioner: you@example.com (JWK)
✔ Super admin subject: step

Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.

Next, let's get your certificates in place.

$ sudo mv /root/root_ca.crt /root/intermediate_ca.crt /etc/step-ca/certs
$ sudo rm -rf /etc/step-ca/secrets

Next, you'll need to configure step-ca to use your YubiKey to sign certificates, using the intermediate key on the YubiKey. Notice that the default YubiKey PIN (123456) is shown here, too.

You should change your YubiKey PIN, PUK, and management key if you haven't already! Learn how in this guide. Now edit the file /etc/step-ca/config/ca.json. You'll want the top of the file to look like this:

{
        "root": "/etc/step-ca/certs/root_ca.crt",
        "federatedRoots": [],
        "crt": "/etc/step-ca/certs/intermediate_ca.crt",
        "key": "yubikey:slot-id=9c",
        "kms": {
            "type": "yubikey",
            "pin": "123456"
        },
        "address": ":443",
...

Now you'll start up the CA and make sure it's running properly:

$ sudo chown -R step:step /etc/step-ca
$ sudo -u step step-ca /etc/step-ca/config/ca.json
2020/12/08 14:17:06 Serving HTTPS on :443 ...

In another window, you'll generate a test certificate for localhost. This is where you'll need the CA fingerprint, which is displayed when you start up the CA. Run:

$ step ca bootstrap --ca-url "https://tinyca.internal" --fingerprint d6b3b9ef79a42aeeabcd5580b2b516458ddb25d1af4ea7ff0845e624ec1bb609
The root certificate has been saved in /home/ubuntu/.step/certs/root_ca.crt.
Your configuration has been saved in /home/ubuntu/.step/config/defaults.json.
$ step ca certificate "localhost" localhost.crt localhost.key
✔ Provisioner: you@example.com (JWK) [kid: izgi9tn1YWbVnY_rmIUKzE-Dn-XIuKz-_J1dnnKeDRA]
✔ Please enter the password to decrypt the provisioner key:
✔ CA: https://tinyca.internal:443
✔ Certificate: localhost.crt
✔ Private Key: localhost.key
$ step certificate inspect localhost.crt --short
X.509v3 TLS Certificate (ECDSA P-256) [Serial: 2903...3061]
  Subject:     localhost
  Issuer:      Tiny Intermediate CA
  Provisioner: you@example.com [ID: izgi...eDRA]
  Valid from:  2023-02-16T23:03:52Z
          to:  2023-02-17T23:04:52Z

Great! You just signed your first X.509 TLS leaf certificate using the YubiKey and step-ca.

When you ask the CA to issue a leaf certificate for a TLS endpoint, you'll get a certificate file and a (locally-generated) private key file. The certificate file will contain both the intermediate CA certificate and the leaf certificate you requested. This way, a device which trusts your root CA can verify the chain of trust from the root to the intermediate, and from the intermediate to the leaf.

Finally, you'll add an ACME provisioner, which will turn your Tiny CA into a tiny Let's Encrypt!

$ step ca provisioner add acme --type acme --admin-name step
No admin credentials found. You must login to execute admin commands.
✔ Provisioner: you@example.com (JWK) [kid: izgi9tn1YWbVnY_rmIUKzE-Dn-XIuKz-_J1dnnKeDRA]
Please enter the password to decrypt the provisioner key:

Sign in with your admin password, and your new new ACME provisioner will be created.

You can now shut down the step-ca process you started in the other terminal window.

Configure systemd to start the CA

In this section you'll set up a systemd service for step-ca so it starts when the system starts up. You'll also configure systemd to stop the CA when the YubiKey is removed, and restart it when the YubiKey is reinserted. First, you need to tell udev about your YubiKey by adding some udev rules, which will help make the YubiKey visible to systemd as a device.

$ sudo tee /etc/udev/rules.d/75-yubikey.rules > /dev/null << EOF
ACTION=="add", SUBSYSTEM=="usb", ENV{PRODUCT}=="1050/407/*", TAG+="systemd", SYMLINK+="yubikey"
ACTION=="remove", SUBSYSTEM=="usb", ENV{PRODUCT}=="1050/407/*", TAG+="systemd"
EOF
$ sudo udevadm control --reload-rules

Here, the format of the ENV{PRODUCT} value is {vendorId}/{productId}/*. Yubico's vendor ID is 1050, and 407 is the product ID for the YubiKey 5 NFC. If you're using a different YubiKey, you can find your model number here.

Now you'll set up the CA as a systemd service that will:

  • run on system startup, when the YubiKey is inserted
  • stop when the YubiKey is removed
  • start again when the YubiKey is reinserted
$ sudo tee /etc/systemd/system/step-ca.service > /dev/null << EOF
[Unit]
Description=step-ca
BindsTo=dev-yubikey.device
After=dev-yubikey.device
[Service]
User=step
Group=step
ExecStart=/bin/sh -c '/usr/local/bin/step-ca /etc/step-ca/config/ca.json'
Type=simple
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
$ sudo mkdir /etc/systemd/system/dev-yubikey.device.wants
$ sudo ln -s /etc/systemd/system/step-ca.service /etc/systemd/system/dev-yubikey.device.wants/
$ sudo systemctl daemon-reload
$ sudo systemctl enable step-ca

Now insert the YubiKey and the service should start:

$ sudo systemctl status step-ca
● step-ca.service - step-ca
     Loaded: loaded (/etc/systemd/system/step-ca.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2020-12-08 14:27:02 PST; 3s ago
   Main PID: 3269 (sh)
      Tasks: 9 (limit: 2099)
     CGroup: /system.slice/step-ca.service
             ├─3269 /bin/sh -c /usr/local/bin/step-ca /etc/step-ca/config/ca.json
             └─3270 /usr/local/bin/step-ca /etc/step-ca/config/ca.json
Dec 08 14:27:02 tinyca systemd[1]: Started step-ca.
Dec 08 14:27:02 tinyca sh[3270]: 2020/12/08 14:27:02 Serving HTTPS on :443 ...

Now restart your system and ensure that the CA starts up automatically.

Test out removing the YubiKey, and you should see that the CA stops.

Reinsert it, and the CA should start up again.

Finally, turn on the firewall and disable SSH access

Your tiny CA will be most secure without any SSH access at all. The only open port will be 443, for the CA. For maintenance, you'll need to plug in a keyboard and a display.

$ sudo tee /etc/ufw/applications.d/step-ca-server > /dev/null << EOF
[step-ca]
title=Smallstep CA
description=step-ca is an online X.509 and SSH Certificate Authority
ports=443/tcp
EOF
$ sudo ufw allow step-ca
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Using Your CA

You did it! Your CA is up and running.

Bootstrapping a new device into your PKI

When you run step ca bootstrap (as above) on a new device, the root certificate root_ca.crt is downloaded from the CA. If you run step ca bootstrap --install --ca-url=https://your.ca --fingerprint=your-ca-fingerprint, it will install the root certificate into your device's trust store.

You can also use the step command for easy installation of your root CA certificate (step certificate install), for ACME enrollment (step ca certificate example.com example.crt example.key --provisioner acme) and for renewal of any certificate that hasn't yet expired (step ca renew example.crt example.key).

For mobile devices, you can usually install a certificate by sending it to yourself via Bluetooth or AirDrop, or as an email attachment. Make sure the certificate isn't just installed, but actually trusted by the device. This usually involves a couple of confirmation steps on the device.

Use ACME!

With the ACME provisioner, you can use software like Certbot or LEGO CLI to easily get and renew certificates for any endpoint. Our tutorials on running a private ACME server and configuring popular ACME clients to use a private ACME server will show you how to get ACME certificates from your CA using the most common ACME clients and ACME-supporting services.

Automating certificate renewal

Because certificates from your CA have a 24-hour lifetime, you'll want to renew them every 16ish hours. Our renewal documentation has a few options for setting up renewal on your clients.

Further Reading

Now that you have an internal CA, here's a few useful resources:

Subscribe to updates

Unsubscribe anytime, see Privacy Policy

Carl Tashian (Website, LinkedIn) is an engineer, writer, exec coach, and startup all-rounder. He's currently an Offroad Engineer at Smallstep. He co-founded and built the engineering team at Trove, and he wrote the code that opens your Zipcar. He lives in San Francisco with his wife Siobhan and he loves to play the modular synthesizer 🎛️🎚️