Generic certificate renewal manager for Smallstep CA. Designed for homelabs and internal infrastructure.
step-certctl provides a simple, config-driven approach to managing TLS certificates issued by Smallstep CA. It's designed to scale from a single Proxmox node to entire fleets of VMs, containers, and services.
- Config-driven: One config file per certificate
- Automatic renewal: Systemd timer-based background renewal
- Multi-service support: One package handles Proxmox, nginx, custom apps, etc.
- Smart reloading: Compares public keys to avoid unnecessary service restarts
- Flexible: Custom ownership, permissions, and reload commands per certificate
- Scalable: Templated systemd units handle multiple certificates per host
/usr/bin/step-certctl # Main command
/usr/lib/step-certctl/functions.sh # Shared functions
/etc/step-certctl/*.conf # Per-certificate configs
/etc/systemd/system/step-certctl@.service # Templated service
/etc/systemd/system/step-certctl@.timer # Templated timer
- You create a config file:
/etc/step-certctl/pveproxy.conf - Issue the certificate:
step-certctl issue pveproxy - Enable automatic renewal:
step-certctl install-timer pveproxy - The timer runs every 6 hours, renewing the certificate when needed
- If the certificate changes, the reload command runs automatically
# Install the package
sudo apt install ./step-certctl_0.1.1_all.deb
# Copy your Smallstep CA root certificate
sudo cp root_ca.crt /etc/step/certs/root_ca.crt# Clone the repository
git clone https://github.com/yourusername/step-certctl.git
cd step-certctl
# Run the build script
sudo ./build.sh installCreate /etc/step-certctl/pveproxy.conf:
# Certificate and key file paths
CERT_FILE=/etc/pve/local/pveproxy-ssl.pem
KEY_FILE=/etc/pve/local/pveproxy-ssl.key
# Smallstep CA configuration
CA_URL=https://stepca.example.com:9000
ROOT_CA=/etc/step/certs/root_ca.crt
# Certificate details
COMMON_NAME=pve01.example.com
SAN=pve01.example.com,pve01,10.0.0.10
# Renewal settings
EXPIRES_IN=8h
# Post-renewal action
RELOAD_CMD=systemctl reload pveproxy
# File ownership and permissions
OWNER=root
GROUP=www-data
CERT_MODE=0644
KEY_MODE=0600See examples/ directory for more configurations.
sudo step-certctl issue pveproxyThis will:
- Request a new certificate from your CA
- Save it to the configured paths
- Set ownership and permissions
- Run the reload command
sudo step-certctl install-timer pveproxyThis creates a systemd timer that renews the certificate every 6 hours.
# Check certificate status
sudo step-certctl validate pveproxy
# Check timer status
sudo systemctl status step-certctl@pveproxy.timer
# View upcoming renewal times
sudo systemctl list-timers step-certctl@*
# Check logs
sudo journalctl -u step-certctl@pveproxy.servicestep-certctl issue <name>Issues a new certificate based on the config file. Use this for:
- Initial certificate issuance
- Changing SANs or other certificate properties
- Recovering from expired certificates
step-certctl renew <name>Renews an existing certificate. The systemd timer calls this automatically.
Features:
- Compares public keys before/after renewal
- Only reloads service if certificate actually changed
- Backs up old certificate before replacing
step-certctl validate <name>Validates:
- Config file exists and is readable
- Certificate and key files exist
- Certificate is not expired
- Root CA is accessible
- CA endpoint is reachable
step-certctl install-timer <name>Enables and starts the systemd timer for automatic renewal.
step-certctl remove-timer <name>Stops and disables the systemd timer. The certificate remains unchanged.
step-certctl listShows all configured certificates with their status, expiry, and timer state.
step-certctl versionShows version and dependency information.
| Variable | Description | Example |
|---|---|---|
CERT_FILE |
Path to certificate file | /etc/nginx/tls/cert.pem |
KEY_FILE |
Path to private key file | /etc/nginx/tls/key.pem |
CA_URL |
Smallstep CA URL | https://stepca.example.com:9000 |
ROOT_CA |
Path to root CA certificate | /etc/step/certs/root_ca.crt |
COMMON_NAME |
Certificate common name | server.example.com |
| Variable | Description | Default | Example |
|---|---|---|---|
SAN |
Subject alternative names (comma-separated; use bare IPs — step-cli auto-detects; quote the value if it contains spaces) | ${COMMON_NAME} |
server.example.com,server,10.0.0.1 |
EXPIRES_IN |
Certificate lifetime (hours: 8h, 24h; days: 1d, 7d) |
8h |
24h, 4d, 168h |
RELOAD_CMD |
Command to run after renewal | (none) | systemctl reload nginx |
OWNER |
Certificate file owner | root |
nginx, www-data |
GROUP |
Certificate file group | root |
nginx, www-data |
CERT_MODE |
Certificate file permissions | 0644 |
0640 |
KEY_MODE |
Private key file permissions | 0600 |
0600 |
PROVISIONER |
Smallstep CA provisioner name | (CA default) | my-jwk, acme |
PROVISIONER_PASSWORD_FILE |
Path to provisioner key password file | (none) | /etc/step-certctl/provisioner.pass |
CERT_TEMPLATE |
Path to JSON file for certificate subject metadata | (none) | /etc/step-certctl/templates/default.tpl |
By default step ca certificate uses the CA's default provisioner. To select one explicitly and provide its password non-interactively:
PROVISIONER=my-jwk-provisioner
PROVISIONER_PASSWORD_FILE=/etc/step-certctl/provisioner.passThe password file should be owned by root and mode 0600:
echo "your-provisioner-password" | sudo tee /etc/step-certctl/provisioner.pass
sudo chmod 600 /etc/step-certctl/provisioner.passTo embed subject metadata (O, OU, C, etc.) in issued certificates, create a JSON file and reference it with CERT_TEMPLATE:
CERT_TEMPLATE=/etc/step-certctl/templates/default.tplExample template file (default.tpl):
{
"O": "My Org",
"OU": "Infrastructure",
"C": "US"
}The template is passed to step ca certificate via --set-file. CN and SANs are always sourced from COMMON_NAME and SAN in the config — they do not need to be in the template file.
See examples/templates/default.tpl for a starter template.
Manage pveproxy certificates across multiple Proxmox hosts:
# On each node, create /etc/step-certctl/pveproxy.conf
# Adjust COMMON_NAME and SAN per node
step-certctl issue pveproxy
step-certctl install-timer pveproxyProxmox-specific notes:
/etc/pve/local/is a FUSE filesystem (pmxcfs) that manages its own permissions —OWNER,GROUP,CERT_MODE, andKEY_MODEhave no effect there and should be omitted from the config.- Your Smallstep CA provisioner template must include
clientAuthin the Extended Key Usage. Without it, mTLS renewal will fail withtls: bad certificate. Example template:After updating the template, re-issue the certificate once — existing certs are not retroactively updated.{ "extKeyUsage": ["serverAuth", "clientAuth"] }
# Create /etc/step-certctl/nginx.conf
CERT_FILE=/etc/nginx/tls/fullchain.pem
KEY_FILE=/etc/nginx/tls/privkey.pem
CA_URL=https://stepca.example.com:9000
ROOT_CA=/etc/step/certs/root_ca.crt
COMMON_NAME=www.example.com
SAN=www.example.com,example.com,*.example.com
EXPIRES_IN=8h # hours (8h, 24h) or days (1d, 7d)
RELOAD_CMD=systemctl reload nginx
OWNER=root
GROUP=root# Create /etc/step-certctl/myapp.conf
CERT_FILE=/opt/myapp/tls/tls.crt
KEY_FILE=/opt/myapp/tls/tls.key
CA_URL=https://stepca.example.com:9000
ROOT_CA=/etc/step/certs/root_ca.crt
COMMON_NAME=myapp.example.com
EXPIRES_IN=8h # hours (8h, 24h) or days (1d, 7d)
RELOAD_CMD=systemctl restart myapp
OWNER=myapp
GROUP=myappYou can manage multiple certificates on the same host:
# Web server certificate
step-certctl issue nginx
step-certctl install-timer nginx
# API server certificate
step-certctl issue api
step-certctl install-timer api
# Internal service certificate
step-certctl issue internal
step-certctl install-timer internalEach gets its own config file and systemd timer instance.
The timer runs:
- 5 minutes after boot
- Every 6 hours after that
- With a randomized 15-minute delay to avoid thundering herd
View timer schedule:
systemctl list-timers step-certctl@*View service logs:
journalctl -u step-certctl@pveproxy.service# Check timer is enabled
systemctl status step-certctl@pveproxy.timer
# Check recent service runs
journalctl -u step-certctl@pveproxy.service -n 50
# Manually trigger renewal to see errors
sudo step-certctl renew pveproxy# Validate configuration
sudo step-certctl validate pveproxy
# Test CA connectivity
curl --cacert /etc/step/certs/root_ca.crt https://stepca.example.com:9000/health# Check current permissions
ls -la /etc/pve/local/pveproxy-ssl.*
# Re-issue to fix permissions
sudo step-certctl issue pveproxyIf you see chmod: Operation not permitted on Proxmox, the cert files are on pmxcfs which controls permissions itself. Remove OWNER, GROUP, CERT_MODE, and KEY_MODE from the config — pmxcfs already sets 0640 root:www-data which is correct for pveproxy.
If renewal fails with tls: bad certificate or x509: certificate specifies an incompatible key usage, the certificate's Extended Key Usage is missing clientAuth. The CA provisioner template only included serverAuth.
Fix on the CA server — update the provisioner template to include both:
{
"extKeyUsage": ["serverAuth", "clientAuth"]
}Then re-issue the certificate to pick up the new EKU:
sudo step-certctl issue pveproxyRenewals will work automatically after that.
The service only reloads if the certificate's public key changes. This is intentional to avoid unnecessary restarts.
To force a reload:
# Re-issue the certificate
sudo step-certctl issue pveproxy./build.shThis creates step-certctl_0.1.1_all.deb.
# Prepare package directory
mkdir -p pkg/usr/bin
mkdir -p pkg/usr/lib/step-certctl
mkdir -p pkg/etc/systemd/system
mkdir -p pkg/usr/share/doc/step-certctl/examples
mkdir -p pkg/DEBIAN
# Copy files
cp bin/step-certctl pkg/usr/bin/
cp lib/step-certctl-functions.sh pkg/usr/lib/step-certctl/
cp systemd/*.service systemd/*.timer pkg/etc/systemd/system/
cp examples/* pkg/usr/share/doc/step-certctl/examples/
cp debian/* pkg/DEBIAN/
# Build package
dpkg-deb --build pkg step-certctl_0.1.1_all.debCreate a role that:
- Installs the
step-certctlpackage - Copies the root CA
- Templates config files per host
- Issues initial certificates
- Enables timers
Example playbook:
- hosts: proxmox_nodes
tasks:
- name: Install step-certctl
apt:
deb: /path/to/step-certctl_0.1.1_all.deb
- name: Copy root CA
copy:
src: root_ca.crt
dest: /etc/step/certs/root_ca.crt
- name: Create pveproxy config
template:
src: pveproxy.conf.j2
dest: /etc/step-certctl/pveproxy.conf
- name: Issue certificate
command: step-certctl issue pveproxy
args:
creates: /etc/pve/local/pveproxy-ssl.pem
- name: Enable renewal timer
command: step-certctl install-timer pveproxyHost the .deb file in a local repository for easier distribution:
# On repo server
mkdir -p /var/www/apt/pool/main
cp step-certctl_0.1.1_all.deb /var/www/apt/pool/main/
cd /var/www/apt
dpkg-scanpackages pool/main /dev/null | gzip -9c > dists/stable/main/binary-amd64/Packages.gz
# On clients
echo "deb [trusted=yes] http://repo.example.com/apt stable main" | sudo tee /etc/apt/sources.list.d/local.list
sudo apt update
sudo apt install step-certctl- Private keys are set to
0600permissions by default - The systemd service runs as root (required to write to protected directories)
- Security hardening is applied:
PrivateTmp,ProtectSystem,NoNewPrivileges - Config files should be readable only by root:
chmod 600 /etc/step-certctl/*.conf - The root CA certificate must be authentic and protected
Contributions welcome. Please:
- Test on Debian/Proxmox systems
- Follow existing code style
- Update documentation for new features
- Add example configs for new use cases
MIT License - see LICENSE file
Built for managing certificates across homelab infrastructure using Smallstep CA.
Inspired by the need to move beyond one-off scripts to a maintainable, scalable solution.