Public TLS certificates from Let’s Encrypt work great for internet-facing
services, but they fall short for internal-only hosts. If you run a
homelab with services on .internal, .local, or RFC 1918 IP addresses,
you cannot get publicly trusted certificates for them. The typical
workaround — self-signed certificates — means browsers throw security
warnings, your monitoring tools fail TLS checks, and every new service
requires manual certificate generation and distribution.
You need an internal Certificate Authority (CA) that your devices trust, combined with ACME automation so you never touch a certificate by hand.
step-ca from Smallstep is the best tool for this job. It runs in a single Docker container, speaks ACME (the same protocol Let’s Encrypt uses), supports DNS-01 and HTTP-01 challenges, and integrates directly with Traefik, Caddy, and certbot.
This guide walks through deploying step-ca in Docker, bootstrapping clients, setting up ACME, integrating with Traefik for automatic certificate provisioning, and distributing the root CA to every device in your homelab.
Why an Internal CA Beats Self-Signed Certificates
Self-signed certificates work for testing, but they have real problems in a homelab:
- Browser warnings: Every self-signed cert triggers a full-page “Your connection is not private” warning with no easy way to trust it permanently.
- No automation: You regenerate and redistribute certs manually every time they expire.
- No revocation: If a key is compromised, you cannot revoke the certificate.
An internal CA solves all three. You install the root CA once on each device, and every certificate signed by that CA is automatically trusted. ACME automates issuance and renewal. And step-ca supports certificate revocation via CRL and OCSP if needed.
Deploying step-ca with Docker Compose
Create a project directory and a docker-compose.yml for step-ca:
|
|
Bind the port to a specific IP (your Docker host’s LAN address) instead of
0.0.0.0 to avoid exposing the CA to VLANs that should not talk to it
directly.
Fire it up:
|
|
On first run, the container initializes the CA. It generates a root certificate, an intermediate certificate, a password for the encrypted CA keys, and the initial JWK provisioner. The CA password is saved inside the Docker volume.
Retrieve the fingerprint needed for client bootstrapping:
|
|
Save this fingerprint — you need it for every client that bootstraps with the CA.
Retrieve the automatically generated CA password:
|
|
You need this password when issuing certificates with the admin provisioner.
Installing the step CLI on Clients
Every machine that needs to interact with the CA — your Docker host,
workstations, or other servers — needs the step CLI.
On Debian/Ubuntu
|
|
On macOS
|
|
On Alpine Linux
|
|
Bootstrapping Clients
With the CLI installed and the CA running, bootstrap a client:
|
|
This downloads the root CA certificate, saves it to ~/.step/certs/root_ca.crt,
and writes the defaults configuration. After bootstrapping, you can issue
certificates directly from the client:
|
|
You are prompted for the admin provisioner password (from the secrets file). The result is a TLS certificate signed by your internal CA, valid for 24 hours by default.
Enabling the ACME Provisioner
The JWK provisioner works fine for manual operations, but ACME is where step-ca really shines. Add an ACME provisioner to the CA configuration.
Read the current CA config from the volume:
|
|
Look for the "provisioners" array. Add an ACME provisioner entry: create
a file called ca.json on the volume path by copying the existing config
and adding:
|
|
The easier approach — edit the config file stored in the Docker volume. On the Docker host:
|
|
Now any ACME client — Traefik, Caddy, or certbot — can request certificates from your internal CA.
Integrating with Traefik
If you run Traefik as your reverse proxy (covered in our Traefik setup guide), adding the internal CA as an ACME certificate provider is straightforward.
First, make Traefik trust your internal CA. Mount the root certificate and update the Traefik static configuration:
|
|
And mount the root CA into the Traefik container:
|
|
Run update-ca-certificates inside the container or rebuild the image with
the CA bundled. For the easiest path, bind-mount the CA file and configure
Traefik to use the system trust store by adding to the entrypoint.
Now annotate services that need internal certificates with the internal
resolver in their Docker labels:
|
|
Traefik automatically requests a certificate from step-ca via ACME for
grafana.internal, renews it before expiry, and terminates TLS.
DNS-01 Challenges for Wildcard Certificates
For wildcard certificates like *.internal, use DNS-01 instead of HTTP-01.
step-ca supports DNS-01 out of the box, and you can pair it with any DNS
provider that has an API.
If you run your own DNS server (like Pi-hole or Unbound), the simplest approach is a manual DNS challenge script. Create a script that the ACME client calls to set and clear TXT records:
|
|
Or use certbot with step-ca’s ACME endpoint and the DNS plugin for your provider:
|
|
Managing Certificate Lifetime
step-ca issues short-lived certificates by default — 24 hours — which means renewal is frequent but revocation risk is minimal. For homelab use, 24 hours is short enough to be annoying if something breaks. Extend the default via the CA configuration:
|
|
This sets the default to 30 days (720h), with a maximum of 90 days (2160h). ACME clients can request a specific duration within this range.
Distributing the Root CA to Clients
Every device that needs to trust your internal certificates must have the root CA installed. Here is how to distribute it to common platforms.
Linux (Debian/Ubuntu)
|
|
For other machines, copy the root CA file and run the same commands.
Windows (via Group Policy or script)
|
|
macOS
|
|
Android
Copy the .crt file to the device, then go to Settings → Security →
Encryption & credentials → Install a certificate → CA certificate and
select the file.
iOS/iPadOS
Send the .crt file via email or AirDrop, tap to install, then go to
Settings → General → About → Certificate Trust Settings and enable
the root CA.
Firefox (separate trust store)
Firefox uses its own certificate store, not the system one:
|
|
Health Checks and Monitoring
Verify the CA is running and healthy:
|
|
Add step-ca to your monitoring stack:
|
|
Monitor certificate expiry for your critical services using
Uptime Kuma or
Prometheus Blackbox Exporter
with the ssl_certificate_expiry probe. Set alerts at 7, 3, and 1 day
before expiry.
Security Considerations
Running an internal CA is powerful, but treat it with care:
- Keep the CA password safe: The encrypted root key is in the Docker
volume’s
secrets/directory. Backup this directory, but never share the password. - Restrict network access: Bind step-ca to a management IP or use firewall rules to limit who can reach port 9000.
- Use a dedicated volume: Step-ca stores private keys on the Docker volume. Keep regular backups.
- Rotate provisioner keys: The admin JWK provisioner is powerful. Create separate ACME provisioners with shorter lifespans for different use cases.
- Audit certificate issuance: step-ca logs every certificate request. Monitor logs for unexpected issuance.
Backup the step-ca volume regularly:
|
|
To restore, extract the archive into a new volume of the same name.
Summary
An internal CA with ACME support is the missing piece in most homelab TLS setups. With step-ca in Docker you get:
- Automatic certificates for any internal hostname or IP
- No browser warnings once the root CA is trusted
- Traefik integration for seamless reverse proxy TLS
- Short-lived certificates that minimize revocation risk
- Open source with no licensing costs
One deploy and root CA distribution session eliminates self-signed certificate pain forever. Every new service you add — whether it is Grafana, Home Assistant, or a custom API — gets valid TLS automatically.