RedFlag/RAF/verification/03-key-rotation.md
Fimeg 67e26be2d9 projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
2026-09-08 21:59:33 -04:00

138 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Key Rotation Support
**Ed25519 signing key rotation without downtime.**
---
## Overview
RedFlag supports rotating the server's Ed25519 signing key while maintaining agent trust continuity via the TOFU (Trust On First Use) model.
---
## Key Storage
**Server-side:**
- Private key: `REDFLAG_SIGNING_PRIVATE_KEY` (env var or Docker secret)
- Public key: Stored in database table `server_public_keys`
- Key format: Ed25519 (32-byte curve25519)
**Agent-side:**
- Public key cached at registration (`~/.config/redflag/server_keys.json`)
- Cached keys validated against active keys in database
- TOFU: First key accepted becomes trusted forever
---
## Rotation Process
### Server-Side Rotation
```
1. Generate new key pair (Ed25519)
2. INSERT new public key into server_public_keys (status = 'pending')
3. Update server config to use new private key
4. Sign next command with new key
5. Agents verify signature against new key (fails old key check)
6. Agents accept new key as valid (TOFU update)
7. Old key marked deprecated in database
8. Old keys removed after grace period
```
**Implementation:**
- `server/internal/services/signing.go:SetPrimaryKey()`
- `server/internal/database/migrations/035_server_public_keys.up.sql`
---
## Agent Verification Flow
```
1. Agent receives signed command
2. Extract public key from signature (if v3 format)
3. Check if key exists in cached trusted keys
4. If not cached → fetch from server_public_keys table
5. Validate key signature against command
6. Accept key as trusted (TOFU)
7. Update local cache
```
**Implementation:**
- `agent/internal/crypto/pubkey.go` (TOFU key caching)
- `agent/internal/crypto/verification.go:VerifyCommandWithTimestamp()`
---
## Key Lifecycle States
| State | Description | Database Field |
|-------|-------------|----------------|
| `active` | Currently signing commands | `status = 'active'` |
| `pending` | Newly added, awaiting agent adoption | `status = 'pending'` |
| `deprecated` | Old key, still accepted | `status = 'deprecated'` |
| `revoked` | Compromised or removed key | `status = 'revoked'` |
---
## Security Considerations
### During Rotation
- Old key remains `active` until new key is confirmed by agents
- Grace period prevents agents from being orphaned
- No downtime for command dispatch
### Key Compromise
- Immediately revoke compromised key
- Force agents to re-fetch public key list
- Affected commands can be replayed until key revocation propagates
### Forward-Only Enforcement at the Agent Key Path
A rotated-out or revoked key must stop being trusted. Two agent-side rules make
that hold even when the agent cannot reach the server (SEC-028):
- **Bounded stale cache.** When the public-key fetch fails (network down), the
agent keeps serving its last cached key only within a bounded staleness
window. Past the window — or when the cache age cannot be established (no
metadata sidecar) — it fails closed rather than trusting a possibly
rotated-out key indefinitely. Acceptance within the window is surfaced at
ERROR, not WARNING: it is a degraded-trust state, not routine.
- The **window length** is operator policy: security setting
`command_signing.stale_key_max_age_hours` (default 168h / 7d), delivered
fleet-wide via `GET /api/v1/agents/:id/config` and overridable per
deployment for sites with long offline windows.
- The **existence of a fail-closed ceiling is doctrine, not a knob.** The
agent clamps any configured value to `[1h, 30d]` (`SetStaleKeyMaxAge`) and
the server rejects out-of-range writes (1720h validation). No setting and
no tampered local config can disable the ceiling or set it to infinite.
- **Active-set refusal.** When a command names a `key_id` the server's active
set does not contain, the agent refuses verification (returns an error that
the command handler surfaces as a verification failure) instead of falling
back to the primary cached key. A key the server has rotated out is dead.
**Implementation:**
- `agent/internal/crypto/pubkey.go` (`FetchAndCacheServerPublicKey`, `SetStaleKeyMaxAge`)
- `agent/internal/crypto/verification.go:CheckKeyRotation()`
- `server/internal/services/security_settings_service.go` (`command_signing.stale_key_max_age_hours` default + validation)
### TOFU Limitations
- Compromised initial key → all future keys trusted
- Mitigation: Monitor agent registration patterns
- Mitigation: Periodic key rotation limits exposure window
---
## Footer: Assumptions & Connections
**Assumption:** Key rotation is a rare operation — expected once per year or less.
**Connection:** Rotation process (`verification/03-key-rotation.md`) complements TOFU caching (`verification/02-agent-verification.md`).
**Connection:** `server_public_keys` table enables rotation without code changes.
**Connection:** Agent-side key caching (`agent/internal/crypto/pubkey.go`) implements TOFU.
---
*Last reviewed: 2026-05-26*