You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Foundation for issue #72's rotation work. Adds the data structures and
storage required for N-entry keyrings on both the agent and dashboard
sides, with no operator-visible behaviour change yet — rotation
endpoints and UI follow in Phase 2.
Agent side
----------
- `internal/framework/crypto/keyring.go` — `KeyRing` type with up to
`MaxKeyRingEntries = 4` accepted keys, atomic tmpfile+rename on disk,
AES-256-GCM (GBE1) encryption when `GEARBOX_AGENT_ENCRYPTION_KEY` is
set. Wire token format: `gbx_<6-hex-kid>_<base64url(32 random bytes)>`,
with legacy 64-hex tokens still accepted for one release cycle.
- `LoadOrCreateKeyRing(keyringPath, legacyAPIKeyPath)` migrates an
existing `/var/lib/gearbox-agent/api-key` file into a single keyring
entry tagged `kid="legacy"`, role=primary. Legacy file stays on disk
as a read-only fallback.
- `KeyRingPointer` wraps `atomic.Pointer[KeyRing]` so Phase 2's
install/use/remove endpoints can swap the live keyring without
middleware restart. Verified by the new auth-middleware test
`TestAPIKeyAuth_HotSwapVisibleImmediately`.
- `internal/framework/middleware/auth.go` rewritten to take a keyring
pointer instead of a static key. Accepts both prefixed and legacy
token formats; matched `kid` echoed back as `X-Gearbox-Kid:` header
on every authenticated response so the dashboard can detect drift
(consumed in Phase 5). Auth with a secondary key logs at INFO so
the audit log can later flag "old key still in use after rotation".
- New endpoint `GET /api/v1/system/keyring` (authenticated) returns
metadata only — kids, roles, created_at, sha256-prefix fingerprint
for diagnostic equality checks — never the secret bytes themselves.
- `--show-api-key` and `--rotate-api-key` CLI flags work against the
keyring; the printed key uses the new `gbx_<kid>_<b64>` wire format
the dashboard can paste verbatim.
- `GEARBOX_AGENT_KEYRING_PATH` env var (default
`<DataDir>/keyring.json`) is now a config field alongside the legacy
`HAPROXY_AGENT_API_KEY_PATH`.
Dashboard side
--------------
- Migration `000002_add_box_agent_keys` adds the
`(box_id, kid)`-keyed `box_agent_keys` table and idempotently
backfills one `kid='legacy'` row per existing box from
`boxes.api_key_encrypted`. The legacy column stays for one release.
- `database/box_agent_keys.go` exposes Get/Insert/SetPrimary/Delete/
TouchLastUsed — the storage primitives Phase 2's rotator service
composes into the install→use→remove dance.
Tests
-----
- 19 keyring unit tests covering token parsing (prefixed + legacy +
malformed), keyring mutation, file round-trip with and without
encryption, legacy api-key migration, and pointer hot-swap.
- 8 auth-middleware integration tests covering bearer parsing, kid
header echo, secondary-key acceptance, and the live hot-swap path
Phase 2 depends on.
- 5 storage tests covering primary-key lookup, atomic role flip,
delete-refuses-last guard, and last_used_at touch.
Carry-overs to Phase 2 (intentional gaps surfaced from this PR)
---------------------------------------------------------------
- `DeleteBox` does not yet cascade to `box_agent_keys` (SQLite
`PRAGMA foreign_keys` is off in this codebase; enabling it is a
broader change). Phase 2's box-delete path will clean dependent
rows explicitly. Documented in box_agent_keys_test.go.
- The dashboard's `agent.Client` does not yet send `X-Gearbox-Kid`
on outbound requests — there's no kid to send while every box's
keyring contains only the legacy entry. Phase 2 wires this when
the rotator starts mutating keyrings.
Refs: research summary and implementation plan posted to #72.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
// and short fingerprints — but never the actual secrets.
40
+
//
41
+
// @Summary Get agent keyring metadata
42
+
// @Description Returns the currently-installed API keys' metadata. Secrets are NEVER exposed. The fingerprint is the first 8 hex chars of sha256(secret); useful to verify the dashboard has the same secret without round-tripping the secret itself.
0 commit comments