Implement CA/PKI engine with two-tier X.509 certificate issuance

Add the first concrete engine implementation: a CA (PKI) engine that generates
a self-signed root CA at mount time, issues scoped intermediate CAs ("issuers"),
and signs leaf certificates using configurable profiles (server, client, peer).

Engine framework updates:
- Add CallerInfo struct for auth context in engine requests
- Add config parameter to Engine.Initialize for mount-time configuration
- Export Mount.Engine field; add GetEngine/GetMount on Registry

CA engine (internal/engine/ca/):
- Two-tier PKI: root CA → issuers → leaf certificates
- 10 operations: get-root, get-chain, get-issuer, create/delete/list issuers,
  issue, get-cert, list-certs, renew
- Certificate profiles with user-overridable TTL, key usages, and key algorithm
- Private keys never stored in barrier; zeroized from memory on seal
- Supports ECDSA, RSA, and Ed25519 key types via goutils/certlib/certgen

Server routes:
- Wire up engine mount/request handlers (replace Phase 1 stubs)
- Add public PKI routes (/v1/pki/{mount}/ca, /ca/chain, /issuer/{name})
  for unauthenticated TLS trust bootstrapping

Also includes: ARCHITECTURE.md, deploy config updates, operational tooling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 21:57:52 -07:00
parent 4ddd32b117
commit 8f77050a84
26 changed files with 2980 additions and 129 deletions

View File

@@ -51,9 +51,10 @@ This creates:
| Path | Purpose |
|---|---|
| `/usr/local/bin/metacrypt` | Binary |
| `/etc/metacrypt/metacrypt.toml` | Configuration |
| `/etc/metacrypt/certs/` | TLS certificates |
| `/var/lib/metacrypt/` | Database and backups |
| `/srv/metacrypt/metacrypt.toml` | Configuration |
| `/srv/metacrypt/certs/` | TLS certificates |
| `/srv/metacrypt/backups/` | Database backups |
| `/srv/metacrypt/metacrypt.db` | Database (created on first run) |
### Docker Install
@@ -65,7 +66,7 @@ make docker
docker compose -f deploy/docker/docker-compose.yml up -d
```
The Docker container mounts a single volume at `/data` which must contain:
The Docker container mounts a single volume at `/srv/metacrypt` which must contain:
| File | Required | Description |
|---|---|---|
@@ -81,8 +82,8 @@ To prepare a Docker volume:
docker volume create metacrypt-data
# Copy files into the volume
docker run --rm -v metacrypt-data:/data -v $(pwd)/deploy/examples:/src alpine \
sh -c "cp /src/metacrypt-docker.toml /data/metacrypt.toml && mkdir -p /data/certs"
docker run --rm -v metacrypt-data:/srv/metacrypt -v $(pwd)/deploy/examples:/src alpine \
sh -c "cp /src/metacrypt-docker.toml /srv/metacrypt/metacrypt.toml && mkdir -p /srv/metacrypt/certs"
# Then copy your TLS certs into the volume the same way
```
@@ -96,7 +97,7 @@ Configuration is loaded from TOML. The config file location is determined by (in
1. `--config` flag
2. `METACRYPT_CONFIG` environment variable (via viper)
3. `metacrypt.toml` in the current directory
4. `/etc/metacrypt/metacrypt.toml`
4. `/srv/metacrypt/metacrypt.toml`
All config values can be overridden via environment variables with the `METACRYPT_` prefix (e.g., `METACRYPT_SERVER_LISTEN_ADDR`).
@@ -131,7 +132,7 @@ For production, use certificates from your internal CA or a public CA.
### Option A: CLI (recommended for servers)
```bash
metacrypt init --config /etc/metacrypt/metacrypt.toml
metacrypt init --config /srv/metacrypt/metacrypt.toml
```
This prompts for a seal password, generates the master encryption key, and stores the encrypted MEK in the database. The service is left in the unsealed state.
@@ -161,7 +162,7 @@ sudo systemctl start metacrypt
docker compose -f deploy/docker/docker-compose.yml up -d
# Manual
metacrypt server --config /etc/metacrypt/metacrypt.toml
metacrypt server --config /srv/metacrypt/metacrypt.toml
```
The service starts **sealed**. It must be unsealed before it can serve requests.
@@ -230,7 +231,7 @@ Users with the MCIAS `admin` role automatically get admin privileges in Metacryp
```bash
# CLI
metacrypt snapshot --config /etc/metacrypt/metacrypt.toml --output /var/lib/metacrypt/backups/metacrypt-$(date +%Y%m%d).db
metacrypt snapshot --config /srv/metacrypt/metacrypt.toml --output /srv/metacrypt/backups/metacrypt-$(date +%Y%m%d).db
# Using the backup script (with 30-day retention)
deploy/scripts/backup.sh 30
@@ -249,8 +250,8 @@ This runs a backup daily at 02:00 with up to 5 minutes of jitter.
### Restoring from Backup
1. Stop the service: `systemctl stop metacrypt`
2. Replace the database: `cp /var/lib/metacrypt/backups/metacrypt-20260314.db /var/lib/metacrypt/metacrypt.db`
3. Fix permissions: `chown metacrypt:metacrypt /var/lib/metacrypt/metacrypt.db && chmod 0600 /var/lib/metacrypt/metacrypt.db`
2. Replace the database: `cp /srv/metacrypt/backups/metacrypt-20260314.db /srv/metacrypt/metacrypt.db`
3. Fix permissions: `chown metacrypt:metacrypt /srv/metacrypt/metacrypt.db && chmod 0600 /srv/metacrypt/metacrypt.db`
4. Start the service: `systemctl start metacrypt`
5. Unseal with the original seal password
@@ -290,8 +291,8 @@ journalctl -u metacrypt --priority=err --since="1 hour ago"
|---|---|---|
| Service state | `GET /v1/status` | `state != "unsealed"` for more than a few minutes after restart |
| TLS certificate expiry | External cert checker | < 30 days to expiry |
| Database file size | `stat /var/lib/metacrypt/metacrypt.db` | Unexpectedly large growth |
| Backup age | `find /var/lib/metacrypt/backups -name '*.db' -mtime +2` | No backup in 48 hours |
| Database file size | `stat /srv/metacrypt/metacrypt.db` | Unexpectedly large growth |
| Backup age | `find /srv/metacrypt/backups -name '*.db' -mtime +2` | No backup in 48 hours |
| MCIAS connectivity | Login attempt | Auth failures not caused by bad credentials |
---
@@ -303,7 +304,7 @@ journalctl -u metacrypt --priority=err --since="1 hour ago"
| Symptom | Cause | Fix |
|---|---|---|
| `config: server.tls_cert is required` | Missing or invalid config | Check config file path and contents |
| `db: create file: permission denied` | Wrong permissions on data dir | `chown -R metacrypt:metacrypt /var/lib/metacrypt` |
| `db: create file: permission denied` | Wrong permissions on data dir | `chown -R metacrypt:metacrypt /srv/metacrypt` |
| `server: tls: failed to find any PEM data` | Bad cert/key files | Verify PEM format: `openssl x509 -in server.crt -text -noout` |
### Unseal fails
@@ -326,14 +327,14 @@ journalctl -u metacrypt --priority=err --since="1 hour ago"
```bash
# Check database integrity
sqlite3 /var/lib/metacrypt/metacrypt.db "PRAGMA integrity_check;"
sqlite3 /srv/metacrypt/metacrypt.db "PRAGMA integrity_check;"
# Check WAL mode
sqlite3 /var/lib/metacrypt/metacrypt.db "PRAGMA journal_mode;"
sqlite3 /srv/metacrypt/metacrypt.db "PRAGMA journal_mode;"
# Should return: wal
# Check file permissions
ls -la /var/lib/metacrypt/metacrypt.db
ls -la /srv/metacrypt/metacrypt.db
# Should be: -rw------- metacrypt metacrypt
```
@@ -360,18 +361,18 @@ Sealing the service (`POST /v1/seal`) explicitly zeroizes all key material from
| Path | Mode | Owner |
|---|---|---|
| `/etc/metacrypt/metacrypt.toml` | 0640 | metacrypt:metacrypt |
| `/etc/metacrypt/certs/server.key` | 0600 | metacrypt:metacrypt |
| `/var/lib/metacrypt/metacrypt.db` | 0600 | metacrypt:metacrypt |
| `/var/lib/metacrypt/backups/` | 0700 | metacrypt:metacrypt |
| `/srv/metacrypt/metacrypt.toml` | 0640 | metacrypt:metacrypt |
| `/srv/metacrypt/certs/server.key` | 0600 | metacrypt:metacrypt |
| `/srv/metacrypt/metacrypt.db` | 0600 | metacrypt:metacrypt |
| `/srv/metacrypt/backups/` | 0700 | metacrypt:metacrypt |
### systemd Hardening
The provided service unit applies: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `PrivateDevices`, `MemoryDenyWriteExecute`, and namespace restrictions. Only `/var/lib/metacrypt` is writable.
The provided service unit applies: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp`, `PrivateDevices`, `MemoryDenyWriteExecute`, and namespace restrictions. Only `/srv/metacrypt` is writable.
### Docker Security
The container runs as a non-root `metacrypt` user. The `/data` volume should be owned by the container's metacrypt UID (determined at build time). Do not run the container with `--privileged`.
The container runs as a non-root `metacrypt` user. The `/srv/metacrypt` volume should be owned by the container's metacrypt UID (determined at build time). Do not run the container with `--privileged`.
---
@@ -388,7 +389,7 @@ The container runs as a non-root `metacrypt` user. The `/data` volume should be
There is no online password rotation in Phase 1. To change the seal password:
1. Create a backup: `metacrypt snapshot --output pre-rotation.db`
1. Create a backup: `metacrypt snapshot --output /srv/metacrypt/backups/pre-rotation.db`
2. Stop the service
3. Re-initialize with a new database and password
4. Migrate data from the old barrier (requires custom tooling or a future `metacrypt rekey` command)
@@ -398,8 +399,8 @@ There is no online password rotation in Phase 1. To change the seal password:
If the server is lost but you have a database backup and the seal password:
1. Install Metacrypt on a new server (see Installation)
2. Copy the backup database to `/var/lib/metacrypt/metacrypt.db`
3. Fix ownership: `chown metacrypt:metacrypt /var/lib/metacrypt/metacrypt.db`
2. Copy the backup database to `/srv/metacrypt/metacrypt.db`
3. Fix ownership: `chown metacrypt:metacrypt /srv/metacrypt/metacrypt.db`
4. Start the service and unseal with the original password
The database backup contains the encrypted MEK and all barrier data. No additional secrets beyond the seal password are needed for recovery.
@@ -407,7 +408,7 @@ The database backup contains the encrypted MEK and all barrier data. No addition
### Upgrading Metacrypt
1. Build or download the new binary
2. Create a backup: `metacrypt snapshot --output pre-upgrade.db`
2. Create a backup: `metacrypt snapshot --output /srv/metacrypt/backups/pre-upgrade.db`
3. Replace the binary: `install -m 0755 metacrypt /usr/local/bin/metacrypt`
4. Restart: `systemctl restart metacrypt`
5. Unseal and verify