step-ca Tutorial: An Internal ACME CA for Network Devices - 夜莺博客

step-ca Tutorial: An Internal ACME CA for Network Devices

Certificate expiry is one of the few outages that is entirely self-inflicted and entirely preventable. The classic form is an appliance - a firewall, a switch management interface, a VPN concentrator - whose self-signed certificate quietly expired nine months ago, and the first person to notice is a user staring at a browser warning. The fix is not better calendar reminders. It is an internal certificate authority that issues short-lived certificates and renews them automatically, the same way public certificates renew on your web servers.

This guide builds that with step-ca: a two-tier PKI, an ACME endpoint that any standards-compliant client can use, and the operational work of getting the root into device trust stores.

Why Self-Signed Certificates Scale Badly

The failure threshold is around ten devices. Below that, pasting a self-signed certificate into a browser exception is tolerable. Above it, you lose four things at once:

  • Revocation. There is no CRL or OCSP endpoint, so a leaked device key works until the certificate expires.
  • Automated issuance. Every new device needs a manual CSR and a manual copy.
  • Audit trail. No record of who requested what, when, and with which key.
  • Key custody. The signing key ends up on somebody's laptop with a name like ca-key-final-v2.pem.

Two-Tier Design

Keep the root offline and let an online intermediate do the signing. This is the only structure that makes compromise survivable.

Tier State Validity Role
Root CA Offline / HSM 10-20 years Signs intermediates only
Intermediate CA Online, on the CA host 1-5 years Signs leaf certificates daily
Leaf certificates Distributed Hours to weeks Device and service identity

If the intermediate is compromised you revoke it and issue a new one from the root. If the root is compromised you are rebuilding every trust store on the network, including the ones nobody documented - which is the whole argument for the two-tier structure.

Install and Initialise

# Debian/Ubuntu packages from Smallstep
wget https://dl.smallstep.com/cli/docs-ca-install/latest/step-cli_amd64.deb
wget https://dl.smallstep.com/certificates/docs-ca-install/latest/step-ca_amd64.deb
sudo dpkg -i step-cli_amd64.deb step-ca_amd64.deb

export STEPPATH=/etc/step-ca
sudo -E step ca init \
  --name "Internal Network CA" \
  --dns ca.internal.example.com \
  --address :8443 \
  --provisioner ops@example.com \
  --deployment-type standalone

# inspect what was created
sudo ls -l /etc/step-ca/certs /etc/step-ca/secrets
step certificate fingerprint /etc/step-ca/certs/root_ca.crt

Export the root private key to offline storage immediately and remove the on-disk copy. Keep the intermediate key on the CA host, encrypted, owned by the service account, mode 0400.

sudo chmod 0400 /etc/step-ca/secrets/intermediate_ca_key
sudo chown -R step:step /etc/step-ca
sudo systemctl enable --now step-ca
step ca health

Enable the ACME Provisioner

step ca provisioner add acme --type ACME
sudo systemctl restart step-ca

# the directory endpoint should answer
curl -sS --cacert /etc/step-ca/certs/root_ca.crt \
  https://ca.internal.example.com:8443/acme/acme/directory

A fresh ACME provisioner with no policy denies every identifier, and the error message - The server will not issue certificates for the identifier - makes it look like a client problem. It is not. Add an explicit policy:

{
  "type": "ACME",
  "name": "acme",
  "claims": {
    "minTLSCertDuration": "5m",
    "maxTLSCertDuration": "24h",
    "defaultTLSCertDuration": "24h"
  },
  "policy": {
    "x509": { "allow": { "dns": ["*.internal.example.com"] } }
  }
}

Note the consequence: certificates must have a DNS name. ACME will not issue for a bare IP, which means every device that needs a trusted certificate also needs an internal DNS record. That is a good constraint - it is what lets clients validate the certificate instead of disabling validation.

Issuing to Devices

Devices fall into three classes, and each needs a different mechanism.

Class 1: devices that speak ACME

Many firewalls and load balancers have a built-in ACME client. Point them at https://ca.internal.example.com:8443/acme/acme/directory, set the CA type to custom, and supply the root certificate so the client trusts the CA. Remember that the account email is not validated by step-ca, and that the provisioner name in the URL must be URL-encoded if it contains an @.

Class 2: devices with a static certificate field

Issue once with the step CLI and push the files. This is where an Ansible or GitOps pipeline earns its keep.

step ca certificate sw-core-01.internal.example.com sw-core-01.crt sw-core-01.key \
  --provisioner ops@example.com --not-after 720h

openssl x509 -in sw-core-01.crt -noout -subject -issuer -dates -ext subjectAltName
- name: push device certificate
  ansible.builtin.copy:
    content: "{{ lookup('file', 'pki/{{ inventory_hostname }}.crt') }}"
    dest: /etc/ssl/device-cert.pem
    mode: '0644'
  notify: reload management services

Class 3: devices with an API for certificates

This is the cleanest case. Network operating systems with gNOI certificate management RPCs accept a certificate and key over gRPC, which means the renewal loop is entirely automated with no file copying at all. The RPC families involved are described in this gNOI and gRIBi guide.

Getting the Root Into Trust Stores

# Linux
sudo cp root_ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates

# macOS (GUI import fails on recent releases; use the CLI)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain root_ca.crt

# Windows (elevated PowerShell)
Import-Certificate -FilePath C:\\pki\\root_ca.crt \
  -CertStoreLocation Cert:\\LocalMachine\\Root

Two traps. First, update-ca-certificates is not optional on most distributions - dropping the file into the directory does nothing until the bundle is regenerated. Second, container images carry their own frozen trust store, so installing the root on the host does nothing for a process inside a container; the root has to be baked in or bind-mounted. The systematic way to chase down a failing chain is covered in this certificate chain verification guide.

Network Requirements

  • Internal DNS for the CA name on every client that will renew. A client pointed at a public resolver cannot resolve ca.internal.example.com and ACME fails.
  • HTTP-01 flows inbound to the client. step-ca connects to port 80 on the requesting host, not the other way round. If the device cannot be reached inbound, use DNS-01 with a provider plugin instead.
  • Restrict the CA endpoint to the management and server networks. Do not expose the admin provisioner to untrusted networks.
  • Permit OCSP/CRL and NTP from clients. A client with a wrong clock will fail validation regardless of how correct the certificate is.

Monitoring and Incident Response

# expiry monitoring - alert well before 20% of lifetime remains
step certificate inspect --short https://ca.internal.example.com:8443/roots.pem
promtool check config /etc/prometheus/tls-exporter.yml

# is anything about to expire?
step ca certificate-list --ca-url https://ca.internal.example.com:8443 --root root_ca.crt

For intermediate compromise, the runbook in order: stop step-ca, bring the root online, generate and sign a new intermediate, publish it to distribution channels, push the CRL revoking the old intermediate, force-renew every leaf, and audit the issuance log to know exactly which ones. At 24-hour lifetimes step 6 happens naturally within a day - which is the real argument for short-lived certificates, because revocation at scale is otherwise mostly theatre. Device management credentials have the same expiry problem on the authentication side; the EAP certificate checks in this EAP-TLS FreeRADIUS guide are the mirror image of this problem on the wireless and wired 802.1X path.

Pitfalls

  • Provisioner edits need a restart. Changes land in ca.json, but the running process holds the old configuration in memory.
  • Certificate lifetimes stay at 24 hours after you change them - same cause. Confirm with step ca provisioner list.
  • RSA key generation is CPU-heavy. If you are renewing a fleet every few minutes, use ECDSA and run the CA on real x86 hardware, not a single-board computer.
  • Pinning the wrong thing. Pin the root only. Services that pinned the intermediate break the moment you rotate it.

原文链接:https://stackharbor.com/en/knowledge-base/step-ca-internal-acme-pki