step-ca is an open-source certificate authority from Smallstep that issues X.509 certificates for internal services. It speaks ACME, the same protocol Let's Encrypt uses, so tools like Certbot and Caddy can obtain certificates for hosts that are not reachable from the internet, such as db01.internal.example. In this tutorial you will install step-ca on Ubuntu 24.04, run it as an unprivileged systemd service, trust its root certificate on a client and issue certificates both manually and through ACME.

Prerequisites

To follow this guide you need:

  • A server for the CA running Ubuntu 24.04 LTS, for example a small CubePath VPS, with a non-root user that has sudo privileges. step-ca is lightweight; 1 GB of RAM is enough.
  • A DNS name for the CA that clients can resolve, such as ca.internal.example. This guide uses that name and the port 9000.
  • At least one client machine running Ubuntu 24.04 that will request certificates. For the ACME part, its hostname (for example web01.internal.example) must resolve from the CA server and its port 80 must be reachable from the CA.

Replace ca.internal.example and web01.internal.example with your own names throughout.

Step 1 - Installing step-ca and the step CLI

Smallstep publishes signed packages in its own APT repository. Install the tools needed to add it:

sudo apt update
sudo apt install curl gpg ca-certificates

Download the repository signing key into /etc/apt/keyrings and add the repository:

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o /etc/apt/keyrings/smallstep.asc
echo "deb [signed-by=/etc/apt/keyrings/smallstep.asc] https://packages.smallstep.com/stable/debian debs main" | sudo tee /etc/apt/sources.list.d/smallstep.list

Install the CA server (step-ca) and the command-line client (step-cli, which provides the step command):

sudo apt update
sudo apt install step-ca step-cli

Confirm both are available:

step version
step-ca version
Smallstep CLI/0.28.7 (linux/amd64)
Smallstep CA/0.28.4 (linux/amd64)

Your version numbers will be newer or different; any current release works with this guide.

Step 2 - Creating a service user and password files

Run the CA as a dedicated system user that owns its configuration directory, /etc/step-ca:

sudo useradd --system --user-group --home-dir /etc/step-ca --shell /usr/sbin/nologin step
sudo install -d -m 0700 -o step -g step /etc/step-ca

step-ca encrypts its private keys with a password that the service reads at startup. Generate a random one, plus a separate password for the admin provisioner that you will use to request certificates by hand:

openssl rand -base64 32 | sudo -u step tee /etc/step-ca/password.txt > /dev/null
openssl rand -base64 32 | sudo tee /root/step-provisioner-password.txt > /dev/null
sudo chmod 600 /etc/step-ca/password.txt /root/step-provisioner-password.txt

Print the provisioner password once and store it in your password manager; step ca certificate asks for it later:

sudo cat /root/step-provisioner-password.txt

Step 3 - Initializing the CA

step ca init creates a root CA, an intermediate CA that signs the actual certificates, and the configuration file. Run it as the step user with STEPPATH pointing at /etc/step-ca. The provisioner password file is copied to a location the step user can read only for the duration of the command:

sudo install -m 0400 -o step -g step /root/step-provisioner-password.txt /etc/step-ca/provisioner-password.tmp
sudo -u step env STEPPATH=/etc/step-ca step ca init \
  --deployment-type standalone \
  --name "Example Internal CA" \
  --dns ca.internal.example --dns localhost \
  --address :9000 \
  --provisioner admin \
  --password-file /etc/step-ca/password.txt \
  --provisioner-password-file /etc/step-ca/provisioner-password.tmp \
  --acme
sudo rm /etc/step-ca/provisioner-password.tmp

The --acme flag adds a second provisioner named acme. The output (abbreviated here) lists the generated files and the root fingerprint, which clients use to verify they are talking to the right CA:

Root certificate: /etc/step-ca/certs/root_ca.crt
Root private key: /etc/step-ca/secrets/root_ca_key
Root fingerprint: 5f2a0b6c3e8d...a91c
Intermediate certificate: /etc/step-ca/certs/intermediate_ca.crt
Intermediate private key: /etc/step-ca/secrets/intermediate_ca_key
Database folder: /etc/step-ca/db
Default configuration: /etc/step-ca/config/defaults.json
Certificate Authority configuration: /etc/step-ca/config/ca.json

You can print the fingerprint again at any time:

sudo step certificate fingerprint /etc/step-ca/certs/root_ca.crt

Step 4 - Running step-ca with systemd

Create a unit file based on the one Smallstep recommends for production:

sudo nano /etc/systemd/system/step-ca.service
[Unit]
Description=step-ca service
Documentation=https://smallstep.com/docs/step-ca
After=network-online.target
Wants=network-online.target
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 -HUP $MAINPID
Restart=on-failure
RestartSec=5
ProtectSystem=full
ReadWritePaths=/etc/step-ca/db
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

ProtectSystem=full makes /etc read-only for the service, so ReadWritePaths re-opens the database directory, the only place step-ca writes to. If command -v step-ca prints a different path than /usr/bin/step-ca, adjust ExecStart. Start the service and enable it at boot:

sudo systemctl daemon-reload
sudo systemctl enable --now step-ca
sudo systemctl status step-ca --no-pager

The log should end with the listening address:

step-ca[1234]: 2026/09/25 10:02:11 Serving HTTPS on :9000 ...

Check the health endpoint, trusting the new root explicitly:

curl --cacert /etc/step-ca/certs/root_ca.crt https://localhost:9000/health
{"status":"ok"}

Allow clients on your internal network to reach the CA. Replace 10.0.0.0/24 with your private subnet:

sudo ufw allow from 10.0.0.0/24 to any port 9000 proto tcp

Step 5 - Trusting the CA on a client

On each client, install step-cli using the same repository commands as in Step 1 (only the step-cli package). Then bootstrap it with the CA URL and the root fingerprint from Step 3. --install also adds the root certificate to the system trust store (it asks for your sudo password), so tools like curl and Certbot trust certificates from your CA:

step ca bootstrap --ca-url https://ca.internal.example:9000 \
  --fingerprint your_root_fingerprint --install
The root certificate has been saved in /home/your_user/.step/certs/root_ca.crt.
The authority configuration has been saved in /home/your_user/.step/config/defaults.json.
Installing the root certificate in the system truststore... done.

Verify the connection:

step ca health
ok

Step 6 - Issuing a certificate with the step CLI

The admin provisioner authenticates with the password you saved in Step 2. Request a certificate for the client:

step ca certificate web01.internal.example web01.crt web01.key

Choose the admin provisioner if prompted and enter its password. Inspect the result:

step certificate inspect web01.crt --short
X.509v3 TLS Certificate (ECDSA P-256) [Serial: 2461...8830]
  Subject:     web01.internal.example
  Issuer:      Example Internal CA Intermediate CA
  Provisioner: admin [ID: 3sd7...Qk9M]
  Valid from:  2026-09-25T10:10:44Z
          to:  2026-09-26T10:11:44Z

By default step-ca issues certificates valid for 24 hours. Short lifetimes are the point: you renew often instead of relying on revocation. Renew before expiry with the existing certificate as proof of identity, no password required:

step ca renew --force web01.crt web01.key

For a long-running service, step ca renew --daemon web01.crt web01.key keeps renewing in the background, and --exec "systemctl reload nginx" can reload the service after each renewal.

Step 7 - Issuing certificates with ACME and Certbot

The acme provisioner exposes a directory at https://ca.internal.example:9000/acme/acme/directory. Check it from the client:

curl -s https://ca.internal.example:9000/acme/acme/directory
{"newNonce":"https://ca.internal.example:9000/acme/acme/new-nonce","newAccount":"https://ca.internal.example:9000/acme/acme/new-account","newOrder":"https://ca.internal.example:9000/acme/acme/new-order","revokeCert":"https://ca.internal.example:9000/acme/acme/revoke-cert","keyChange":"https://ca.internal.example:9000/acme/acme/key-change"}

A 24-hour certificate is awkward for Certbot, which checks for renewal twice a day. On the CA server, raise the default and maximum lifetime for the ACME provisioner to 30 and 90 days, then reload the CA:

sudo -u step env STEPPATH=/etc/step-ca step ca provisioner update acme \
  --x509-default-dur 720h --x509-max-dur 2160h
sudo systemctl reload step-ca

On the client, install Certbot and request a certificate from your CA instead of Let's Encrypt. The standalone authenticator starts a temporary web server on port 80, which the CA connects to for the HTTP-01 challenge:

sudo apt install certbot
sudo certbot certonly --standalone \
  --server https://ca.internal.example:9000/acme/acme/directory \
  -d web01.internal.example \
  --register-unsafely-without-email --agree-tos
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/web01.internal.example/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/web01.internal.example/privkey.pem

Certbot saves the custom --server in the renewal configuration, so the certbot.timer that ships with the package renews it automatically. If a web server already uses port 80, use --nginx or --webroot -w /var/www/html instead of --standalone, exactly as you would with Let's Encrypt.

Confirm the issuer:

sudo openssl x509 -in /etc/letsencrypt/live/web01.internal.example/cert.pem -noout -issuer -enddate
issuer=O = Example Internal CA, CN = Example Internal CA Intermediate CA
notAfter=Oct 25 10:20:31 2026 GMT

Troubleshooting

step ca bootstrap fails with "fingerprint mismatch". You copied the fingerprint incorrectly, or the name resolves to another host. Print it again on the CA with step certificate fingerprint /etc/step-ca/certs/root_ca.crt.

Certbot reports "certificate verify failed". The client does not trust the root yet. Run step certificate install $(step path)/certs/root_ca.crt or repeat the bootstrap with --install.

ACME order fails with a connection error in the challenge. The CA must resolve web01.internal.example and reach port 80 on it. Test from the CA server with curl -I http://web01.internal.example and check the firewall on the client (sudo ufw allow 80/tcp).

The service does not start. Read the log with sudo journalctl -u step-ca -n 50. A wrong password in password.txt shows as a decryption error, and a syntax error in ca.json shows the offending line.

Conclusion

You now run a private certificate authority with step-ca that issues short-lived certificates through its own CLI and standard ACME clients, with the root trusted on your clients. Useful next steps are moving the root key offline, putting step-ca behind your internal DNS for all hosts, and using the same CA for mutual TLS by issuing client certificates to services.