Introduction
If you’re someone like me who runs a lot of servers and applications in your own network, but hates seeing security warnings from every web UI using a self-signed certificate, then hosting your own Certificate Authority is the best solution! Sure, you could simply get free trusted certificates from LetsEncrypt, but that requires using public domains and doesn’t provide half the fun and bragging rights of conquering the complex world of PKI (Public Key Infrastructure) with your own private CA. Not to mention it’s another cool use for that spare Raspberry Pi you have kicking around.
A proper CA will protect its private keys via an HSM (Hardware Security Module), but those are typically highly expensive enterprise appliances. Luckily we can make similar use of a Yubikey for a small-scale CA such as this.
Prerequisites
You’ll need the following hardware for this project:
- A Raspberry Pi computer. In my case I am using a Raspberry Pi 4 Model B, but the Pi 3 or Pi 5 models should work just as well. See my other guide: How to Prepare a Headless Raspberry Pi For Your Network
- A Yubikey with PIV functionality enabled. You can check your model here. (Also make sure you have a PIN and PUK configured. This can be done with Yubico Autheticator.)
- A regular USB drive to store an offline backup for our CA certificates.
Personal Identity Verification (PIV) is a standard originally created by the U.S. government for secure digital identity and authentication using smart cards. Since it utilizes the X.509 standard and we only need a couple of keys stored, we can use it as a rudimentary HSM (Hardware Security Module) to protect the private keys for our CA. A proper HSM can store thousands of keys and is MUCH more expensive.
Once you’re prepared, let’s SSH into your Raspberry Pi and get started!
Install Yubikey Manager
First you’ll need to install ykman, the YubiKey Manager CLI. This will allow us to interface with the Yubikey.
First let’s install the yubikey-manager package. It should be available in the default repositories.
sudo apt install -y yubikey-manager
Usually this should be all there is to it, but recent changes to the pcscd smart card service mean that we can only run ykman with sudo by default. This will be a problem later, so we need to configure proper access. First, create a group named yubico and add your user to it.
sudo groupadd yubico
sudo usermod -a -G yubico $USER
Next, create a file under /etc/polkit-1/rules.d named 67-yubikey.rules.
sudo nano /etc/polkit-1/rules.d/67-yubikey.rules
Copy the following config into the file and save it. This will grant users in the yubico group access to the smart card service.
polkit.addRule(function(action, subject) {
if (action.id == "org.debian.pcsc-lite.access_card" &&
subject.isInGroup("yubico")) {
return polkit.Result.YES;
}
});
polkit.addRule(function(action, subject) {
if (action.id == "org.debian.pcsc-lite.access_pcsc" &&
subject.isInGroup("yubico")) {
return polkit.Result.YES;
}
});
Now you should be able to run the ykman command to view your Yubikey info. Make sure that it shows PIV enabled for USB on your device. On mine I have previously disabled all other services.
$ ykman info
Device type: YubiKey 5C NFC
Serial number: 16486082
Firmware version: 5.2.7
Form factor: Keychain (USB-C)
Enabled USB interfaces: CCID
NFC transport is enabled
Applications USB NFC
Yubico OTP Disabled Disabled
FIDO U2F Disabled Disabled
FIDO2 Disabled Disabled
OATH Disabled Disabled
PIV Enabled Disabled
OpenPGP Disabled Disabled
YubiHSM Auth Not available Not available
Install Go
Since step-ca is written in the Go language, we will need to install Go in order to build it.
First fetch the tarball.
curl -LO https://go.dev/dl/go1.25.1.linux-arm64.tar.gz
Unpack it to usr/local.
sudo tar -C /usr/local -xzf go1.25.1.linux-arm64.tar.gz
Add the binary to your PATH variable.
echo "export PATH=\$PATH:/usr/local/go/bin" >> .profile
Load the updated variable to your current shell session with source.
source .profile
Now test that it works by checking the Go version.
$ go version
go version go1.25.1 linux/arm64
Install Step Applications
Install Step-CA
The step-ca application is the actual server application for the CA. It can more easily be installed via repository, but since its support for Yubikeys is experimental, we will need to build it from source.
First, download the tarball, unpack it, and enter the certificates directory that is created.
curl -LO https://github.com/smallstep/certificates/archive/refs/tags/v0.28.4.tar.gz
tar -xvzf v0.28.4.tar.gz
cd certificates-0.28.4
Ensure that necessary dependencies are installed.
sudo apt-get install -y libpcsclite-dev gcc make pkg-config
Now run the following commands to build the application. Each one will take some time to run, so be patient.
make bootstrap
make build GOFLAGS=""
Once that is completed, copy the step-ca binary to /user/local/bin.
sudo cp bin/step-ca /usr/local/bin
Now we’ll use setcap to set necessary capabilities on the binary.
sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
Now step-ca should be ready to run. We can test it by checking the version.
$ step-ca version
Smallstep CA/0.28.4 (linux/arm64)
Release Date: 2025-09-29 16:26 UTC
Install Step CLI
The step application is the client-side command line application used control and interface with step-ca. We can quickly download and install the prebuilt binary.
First return to your home directory.
cd
Download and unpack the tarball.
curl -LO https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.28.7/step_linux_0.28.7_arm64.tar.gz
tar xvzf step_linux_0.28.7_arm64.tar.gz
Copy the binary to /usr/local/bin.
sudo cp step_0.28.7/bin/step /usr/local/bin
Test it by checking the version.
$ step version
Smallstep CLI/0.28.7 (linux/arm64)
Release Date: 2025-07-14 05:36 UTC
Configure your CAs
Now it is time to create and configure your Certificate Authorities. As is the recommended practice for most PKI implementations, we will create a Root CA keypair and an Intermediate CA keypair. The Root CA will sign the Intermediate CA’s public key (certificate), and the Intermediate CA will then be used to sign issued leaf certificates. But because we are using the Yubikey to secure the private keys for these CAs, the process becomes a little more complicated. The process will go as follows.
- The CAs will be generated and stored on the backup USB drive.
- The CA certificates and keys will be imported into the Yubikey.
- The CA certificates will be copied to the
/rootdirectory of the system. - New CAs will be generated on the system, but only to create the configuration files.
- The original CA certificates stored in
/rootwill replace the new certificates, and the new private keys will be deleted. - The system will be configured to utilize the original private keys stored in the Yubikey.
For a paranoid level of security, you may want to perform the first 3 steps with the Raspberry Pi disconnected from the network.
Generate CAs on Backup USB Drive
First we will want to connect and mount the backup USB drive. Use the fdisk or lsblk commands to find the drive number. If there are no other USB storage devices connected, it will most likely be /dev/sda. Also make sure it has a partition with a compatible file system configured. In my example, I will be mounting /dev/sda1 to /mnt.
sudo mount /dev/sda1 /mnt
Enter the /mnt directory and create a new directory named ca.
cd /mnt
mkdir ca
Configure the STEPPATH variable to point to this directory. This will be a temporary configuration to generate the CA certificates on the backup USB drive.
export STEPPATH=/mnt/ca
Next we’ll use step ca init to generate our CAs in the ca directory that we created. Change the name to whatever suits you, as this will appear on all issued certificates.
step ca init --pki --name="Hacksanity" --deployment-type standalone
Generate a secure password for the CA keys and keep it somewhere safe. Also save the root fingerprint displayed, as it will be needed later.
Choose a password for your CA keys.
✔ [leave empty and we'll generate one]: █
Generating root certificate... done!
Generating intermediate certificate... done!
✔ Root certificate: /mnt/ca/certs/root_ca.crt
✔ Root private key: /mnt/ca/secrets/root_ca_key
✔ Root fingerprint: 3073240re32cf89b850d0e4b33b3ee5890bd264a2bb3f8076f855169fcd034af
✔ 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 at feedback@smallstep.com or join GitHub Discussions
https://github.com/smallstep/certificates/discussions and our Discord
https://u.step.sm/discord.
Import CAs to Yubikey
First, make sure that the pcscd service is enabled and running. This is the smart card service that manages communication with the Yubikey.
sudo systemctl enable pcscd
sudo systemctl start pcscd
systemctl status pcscd
First we will import the root CA certificate and key. For this you will need the PIN for your Yubikey as well as the password previously created for the CA key.
ykman piv certificates import 9a /mnt/ca/certs/root_ca.crt
ykman piv keys import 9a /mnt/ca/secrets/root_ca_key
Now do the same for the Intermediate CA.
ykman piv certificates import 9c /mnt/ca/certs/intermediate_ca.crt
ykman piv keys import 9c /mnt/ca/secrets/intermediate_ca_key
Now you should be able to view the CA certificates on your Yubikey.
ykman piv info
PIV version: 5.2.7
PIN tries remaining: 3
Management key algorithm: TDES
Management key is stored on the YubiKey, protected by PIN.
CHUID: 3019d4e739da739ced39ce739d836858210842108421c84210c3eb34101b713b784b1e45809df915954de49dee350832303330303130313e00fe00
CCC: No data available
Slot 9A (AUTHENTICATION):
Private key type: EMPTY
Public key type: ECCP256
Subject DN: CN=Hacksanity Root CA,O=Hacksanity
Issuer DN: CN=Hacksanity Root CA,O=Hacksanity
Serial: 00:00:00:00:f9:b7:49:ff:4d:e3:70:9a:25:dc:7d:8d:8f:af:e4:13
Fingerprint: 3873240ce32cf89b850d0e4b33b3ee5890bd268a2bb3f8078f855869fcb034af
Not before: 2025-11-23T20:23:32+00:00
Not after: 2035-11-21T20:23:32+00:00
Slot 9C (SIGNATURE):
Private key type: EMPTY
Public key type: ECCP256
Subject DN: CN=Hacksanity Intermediate CA,O=Hacksanity
Issuer DN: CN=Hacksanity Root CA,O=Hacksanity
Serial: 00:00:00:00:03:b7:82:0e:22:cf:e7:2a:99:97:34:2e:f1:b5:26:90
Fingerprint: ce2d428792ecbbb4eca24a0f0436b362e0179ffb6b5b70d7a7c0c3f3fcc397f1
Not before: 2025-11-23T20:23:33+00:00
Not after: 2035-11-21T20:23:33+00:00
Copy out Certificate Files
Next we will copy the root and intermediate CA certificates to the /root directory temporarily. The private keys stay on the backup USB drive and Yubikey.
sudo cp /mnt/ca/certs/intermediate_ca.crt /mnt/ca/certs/root_ca.crt /root
Return to your home directory and unmount the USB drive.
cd
sudo umount /mnt
Store it somewhere safe, as it contains the backup copies of your CA private keys.
Generate New CA
First we will create a user named step. The command below will also create a group with the same name and add the user to it, configure it as a system user, set its home folder to /etc/step-ca, and configure the shell to /bin/false to prevent login with the user.
$ sudo useradd --user-group --system --home /etc/step-ca --shell /bin/false step
sudo passwd -l step
We’ll also need to add this user to the yubico group that we created previously, so that it can work with the Yubikey.
sudo usermod -a -G yubico step
Create a new directory under /etc named step-ca, then configure the STEPPATH variable to point to it.
sudo mkdir /etc/step-ca
export STEPPATH=/etc/step-ca
Now we are going to run step ca init again, but we are not going to use the CA certificates and keys that that are generated; as we will replace them with the CAs that we previously created. This step is simply to generate the configuration. Replace the name, hostname, IP address, and provisioner email examples with your own.
sudo --preserve-env step ca init --name="Hacksanity CA" \
--dns="ca.hacksanity.local,172.16.0.3" --address=":443" \
--provisioner="email@hacksanity.com" \
--deployment-type standalone \
--remote-management
Create a password for the provisioner. This will be used to administer the CA and manually request certificates from it. For security, it is best to keep it different from the password that was previously created for the CA keys.
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: a63d82bab806c3aff77f601f6d798eff409ga97b102177ay2335bf24e8fc41c0
✔ Intermediate certificate: /etc/step-ca/certs/intermediate_ca.crt
✔ Intermediate private key: /etc/step-ca/secrets/intermediate_ca_key
badger 2025/12/06 18:00:57 INFO: All 0 tables opened in 0s
badger 2025/12/06 18:00:57 INFO: Storing value log head: {Fid:0 Len:30 Offset:2956}
badger 2025/12/06 18:00:57 INFO: [Compactor: 173] Running compaction: {level:0 score:1.73 dropPrefixes:[]} for level: 0
badger 2025/12/06 18:00:57 INFO: LOG Compact 0->1, del 1 tables, add 1 tables, took 11.789781ms
badger 2025/12/06 18:00:57 INFO: [Compactor: 173] Compaction for level: 0 DONE
badger 2025/12/06 18:00:57 INFO: Force compaction on level 0 done
✔ 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: email@hacksanity.com (JWK)
✔ Super admin subject: step
Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.
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 at feedback@smallstep.com or join GitHub Discussions
https://github.com/smallstep/certificates/discussions and our Discord
https://u.step.sm/discord.
Replace CA Certificates
Now we’ll move our CA certificates that we previously copied to /root and overwrite the certificates that were just generated.
sudo mv /root/root_ca.crt /root/intermediate_ca.crt /etc/step-ca/certs
We’ll also remove the generated private keys, since we’ll be using the ones stored on the Yubikey.
sudo rm -rf /etc/step-ca/secrets
Configure Step-CA for Yubikey
Next we’ll configure step-ca to use the Yubikey as the path to the keys. To do this, edit the ca.json with a text editor of your choice.
sudo nano /etc/step-ca/config/ca.json
Change the top part of the config as below. We’ll change the private key file path to the Yubikey slot where we imported the private key for the intermediate CA certificate. We’ll also add lines to specify the KMS (Key Management Service) type and pin. The default Yubikey pin is shown in the example. Be sure to change it to your own.
{
"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",
...
Test the CA
It’s time to try out our our CA. First let’s do a recursive chown on the /etc/step-ca directory to ensure the step user has rights to all files.
sudo chown -R step:step /etc/step-ca
Next we’ll manually start the service using our config file.
sudo -u step step-ca /etc/step-ca/config/ca.json
The output will include a Root Fingerprint. Copy and save this, as it will be needed in the next step. The service is running in the current terminal session, so do not end it.
badger 2026/01/19 16:24:20 INFO: All 1 tables opened in 1ms
badger 2026/01/19 16:24:20 INFO: Replaying file id: 0 at offset: 2986
badger 2026/01/19 16:24:20 INFO: Replay took: 7.185µs
2026/01/19 16:24:20 Building new tls configuration using step-ca x509 Signer Interface
2026/01/19 16:24:21 Starting Smallstep CA/ (linux/arm64)
2026/01/19 16:24:21 Documentation: https://u.step.sm/docs/ca
2026/01/19 16:24:21 Community Discord: https://u.step.sm/discord
2026/01/19 16:24:21 Config file: /etc/step-ca/config/ca.json
2026/01/19 16:24:21 The primary server URL is https://ca.hacksanity.lan:443
2026/01/19 16:24:21 Root certificates are available at https://ca.hacksanity.lan:443/roots.pem
2026/01/19 16:24:21 Additional configured hostnames: 172.16.253.3
2026/01/19 16:24:21 X.509 Root Fingerprint: 0c4ac4f3c09d01865ae5249b2a30418789c62700c1739f4fdb1bf3a005d7a8d6
2026/01/19 16:24:21 Serving HTTPS on :443 ...
In a new terminal session for your Raspberry Pi, run the step ca bootstrap command and specify your URL and Root Fingerprint saved from the previous session. This will download the root certificate from the certificate authority and configure the current environment to use it. You can also run it on any other system that has step-cli installed and network access to your CA.
step ca bootstrap --ca-url "https://ca.hacksanity.local" --fingerprint 0c4ac4f3c09d01865ae5249b2a30418789c62700c1739f4fdb1bf3a005d7a8d6
Now you can use the step ca certificate command to manually request a certificate. After running the previous bootstrap command to configure your environment, you no longer need to specify the CA URL or root fingerprint.
step ca certificate "test.hacksanity.local" test.crt test.key
✔ Provisioner: administrator@hacksanity.com (JWK) [kid: Z__uo94-QXAtwumLLdMhO6uUG-G85S_mpNmsgejBCww]
Please enter the password to decrypt the provisioner key:
✔ CA: https://ca.hacksanity.local
✔ Certificate: test.crt
✔ Private Key: test.key
Note: If you examine the certificate that we just issued here, you’ll find that the validity period is just 24 hours. This is the default validity period for step-ca, since the PKI industry is generally moving towards shorter certificate lifecycles and automation. In practice you’ll want to set a longer validity period or use an automated protocol such as ACME for renewals. The step command that we just used can also be configured for automation. See: https://smallstep.com/docs/step-ca/renewal/
Configure a Systemd Daemon
Now we’ll create a service for step-ca managed by systemd.
The original guide on Smallstep’s blog configures the step-ca service to start and stop with insertion and removal of the Yubikey. I was unable to get this to work reliably, so I have utilized their standard documentation here.
Create and edit a systemd unit file named step-ca.service.
$ sudo nano /etc/systemd/system/step-ca.service
Copy/paste the following contents into the file.
[Unit]
Description=step-ca service
Documentation=https://smallstep.com/docs/step-ca
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=30
StartLimitBurst=3
ConditionFileNotEmpty=/etc/step-ca/config/ca.json
ConditionFileNotEmpty=/etc/step-ca/password.txt
[Service]
Type=simple
User=step
Group=step
Environment=STEPPATH=/etc/step-ca
WorkingDirectory=/etc/step-ca
ExecStart=/usr/local/bin/step-ca config/ca.json --password-file password.txt
ExecReload=/bin/kill --signal HUP $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
StartLimitInterval=30
StartLimitBurst=3
; Process capabilities & privileges
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SecureBits=keep-caps
NoNewPrivileges=yes
; Sandboxing
ProtectSystem=full
ProtectHome=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
PrivateTmp=true
PrivateDevices=true
ProtectClock=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectKernelLogs=true
ProtectKernelModules=true
LockPersonality=true
RestrictSUIDSGID=true
RemoveIPC=true
RestrictRealtime=true
SystemCallFilter=@system-service
SystemCallArchitectures=native
MemoryDenyWriteExecute=true
ReadWriteDirectories=/etc/step-ca/db
[Install]
WantedBy=multi-user.target
Save the file and then run a systemctl daemon-reload.
sudo systemctl daemon-reload
Enable and start the service.
sudo systemctl enable --now step-ca
Check the status of the service to ensure it started successfully.
sudo systemctl status step-ca
If there are any issues, check the logs for the service for further details.
sudo journalctl --follow --unit=step-ca
Add an ACME Provisioner
Now that our CA is up and running, step-ca provides many different options for issuing certificates via provisioners. ACME is a popular example, so we’ll add an ACME provisioner here using the command below. In my example, hacksanity-acme is the name I have chosen for the name of the provisioner. You can use any name you want.
step ca provisioner add hacksanity-acme --type acme --admin-name step
That’s it. Now we can use ACME clients to submit certificate requests to the ACME URL for the provisioner, which will be https://{ca-host}/acme/{provisioner-name}/directory. In my example, it will be:
https://ca.hacksanity.lan/hacksanity-acme/acme/directory
Secure your CA
If you intend to make serious use of your CA, it will be important to protect it from any authorized access. The most effective way to do this is to enable the firewall and allow only the step-ca service through it. This also means disabling SSH access, so any future config changes will require access to your Pi via a keyboard and monitor.
First let’s create an application file for UFW (Uncomplicated Firewall) for the step-ca service.
sudo nano /etc/ufw/applications.d/step-ca-server
Put the following config and save the file.
[step-ca]
title=Smallstep CA
description=step-ca is an online X.509 and SSH Certificate Authority
ports=443/tcp
Now set the firewall to allow step-ca and enable it.
sudo ufw allow step-ca
sudo ufw enable
Finish!
If you made it this far successfully, congrats! This is not a simple topic and I hope my guide has been helpful to you. Have fun!