Getting Started With Smallstep
Estimated reading time: 7 minutes

I needed to host an internal PKI (Private Key Infrastructure) to test a secrets management solution. Microsoft Windows PKI requires a complete Active Directory setup, which is overkill for what I needed. Plus, I wanted something open-source. Smallstep’s step-ca is open source and is a well-featured private key solution. This post will explain how I set it up using a Nitrokey HSM on a Raspberry Pi 4.
Requirements
For this project, I am going to use:
- (1) Raspberry Pi 4 2GB
- Ubuntu 24.04 LTS
- Nitrokey HSM
- At least one USB thumb drive to store data offline
The rest of this article assumes that the operating system is already functioning.
Install pre-requisite software
- Log in and get the latest package list
sudo apt update
- Set the hostname
sudo hostnamectl set-hostname ca --static
- Set the timezone
sudo timedatectl set-timezone America/New_York
- Disable the timesyncd service
sudo systemctl disable --now systemd-timesyncd
- Install required packages
sudo apt install --yes \
chrony \
gcc \
hsmwiz \
golang-go \
gnutls-bin \
libpcsclite-dev \
make \
opensc \
pkg-config
- Add the following configuration files for Chrony time synchronization. Here, I have an NTP server at 172.16.2.1. Adjust to your circumstances.
sudo tee /etc/chrony/sources.d/homelab-ntp-server.sources << EOF > /dev/null
server 172.16.2.1 prefer iburst minpoll 2 maxpoll 2 xleave
pool us.pool.ntp.org iburst maxsources 2
EOF
sudo tee /etc/chrony/conf.d/homelab-ntp.conf << EOF > /dev/null
driftfile /var/lib/chrony/chrony.drift
makestep 0.1 3
rtcsync
keyfile /etc/chrony/chrony.keys
logdir /var/log/chrony
leapsectz right/UTC
EOF
- Restart the Chrony service
sudo systemctl restart chronyd
- Enable and start the smart card daemon
sudo systemctl enable pcscd
sudo systemctl start pcscd
- Install all updates and reboot
sudo apt --yes full-upgrade && reboot
All required software, but step/step-ca is now installed. Now, on to configure the solution.
HSM Configuration and installation of step-ca/step/step-kms-plugin
- Insert the USB thumb drive and prepare it
sudo fdisk /dev/sda
sudo mkfs.ext4 /dev/sda1 -v
sudo mount /dev/sda1 /mnt
sudo mkdir /mnt/ca
sudo chown $USER:$USER /mnt/ca
- Create a DKEK share and save to the USB thumb drive
sc-hsm-tool --create-dkek-share /mnt/ca/dkek-share-stepca.pbe
- Insert the NitroKey HSM2 and initialize it
--so-pin
must be composed of 16 hexadecimal characters
--pin
is recommended to be 6 numeric characters
# If brand-new
sudo sc-hsm-tool --initialize \
--so-pin <new-so-pin> \
--pin <new-user-pin> \
--label "step-ca_hsm" \
--dkek-shares 1
sudo sc-hsm-tool --import-dkek-share /mnt/ca/dkek-share-stepca.pbe
# Reformatting back to new and initialize
sudo hsmwiz format --so-pin <current-so-pin>
sudo sc-hsm-tool --initialize \
--so-pin 3537363231383830 \
--pin 648219 \
--label "step-ca_hsm" \
--dkek-shares 1
sudo hsmwiz changepin --old 3537363231383830 --new <new-so-pin> --affect-so-pin
sudo hsmwiz changepin --old 648219 --new <new-user-pin>
# Optional: Verify PINs
sudo hsmwiz verifypin --pin <new-user-pin>
sudo hsmwiz verifypin --pin <new-so-pin> --verify-sopin
sudo sc-hsm-tool --import-dkek-share /mnt/ca/dkek-share-stepca.pbe
- Download the latest version of step and step-ca (with CGO support)
# Install step-ca
curl -fsSLO https://github.com/smallstep/certificates/archive/refs/tags/v0.27.2.tar.gz
mkdir step-ca
tar -xf v0.27.2.tar.gz -C step-ca/
cd step-ca/certificates-0.27.2/
make bootstrap && make build GO_ENVS="CGO_ENABLED=1"
sudo cp certificates/bin/step-ca /usr/local/
cd ~ && rm -rf go step-ca *.tar.gz
# Validate step-ca
step-ca version
# Install step-kms-plugin
curl -fsSLO https://github.com/smallstep/step-kms-plugin/releases/download/v0.11.4/step-kms-plugin_0.11.4_linux_arm64.tar.gz
tar -xf step-kms-plugin_0.11.4_linux_arm64.tar.gz
sudo cp step-kms-plugin_0.11.4/step-kms-plugin /usr/local/bin/
# Install step
curl -fsSLO https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.1/step_linux_0.27.1_armv7.tar.gz
tar -xvf step_linux_0.27.1_armv7.tar.gz
sudo mv step_0.27.1/bin/step /usr/local/bin
# Validate step
step version
Sweet! Now, let me create the actual PKI.
Creating the Private Key Infrastructure
Now, let me collect some information so that I can continue.
# Get the label for the HSM
TOKEN=$(sudo pkcs11-tool --list-token-slots | grep 'token label' | cut -d ':' -f 2 | xargs)
# Get the PKCS11 module path
PKCS_MODULE_PATH=$(p11-kit list-modules | grep 'opensc-pkcs11.so' | cut -d ':' -f 2 | xargs)
# Assign the card's user-pin to a variable
read -s PKCS_USER_PIN
# Now construct the full URI
PKCS_URI="pkcs11:module-path=${PKCS_MODULE_PATH};token=${TOKEN}?pin-value=${PKCS_USER_PIN}"
- Write out a random private key decryption password to the USB disk. This provisioner password will be required when provisioning certificates.
echo $(step crypto rand 32 --format=ascii) > /mnt/ca/decrpyt-pass.txt
-
Decide what ID numerical (I believe these are binary) strings you’ll use for the root and intermediate keys. In the next few steps, I use 1000 and 1001 for the root and intermediate keys, respectively.
-
Let’s generate the root private key
sudo step kms create \
--json \
--kty "EC" \
--crv "P384" \
--kms "$PKCS_URI" "pkcs11:id=1000;object=root-ca"
- Now sign the root certificate using the “name” value from the JSON output
sudo step certificate create \
--profile root-ca \
--kms "$PKCS_URI" \
--key "pkcs11:id=1000;object=root-ca" \
"HTE Root CA" /mnt/ca/root_ca.crt
- Create the Intermediate private key
sudo step kms create \
--json \
--kty "EC" \
--crv "P384" \
--kms "$PKCS_URI" "pkcs11:id=1001;object=intermediate-ca"
- Sign the Intermediate certificate using the root certificate and key stored on the HSM
# --ca-key is set to the root key id
# --key is set to the name of the root key
sudo step certificate create --profile intermediate-ca \
--kms "$PKCS_URI" \
--ca-kms "$PKCS_URI" \
--ca /mnt/ca/root_ca.crt \
--ca-key "pkcs11:id=1000;object=root-ca" \
--key "pkcs11:id=1001;object=intermediate-ca" \
"HTE Intermediate CA" /mnt/ca/intermediate_ca.crt
- List available keys on the Nitrokey HSM and make a note of the “ID” fields in the output
sudo pkcs15-tool --list-keys
- Export the private key wrappers for root and intermediate private keys, substituting “<reference_id>” for each key’s ID in the previous step
sudo sc-hsm-tool \
--wrap-key /mnt/ca/root_wrap.bin \
--key-reference <reference_id> \
--pin <current-user-pin>
sudo sc-hsm-tool \
--wrap-key /mnt/ca/intermediate_wrap.bin \
--key-reference <reference_id> \
--pin <current-user-pin>
The public key pairs for the certificate authority have been created. Let me configure step-ca to use these.
Configuring step-ca
- Initialize step-ca, substituting values for:
- CA_NAME, SERVER_FQDN
- SERVER_IP_ADDRESS
- AN_EMAIL_ADDRESS_OR_UNIQUE_STRING
sudo mkdir /etc/step-ca
export STEPPATH="/etc/step-ca"
sudo --preserve-env step ca init \
--name="<CA_NAME>" \
--dns="<SERVER_FQDN>,<SERVER_IP_ADDRESS>" \
--address=":443" \
--provisioner="<AN_EMAIL_ADDRESS_OR_UNIQUE_STRING>" \
--deployment-type="standalone" \
--remote-management \
--password-file="/mnt/ca/decrpyt-pass.txt"
- Delete the default step-ca certificates and move our custom certificates to /etc/step-ca/certs
sudo shred -u /etc/step-ca/secrets/root_ca_key
sudo shred -u /etc/step-ca/secrets/intermediate_ca_key
sudo cp -v /mnt/ca/{root,intermediate}_ca.crt /etc/step-ca/certs/
- Reset the step-ca defaults.json file for the custom root certificate
export FINGERPRINT=$(step certificate fingerprint /etc/step-ca/certs/root_ca.crt)
sed -i -r "s/^(\s+\"fingerprint\": ).*/\1\"${FINGERPRINT}\",/" /etc/step-ca/config/defaults.json
- Write a new configuration file for step-ca, substituting
<INTERMEDIATE_KEY_ID>
for the value that was chosen earlier
sudo tee /etc/step-ca/config/ca.json << EOF > /dev/null
{
"root": "/etc/step-ca/certs/root_ca.crt",
"federatedRoots": null,
"crt": "/etc/step-ca/certs/intermediate_ca.crt",
"key": "pkcs11:id=<INTERMEDIATE_KEY_ID>;object=intermediate-ca",
"kms": {
"type": "pkcs11",
"uri": "pkcs11:module-path=/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so;token=step-ca_hsm (UserPIN)?pin-value=${PKCS_USER_PIN}"
},
"address": ":443",
"insecureAddress": "",
"dnsNames": [
"<SERVER_FQDN>",
"<SERVER_IP_ADDRESS>"
],
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"dataSource": "/etc/step-ca/db",
"badgerFileLoadingMode": ""
},
"authority": {
"enableAdmin": true
},
"tls": {
"cipherSuites": [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
],
"minVersion": 1.2,
"maxVersion": 1.3,
"renegotiation": false
}
}
EOF
- Perform a smoke test by starting step-ca manually. If everything is configured correctly, you should not get an error and can validate that it is running via journalctl. When satisfied, shut down the process
step-ca /etc/step-ca/config/ca.json
- Optionally, clone the USB drive to have a backup of all configuration material
sudo dd if=/dev/sda of=/dev/sdb bs=1M
- Unmount the USB drive and lock it away
sudo umount /mnt
Bringing step-ca into production
All software has been installed, certificates created, step-ca configured and tested. Now let me configure step-ca to run under systemd.
- Create a service account
sudo useradd \
--user-group --system \
--home-dir /etc/step-ca --shell /bin/false \
step
- Allow the service to use ports less than 1024
sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
- Let the service account be the owner of all files/folders in /etc/step-ca
sudo chown -R step:step /etc/step-ca
- Create a systemd unit file for step-ca
sudo tee /etc/systemd/system/step-ca.service << EOF > /dev/null
[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
[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
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
EOF
- Grant the service account rights to access pcsc and the HSM by adding a polkit rule file
sudo tee /etc/polkit-1/rules.d/step-ca.rules << EOF > /dev/null
polkit.addRule(function(action, subject) {
if (action.id == "org.debian.pcsc-lite.access_pcsc" &&
subject.user == "step") {
return polkit.Result.YES;
}
});
polkit.addRule(function(action, subject) {
if (action.id == "org.debian.pcsc-lite.access_card" &&
action.lookup("reader") == 'Nitrokey Nitrokey HSM (DENK03018290000 ) 00 00' &&
subject.user == "step") {
return polkit.Result.YES; }
});
EOF
- Enable and start the service. Validate that no errors are produced
sudo systemctl daemon-reload
sudo systemctl enable --now step-ca
sudo journalctl --follow --unit=step-ca
- Create a test certificate. You will be prompted for the provisioner password that was generated earlier
sudo step ca certificate "localhost" localhost.crt localhost.key --ca-url https://ca.lab.howto.engineer --root /etc/step-ca/certs/root_ca.crt
# Output below
✔ Provisioner: REDACTED (JWK) [kid: REDACTED]
Please enter the password to decrypt the provisioner key:
✔ CA: https://my.server.fqdn
✔ Certificate: localhost.crt
✔ Private Key: localhost.key
Conclusion
In this post, I showed how to prepare and install Smallstep’s step-ca certificate authority with a hardware HSM from Nitrokey. All running on a Raspberry Pi 4, providing certificate services to your environment. In a future post, I will dive into step-ca features such as ACME and SSH