Intro#
I wanted a way to get valid, trusted TLS certificates for all my internal tools (like Proxmox itself, my UniFi controller, etc.) without the need to configure Let’s Encrypt on every device or use a reverse proxy. The goal was to have a central, automated service inside my network that could talk to Let’s Encrypt on behalf of my local devices.
This is where step-ca comes in. It can act as a “Registration Authority” (RA). In this setup, it will take certificate requests from my local devices, use the ACME DNS challenge with my OVH domain to get a real certificate from Let’s Encrypt, and then hand it back to the device.
This guide walks through setting up step-ca in a lightweight Proxmox LXC container to do exactly that. We’ll use SCEP for the local devices to request their certificates, which is well-supported by many systems.
Part 1: The Foundation - Proxmox LXC#
First, we need a home for our new service. A minimal LXC container is perfect for this – it’s lightweight and easy to manage.
Step 1: Create the LXC Container#
You can do this through the Proxmox web UI.
I’m giving it 1 core and 512MB of RAM, which is more than enough for step-ca. Make sure to adjust the storage (syn-iscsi-lvm, my Synology storage) and network bridge (vmbr0) to match your Proxmox setup.
(Here you can add a screenshot of the LXC container in the Proxmox UI)
Part 2: Installing the Star of the Show - Step-CA#
Now let’s get into the container and install the step-ca software.
Step 2: Install step-ca#
Start the container and open a console to it.
# Download and install the latest step-ca package
apt-get update && apt-get install -y --no-install-recommends curl vim gpg ca-certificates
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' \
| tee /etc/apt/sources.list.d/smallstep.list
apt-get update && apt-get -y install step-cli
Step 3: Initialize the CA#
With the software installed, we need to create the initial configuration. The step ca init command makes this easy.
step ca init
This will lead to a guided setup of step-ca.
root@step-ca:~# step ca init
✔ Deployment Type: Standalone
What would you like to name your new PKI?
✔ (e.g. Smallstep): knet-ca
What DNS names or IP addresses will clients use to reach your CA?
✔ (e.g. ca.example.com[,10.1.2.3,etc.]): ca.kohnkenet.de
What IP and port will your new CA bind to? (:443 will bind to 0.0.0.0:443)
✔ (e.g. :443 or 127.0.0.1:443): :443
What would you like to name the CA's first provisioner?
✔ (e.g. you@smallstep.com): bjarne
Choose a password for your CA keys and first provisioner.
✔ [leave empty and we'll generate one]:
✔ Password: very-strong-password
Generating root certificate... done!
Generating intermediate certificate... done!
✔ Root certificate: /root/.step/certs/root_ca.crt
✔ Root private key: /root/.step/secrets/root_ca_key
✔ Root fingerprint: e1607ee440b3b8873c71c6b83708cef5c7f32076c2682481ea33249b93432f16
✔ Intermediate certificate: /root/.step/certs/intermediate_ca.crt
✔ Intermediate private key: /root/.step/secrets/intermediate_ca_key
✔ Database folder: /root/.step/db
✔ Default configuration: /root/.step/config/defaults.json
✔ Certificate Authority configuration: /root/.step/config/ca.json
Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.
FEEDBACK 😍 🍻
The step utility is not instrumented for usage statistics. It does not phone
home. But your feedback is extremely valuable. Any information you can provide
regarding how you’re using `step` helps. Please send us a sentence or two,
good or bad at feedback@smallstep.com or join GitHub Discussions
https://github.com/smallstep/certificates/discussions and our Discord
https://u.step.sm/discord.
root@step-ca:~#
After this, you’ll have a new directory at /root/.step/ containing all your CA’s configuration.
root@step-ca:~# ls -al .step/
total 28
drwx------ 7 root root 4096 Sep 20 20:32 .
drwx------ 5 root root 4096 Sep 20 20:31 ..
drwx------ 2 root root 4096 Sep 20 20:32 certs
drwx------ 2 root root 4096 Sep 20 20:32 config
drwx------ 2 root root 4096 Sep 20 20:32 db
drwx------ 2 root root 4096 Sep 20 20:32 secrets
drwx------ 2 root root 4096 Sep 20 20:31 templates
Part 3: Connecting to the Outside World#
Now for the next part: configuring step-ca to talk to Let’s Encrypt using our OVH domain for DNS validation.
Step 4: Configure step-ca as an ACME RA#
We need to tell our CA that it’s not the final authority, but rather a proxy for Let’s Encrypt. Edit the main config file at /root/.step/config/ca.json and add the authority block.
{
"root": "/root/.step/certs/root_ca.crt",
"federatedRoots": null,
"crt": "/root/.step/certs/intermediate_ca.crt",
"key": "/root/.step/secrets/intermediate_ca_key",
"address": ":443",
"insecureAddress": "",
"dnsNames": [
"ca.kohnkenet.de"
],
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"dataSource": "/root/.step/db",
"badgerFileLoadingMode": ""
},
"authority": {
"provisioners": [
{
"type": "JWK",
"name": "bjarne",
"key": {
"use": "sig",
"kty": "EC",
"kid": "EkBhqwefas5wvMP1tq2w_-asdgANsGTasfsdgRU",
"crv": "P-256",
"alg": "ES256",
"x": "54JxXDPlKmu30J26YVr-MxPdfggfdx4RY",
"y": "STg7sdfsdfEQ_GNgdfgdfg5FVvAY9KZqps"
},
"encryptedKey": "eyJhbGciOiJQQksdfsdfIsImVuYyI6IkEyNTZHQ00sdfsdfjRfOUZVVGxWaW5HazQ1dyJ9.mKWptI3v8_KIG_VQMdLo45rz6GQYtwIGyTtbKGs1>"
},
{
"type": "acme",
"ca": "https://acme-v02.api.letsencrypt.org/directory"
}
]
},
"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
}
}
Step 5: Configure the OVH DNS Challenge#
Next, we’ll add the ACME issuer configuration, telling step-ca how to use the OVH API to solve DNS challenges. You’ll need to generate OVH API credentials (applicationKey, applicationSecret, consumerKey) from your OVH control panel first.
Create the OVH API key as followed:

We need the GET, PUT, POST and DELETE rights for the following API endpoint:
/domain/zone/*
If you only need a single domain put it after the zone like this:
/domain/zone/kohnkenet.de/*
If you use a different provider for your DNS have a look at the certbot-dns-ovh documentation. Here is the one for OVH: https://certbot-dns-ovh.readthedocs.io/en/stable/
Add the acme block inside the authority block in your ca.json so that it looks like this:
{
"authority": {
"type": "acme",
"ca": "https://acme-v02.api.letsencrypt.org/directory",
"acme": {
"email": "your-email@example.com",
"challenges": [
{
"type": "dns-01",
"provider": {
"name": "ovh",
"endpoint": "ovh-eu",
"applicationKey": "YOUR_APP_KEY",
"applicationSecret": "YOUR_APP_SECRET",
"consumerKey": "YOUR_CONSUMER_KEY"
}
}
]
}
}
}
Part 4: Serving Certificates Internally#
The final piece is to configure how our local devices can request these certificates. We’ll set up a SCEP provisioner.
Step 6: Configure a SCEP Provisioner#
Add a scep provisioner to the provisioners array in your ca.json. This defines a shared secret that SCEP clients will use to authenticate.
{
"provisioners": [
{
"type": "scep",
"name": "my-scep-provisioner",
"forceCN": true,
"challenge": {
"type": "static",
"password": "a-very-secret-scep-password"
}
}
]
}
Make sure to use a strong, unique password for your SCEP challenge.
Step 7: Running step-ca as a service#
To let step-ca always run as a service you need to add a user to run the service as, move the files to a global directory and then add the service file. We also need to provide a way for the service to decrypt the private key if we ever want to use it as an actual internal CA.
mkdir /etc/step-ca
mv .step/* /etc/step-ca
export STEPPATH=/etc/step-ca
sudo useradd --user-group --system --home /etc/step-ca --shell /bin/false step
Replace /root/.step/ with /etc/step-ca in the /etc/step-ca/ca.json and the /etc/step-ca/defaults.json file.
Create the password file with your password from the initialization
nano /etc/step-ca/password.txt
sudo chown -R step:step /etc/step-ca
Create the systemd file with the following content:
[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
Then restart the systemctl daemon and enable and start the service:
systemctl daemon-reload
systemctl enable step-ca
systemctl start step-ca
systemctl status step-ca
systemctl restart step-ca
Conclusion#
And that’s it! You now have a fully functional, internal certificate authority that can issue valid Let’s Encrypt certificates to your local devices.
You can now point your devices (like a Synology NAS, a UniFi controller, or anything that supports SCEP) to https://step-ca.your.lan:8443/scep/scep and use the shared secret you configured. The devices will get a valid certificate for their local domain name, and your browsers will be happy!
