certauth/README.md
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

108 lines
3.8 KiB
Markdown

# 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).