Jarian Cottingham 4bd538b217 fix: YubiKey CA signing, remove inline API copies, env-driven config
Critical: build_leaf_cert self-signed leaves with the leaf key instead of
the YubiKey-held Intermediate CA key. Now extracts TBS, signs via
pkcs11-tool (ECDSA-SHA384), reassembles, and verifies against the
intermediate CA public key before returning.

- setup-certauth.sh: ~1100 lines of stale inline api/ copies replaced with
  copy-from-repo (single source of truth); writes private
  /etc/certauth/certauth.env (0600); DB init loads env, no more
  swallowed errors; systemd unit gets EnvironmentFile=
- config.py: YubiKey serials no longer hard-coded (env, fail closed);
  aarch64-only PKCS#11 path replaced with arch-neutral default; all
  paths env-overridable (CERTAUTH_*)
- main.py: removed dead fastapi.security.CSRFProtection import (crashed
  startup); module-relative static/templates dirs; created_by resolved
  from the authenticated user instead of hard-coded 1; unclosed file
  handles fixed; domain_id 0 stored as NULL (FK bug)
- models.py: certificates.domain_id FK pointed at users(id), now domains(id)
- login: CSRF token now actually sent and validated
- tests: 23 tests (auth, API flows, DER helpers, signing pipeline)
- README, LICENSE, requirements.txt, pyproject.toml
2026-08-20 22:03:01 +00:00

CertAuth Key Vault

Self-contained Certificate Authority with YubiKey-backed signing. Two YubiKey 5 Nano devices act as HSMs — one for the Root CA, one for the Intermediate CA. Every certificate sign requires physical touch + PIN, so compromising the server does not compromise the CA keys.

Root CA (YubiKey 1)  →  Intermediate CA (YubiKey 2)  →  Leaf certs
   25-year validity        15-year validity              1-year validity

Components

Path Purpose
api/ FastAPI service: REST API + Jinja2 web UI
setup-certauth.sh Idempotent provisioner: fresh Ubuntu 24.04+ → full CA (aarch64/x86_64)
certauth-api.service systemd unit for the API
Caddyfile Caddy TLS-terminating reverse proxy in front of the API
nginx-example.com Reference nginx config for the wider homelab vhost layout
landing/ Landing pages for the CA domain
SKILL.md Operator/agent runbook: API usage, endpoints, workflows
.env.example All environment variables documented

Architecture

  • Signing: pkcs11-tool against the OpenSC PKCS#11 module; ECDSA-SHA384. Private keys never leave the YubiKeys; only extracted public keys are cached on disk.
  • Auth: JWT bearer tokens for the REST API, session cookie for the web UI, per-request CSRF tokens on all state-changing web endpoints.
  • Storage: SQLite (WAL) for domains, certificate requests, audit log, and CRL entries.
  • CRL: revoked serials tracked in the DB; scripts-style daily CRL refresh is documented in SKILL.md.

Quick start (provision a node)

sudo bash setup-certauth.sh

Prerequisites: fresh Ubuntu 24.04+, two YubiKey 5 Nanos plugged in, root. The script configures everything (CA hierarchy, API, Caddy, systemd) and prints the generated PINs. Configuration is overridable via environment (CA_ORG, YK1_SERIAL, NETWORK_CIDR, ...).

Running the API standalone

pip install -r requirements.txt
export YK_ROOT_SERIAL=... YK_INT_SERIAL=...
export YK_ROOT_PIN=... YK_INT_PIN=...
export JWT_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
export ADMIN_PASSWORD=...
python3 -m uvicorn main:app --host 127.0.0.1 --port 8000   # from api/

All filesystem paths default to the production layout (/etc/ssl/ca, /var/lib/certauth) and are overridable via CERTAUTH_* environment variables — see .env.example.

API (summary)

Method Path Purpose
POST /api/token Login → JWT
GET /api/me Validate token
GET/POST /api/domains List / register domain
GET /api/certs List certificate requests
POST /api/certs/request Create pending request (cn, sans)
POST /api/certs/{id}/sign Sign via YubiKey (touch + PIN)
GET /api/certs/{id}/pem Download leaf + chain
GET /api/certs/{id}/pfx Download PKCS#12 (password-protected)
GET /api/health Liveness
GET /api/ca-chain Download CA chain

Web UI: / dashboard, /login, /domains, /certs, /history, /setup (provisioning helper pages).

Full usage examples: see SKILL.md.

Tests

pip install pytest
pytest tests/ -v

Unit tests cover auth (JWT round-trip, rejection of invalid tokens), login/token endpoints, domain + certificate-request flows, and password hashing. Signing paths that require a physical YubiKey are exercised at the integration level on the provisioned node.

Security notes

  • YubiKey serials, PINs, JWT secret, and admin password are environment driven and fail closed at import time — the service will not start with missing configuration.
  • Leaf keys are written 0600, certs 0640, under the issued directory.
  • CSRF tokens use constant-time comparison (secrets.compare_digest).
  • Error strings are HTML-escaped before rendering into pages.

License

MIT — see LICENSE.

Description
Self-contained certificate authority with YubiKey-backed signing. Root + intermediate CA keys on two YubiKeys; every signing requires physical touch + PIN.
Readme MIT 134 KiB
Languages
Python 47.1%
HTML 27.3%
Shell 25.6%