Deploying Step CA: Your Own Certificate Authority for Internal Services

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.


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

  1. Initialize the Certificate Authority
step ca init
PromptAnswer
PKI Modestandalone
NameMaksonlee.com Root CA
DNS namestepca.maksonlee.com
Address:443
Provisioner nameadmin
Passwordchangeme

Save the password:

echo "changeme" > ~/.step/password.txt

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

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

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

  1. 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.*

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top