Getting Started With Smallstep

Estimated reading time: 7 minutes

Gerard Samuel Gerard Samuel's profile photo
Photo by Shahadat Rahman on Unsplash

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

--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