Internal CA (step-ca)#

FalseBlue runs a private step-ca certificate authority on ereshkigal (modules/nixos/pki/step-ca.nix). Every NixOS/darwin host in the flake gets and auto-renews its cert over ACME with no manual steps (tsunaminoai.pki.acmesecurity.acme pointed at https://ca.falseblue.com/acme/acme/directory). You only need the commands below for devices that aren’t in the flake — the biggest one being the Home Assistant appliance (HAOS), which serves its own cert on :8123.

Fact Value
CA URL https://ca.falseblue.com
Root fingerprint 66b8732f7a4630403455da2ef05fef269ae63e390c6b5f50f82ecdf872585fe6
Root cert (in repo) pki/authorities/falseblue.crt (CN=falseblue.com, valid to 2032)
Interactive provisioner tsunami@falseblue.com (JWK, password-protected)
ACME provisioner acme (what the flake hosts use)
Cert lifetimes default 32 days, max 90 days (--not-after up to 2160h)

step-ca listens on :9443 and is fronted at https://ca.falseblue.com (:443). Certificates for *.falseblue.com, *.<tailnet>, homeassistant, and any 192.168.0.0/24 / 100.0.0.0/8 IP are permitted by policy.

Run the step client on a flake host

ereshkigal (and any NixOS host importing the PKI module) already has step-cli on PATH and trusts the root system-wide, so it’s the easiest place to run these commands. HAOS can’t easily run step itself — issue the cert on ereshkigal, then copy the files onto the appliance.

One-time: bootstrap trust to the CA#

On a machine that doesn’t already trust the CA (skip on flake hosts):

step ca bootstrap \
  --ca-url https://ca.falseblue.com \
  --fingerprint 66b8732f7a4630403455da2ef05fef269ae63e390c6b5f50f82ecdf872585fe6

This writes ~/.step/ with the root and CA config so later step ca … commands need no --ca-url/--root flags.

Renew a cert that is still valid#

If the certificate hasn’t expired yet, renewal is a single command — it re-authenticates with the existing cert+key (mutual TLS), no provisioner password needed:

step ca renew host.crt host.key --force

Expired certs cannot be renewed

The CA sets allowRenewalAfterExpiry = false. Once a cert is past notAfter, step ca renew fails — you must re-issue it (next section). This is the case for the Home Assistant cert today (it expired before anyone noticed — the exact gap the status page now closes).

Issue (or re-issue) a certificate#

Use the interactive JWK provisioner. You’ll be prompted for the provisioner password (set at step ca init; kept in sops under step-ca/password-file).

Example for Home Assistant (both the homeassistant DNS name and its LAN IP as SANs, 90-day lifetime):

step ca certificate "homeassistant" ha.crt ha.key \
  --provisioner "tsunami@falseblue.com" \
  --san homeassistant \
  --san 192.168.0.55 \
  --not-after 2160h

For a scripted / non-interactive flow, split it into a token + issuance (the token carries the authorization; hand it to whoever generates the CSR):

TOKEN=$(step ca token homeassistant \
  --provisioner "tsunami@falseblue.com" \
  --san homeassistant --san 192.168.0.55 \
  --not-after 2160h \
  --password-file /run/secrets/step-ca/password-file)   # on ereshkigal

step ca certificate homeassistant ha.crt ha.key --token "$TOKEN"
or
step ca token homeassistant.armadillo-banfish.ts.net \
                     --provisioner "tsunami@falseblue.com" \
                     --ca-url https://ca.falseblue.com:9443 \
                     --root /home/tsunami/code/nix/nix-flake-final/pki/authorities/falseblue.crt \
                     --san 192.168.0.55 \
                     --san homeassistant.gensokyo \
                     --san homeassistant \
                     --san homeassistant.armadillo-banfish.ts.net

Install the cert on Home Assistant (HAOS)#

HAOS is not managed by this flake, so this part is manual:

  1. Copy ha.crt and ha.key into the appliance’s /ssl/ directory (via the Samba/File-Editor add-on, or scp if SSH is enabled).
  2. In configuration.yaml:
    http:
      ssl_certificate: /ssl/ha.crt
      ssl_key: /ssl/ha.key
    
  3. Restart Home Assistant (Developer Tools → YAML → Restart, or reboot).
  4. Verify: openssl s_client -connect 192.168.0.55:8123 -servername homeassistant </dev/null 2>/dev/null | openssl x509 -noout -issuer -dates should now show issuer=CN=falseblue.com and a fresh notAfter.

Automating renewal#

For a Linux host you control, run step ca renew on a timer well before expiry (renew at ~2/3 of lifetime):

step ca renew --daemon --expires-in 20d host.crt host.key \
  --exec "systemctl reload nginx"

HAOS makes long-running daemons awkward; the pragmatic path for the appliance is a calendar reminder to re-issue every ~2 months, or move HA behind the step-ca nginx edge like the other services so it never needs a device-local cert at all.

How this ties into monitoring#

  • The status page probes Home Assistant with client.insecure = true (we don’t trust its self-signed cert), and that endpoint is labelled extra-labels.cert = "external" so the ProbedCertificateExpiringSoon alert skips it — an unmanaged appliance cert isn’t our renewal to watch.
  • Every endpoint whose cert is ours (the *.falseblue.com / *.<tailnet> vhosts) is watched: that alert fires at <14 days remaining, catching a silently-stuck ACME renewal before browsers start erroring.
  • If you issue a proper FalseBlue cert for HA (above) and want it monitored, drop client.insecure and the cert = "external" label from its endpoint in the ereshkigal host config.
  • status-page.md — the synthetic checks + cert-expiry alert.
  • backups.md — the step-ca BadgerDB is backed up to voile/borg daily.