Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/RAF/flows/03-agent-upgrade.md
Fimeg 35732a63fd fix(GATE-005): harden helper trusted-input — argv separator + UUID v4 request_id
- apt build_plan: add POSIX '--' before user-derived name=version tokens
- dnf build_plan: add POSIX '--' before user-derived name-version token
- validate_mint_request: enforce canonical UUID v4 on request_id at intake
- RAF components/04: document '--' separator in pipeline step 7
- RAF security/05: document hardened argv contract + UUID v4 validation
- RAF flows/03: add stale banner (5 deviations from current code)

14 tests pass. Deferred: artifact_path staging root, name/version charset
regex (both need design decisions — see task file).
2026-06-17 22:07:36 -04:00

8.7 KiB
Raw Blame History

⚠ STALE — known deviations from current code (flagged 2026-06-16 by Vanguard). This flow doc describes code that no longer exists. It will be rewritten for v0.3.0 when the Windows mutation helper (SEC-030) lands. Until then, treat the below as historical reference only — not design of record.

Known deviations:

  1. Params: {"version": "latest"} — server now resolves to serverVersion.AgentVersion before sending (CRITICAL-009 fix). "latest" rendered as vlatest and broke the gate.
  2. File paths are wrong. agent/internal/orchestrator/update_handler.go does not exist. Actual handler: agent/internal/handlers/agent_update.go.
  3. Two execution paths now exist (not one "atomic swap"):
    • Linux: capability-token helper owns the swap (installAgentViaHelper). Agent holds no sudo for cp/chmod/restart.
    • Non-Linux (Windows): in-process binary swap + detached sc restart fallback. No capability token. Tracked as doctrine gap in SEC-030.
  4. Watchdog/rollback removed. runUpdateWatchdog() and rollbackUpdate() were intentionally deleted — "could not survive systemd's SIGTERM and has been removed. Server-side reconcileAgentUpdates closes the command." The code samples below show functions that do not exist.
  5. Capability-token authorization is not mentioned at all — it is now the load-bearing authorization for Linux self-upgrade.

Agent Self-Upgrade Flow

7-step agent upgrade with rollback and verification.


Overview

Agents can self-update without manual intervention. The flow includes nonce validation, checksum verification, Ed25519 signature verification, atomic binary swap, and automatic rollback on failure.

Cross-references:

  • flows/02-command-execution.md (command dispatch)
  • verification/01-signing-pipeline.md (binary signing)
  • verification/02-agent-verification.md (binary verification)
  • verification/04-replay-protection.md (nonce validation)
  • security/02-authentication-stack.md (machine binding)

Step-by-Step Flow

1. Admin Triggers Update

File: server/internal/api/handlers/agent_updates.go

func (h *AgentUpdateHandler) TriggerAgentUpdate(w http.ResponseWriter, r *http.Request) {
    // 1. Validate agent ID
    agentID := r.URL.Query().Get("agent_id")
    if agentID == "" {
        http.Error(w, "agent_id required", http.StatusBadRequest)
        return
    }

    // 2. Create signed update_agent command
    cmd := &Command{
        AgentID: agentID,
        ID:      uuid.New(),
        Type:    "update_agent",
        Params:  `{"version": "latest"}`,
    }

    // 3. Generate nonce (2× check-in interval)
    nonce := h.nonceService.GenerateNonce()

    // 4. Sign command with Ed25519
    signature := h.signingService.SignCommand(cmd, nonce)

    // 5. Store command in database
    h.db.CreateCommand(cmd, signature)

    logSecurityEvent("[operation] [server] [update] Triggered update for agent:", agentID)
}

2. Agent Receives Command

File: agent/internal/agent/loop.go

commands, err := agent.client.GetCommands(ctx, agent.agentID, agent.machineID, agent.metrics)
for _, cmd := range commands {
    if cmd.Type == "update_agent" {
        // Validate nonce first
        if err := agent.validateNonce(cmd); err != nil {
            logSecurityEvent("[security] [agent] [nonce] Nonce validation failed:", err)
            continue
        }

        // Execute update_agent command
        result := agent.orchestrator.ExecuteCommand(cmd)
    }
}

3. Verify Command Nonce

File: agent/internal/orchestrator/command_handler.go

func (c *CommandHandler) validateNonce(cmd *Command) error {
    nonce := cmd.Nonce
    maxAge := time.Duration(c.timeoutConfig.NonceMaxAge)

    if time.Since(cmd.CreatedAt) > maxAge {
        return errors.New("nonce expired")
    }

    return nil
}

4. Verify Command Signature

File: agent/internal/crypto/verification.go

func (v *Verifier) VerifyCommand(cmd *Command) error {
    // 1. Parse signature from v3 format
    parts := strings.Split(cmd.Signature, ":")
    if len(parts) != 6 {
        return errors.New("invalid signature format")
    }

    // 2. Verify Ed25519 signature
    agentID, id, cmdType, paramsHash, timestamp := parts[0], parts[1], parts[2], parts[3], parts[4]
    expected := fmt.Sprintf("%s:%s:%s:%s:%s", agentID, id, cmdType, paramsHash, timestamp)

    if !ed25519.Verify(v.serverPublicKey, []byte(expected), []byte(parts[5])) {
        return errors.New("signature verification failed")
    }

    return nil
}

5. Download Agent Binary

Endpoint: GET /api/v1/downloads/agent?version=latest

File: server/internal/api/handlers/downloads.go

func (h *DownloadHandler) DownloadAgent(w http.ResponseWriter, r *http.Request) {
    platform := r.URL.Query().Get("platform")
    version := r.URL.Query().Get("version")

    // 1. Fetch signed package from DB
    signedPackage := h.db.GetSignedPackage(platform, version)

    if signedPackage == nil {
        http.Error(w, "package not found", http.StatusNotFound)
        return
    }

    // 2. Serve binary with checksum + signature headers
    binaryData, _ := h.fs.ReadFile(signedPackage.BinaryPath)
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Header().Set("X-Content-SHA256", signedPackage.Checksum)
    w.Header().Set("X-Content-Signature", signedPackage.Signature)
    w.Write(binaryData)
}

6. Verify Checksum and Signature

File: agent/internal/orchestrator/update_handler.go

func (h *UpdateHandler) verifyDownload(binaryData []byte, checksum string) error {
    // 1. Verify checksum
    computedChecksum := sha256.Sum256(binaryData)
    if hex.EncodeToString(computedChecksum[:]) != checksum {
        return errors.New("checksum mismatch")
    }

    // 2. Verify Ed25519 signature
    signature, err := h.downloadBinarySignature()
    if err != nil {
        return err
    }

    publicKey, err := h.LoadCachedPublicKey()
    if err != nil {
        return err
    }

    if !ed25519.Verify(publicKey, binaryData, []byte(signature)) {
        return errors.New("signature verification failed")
    }

    return nil
}

7. Atomic Install with Backup

File: agent/internal/orchestrator/update_handler.go

func (h *UpdateHandler) atomicInstall(binaryData []byte) error {
    binaryPath := "/usr/local/bin/redflag-agent"

    // 1. Create backup
    backupPath := binaryPath + ".bak"
    if err := os.Rename(binaryPath, backupPath); err != nil {
        return err
    }

    // 2. Atomic install (write to temp file, then rename)
    tempPath := binaryPath + ".tmp"
    if err := os.WriteFile(tempPath, binaryData, 0755); err != nil {
        // Rollback: restore backup
        os.Rename(backupPath, binaryPath)
        return err
    }

    os.Rename(tempPath, binaryPath)

    // 3. Restart service
    return h.restartService()
}

Watchdog and Rollback

File: agent/internal/orchestrator/update_handler.go

func (a *Agent) runUpdateWatchdog() {
    // 1. Start 15-minute watchdog (configurable)
    done := make(chan bool)
    go func() {
        time.Sleep(15 * time.Minute)  // Default: operational.agent_update_timeout_minutes
        done <- true
    }()

    // 2. Wait for update completion
    select {
    case <-done:
        // Update completed or timed out
        logSecurityEvent("[operation] [agent] [update] Watchdog timeout, rolling back")
        a.rollbackUpdate()

    case <-a.updateCompleteChan:
        // Update acknowledged by server
        logSecurityEvent("[operation] [agent] [update] Server acknowledged new version")
        return
    }
}

func (a *Agent) rollbackUpdate() {
    backupPath := "/usr/local/bin/redflag-agent.bak"
    binaryPath := "/usr/local/bin/redflag-agent"

    // Restore backup
    os.Rename(backupPath, binaryPath)

    // Restart old version
    a.restartService()

    // Report timeout
    a.reportUpdateTimeout()
}

Assumption: Self-upgrade is a privileged operation — requires admin approval and nonce validation.

Connection: Nonce validation (verification/04-replay-protection.md) prevents replay attacks on update commands.

Connection: Machine binding (security/04-machine-binding.md) ensures only authorized agent can receive updates.

Connection: Atomic install (flows/03-agent-upgrade.md) implements ETHOS #4 (idempotency).

Connection: Backup mechanism (flows/03-agent-upgrade.md) enables rollback on failure.

Connection: Watchdog timeout (flows/03-agent-upgrade.md) aligns with operational.agent_update_timeout_minutes setting.


Last reviewed: 2026-05-26