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.
344 lines
11 KiB
Go
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
|
|
} |