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
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
177
RAF/verification/01-signing-pipeline.md
Normal file
177
RAF/verification/01-signing-pipeline.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Ed25519 Signing Pipeline
|
||||
|
||||
**Server-side command and binary signing with Ed25519.**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
All commands and binaries are signed with Ed25519 private key on the server. Agents verify signatures before execution.
|
||||
|
||||
**Cross-references:**
|
||||
- [verification/02-agent-verification](02-agent-verification.md) (agent-side verification)
|
||||
- [verification/03-key-rotation](03-key-rotation.md) (key rotation support)
|
||||
- [security/01-trust-boundaries](../security/01-trust-boundaries.md) (signature as trust boundary)
|
||||
|
||||
---
|
||||
|
||||
## Key Generation
|
||||
|
||||
**Method:** Server startup automatic key registration
|
||||
|
||||
**File:** `server/internal/services/signing.go`
|
||||
|
||||
```go
|
||||
func (s *SigningService) InitializePrimaryKey(ctx context.Context) error {
|
||||
// 1. Get current key fingerprint (SHA-256 of public key, truncated)
|
||||
keyID := s.GetCurrentKeyID()
|
||||
publicKeyHex := s.GetPublicKeyHex()
|
||||
|
||||
// 2. Query next version number from database
|
||||
nextVersion, err := s.signingKeyQueries.GetNextVersion(ctx)
|
||||
if err != nil {
|
||||
nextVersion = 1
|
||||
}
|
||||
|
||||
// 3. Insert key (ON CONFLICT DO NOTHING — safe on every startup)
|
||||
if err := s.signingKeyQueries.InsertSigningKey(ctx, keyID, publicKeyHex, nextVersion); err != nil {
|
||||
return fmt.Errorf("failed to insert signing key: %w", err)
|
||||
}
|
||||
|
||||
// 4. Set as primary
|
||||
if err := s.signingKeyQueries.SetPrimaryKey(ctx, keyID); err != nil {
|
||||
return fmt.Errorf("failed to set primary key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Signing
|
||||
|
||||
**Method:** v3 message format with agent_id binding
|
||||
|
||||
**File:** `server/internal/services/signing.go`
|
||||
|
||||
```go
|
||||
func (s *SigningService) SignCommand(cmd *models.AgentCommand) (string, error) {
|
||||
// 1. Record signing time and key identity
|
||||
now := time.Now().UTC()
|
||||
cmd.SignedAt = &now
|
||||
cmd.KeyID = s.GetCurrentKeyID()
|
||||
|
||||
// 2. Serialize params and hash
|
||||
paramsJSON, _ := json.Marshal(cmd.Params)
|
||||
paramsHash := sha256.Sum256(paramsJSON)
|
||||
paramsHashHex := hex.EncodeToString(paramsHash[:])
|
||||
|
||||
// 3. Create v3 message format
|
||||
// agent_id binding prevents cross-agent replay (F-1 fix)
|
||||
message := fmt.Sprintf("%s:%s:%s:%s:%d",
|
||||
cmd.AgentID.String(),
|
||||
cmd.ID.String(),
|
||||
cmd.CommandType,
|
||||
paramsHashHex,
|
||||
now.Unix())
|
||||
|
||||
// 4. Sign with Ed25519
|
||||
signature := ed25519.Sign(s.privateKey, []byte(message))
|
||||
return hex.EncodeToString(signature), nil
|
||||
}
|
||||
```
|
||||
|
||||
**v3 Message Format Benefits:**
|
||||
- **Agent binding:** Includes agent_id to prevent command relay attacks
|
||||
- **Timestamp:** Prevents replay attacks (4-hour max age)
|
||||
- **Parameter hash:** Hides full parameter data while allowing verification
|
||||
|
||||
---
|
||||
|
||||
## Binary Signing
|
||||
|
||||
**Method:** BuildOrchestrator signs binaries at startup
|
||||
|
||||
**File:** `server/internal/services/build_orchestrator.go`
|
||||
|
||||
```go
|
||||
func (s *BuildOrchestratorService) BuildAndSignAgent(version, platform, architecture string) (*models.AgentUpdatePackage, error) {
|
||||
// 1. Load binary from disk
|
||||
binaryName := "redflag-agent"
|
||||
if strings.HasPrefix(platform, "windows") {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(s.agentDir, "binaries", platform+"-"+architecture, binaryName)
|
||||
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("binary not found for platform %s: %w", platform, err)
|
||||
}
|
||||
|
||||
if s.signingService.IsEnabled() {
|
||||
// 2. Compute checksum and sign
|
||||
signedPackage, err := s.signingService.SignFile(binaryPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign agent binary: %w", err)
|
||||
}
|
||||
|
||||
// 3. Set metadata
|
||||
signedPackage.Version = version
|
||||
signedPackage.Platform = platform
|
||||
signedPackage.Architecture = architecture
|
||||
|
||||
// 4. Store in database
|
||||
err = s.packageQueries.StoreSignedPackage(signedPackage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to store signed package: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [server] [build_orchestrator] package_signed id=%s version=%s platform=%s arch=%s", signedPackage.ID, version, platform, architecture)
|
||||
return signedPackage, nil
|
||||
} else {
|
||||
log.Printf("Signing disabled, creating unsigned package entry")
|
||||
// Create unsigned package entry for backward compatibility
|
||||
unsignedPackage := &models.AgentUpdatePackage{
|
||||
ID: uuid.New(),
|
||||
Version: version,
|
||||
Platform: platform,
|
||||
Architecture: architecture,
|
||||
BinaryPath: binaryPath,
|
||||
Signature: "",
|
||||
Checksum: "",
|
||||
CreatedBy: "build-orchestrator",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Get file info
|
||||
if info, err := os.Stat(binaryPath); err == nil {
|
||||
unsignedPackage.FileSize = info.Size()
|
||||
}
|
||||
|
||||
// Store unsigned package
|
||||
err := s.packageQueries.StoreSignedPackage(unsignedPackage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to store unsigned package: %w", err)
|
||||
}
|
||||
|
||||
return unsignedPackage, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Footer: Assumptions & Connections
|
||||
|
||||
**Assumption:** Ed25519 signing is enabled when `REDFLAG_SIGNING_PRIVATE_KEY` environment variable is set.
|
||||
|
||||
**Connection:** [verification/02-agent-verification](02-agent-verification.md) (agent-side signature verification)
|
||||
|
||||
**Connection:** [verification/03-key-rotation](03-key-rotation.md) (multi-key rotation support)
|
||||
|
||||
**Connection:** [security/01-trust-boundaries](../security/01-trust-boundaries.md) (cryptographic trust boundary)
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-06-14*
|
||||
162
RAF/verification/02-agent-verification.md
Normal file
162
RAF/verification/02-agent-verification.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Agent-Side Verification
|
||||
|
||||
**Agent verifies commands and binaries before execution.**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Agent verifies Ed25519 signatures, timestamps, and nonces before executing commands.
|
||||
|
||||
**Cross-references:**
|
||||
- [verification/01-signing-pipeline](01-signing-pipeline.md) (server-side signing)
|
||||
- [verification/03-key-rotation](03-key-rotation.md) (key rotation)
|
||||
- [verification/04-replay-protection](04-replay-protection.md) (replay protection)
|
||||
|
||||
---
|
||||
|
||||
## Public Key Caching (TOFU)
|
||||
|
||||
**Method:** Trust On First Use — TTL+key_id cache with rotation awareness
|
||||
|
||||
**File:** `agent/internal/crypto/pubkey.go`
|
||||
|
||||
```go
|
||||
func FetchAndCacheServerPublicKey(serverURL string) (ed25519.PublicKey, error) {
|
||||
// 1. Check if cache is valid (TTL + key_id match)
|
||||
if meta, err := loadCacheMetadata(); err == nil && meta.KeyID != "" && !meta.IsExpired() {
|
||||
if cachedKey, loadErr := LoadCachedPublicKey(); loadErr == nil {
|
||||
return cachedKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch from server GET /api/v1/public-key
|
||||
pubKeyBytes, err := httpGetPublicKey(serverURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch public key: %w", err)
|
||||
}
|
||||
|
||||
// 3. Cache to disk
|
||||
if err := cachePublicKey(pubKeyBytes); err != nil {
|
||||
fmt.Printf("Warning: Failed to cache public key: %v\n", err)
|
||||
}
|
||||
|
||||
return pubKeyBytes, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Verification
|
||||
|
||||
**Method:** Verify v3 signature with timestamp, falls back to older formats for backward compatibility
|
||||
|
||||
**File:** `agent/internal/crypto/verification.go`
|
||||
|
||||
```go
|
||||
func (v *CommandVerifier) VerifyCommandWithTimestamp(
|
||||
cmd client.Command,
|
||||
serverPubKey ed25519.PublicKey,
|
||||
maxAge time.Duration,
|
||||
clockSkew time.Duration,
|
||||
) error {
|
||||
// 1. If cmd.SignedAt is nil, fall back to oldest format (backward compat)
|
||||
if cmd.SignedAt == nil {
|
||||
fmt.Printf("[WARNING] [agent] [crypto] command_uses_oldest_format command_id=%s no_signed_at=true upgrade_server_recommended\n", cmd.ID)
|
||||
return v.VerifyCommand(cmd, serverPubKey)
|
||||
}
|
||||
|
||||
// 2. Validate timestamp window
|
||||
now := time.Now().UTC()
|
||||
age := now.Sub(*cmd.SignedAt)
|
||||
if age > maxAge {
|
||||
return fmt.Errorf("command timestamp too old: signed %v ago (max %v)", age.Round(time.Second), maxAge)
|
||||
}
|
||||
if age < -clockSkew {
|
||||
return fmt.Errorf("command timestamp is in the future: %v ahead (max skew %v)", (-age).Round(time.Second), clockSkew)
|
||||
}
|
||||
|
||||
// 3. Try v3 format first (with agent_id) if AgentID is present
|
||||
if cmd.AgentID != "" {
|
||||
message, err := v.reconstructMessageV3(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconstruct v3 message: %w", err)
|
||||
}
|
||||
if ed25519.Verify(serverPubKey, message, sig) {
|
||||
return nil // v3 verification succeeded
|
||||
}
|
||||
// v3 failed — try v2 as fallback
|
||||
}
|
||||
|
||||
// 4. v2 format: timestamp but no agent_id (backward compat)
|
||||
message, err := v.reconstructMessageWithTimestamp(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reconstruct timestamped message: %w", err)
|
||||
}
|
||||
if !ed25519.Verify(serverPubKey, message, sig) {
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Verification Modes:**
|
||||
- **Strict:** Reject all verification failures (default)
|
||||
- **Warning:** Log failure but execute command
|
||||
- **Disabled:** Skip verification entirely
|
||||
|
||||
**Fallback Chain:** v3 (agent_id + timestamp) → v2 (timestamp only) → oldest (no timestamp)
|
||||
```
|
||||
|
||||
**Verification Modes:**
|
||||
- **Strict:** Reject all verification failures (default)
|
||||
- **Warning:** Log failure but execute command
|
||||
- **Disabled:** Skip verification entirely
|
||||
|
||||
---
|
||||
|
||||
## Binary Verification
|
||||
|
||||
**Method:** Verify checksum and Ed25519 signature before installation
|
||||
|
||||
**File:** `agent/internal/orchestrator/update_handler.go`
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- **Server signing** → [verification/01-signing-pipeline](01-signing-pipeline.md)
|
||||
- **Key rotation** → [verification/03-key-rotation](03-key-rotation.md)
|
||||
- **Replay protection** → [verification/04-replay-protection](04-replay-protection.md)
|
||||
- **Install script** → [flows/01-registration](../flows/01-registration.md) (TOFU key caching)
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-06-14*
|
||||
138
RAF/verification/03-key-rotation.md
Normal file
138
RAF/verification/03-key-rotation.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# 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 (1–720h 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*
|
||||
129
RAF/verification/04-replay-protection.md
Normal file
129
RAF/verification/04-replay-protection.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Replay Protection
|
||||
|
||||
**Multi-layer defense against command replay attacks.**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
RedFlag implements defense-in-depth against replay attacks at three layers:
|
||||
1. **Nonce validation** (temporal binding for update commands)
|
||||
2. **Command deduplication** (disk-based ID tracking)
|
||||
3. **Timestamp validation** (command age limits)
|
||||
|
||||
---
|
||||
|
||||
## Layer 1: Update Nonce Validation
|
||||
|
||||
**Purpose:** Bind update commands to specific time window.
|
||||
|
||||
**Mechanism:**
|
||||
- Server generates nonce with max age = 2× check-in interval
|
||||
- Nonce embedded in `update_agent` command signature
|
||||
- Agent must present valid nonce within expiry window
|
||||
- Server validates nonce age before execution
|
||||
|
||||
**Implementation:**
|
||||
- Server: `server/internal/services/update_nonce.go`
|
||||
- Validation: `server/internal/middleware/machine_binding.go:validateNonce()`
|
||||
- Nonce expiry: 2× `check_in_interval` (default 10 minutes)
|
||||
|
||||
**Security Model:**
|
||||
```
|
||||
Command dispatched → Nonce generated (age=0)
|
||||
Agent receives → Nonce cached (age < maxAge/2)
|
||||
Agent executes → Nonce validated (age < maxAge)
|
||||
Command executed → Nonce consumed (single-use)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Command Deduplication
|
||||
|
||||
**Purpose:** Prevent duplicate execution after agent restart.
|
||||
|
||||
**Mechanism:**
|
||||
- Executed command IDs persisted to `executed_commands.json`
|
||||
- 4-hour max age window (aligns with command TTL)
|
||||
- Atomic writes prevent corruption
|
||||
|
||||
**Implementation:**
|
||||
- Storage: `agent/internal/orchestrator/executed_commands.json`
|
||||
- Load: `loadExecutedCommands()`
|
||||
- Add: `executedIDs.Add(cmd.ID)`
|
||||
- Save: `saveExecutedCommands(executedIDs)`
|
||||
|
||||
**Deduplication Flow:**
|
||||
```
|
||||
Command received → Check executed IDs
|
||||
If duplicate → Reject with security event
|
||||
If unique → Add to executed IDs
|
||||
Execute command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 3: Timestamp Validation
|
||||
|
||||
**Purpose:** Reject stale commands regardless of nonce.
|
||||
|
||||
**Mechanism:**
|
||||
- Commands must have `created_at` within TTL window
|
||||
- Default TTL: 4 hours
|
||||
- Server rejects commands older than TTL
|
||||
|
||||
**Implementation:**
|
||||
- Validation: `server/internal/handlers/agents.go:validateCommandTimestamp()`
|
||||
- TTL: 4 hours (configurable via `security_settings.command_ttl_hours`)
|
||||
|
||||
---
|
||||
|
||||
## Combined Protection Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Command Execution │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. Agent polls server for commands │
|
||||
│ 2. Server returns signed command with nonce │
|
||||
│ 3. Agent validates nonce age (Layer 1) │
|
||||
│ 4. Agent checks executed_commands.json (Layer 2) │
|
||||
│ 5. Agent executes command │
|
||||
│ 6. Agent records command ID in executed_commands.json │
|
||||
│ 7. Command times out (Layer 3) → rejected on retry │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Circuit Breaker Integration
|
||||
|
||||
**Purpose:** Prevent replay during scanner failures.
|
||||
|
||||
**Mechanism:**
|
||||
- Circuit breaker per subsystem (APT, DNF, Winget, WUA, Docker)
|
||||
- Opens after N failures in T window
|
||||
- Blocks all scanner commands while open
|
||||
|
||||
**Implementation:**
|
||||
- `agent/internal/circuitbreaker/circuitbreaker.go`
|
||||
- Failure threshold: 5 failures in 60 seconds
|
||||
- Open duration: 5 minutes
|
||||
|
||||
---
|
||||
|
||||
## Footer: Assumptions & Connections
|
||||
|
||||
**Assumption:** Replay attacks are rare — protection is defense-in-depth, not primary security.
|
||||
|
||||
**Connection:** Nonce validation (`verification/04-replay-protection.md`) complements machine binding (`security/02-authentication-stack.md`).
|
||||
|
||||
**Connection:** Command deduplication (`verification/04-replay-protection.md`) implements ETHOS #4 (idempotency).
|
||||
|
||||
**Connection:** Circuit breaker (`verification/04-replay-protection.md`) implements ETHOS #3 (assume failure).
|
||||
|
||||
**Connection:** Nonce service (`server/internal/services/update_nonce.go`) ties to security settings (`security_settings.operational`).
|
||||
|
||||
---
|
||||
|
||||
*Last reviewed: 2026-05-26*
|
||||
Loading…
Reference in a new issue