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
108 lines
3.8 KiB
Markdown
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).
|