v0.7.0 — Machine auth, host-key verification & automation
Completes the remediation started in 0.6.1, and makes RackMap usable as a source of truth for automation.
Some deployments need configuration changes — see Action required.
🔑 API keys actually authenticate
The apiKeyAuth middleware has existed since 0.5 and was never mounted, so every key minted since then authenticated nothing. It is now wired to every /api/v1 route.
curl -H "Authorization: Bearer sk_..." https://rackmap.example.com/api/v1/serversKeys carry a role ceiling (scopeRole, defaulting to viewer) and an optional expiry. A key can never exceed its creator's role, and it is re-capped at request time against the owner's current role — so demoting or banning a user immediately demotes their keys. Disabled, revoked and expired keys are rejected.
🔒 SSH host-key verification
Host keys are pinned on first contact and compared on every later connection. Verification runs during key exchange, before any credential is offered, so a changed key cannot harvest your password — which mattered, because the auth fallback answers every keyboard-interactive prompt with the decrypted password.
SSH_HOST_POLICY=tofu refuses a changed key. The accept-any default pins and warns loudly but still connects, so an existing fleet can populate the store without an outage. A mismatch never overwrites the stored key — self-healing would erase the evidence.
Fingerprints are standard OpenSSH SHA256: values, so they compare directly against ssh-keygen -lf.
🛡️ Vault rotation no longer destroys data
POST /api/v1/vault/reset now takes { currentPassphrase, newPassphrase } and re-wraps the existing data-encryption key, preserving every stored credential. The old behaviour — mint a new key and orphan everything — requires an explicit forceDestroy: true, and the UI puts it behind a separate checkbox.
A request supplying neither is rejected. The API will not guess which one you meant.
🔐 New encryption envelope
Secrets are now sealed as v3. with a random per-secret salt and a scrypt-derived key, replacing an unsalted, single-round SHA-256 derivation that was fully offline-attackable against an exfiltrated database.
Existing v1. data still decrypts. No migration, no re-encryption, no downtime. The derived key is cached so the SSH path does not pay the KDF cost per connection.
🤖 Automation
- Prometheus exporter at
/api/v1/metrics— counts by status, per-host up/down and probe latency, probe staleness (a risingrackmap_server_last_probe_age_secondsmeans the scheduler stopped), GPU counts, certificate expiry. - Ansible dynamic inventory —
contrib/rackmap-inventory.py, grouping by environment, provider, location, type, owning team, tag, status and GPU presence. No dependencies beyond the standard library.
export RACKMAP_URL=https://rackmap.example.com RACKMAP_API_KEY=sk_...
ansible -i contrib/rackmap-inventory.py gpu -m ping🚦 Brute-force protection
Password reveal is limited to 5 per record and 20 per user per 5 minutes; the SSH credential test to 10 per host and 30 per user. The per-user ceiling is the one that matters — it turns "script a loop and dump the fleet" into hours of work and hundreds of audit rows.
The WebSocket terminal now caps password attempts per socket instead of allowing unlimited retries for an hour, and re-checks authorization on an interval so a live root shell closes on ban, role downgrade or access-request expiry.
🗄️ Migration history
The previous history was missing seven tables, so db push (Docker) and migrate deploy (systemd) built different schemas. History is squashed to a single baseline and containers now run migrate deploy.
Pre-existing databases are adopted automatically on first start — the schema is reconciled with db push without --accept-data-loss (so it adds and never drops), then the baseline is recorded. No manual step.
Verified against a copy of a real 125-server database: all rows intact, missing columns added, idempotent on re-run. A fresh database builds all 28 tables from the baseline alone.
Also fixed
- Soft-deleted servers were still being SSH-polled every five minutes.
- A failure in the server sweep also skipped the service sweep and the history prune.
- Metrics-check failures were swallowed entirely — a server with rotated credentials silently stopped being checked, with nothing in the logs.
- A WebSocket that never reached a shell had no maximum-duration cap, and any inbound traffic reset its idle timer.
- Locking your own vault session no longer stops every background job.
X-Forwarded-Foris only honoured behindTRUST_PROXY; it previously set the audit IP and rate-limit bucket unconditionally.- Bans and role changes take effect on the next request rather than up to five minutes later.
- Containers run as non-root, install from a frozen lockfile, and seed idempotently. They still carry devDependencies — pruning them breaks Prisma client resolution under pnpm, and a larger image beats a broken one.
Action required on upgrade
TRUST_PROXYdefaults tofalse. Behind a reverse proxy, setTRUST_PROXY=trueor every audit entry records the proxy's address. The bundleddocker-compose.ymlsets it for you.- Existing API keys have no
scopeRoleand inherit their owner's role. Re-mint any automation key withscopeRole: "viewer". - For strict host-key checking, run on
accept-anyuntil every host has been contacted at least once, review what was pinned, then setSSH_HOST_POLICY=tofu. See the README.
git pull
docker compose up -d --buildTake a database backup first, as with any release that touches migrations.
Verification
pnpm build, pnpm -r typecheck and pnpm test all pass. 208 tests, up from 203 in 0.6.1 and 35 before this work started.
Still open
- Metrics are still not persisted as a time series — that is deliberate; use the Prometheus exporter.
- No scheduler locking, so running more than one API replica will double-probe.
- 27 Dependabot alerts remain on the default branch.
Full changelog: https://github.com/deziss/rackmap/blob/v0.7.0/CHANGELOG.md