Managing digital certificates in-house is becoming increasingly important, not just for public HTTPS endpoints, but for securing internal infrastructure, onboarding devices, enabling mutual TLS, and managing identity across microservices.
Smallstep provides a modern, open-source tool called step-ca
that simplifies running your own private Certificate Authority. It supports short-lived certificates, secure provisioning with tokens, customizable templates, ACME, and even SSH certificates.
In this post, I’ll walk through how to install, initialize, and run step-ca
as a systemd service on Ubuntu. We’ll also explore how to define provisioners and templates, bootstrap trust from remote clients, and issue both server and client certificates securely using signed tokens.
As a practical example, we’ll issue a certificate for an IoT device, but the same setup can support a wide range of use cases, including Kubernetes clusters, CI/CD pipelines, VPN access, internal APIs, and more.
- Install Step CA on the Server
sudo apt-get update && sudo apt-get install -y --no-install-recommends curl vim gpg ca-certificates
sudo curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/trusted.gpg.d/smallstep.asc && \
echo 'deb [signed-by=/etc/apt/trusted.gpg.d/smallstep.asc] https://packages.smallstep.com/stable/debian debs main' \
| sudo tee /etc/apt/sources.list.d/smallstep.list
sudo apt-get update && sudo apt-get -y install step-cli step-ca
- Initialize the Certificate Authority
step ca init
Prompt | Answer |
---|---|
PKI Mode | standalone |
Name | Maksonlee.com Root CA |
DNS name | stepca.maksonlee.com |
Address | :443 |
Provisioner name | admin |
Password | changeme |
Save the password:
echo "changeme" > ~/.step/password.txt
- Set Up as a systemd Service
sudo useradd --user-group --system --home /etc/step-ca --shell /bin/false step
sudo setcap CAP_NET_BIND_SERVICE=+eip $(which step-ca)
sudo mkdir -p /etc/step-ca
sudo mv $(step path)/* /etc/step-ca
sudo chown -R step:step /etc/step-ca
sudo sed -i "s|/home/${USER}/.step|/etc/step-ca|g" /etc/step-ca/config/ca.json
Create a systemd unit:
sudo vi /etc/systemd/system/step-ca.service
[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/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
Start and enable the CA:
sudo systemctl daemon-reload
sudo systemctl status step-ca
sudo systemctl enable --now step-ca
sudo journalctl --follow --unit=step-ca
- Define Certificate Template
sudo vi /etc/step-ca/templates/iot-device.tpl
{
"subject": {
"country": "TW",
"province": "Taipei",
"locality": "Taipei",
"organization": "maksonlee.com",
"organizationalUnit": "Devices",
"commonName": "{{ .Subject.CommonName }}"
},
"sans": {{ toJson .SANs }},
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["clientAuth"],
"validity": "26280h",
"keyType": "RSA",
"keyBits": 2048,
"customExtensions": []
}
sudo chown -R step:step /etc/step-ca
- Manage Provisioners
Update existing admin
provisioner with new default and maximum durations for X.509 certificates
sudo step ca provisioner update admin \
--x509-default-dur 26280h \
--x509-max-dur 26280h \
--ca-url https://stepca.maksonlee.com \
--root /etc/step-ca/certs/root_ca.crt \
--ca-config /etc/step-ca/config/ca.json
Add a new iot
provisioner (client certs):
sudo step ca provisioner add iot \
--type JWK \
--create \
--x509-default-dur=26280h \
--x509-max-dur=26280h \
--x509-min-dur=26280h \
--x509-template /etc/step-ca/templates/iot-device.tpl \
--disable-renewal \
--ca-url https://stepca.maksonlee.com \
--root /etc/step-ca/certs/root_ca.crt \
--ca-config /etc/step-ca/config/ca.json
Restart CA:
sudo systemctl restart step-ca
- Issue Server Certificate
sudo step ca certificate "thingsboard.maksonlee.com" server.crt server.key \
--provisioner admin \
--san thingsboard.maksonlee.com \
--not-after 19800h \
--ca-url https://stepca.maksonlee.com \
--root /etc/step-ca/certs/root_ca.crt \
--provisioner-password-file /etc/step-ca/password.txt
Create chain:
sudo cat /etc/step-ca/certs/intermediate_ca.crt /etc/step-ca/certs/root_ca.crt > server-chain.pem
sudo chown administrator:administrator server.*
- Issue Client Certificate
- Bootstrap CA Trust on Clients
Run following command on CA server
sudo step certificate fingerprint /etc/step-ca/certs/root_ca.crt
Run following command on Client,
step ca bootstrap \
--ca-url https://stepca.maksonlee.com \
--fingerprint "fingerprint get from above command"
- Issue Client Certificate
step ca certificate "SN-000001" device.crt device.key --issuer iot