package services import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/hex" "fmt" "io" "os" "path/filepath" "runtime" ) // SecretsManager handles Docker secrets creation and management type SecretsManager struct { secretsPath string encryptionKey string } // NewSecretsManager creates a new secrets manager func NewSecretsManager() *SecretsManager { secretsPath := getSecretsPath() return &SecretsManager{ secretsPath: secretsPath, } } // CreateDockerSecrets creates Docker secrets from the provided secrets map func (sm *SecretsManager) CreateDockerSecrets(secrets map[string]string) error { if len(secrets) == 0 { return nil } // Ensure secrets directory exists if err := os.MkdirAll(sm.secretsPath, 0755); err != nil { return fmt.Errorf("failed to create secrets directory: %w", err) } // Generate encryption key if not provided if sm.encryptionKey == "" { key, err := sm.GenerateEncryptionKey() if err != nil { return fmt.Errorf("failed to generate encryption key: %w", err) } sm.encryptionKey = key } // Create each secret for name, value := range secrets { if err := sm.createSecret(name, value); err != nil { return fmt.Errorf("failed to create secret %s: %w", name, err) } } return nil } // createSecret creates a single Docker secret func (sm *SecretsManager) createSecret(name, value string) error { secretPath := filepath.Join(sm.secretsPath, name) // Encrypt sensitive values encryptedValue, err := sm.encryptSecret(value) if err != nil { return fmt.Errorf("failed to encrypt secret: %w", err) } // Write secret file with restricted permissions if err := os.WriteFile(secretPath, encryptedValue, 0400); err != nil { return fmt.Errorf("failed to write secret file: %w", err) } return nil } // encryptSecret encrypts a secret value using AES-256-GCM func (sm *SecretsManager) encryptSecret(value string) ([]byte, error) { // Generate key from master key keyBytes, err := hex.DecodeString(sm.encryptionKey) if err != nil { return nil, fmt.Errorf("invalid encryption key format: %w", err) } // Create cipher block, err := aes.NewCipher(keyBytes) if err != nil { return nil, fmt.Errorf("failed to create cipher: %w", err) } // Create GCM gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) } // Generate nonce nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } // Encrypt ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil) // Prepend nonce to ciphertext result := append(nonce, ciphertext...) return result, nil } // decryptSecret decrypts a secret value using AES-256-GCM func (sm *SecretsManager) decryptSecret(encryptedValue []byte) (string, error) { if len(encryptedValue) < 12 { // GCM nonce size return "", fmt.Errorf("invalid encrypted value length") } // Generate key from master key keyBytes, err := hex.DecodeString(sm.encryptionKey) if err != nil { return "", fmt.Errorf("invalid encryption key format: %w", err) } // Create cipher block, err := aes.NewCipher(keyBytes) if err != nil { return "", fmt.Errorf("failed to create cipher: %w", err) } // Create GCM gcm, err := cipher.NewGCM(block) if err != nil { return "", fmt.Errorf("failed to create GCM: %w", err) } // Extract nonce and ciphertext nonce := encryptedValue[:gcm.NonceSize()] ciphertext := encryptedValue[gcm.NonceSize():] // Decrypt plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", fmt.Errorf("failed to decrypt secret: %w", err) } return string(plaintext), nil } // GenerateEncryptionKey generates a new encryption key func (sm *SecretsManager) GenerateEncryptionKey() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("failed to generate encryption key: %w", err) } return hex.EncodeToString(bytes), nil } // SetEncryptionKey sets the master encryption key func (sm *SecretsManager) SetEncryptionKey(key string) { sm.encryptionKey = key } // GetEncryptionKey returns the current encryption key func (sm *SecretsManager) GetEncryptionKey() string { return sm.encryptionKey } // GetSecretsPath returns the current secrets path func (sm *SecretsManager) GetSecretsPath() string { return sm.secretsPath } // ValidateSecrets validates that all required secrets exist func (sm *SecretsManager) ValidateSecrets(requiredSecrets []string) error { for _, secretName := range requiredSecrets { secretPath := filepath.Join(sm.secretsPath, secretName) if _, err := os.Stat(secretPath); os.IsNotExist(err) { return fmt.Errorf("required secret not found: %s", secretName) } } return nil } // ListSecrets returns a list of all created secrets func (sm *SecretsManager) ListSecrets() ([]string, error) { entries, err := os.ReadDir(sm.secretsPath) if err != nil { if os.IsNotExist(err) { return []string{}, nil } return nil, fmt.Errorf("failed to read secrets directory: %w", err) } var secrets []string for _, entry := range entries { if !entry.IsDir() { secrets = append(secrets, entry.Name()) } } return secrets, nil } // RemoveSecret removes a Docker secret func (sm *SecretsManager) RemoveSecret(name string) error { secretPath := filepath.Join(sm.secretsPath, name) return os.Remove(secretPath) } // Cleanup removes all secrets and the secrets directory func (sm *SecretsManager) Cleanup() error { if _, err := os.Stat(sm.secretsPath); os.IsNotExist(err) { return nil } // Remove all files in the directory entries, err := os.ReadDir(sm.secretsPath) if err != nil { return fmt.Errorf("failed to read secrets directory: %w", err) } for _, entry := range entries { if !entry.IsDir() { if err := os.Remove(filepath.Join(sm.secretsPath, entry.Name())); err != nil { return fmt.Errorf("failed to remove secret %s: %w", entry.Name(), err) } } } // Remove the directory itself return os.Remove(sm.secretsPath) } // getSecretsPath returns the platform-specific secrets path func getSecretsPath() string { if runtime.GOOS == "windows" { return `C:\ProgramData\Docker\secrets` } return "/run/secrets" } // IsDockerEnvironment checks if 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 := os.ReadFile("/proc/1/cgroup"); err == nil { if containsString(string(data), "docker") { return true } } return false } // containsString checks if a string contains a substring func containsString(s, substr string) bool { return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr))) }