step-ca

Installation
SKILL.md

Identity

  • Binary: step-ca (the CA server), step (the CLI client)
  • Config: $(step path)/config/ca.json — typically ~/.step/config/ca.json or /etc/step-ca/config/ca.json when running as a service
  • Root CA cert: $(step path)/certs/root_ca.crt
  • Intermediate cert: $(step path)/certs/intermediate_ca.crt
  • Service: step-ca.service (systemd) or a Docker container
  • Default port: 9000 (configurable in ca.jsonaddress)
  • Logs: journalctl -u step-ca (systemd) or container stdout

Key Operations

Operation Command
Initialize new CA step ca init
Start CA (foreground) step-ca $(step path)/config/ca.json
Start CA (systemd) sudo systemctl start step-ca
Check service status systemctl status step-ca
Health check endpoint curl -k https://localhost:9000/health
Issue a certificate step ca certificate myhost.local myhost.crt myhost.key
Issue with SAN step ca certificate myhost.local myhost.crt myhost.key --san myhost.local --san 192.168.1.10
Renew certificate (manual) step ca renew myhost.crt myhost.key
Renew (daemon mode) step ca renew --daemon myhost.crt myhost.key
Revoke certificate step ca revoke --cert myhost.crt
Inspect certificate step certificate inspect myhost.crt
Inspect CA health + version step ca health
List provisioners step ca provisioner list
Add ACME provisioner step ca provisioner add acme --type ACME
Add JWK provisioner step ca provisioner add myprovisioner --type JWK
Add OIDC provisioner step ca provisioner add sso --type OIDC --oidc-client-id ... --oidc-configuration-endpoint ...
Bootstrap a new host step ca bootstrap --ca-url https://ca.internal:9000 --fingerprint <root-fingerprint>
Test ACME challenge step ca certificate --provisioner acme test.internal test.crt test.key
Get root fingerprint step certificate fingerprint $(step path)/certs/root_ca.crt
Trust root cert (system) step certificate install $(step path)/certs/root_ca.crt

Expected State

  • CA server is running and listening on port 9000 (or configured address)
  • Root CA certificate is installed in the OS trust store on every client that will communicate with services using step-ca-issued certs
  • Clients have run step ca bootstrap to configure their local step CLI to trust the CA
  • At least one provisioner is configured in ca.json

Health Checks

  1. systemctl is-active step-caactive
  2. curl -k https://localhost:9000/health{"status":"ok"}
  3. step ca certificate test.check /tmp/test.crt /tmp/test.key && step certificate inspect /tmp/test.crt → shows valid cert with correct issuer

Common Failures

Symptom Likely cause Check/Fix
x509: certificate signed by unknown authority Root CA not trusted on the client Run step certificate install $(step path)/certs/root_ca.crt on the client, or distribute and trust the root cert manually
ACME order fails with provisioner not found No ACME provisioner configured step ca provisioner list; add one with step ca provisioner add acme --type ACME then restart step-ca
Certificate expired unexpectedly Default ACME lifetime is 24h — much shorter than Let's Encrypt's 90 days Use step ca renew --daemon or a cron job; configure longer lifetimes in ca.json policy
step ca bootstrap errors: connection refused step-ca not running or wrong --ca-url systemctl status step-ca; verify the URL and port; check firewall
Wrong CA URL in client config Bootstrap run with wrong --ca-url Re-run step ca bootstrap --ca-url https://correct-host:9000 --fingerprint <fp>
step-ca.service fails to start Bad ca.json (missing key, wrong path) journalctl -u step-ca -n 50; validate JSON and check all referenced file paths exist
permission denied on key files Key files not readable by the service user ls -l $(step path)/secrets/; chown to the step-ca service user
failed to decrypt on startup Wrong or missing password for intermediate key Confirm the password file path in ca.jsonpassword; re-enter interactively with step-ca $(step path)/config/ca.json

Pain Points

  • Root CA trust is not automatic: The root cert must be explicitly installed on every client — every OS, browser, Docker container, and app that needs to verify certs. step certificate install handles the OS store, but app-level stores (Java, Python, Firefox) need separate handling.
  • Short default lifetimes: ACME-issued certs expire in 24h by default. This is intentional for zero-trust but requires automated renewal from day one. step ca renew --daemon handles this, but it must be set up as a service for every cert-bearing host.
  • Bootstrap is a per-host operation: Every host that issues or validates certs must run step ca bootstrap to configure the step CLI's CA URL and root fingerprint. There is no network-discoverable default.
  • ACME provisioner requires a password: Unlike public ACME (Let's Encrypt), step-ca's ACME provisioner still requires a provisioner password stored on the CA server. Clients do not see or need this password, but it must be present on the CA.
  • Caddy integration is zero-config once bootstrapped: Caddy's built-in ACME client talks directly to step-ca using the ACME provisioner URL. After step ca bootstrap on the Caddy host and setting acme_ca in the Caddyfile, Caddy handles all issuance and renewal with no manual cert management.

References

See references/ for:

  • common-patterns.md — CA init, client bootstrap, ACME setup, Caddy integration, SSH CA, mTLS, and renewal patterns
  • docs.md — official documentation and reference links
Related skills
Installs
1
GitHub Stars
5
First Seen
Mar 18, 2026