Watch
1
0
Fork
You've already forked RedFlag
0

crypto: forward-only key-path ceiling + OSV resilience + token serialization

SEC-028 -- a rotated-out server signing key must stop being trusted even when the agent cannot phone home. pubkey.go: bounded stale-cache window on public-key fetch failure; past the window (or when cache age is unknown) it fails closed instead of trusting the cached key indefinitely. Window length is operator policy (command_signing.stale_key_max_age_hours, default 168h/7d) delivered fleet-wide via GET /agents/:id/config; the [1h, 30d] clamp and the existence of the ceiling are doctrine, not knobs. verification.go: CheckKeyRotation refuses when the named key_id is not in the server active set (no primary fallback), and applies the same bounded-stale ceiling to the active-set fetch-failure path so key_id'd commands are no weaker than keyless ones. Server carries the default + 1-720h validation; web surfaces it in Security Settings.

SEC-029 -- the standalone OSV.dev client retries transient transport/5xx/429 with exponential backoff and trips a process-wide circuit breaker after a run of failures, fast-failing to 'unreachable'. Verdict semantics unchanged and still fail-closed; the resilience only stops a transient scanner blip from forcing an operator override.

GATE-004 #4 -- Consumer.ProcessToken holds a mutex so the replay-state guards are never raced by a concurrent caller. Today's single caller (the poll loop) never overlaps; this enforces the one-token-at-a-time invariant for future callers (local-API trigger, retry worker).

RAF/verification/03 and RAF/security/05 document the key-path and OSV changes. ETHOS #3, #4; forward-only doctrine.
This commit is contained in:
Fimeg 2026-06-14 12:57:04 -04:00
commit 0b1b8124b0
13 changed files with 374 additions and 57 deletions

View file

@ -1892,10 +1892,24 @@ func (h *AgentHandler) GetAgentConfig(c *gin.Context) {
polling["backoff_max_seconds"] = h.securitySettings.GetOperationalInt("backoff_max_seconds", 300)
}
// Command-signing policy (fleet-wide). Default matches the agent's built-in
// stale-key window so a missing settings row or a server without the settings
// service degrades to the same behavior. The agent clamps to its doctrinal
// ceiling regardless (SEC-028).
commandSigning := gin.H{"stale_key_max_age_hours": 168}
if h.securitySettings != nil {
if v, err := h.securitySettings.GetSetting("command_signing", "stale_key_max_age_hours"); err == nil {
if f, ok := v.(float64); ok {
commandSigning["stale_key_max_age_hours"] = int(f)
}
}
}
c.JSON(http.StatusOK, gin.H{
"subsystems": config,
"polling": polling,
"version": time.Now().UTC().Unix(), // Simple version timestamp
"subsystems": config,
"polling": polling,
"command_signing": commandSigning,
"version": time.Now().UTC().Unix(), // Simple version timestamp
})
}

View file

@ -336,6 +336,18 @@ func (s *SecuritySettingsService) ValidateSetting(category, key string, value in
return fmt.Errorf("soak_enforcement must be a string")
}
case "command_signing.stale_key_max_age_hours":
// Bounded, never zero/infinite: forward-only requires a fail-closed
// ceiling. 1h floor, 720h (30d) ceiling — mirrors the agent's doctrinal
// clamp so the UI can't offer a value the agent would reject.
if hours, ok := value.(float64); ok {
if hours < 1 || hours > 720 {
return fmt.Errorf("stale_key_max_age_hours must be between 1 and 720 (30 days)")
}
} else {
return fmt.Errorf("stale_key_max_age_hours must be a number")
}
case "command_signing.algorithm", "update_signing.algorithm":
if algo, ok := value.(string); ok {
if algo != "ed25519" {
@ -401,6 +413,12 @@ func (s *SecuritySettingsService) getDefaultSettings() map[string]map[string]int
"enabled": true,
"enforcement_mode": "strict",
"algorithm": "ed25519",
// stale_key_max_age_hours bounds how long an offline agent keeps
// trusting its cached server public key before it fails closed
// (SEC-028). Operator policy within a doctrinal range — the agent
// enforces a hard ceiling regardless, so this tunes but never disables
// forward-only. Default 168h (7d).
"stale_key_max_age_hours": 168.0,
},
"update_signing": {
"enabled": true,