# 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) ```bash 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 ```bash 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 ```bash 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](LICENSE).