Two-layer structure: Sources (raw notes) + Wiki (compile output) Four domains: Dev (40), Venture (3), Homelab (23), Reference (0) Includes CLAUDE.md spec, index pages at all levels, compile log Co-Authored-By: Lovebug <lovebug@herbylab.dev>
6.1 KiB
6.1 KiB
| created | path | project | status | tags | type | updated | ||||
|---|---|---|---|---|---|---|---|---|---|---|
| 2026-05-08 | Sources/Homelab | ssh-ca-step | active |
|
project-plan | 2026-05-08 |
SSH CA with step-ca + Authentik
Goal
Replace long-lived SSH keypairs with short-lived certificates issued by a self-hosted CA, authenticated via Authentik OIDC. Cover human users, host identities (kill TOFU prompts), and service identities.
Architecture
step-cain a Debian LXC on PVE (matches Knot pattern), VLAN 11 (Lab)- OIDC provisioner pointed at Authentik for human cert requests
- JWK provisioner for service/automation cert requests (n8n, Ansible runners, etc.)
- Host CA signs server SSH host keys → clients trust the CA, no more
yes/no/fingerprintprompts - User CA signs client certs with TTL (8h human, shorter for service)
- Ansible role rolls out
TrustedUserCAKeysandHostCertificateconfig to fleet - Naming:
step-ca.herbylab.dev(capability-based, fits the Traefik convention)
Why this shape
- Authentik is already the SSO surface — same auth path as Traefik/MCP
- LXC keeps it lightweight and reuses Knot's deployment muscle memory
- One canary first → safer, lets us validate the Ansible role before fleet-wide
- Two CAs (user + host) is the recommended
step-capattern; a single CA for both works but mixing principals gets messy
Phases
Phase 1 — CA foundation
- 1.1 Create LXC on PVE: Debian 13, static IP on VLAN 11, hostname
step-ca - 1.2 Install
step-ca+stepCLI from Smallstep apt repo - 1.3 Initialize CA:
step ca initwith two intermediate CAs (user, host) under one root, RSA or Ed25519 - 1.4 Persist CA secrets: password file in
/etc/step-ca/, root key offline-backup to KeePassXC - 1.5 Systemd unit, enable + start, confirm
https://step-ca.herbylab.dev:9000/healthresponds - 1.6 DNS record in Knot once Knot is live; interim:
/etc/hostson canary - 1.7 PVE snapshot:
phase1-ca-base
Phase 2 — OIDC provisioner (Authentik)
- 2.1 Create OAuth2/OIDC provider in Authentik for
step-ca, scopesopenid email profile, redirecthttp://127.0.0.1/sso/oauth/callback - 2.2 Add provisioner to
step-ca:step ca provisioner add authentik --type=OIDC --client-id=... --client-secret=... --configuration-endpoint=https://auth.herbylab.dev/application/o/step-ca/.well-known/openid-configuration - 2.3 Configure principal mapping: Authentik
email→ Unix username (e.g.,tjcherb@plantbasedsoutherner.com→travadmin); use--adminfor admin emails - 2.4 Test from tower:
step ssh login→ browser SSO → cert in agent - 2.5 Verify cert contents:
step ssh inspect, confirm TTL, principals, key-id
Phase 3 — Canary server (user certs only)
- 3.1 Pick canary:
us-test-authy(low blast radius, already main dev surface) - 3.2 Fetch user CA pubkey, install to
/etc/ssh/ca-user.pub - 3.3 Add
TrustedUserCAKeys /etc/ssh/ca-user.pubtosshd_config, reload - 3.4 Test: SSH from tower with cert (key-based access still works as fallback)
- 3.5 Verify auth log shows cert-based login with correct principal
- 3.6 Run for 1 week, confirm no breakage
Phase 4 — Host certs on canary
- 4.1 Issue host cert:
step ssh certificate --host us-test-authy us-test-authy.herbylab.dev /etc/ssh/ssh_host_ed25519_key.pub - 4.2 Configure
sshd_config:HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub - 4.3 On clients: add host CA to
~/.ssh/known_hostsas@cert-authority *.herbylab.dev - 4.4 Confirm fingerprint prompt is gone on fresh client connection
- 4.5 Set up host cert renewal (systemd timer, weekly)
Phase 5 — Ansible role + fleet rollout
- 5.1 Build
step-ca-clientAnsible role: installsstepCLI, distributes user CA pubkey, configuressshd_config, manages host cert renewal timer - 5.2 Inventory tagging:
step_ca_enrolledgroup - 5.3 Roll out to PVE host, NAS, Traefik VM, Knot LXC one-by-one
- 5.4 Update
~/.ssh/configon tower + ThinkPad to use cert-based auth by default - 5.5 Document the "new server" runbook: 1 Ansible play vs old key-shuffle dance
Phase 6 — Service identities
- 6.1 Add JWK provisioner to
step-cafor automation - 6.2 Issue service certs to Ansible runner identity (replaces deploy keys for orchestration tasks where TTL works)
- 6.3 Decide what stays as static keys (true unattended workloads where cert renewal would be fragile) vs. what moves to certs
- 6.4 Document the tier model: human → OIDC certs, semi-attended → JWK certs, fully unattended service → static scoped keys
Phase 7 — Cleanup
- 7.1 Audit
~/.ssh/on tower + ThinkPad, archive obsolete keys to KeePassXC, delete from disk - 7.2 Audit
authorized_keysacross fleet, remove keys made redundant by CA - 7.3 Document key rotation + CA disaster recovery (root key restore from KeePassXC backup)
- 7.4 Add fish abbreviation
sshlogin→step ssh login
Open questions
- Cert TTL policy: 8h for human (re-auth daily) vs 24h (less friction)? Start at 8h, relax if painful.
- Do we want session recording? (Out of scope for step-ca; would need Teleport. Probably not.)
- Host cert renewal: systemd timer per host, or centralized via Ansible cron? Per-host is more resilient.
Dependencies / blockers
- Authentik must be reachable at
auth.herbylab.devwith TLS — already true - Knot LXC blocker (separate project) doesn't block this; we can use
/etc/hostsforstep-ca.herbylab.devuntil DNS is sorted - Traefik project doesn't block this —
step-caexposes its own HTTPS on :9000, no ingress needed for the CA itself
Success criteria
- Fresh laptop: install
step,step ssh login, immediately SSH to any fleet server with no fingerprint prompts and no copied keys - New server provisioning: 1 Ansible play installs CA trust + host cert, server is reachable
- Old laptop revoked:
step ca revoke+ 8h max blast radius ~/.ssh/on personal machines contains onlyconfigandknown_hosts