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.jsonor/etc/step-ca/config/ca.jsonwhen 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.json→address) - 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 bootstrapto configure their localstepCLI to trust the CA - At least one provisioner is configured in
ca.json
Health Checks
systemctl is-active step-ca→activecurl -k https://localhost:9000/health→{"status":"ok"}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.json → password; 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 installhandles 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 --daemonhandles 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 bootstrapto 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 bootstrapon the Caddy host and settingacme_cain 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 patternsdocs.md— official documentation and reference links
Related skills