package migration import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/Fimeg/RedFlag/aggregator-agent/internal/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 }