Skip to content

Commit 6c9581b

Browse files
feat: refactor backup password management for improved security
- Removed Vault dependency and reimplemented password retrieval using local secret stores - Added hierarchical password lookup with 4 fallback methods: 1. Repository-local password file 2. Global secrets directory 3. Repository .env file 4. Environment variables (with warning) - Added new helper functions readPasswordFile and readPasswordFromEnvFile for consistent password handling - Improved error messages and logging to better guide
1 parent df9e99f commit 6c9581b

1 file changed

Lines changed: 83 additions & 38 deletions

File tree

pkg/backup/client.go

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717

1818
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
1919
"github.com/CodeMonkeyCybersecurity/eos/pkg/shared"
20-
"github.com/CodeMonkeyCybersecurity/eos/pkg/vault"
2120
"github.com/uptrace/opentelemetry-go-extra/otelzap"
2221
"go.uber.org/zap"
2322
)
@@ -57,7 +56,7 @@ func NewClient(rc *eos_io.RuntimeContext, repoName string) (*Client, error) {
5756
func (c *Client) RunRestic(args ...string) ([]byte, error) {
5857
logger := otelzap.Ctx(c.rc.Ctx)
5958

60-
// Get password from Vault
59+
// Get repository password
6160
password, err := c.getRepositoryPassword()
6261
if err != nil {
6362
return nil, fmt.Errorf("getting repository password: %w", err)
@@ -139,54 +138,61 @@ func (c *Client) RunRestic(args ...string) ([]byte, error) {
139138
return output, nil
140139
}
141140

142-
// getRepositoryPassword retrieves password from Vault
141+
// getRepositoryPassword retrieves the repository password using local secret stores
143142
func (c *Client) getRepositoryPassword() (string, error) {
144143
logger := otelzap.Ctx(c.rc.Ctx)
145144

146-
vaultPath := fmt.Sprintf("eos/backup/repositories/%s", c.repository.Name)
147-
logger.Info("Retrieving repository password from Vault",
148-
zap.String("path", vaultPath))
149-
150-
vaultAddr := shared.GetVaultAddrWithEnv()
151-
152-
// Try to connect to Vault
153-
vClient, err := vault.NewClient(vaultAddr, logger.Logger().Logger)
154-
if err != nil {
155-
// Fall back to local password file if Vault unavailable
156-
logger.Warn("Vault unavailable, checking local password file",
145+
// 1. Repository-local password file (created by quick backup generator)
146+
localPasswordPath := filepath.Join(c.repository.URL, ".password")
147+
if password, err := readPasswordFile(localPasswordPath); err == nil {
148+
logger.Debug("Using repository-local password file",
149+
zap.String("path", localPasswordPath))
150+
return password, nil
151+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
152+
logger.Warn("Failed to read repository-local password file",
153+
zap.String("path", localPasswordPath),
157154
zap.Error(err))
155+
}
158156

159-
passwordFile := fmt.Sprintf("/var/lib/eos/secrets/backup/%s.password", c.repository.Name)
160-
if data, err := os.ReadFile(passwordFile); err == nil {
161-
return strings.TrimSpace(string(data)), nil
162-
}
163-
164-
// Fallback: repository-local password file (used by quick backups)
165-
localPasswordFile := filepath.Join(c.repository.URL, ".password")
166-
if data, err := os.ReadFile(localPasswordFile); err == nil {
167-
logger.Info("Using repository-local password file",
168-
zap.String("path", localPasswordFile))
169-
return strings.TrimSpace(string(data)), nil
170-
}
171-
172-
return "", fmt.Errorf("vault unavailable and no local password found")
157+
// 2. Global secrets directory fallback (used by managed repositories)
158+
secretsPasswordPath := filepath.Join(SecretsDir, fmt.Sprintf("%s.password", c.repository.Name))
159+
if password, err := readPasswordFile(secretsPasswordPath); err == nil {
160+
logger.Debug("Using secrets directory password file",
161+
zap.String("path", secretsPasswordPath))
162+
return password, nil
163+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
164+
logger.Warn("Failed to read secrets directory password file",
165+
zap.String("path", secretsPasswordPath),
166+
zap.Error(err))
173167
}
174168

175-
secret, err := vClient.GetSecret(c.rc.Ctx, vaultPath)
176-
if err != nil {
177-
return "", fmt.Errorf("reading from vault: %w", err)
169+
// 3. Repository `.env` file (temporary secret storage during Vault testing)
170+
envPath := filepath.Join(c.repository.URL, ".env")
171+
if password, err := readPasswordFromEnvFile(envPath); err == nil {
172+
logger.Debug("Using repository .env file for restic password",
173+
zap.String("path", envPath))
174+
return password, nil
175+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
176+
logger.Warn("Failed to read repository .env file",
177+
zap.String("path", envPath),
178+
zap.Error(err))
178179
}
179180

180-
if secret == nil || secret.Data == nil {
181-
return "", fmt.Errorf("no secret found at %s", vaultPath)
181+
// 4. Environment variable overrides (least preferred, but supported for manual ops)
182+
if password := strings.TrimSpace(os.Getenv("RESTIC_PASSWORD")); password != "" {
183+
logger.Warn("Using RESTIC_PASSWORD environment variable; prefer password files for security")
184+
return password, nil
182185
}
183186

184-
password, ok := secret.Data["password"].(string)
185-
if !ok {
186-
return "", fmt.Errorf("invalid password format in vault")
187+
if passwordFile := strings.TrimSpace(os.Getenv("RESTIC_PASSWORD_FILE")); passwordFile != "" {
188+
if password, err := readPasswordFile(passwordFile); err == nil {
189+
logger.Warn("Using RESTIC_PASSWORD_FILE override; prefer managed password files",
190+
zap.String("path", passwordFile))
191+
return password, nil
192+
}
187193
}
188194

189-
return password, nil
195+
return "", fmt.Errorf("restic repository password not found; expected password file at %s or %s", localPasswordPath, secretsPasswordPath)
190196
}
191197

192198
// InitRepository initializes a new restic repository
@@ -295,6 +301,45 @@ func (c *Client) Backup(profileName string) error {
295301
return nil
296302
}
297303

304+
// readPasswordFile reads and trims a password from the provided file path.
305+
func readPasswordFile(path string) (string, error) {
306+
data, err := os.ReadFile(path)
307+
if err != nil {
308+
return "", err
309+
}
310+
311+
password := strings.TrimSpace(string(data))
312+
if password == "" {
313+
return "", fmt.Errorf("password file %s is empty", path)
314+
}
315+
316+
return password, nil
317+
}
318+
319+
// readPasswordFromEnvFile retrieves a restic password from a .env file if present.
320+
// The file may contain either RESTIC_PASSWORD or RESTIC_PASSWORD_FILE pointing to a
321+
// secondary password file.
322+
func readPasswordFromEnvFile(path string) (string, error) {
323+
if _, err := os.Stat(path); err != nil {
324+
return "", err
325+
}
326+
327+
vars, err := shared.ParseEnvFile(path)
328+
if err != nil {
329+
return "", err
330+
}
331+
332+
if passwordFile, ok := vars["RESTIC_PASSWORD_FILE"]; ok && strings.TrimSpace(passwordFile) != "" {
333+
return readPasswordFile(strings.TrimSpace(passwordFile))
334+
}
335+
336+
if password, ok := vars["RESTIC_PASSWORD"]; ok && strings.TrimSpace(password) != "" {
337+
return strings.TrimSpace(password), nil
338+
}
339+
340+
return "", fmt.Errorf("restic password not found in %s", path)
341+
}
342+
298343
// runBackupWithProgress executes backup with JSON progress parsing
299344
// SECURITY: Uses password file instead of environment variable to prevent
300345
// password exposure via 'ps auxe' (CVSS 7.5 vulnerability mitigation)
@@ -303,7 +348,7 @@ func (c *Client) runBackupWithProgress(args []string) error {
303348

304349
cmd := exec.CommandContext(c.rc.Ctx, "restic", args...)
305350

306-
// Get password from Vault
351+
// Get repository password
307352
password, err := c.getRepositoryPassword()
308353
if err != nil {
309354
return err

0 commit comments

Comments
 (0)