Files
Redflag/aggregator-agent/internal/config/docker.go
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
Complete RedFlag codebase with two major security audit implementations.

== A-1: Ed25519 Key Rotation Support ==

Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management

Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing

== A-2: Replay Attack Fixes (F-1 through F-7) ==

F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH     - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH     - Migration 026: expires_at column with partial index
F-6 HIGH     - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH     - Agent-side executedIDs dedup map with cleanup
F-4 HIGH     - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt

Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.

All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:25:47 -04:00

183 lines
5.5 KiB
Go

package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
)
// DockerSecretsConfig holds Docker secrets configuration
type DockerSecretsConfig struct {
Enabled bool `json:"enabled"`
SecretsPath string `json:"secrets_path"`
EncryptionKey string `json:"encryption_key,omitempty"`
Secrets map[string]string `json:"secrets,omitempty"`
}
// LoadDockerConfig loads Docker configuration if available
func LoadDockerConfig(configPath string) (*DockerSecretsConfig, error) {
dockerConfigPath := filepath.Join(configPath, "docker.json")
// Check if Docker config exists
if _, err := os.Stat(dockerConfigPath); os.IsNotExist(err) {
return &DockerSecretsConfig{Enabled: false}, nil
}
data, err := ioutil.ReadFile(dockerConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to read Docker config: %w", err)
}
var dockerConfig DockerSecretsConfig
if err := json.Unmarshal(data, &dockerConfig); err != nil {
return nil, fmt.Errorf("failed to parse Docker config: %w", err)
}
// Set default secrets path if not specified
if dockerConfig.SecretsPath == "" {
dockerConfig.SecretsPath = getDefaultSecretsPath()
}
return &dockerConfig, nil
}
// getDefaultSecretsPath returns the default Docker secrets path for the platform
func getDefaultSecretsPath() string {
if runtime.GOOS == "windows" {
return `C:\ProgramData\Docker\secrets`
}
return "/run/secrets"
}
// ReadSecret reads a secret from Docker secrets or falls back to file
func ReadSecret(secretName, fallbackPath string, dockerConfig *DockerSecretsConfig) ([]byte, error) {
// Try Docker secrets first if enabled
if dockerConfig != nil && dockerConfig.Enabled {
secretPath := filepath.Join(dockerConfig.SecretsPath, secretName)
if data, err := ioutil.ReadFile(secretPath); err == nil {
fmt.Printf("[DOCKER] Read secret from Docker: %s\n", secretName)
return data, nil
}
}
// Fall back to file system
if fallbackPath != "" {
if data, err := ioutil.ReadFile(fallbackPath); err == nil {
fmt.Printf("[CONFIG] Read secret from file: %s\n", fallbackPath)
return data, nil
}
}
return nil, fmt.Errorf("secret not found: %s", secretName)
}
// MergeConfigWithSecrets merges configuration with Docker secrets
func MergeConfigWithSecrets(config *Config, dockerConfig *DockerSecretsConfig) error {
if dockerConfig == nil || !dockerConfig.Enabled {
return nil
}
// If there's an encrypted config, decrypt and merge it
if encryptedConfigPath, exists := dockerConfig.Secrets["config"]; exists {
if err := mergeEncryptedConfig(config, encryptedConfigPath, dockerConfig.EncryptionKey); err != nil {
return fmt.Errorf("failed to merge encrypted config: %w", err)
}
}
// Apply other secrets to configuration
if err := applySecretsToConfig(config, dockerConfig); err != nil {
return fmt.Errorf("failed to apply secrets to config: %w", err)
}
return nil
}
// mergeEncryptedConfig decrypts and merges encrypted configuration
func mergeEncryptedConfig(config *Config, encryptedPath, encryptionKey string) error {
if encryptionKey == "" {
return fmt.Errorf("no encryption key available for encrypted config")
}
// Create temporary file for decrypted config
tempPath := encryptedPath + ".tmp"
defer os.Remove(tempPath)
// Decrypt the config file
// Note: This would need to import the migration package's DecryptFile function
// For now, we'll assume the decryption happens elsewhere
return fmt.Errorf("encrypted config merge not yet implemented")
}
// applySecretsToConfig applies Docker secrets to configuration fields
func applySecretsToConfig(config *Config, dockerConfig *DockerSecretsConfig) error {
// Apply proxy secrets
if proxyUsername, exists := dockerConfig.Secrets["proxy_username"]; exists {
config.Proxy.Username = proxyUsername
}
if proxyPassword, exists := dockerConfig.Secrets["proxy_password"]; exists {
config.Proxy.Password = proxyPassword
}
// Apply TLS secrets
if certFile, exists := dockerConfig.Secrets["tls_cert"]; exists {
config.TLS.CertFile = certFile
}
if keyFile, exists := dockerConfig.Secrets["tls_key"]; exists {
config.TLS.KeyFile = keyFile
}
if caFile, exists := dockerConfig.Secrets["tls_ca"]; exists {
config.TLS.CAFile = caFile
}
// Apply registration token
if regToken, exists := dockerConfig.Secrets["registration_token"]; exists {
config.RegistrationToken = regToken
}
return nil
}
// IsDockerEnvironment checks if the agent is running in Docker
func IsDockerEnvironment() bool {
// Check for .dockerenv file
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check for Docker in cgroup
if data, err := ioutil.ReadFile("/proc/1/cgroup"); err == nil {
if contains(string(data), "docker") {
return true
}
}
return false
}
// SaveDockerConfig saves Docker configuration to disk
func SaveDockerConfig(dockerConfig *DockerSecretsConfig, configPath string) error {
dockerConfigPath := filepath.Join(configPath, "docker.json")
data, err := json.MarshalIndent(dockerConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal Docker config: %w", err)
}
if err := ioutil.WriteFile(dockerConfigPath, data, 0600); err != nil {
return fmt.Errorf("failed to write Docker config: %w", err)
}
fmt.Printf("[DOCKER] Saved Docker config: %s\n", dockerConfigPath)
return nil
}
// contains checks if a string contains a substring (case-insensitive)
func contains(s, substr string) bool {
s = strings.ToLower(s)
substr = strings.ToLower(substr)
return strings.Contains(s, substr)
}