Trust model: refresh-token rotation paragraph now states the 90-day expiry. OPERATIONS.md (operator runbook) whitelisted in .gitignore — useful for anyone deploying RedFlag.
9.6 KiB
RedFlag Operations Runbook
Operator procedures for RedFlag deployments. Scope: hardware rebind, agent credential renewal/rotation and the instance lock, disaster recovery, signing key rotation.
1. Hardware Change / Machine Rebind
When an agent is migrated to new hardware (motherboard swap, VM rebuild, disk image clone), the recorded machine_id will diverge from what the agent now reports. The agent will be rejected by MachineBindingMiddleware and every command will be denied.
Symptoms
- Agent shows online but every command returns
403 unauthorizedin the server log:[WARN] [server] [middleware] machine_id_mismatch. - Token renewal also fails:
POST /api/v1/agents/renewreturns403and the agent logs a terminalErrMachineMismatch(see §2). A rebind that does not also restore a usable refresh token will leave the agent unable to renew. security_eventstable has rows of typeMACHINE_ID_MISMATCHfor the affected agent.
Procedure
- Verify the agent's host. Do not rebind unless you can confirm the agent is on the expected hardware.
- As an admin, call the rebind endpoint:
curl -X POST https://<server>/api/admin/agents/<agent-uuid>/rebind-machine-id \ -H "Authorization: Bearer <admin-jwt>" \ -H "Content-Type: application/json" \ -d '{"reason": "hardware replacement 2026-05-24"}' - The endpoint clears the stored machine_id and on next check-in the new value is bound. Audit row written to
security_events. - Restart the agent if it does not recover within one heartbeat window.
The endpoint is rate-limited (admin_operations) and requires WebAuthMiddleware. There is no agent-side procedure — rebind is admin-driven.
Do not clone or copy a running agent's
config.jsononto a second host as a shortcut. It carries a machine-bound refresh token; the clone will fail the machine check on its first renewal and, if the original keeps running, can trip refresh-token reuse detection and revoke the whole family (§2). Re-enroll the new host instead.
2. Agent Credentials: Renewal, Rotation, and the Instance Lock
An agent holds two credentials in config.json: a short-lived access token (JWT) and a long-lived refresh token. The access token is presented on every request; the refresh token is used only at POST /api/v1/agents/renew to mint a new access token. Renewal is machine-bound and the refresh token rotates on every use.
What rotation means operationally
- Each successful renewal returns a new refresh token and invalidates the old one. The agent persists the successor to
config.json. - A one-time grace accepts the immediately previous token once more, so an agent that renews but crashes before persisting can still recover on restart.
- Presenting a refresh token after its successor has already been used is treated as reuse (the fingerprint of a stolen, replayed token). RedFlag revokes the entire token family and writes a critical
security_event. This is forward-only — there is no un-revoke. Recovery is re-enrollment of the agent. - Renewal is also machine-bound: the refresh token is checked against
X-Machine-IDbefore rotation. A stolenconfig.jsonreplayed from another host gets403+MACHINE_ID_MISMATCH, never a token.
Symptoms of a revoked family
- Agent logs a terminal
ErrRefreshTokenInvalid(orErrUnauthorized) and the polling loop stops — these are not retried. security_eventsshows a refresh-token reuse / family-revocation row.- Recovery: re-enroll the agent (fresh registration). Do not attempt to hand-edit tokens back into
config.json.
The instance lock
Two agent processes must never share one config.json — they would race each other's renewals and trip reuse detection. The agent takes an exclusive instance lock at startup (agent/internal/instancelock/): a Unix flock on Linux/macOS, a named kernel mutex Global\RedFlagAgent_v1 on Windows. A second process starting against the same config fails fast.
- Symptom of a lock conflict: the agent exits immediately at startup logging that another instance holds the lock.
- This is also why imaging/cloning a running agent is unsafe (§1): the clone either fails the lock (same host) or fails the machine check and trips reuse (different host).
3. Disaster Recovery
What to back up
| Asset | Where | Cadence |
|---|---|---|
PostgreSQL database (redflag) |
pg_dump of the database container |
nightly |
.env / Docker secrets |
filesystem outside docker volumes | on change |
| Ed25519 signing private key | offline secure storage (password manager / hardware key) | once, at setup, and on every rotation |
| Server TLS material (if terminating at server) | filesystem | on change |
The signing private key is the most critical asset. Without it the server cannot sign new agent commands and all existing agents will continue accepting only the old key until their next public-key fetch.
Restore order
- Provision a host with the same Docker stack version.
- Restore the
.env(or recreate secrets) — includingREDFLAG_SIGNING_PRIVATE_KEY. - Start the postgres container with restored data volume (or
pg_restoreinto a fresh volume). - Start the server. Confirm migrations apply cleanly (server logs
[INFO] migrations applied N). - Start agents in batches. Watch
security_eventsforMACHINE_ID_MISMATCH(clone hosts will need rebind, §1). - Verify command flow end-to-end: queue a
scan_apt(or platform equivalent) to one agent and confirmagent_commands.status = 'success'.
Refresh tokens after a database restore
A restored database carries the refresh-token state as of the backup. An agent that renewed after the backup was taken now holds a token the restored server considers already-rotated. On first renewal this can read as reuse and revoke the family (§2). Expect a wave of agents needing re-enrollment proportional to renewal activity between the backup and the restore — keep the backup cadence tight relative to the renewal interval, and treat post-restore re-enrollment as a normal step, not an incident.
What cannot be restored without the signing private key
- Outgoing commands the server signs. Restoring the database is not enough — agents reject unsigned commands in strict mode.
- If the key is lost: generate a new keypair, write it to the database (
signing_keystable — see §4), and accept that there will be a transitional window where commands signed with the new key require agents to fetch the new public key before they will execute.
4. Signing Key Rotation
Implemented via the signing_keys table (migration 020). The table holds versioned keys; agents fetch all currently-active public keys on registration and on demand, so two keys can be valid simultaneously for a controlled transition.
When to rotate
- Suspected compromise of the private key.
- Operator-mandated cadence (recommend annually).
- Personnel change where the prior key holder no longer needs access.
Procedure
-
Generate a new Ed25519 keypair on a clean host (offline if possible):
cd server go run ./cmd/keygen # if not present, generate via openssl ed25519Output: hex-encoded 64-byte private key and 32-byte public key.
-
Register the new key as active alongside the existing one. The signing service has
InitializePrimaryKey()for first registration; for rotation, insert directly into thesigning_keystable with the next version number andstatus = 'active':INSERT INTO signing_keys (key_id, version, public_key_hex, status, created_at) VALUES (gen_random_uuid(), (SELECT COALESCE(MAX(version), 0) + 1 FROM signing_keys), '<hex>', 'active', NOW());Both old and new keys are active. Agents fetch both via the public-key endpoint.
-
Swap server signing key: update
REDFLAG_SIGNING_PRIVATE_KEYin.env(or the Docker secret) to the new private key. Restart server. -
Verify: queue a command to one agent. Confirm the agent verifies the new signature. Repeat across a representative sample of agents.
-
Revoke the old key after a transition window (recommend 7 days, longer if you have agents that may be offline):
UPDATE signing_keys SET status = 'revoked', revoked_at = NOW() WHERE version = <old_version>;Agents will reject commands signed by revoked keys on their next public-key refresh.
-
Securely destroy the old private key copies (password managers, escrow, sealed envelopes).
Rollback
If a rotation causes broad agent failure, revert step 3 (put the old private key back in .env, restart server). The old key remains active in signing_keys until you mark it revoked. The transition window exists precisely to allow this rollback.
5. Supply Chain Checks — Fail-Open Semantics
The OSV.dev vulnerability check (CheckOSVVulnerabilities) and package age gate (GetPackagePublishDate) are both fail-open: if the upstream service is unreachable, the check logs a warning and returns nil — the operation proceeds.
This is deliberate. Blocking package installs because OSV.dev or a package registry has an outage is worse than installing without a check. Infrastructure failures should not block legitimate work. The gate is a best-effort defense, not an access control mechanism.
Coverage:
- OSV.dev: npm, PyPI, apt (Debian), dnf (AlmaLinux) — coverage varies by ecosystem
- Package age gate: npm (registry.npmjs.org), PyPI (pypi.org)
Monitoring: Upstream failures are logged at [WARNING] level. Search server logs for supply_chain / package_age to detect chronic upstream unavailability. There is no metric counter for gate bypasses yet — observe via log volume.