Files
Redflag/aggregator-agent/internal/migration/docker_executor.go
Fimeg 4531ca34c5 refactor: consolidate AgentFile struct into common package
Created aggregator/pkg/common module with shared AgentFile type.
Removed duplicate definitions from migration and services packages.
Both agent and server now use common.AgentFile.
2025-11-10 22:03:43 -05:00

344 lines
11 KiB
Go

package migration
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/Fimeg/RedFlag/aggregator/pkg/common"
)
// DockerSecretsExecutor handles the execution of Docker secrets migration
type DockerSecretsExecutor struct {
detection *DockerDetection
config *FileDetectionConfig
encryption string
}
// NewDockerSecretsExecutor creates a new Docker secrets executor
func NewDockerSecretsExecutor(detection *DockerDetection, config *FileDetectionConfig) *DockerSecretsExecutor {
return &DockerSecretsExecutor{
detection: detection,
config: config,
}
}
// ExecuteDockerSecretsMigration performs the Docker secrets migration
func (e *DockerSecretsExecutor) ExecuteDockerSecretsMigration() error {
if !e.detection.DockerAvailable {
return fmt.Errorf("docker secrets not available")
}
if !e.detection.MigrateToSecrets {
fmt.Printf("[DOCKER] No secrets to migrate\n")
return nil
}
fmt.Printf("[DOCKER] Starting Docker secrets migration...\n")
// Generate encryption key for config files
encKey, err := GenerateEncryptionKey()
if err != nil {
return fmt.Errorf("failed to generate encryption key: %w", err)
}
e.encryption = encKey
// Create backup before migration
if err := e.createSecretsBackup(); err != nil {
return fmt.Errorf("failed to create secrets backup: %w", err)
}
// Migrate each secret file
for _, secretFile := range e.detection.SecretFiles {
if err := e.migrateSecretFile(secretFile); err != nil {
fmt.Printf("[DOCKER] Failed to migrate secret file %s: %v\n", secretFile.Path, err)
continue
}
}
// Create Docker secrets configuration
if err := e.createDockerConfig(); err != nil {
return fmt.Errorf("failed to create Docker config: %w", err)
}
// Remove original secret files
if err := e.removeOriginalSecrets(); err != nil {
return fmt.Errorf("failed to remove original secrets: %w", err)
}
fmt.Printf("[DOCKER] Docker secrets migration completed successfully\n")
fmt.Printf("[DOCKER] Encryption key: %s\n", encKey)
fmt.Printf("[DOCKER] Save this key securely for decryption\n")
return nil
}
// createSecretsBackup creates a backup of secret files before migration
func (e *DockerSecretsExecutor) createSecretsBackup() error {
timestamp := time.Now().Format("2006-01-02-150405")
backupDir := fmt.Sprintf("/etc/redflag.backup.secrets.%s", timestamp)
if err := os.MkdirAll(backupDir, 0755); err != nil {
return fmt.Errorf("failed to create backup directory: %w", err)
}
for _, secretFile := range e.detection.SecretFiles {
backupPath := filepath.Join(backupDir, filepath.Base(secretFile.Path))
if err := copySecretFile(secretFile.Path, backupPath); err != nil {
fmt.Printf("[DOCKER] Failed to backup secret file %s: %v\n", secretFile.Path, err)
} else {
fmt.Printf("[DOCKER] Backed up secret file: %s → %s\n", secretFile.Path, backupPath)
}
}
return nil
}
// migrateSecretFile migrates a single secret file to Docker secrets
func (e *DockerSecretsExecutor) migrateSecretFile(secretFile common.AgentFile) error {
secretName := filepath.Base(secretFile.Path)
secretPath := filepath.Join(e.detection.SecretsMountPath, secretName)
// Handle config.json specially (encrypt it)
if secretName == "config.json" {
return e.migrateConfigFile(secretFile)
}
// Copy secret file to Docker secrets directory
if err := copySecretFile(secretFile.Path, secretPath); err != nil {
return fmt.Errorf("failed to copy secret to Docker mount: %w", err)
}
// Set secure permissions
if err := os.Chmod(secretPath, 0400); err != nil {
return fmt.Errorf("failed to set secret permissions: %w", err)
}
fmt.Printf("[DOCKER] Migrated secret: %s → %s\n", secretFile.Path, secretPath)
return nil
}
// migrateConfigFile handles special migration of config.json with encryption
func (e *DockerSecretsExecutor) migrateConfigFile(secretFile common.AgentFile) error {
// Read original config
configData, err := os.ReadFile(secretFile.Path)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Parse config to separate sensitive from non-sensitive data
var config map[string]interface{}
if err := json.Unmarshal(configData, &config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
// Split config into public and sensitive parts
publicConfig, sensitiveConfig := e.splitConfig(config)
// Write public config back to original location
publicData, err := json.MarshalIndent(publicConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal public config: %w", err)
}
if err := os.WriteFile(secretFile.Path, publicData, 0644); err != nil {
return fmt.Errorf("failed to write public config: %w", err)
}
// Encrypt sensitive config
sensitiveData, err := json.MarshalIndent(sensitiveConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal sensitive config: %w", err)
}
tempSensitivePath := secretFile.Path + ".sensitive"
if err := os.WriteFile(tempSensitivePath, sensitiveData, 0600); err != nil {
return fmt.Errorf("failed to write sensitive config: %w", err)
}
defer os.Remove(tempSensitivePath)
// Encrypt sensitive config
encryptedPath := filepath.Join(e.detection.SecretsMountPath, "config.json.enc")
if err := EncryptFile(tempSensitivePath, encryptedPath, e.encryption); err != nil {
return fmt.Errorf("failed to encrypt config: %w", err)
}
fmt.Printf("[DOCKER] Migrated config with encryption: %s → %s (public) + %s (encrypted)\n",
secretFile.Path, secretFile.Path, encryptedPath)
return nil
}
// splitConfig splits configuration into public and sensitive parts
func (e *DockerSecretsExecutor) splitConfig(config map[string]interface{}) (map[string]interface{}, map[string]interface{}) {
public := make(map[string]interface{})
sensitive := make(map[string]interface{})
sensitiveFields := []string{
"password", "token", "key", "secret", "credential",
"proxy", "tls", "certificate", "private",
}
for key, value := range config {
if e.isSensitiveField(key, value, sensitiveFields) {
sensitive[key] = value
} else {
public[key] = value
}
}
return public, sensitive
}
// isSensitiveField checks if a field contains sensitive data
func (e *DockerSecretsExecutor) isSensitiveField(key string, value interface{}, sensitiveFields []string) bool {
// Check key name
for _, field := range sensitiveFields {
if strings.Contains(strings.ToLower(key), strings.ToLower(field)) {
return true
}
}
// Check nested values
if nested, ok := value.(map[string]interface{}); ok {
for nKey, nValue := range nested {
if e.isSensitiveField(nKey, nValue, sensitiveFields) {
return true
}
}
}
return false
}
// createDockerConfig creates the Docker secrets configuration file
func (e *DockerSecretsExecutor) createDockerConfig() error {
dockerConfig := DockerConfig{
Enabled: true,
SecretsPath: e.detection.SecretsMountPath,
EncryptionKey: e.encryption,
Secrets: make(map[string]string),
}
// Map secret files to their Docker secret names
for _, secretFile := range e.detection.SecretFiles {
secretName := filepath.Base(secretFile.Path)
if secretName == "config.json" {
dockerConfig.Secrets["config"] = "config.json.enc"
} else {
dockerConfig.Secrets[secretName] = secretName
}
}
// Write Docker config
configPath := filepath.Join(e.config.NewConfigPath, "docker.json")
configData, err := json.MarshalIndent(dockerConfig, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal Docker config: %w", err)
}
if err := os.WriteFile(configPath, configData, 0600); err != nil {
return fmt.Errorf("failed to write Docker config: %w", err)
}
fmt.Printf("[DOCKER] Created Docker config: %s\n", configPath)
return nil
}
// removeOriginalSecrets removes the original secret files after migration
func (e *DockerSecretsExecutor) removeOriginalSecrets() error {
for _, secretFile := range e.detection.SecretFiles {
// Don't remove config.json as it's been split into public part
if filepath.Base(secretFile.Path) == "config.json" {
continue
}
if err := os.Remove(secretFile.Path); err != nil {
fmt.Printf("[DOCKER] Failed to remove original secret %s: %v\n", secretFile.Path, err)
} else {
fmt.Printf("[DOCKER] Removed original secret: %s\n", secretFile.Path)
}
}
return nil
}
// copySecretFile copies a file from src to dst (renamed to avoid conflicts)
func copySecretFile(src, dst string) error {
// Read source file
data, err := os.ReadFile(src)
if err != nil {
return err
}
// Ensure destination directory exists
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
// Write destination file
return os.WriteFile(dst, data, 0644)
}
// ValidateDockerSecretsMigration validates that the Docker secrets migration was successful
func (e *DockerSecretsExecutor) ValidateDockerSecretsMigration() error {
// Check that Docker secrets directory exists
if _, err := os.Stat(e.detection.SecretsMountPath); err != nil {
return fmt.Errorf("Docker secrets directory not accessible: %w", err)
}
// Check that all required secrets exist
for _, secretName := range e.detection.RequiredSecrets {
secretPath := filepath.Join(e.detection.SecretsMountPath, secretName)
if _, err := os.Stat(secretPath); err != nil {
return fmt.Errorf("required secret not found: %s", secretName)
}
}
// Check that Docker config exists
dockerConfigPath := filepath.Join(e.config.NewConfigPath, "docker.json")
if _, err := os.Stat(dockerConfigPath); err != nil {
return fmt.Errorf("Docker config not found: %w", err)
}
fmt.Printf("[DOCKER] Docker secrets migration validation successful\n")
return nil
}
// RollbackDockerSecretsMigration rolls back the Docker secrets migration
func (e *DockerSecretsExecutor) RollbackDockerSecretsMigration(backupDir string) error {
fmt.Printf("[DOCKER] Rolling back Docker secrets migration from backup: %s\n", backupDir)
// Restore original secret files from backup
entries, err := os.ReadDir(backupDir)
if err != nil {
return fmt.Errorf("failed to read backup directory: %w", err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
backupPath := filepath.Join(backupDir, entry.Name())
originalPath := filepath.Join(e.config.NewConfigPath, entry.Name())
if err := copySecretFile(backupPath, originalPath); err != nil {
fmt.Printf("[DOCKER] Failed to restore %s: %v\n", entry.Name(), err)
} else {
fmt.Printf("[DOCKER] Restored: %s\n", entry.Name())
}
}
// Remove Docker config
dockerConfigPath := filepath.Join(e.config.NewConfigPath, "docker.json")
if err := os.Remove(dockerConfigPath); err != nil {
fmt.Printf("[DOCKER] Failed to remove Docker config: %v\n", err)
}
fmt.Printf("[DOCKER] Docker secrets migration rollback completed\n")
return nil
}