Add safe repository password rotation
This commit is contained in:
+48
-18
@@ -106,6 +106,19 @@ func (r *Runner) Unlock(ctx context.Context, repo model.Repository) error {
|
||||
return r.run(ctx, repo, []string{"unlock"}, nil, nil)
|
||||
}
|
||||
|
||||
func (r *Runner) ChangePassword(ctx context.Context, repo model.Repository, currentPassword, newPassword string) error {
|
||||
newPasswordPath, cleanup, err := r.writePasswordFile("restic-new-password-*", newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
return r.runWithInputEnvPassword(ctx, repo, []string{"key", "passwd", "--new-password-file", newPasswordPath}, nil, nil, nil, nil, currentPassword)
|
||||
}
|
||||
|
||||
func (r *Runner) TestPassword(ctx context.Context, repo model.Repository, password string) error {
|
||||
return r.runWithInputEnvPassword(ctx, repo, []string{"snapshots", "--json"}, nil, nil, nil, nil, password)
|
||||
}
|
||||
|
||||
func (r *Runner) Backup(ctx context.Context, repo model.Repository, job model.Job, sources []string, progress ProgressCallback) (Summary, error) {
|
||||
args := []string{"backup", "--json", "--compression", job.Compression}
|
||||
for _, tag := range append([]string{"urbm", "job:" + job.ID}, job.Tags...) {
|
||||
@@ -283,28 +296,17 @@ func (r *Runner) runWithInputAndEnv(ctx context.Context, repo model.Repository,
|
||||
if err != nil {
|
||||
return fmt.Errorf("authentication: load repository password: %w", err)
|
||||
}
|
||||
secretDir := filepath.Join(r.RuntimeDir, "secrets")
|
||||
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.CreateTemp(secretDir, "restic-password-*")
|
||||
return r.runWithInputEnvPassword(ctx, repo, args, onLine, capture, input, environment, password)
|
||||
}
|
||||
|
||||
func (r *Runner) runWithInputEnvPassword(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte, input io.Reader, environment []string, password string) error {
|
||||
passwordPath, cleanup, err := r.writePasswordFile("restic-password-*", password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwordPath := f.Name()
|
||||
defer os.Remove(passwordPath)
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(f, password); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
secretDir := filepath.Join(r.RuntimeDir, "secrets")
|
||||
globalArgs := []string{"-r", repositoryLocation(repo)}
|
||||
credentialPath := ""
|
||||
if repo.Type == model.RepositorySFTP && repo.CredentialRef != "" {
|
||||
@@ -397,6 +399,34 @@ func (r *Runner) runWithInputAndEnv(ctx context.Context, repo model.Repository,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) writePasswordFile(pattern, password string) (string, func(), error) {
|
||||
secretDir := filepath.Join(r.RuntimeDir, "secrets")
|
||||
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
f, err := os.CreateTemp(secretDir, pattern)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
passwordPath := f.Name()
|
||||
cleanup := func() { _ = os.Remove(passwordPath) }
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
f.Close()
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
if _, err := io.WriteString(f, password); err != nil {
|
||||
f.Close()
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
cleanup()
|
||||
return "", nil, err
|
||||
}
|
||||
return passwordPath, cleanup, nil
|
||||
}
|
||||
|
||||
func backupEnvironment(job model.Job) []string {
|
||||
if job.CPUCores <= 0 {
|
||||
return nil
|
||||
|
||||
@@ -51,6 +51,42 @@ func TestBackupUsesPasswordFileAndStructuredArguments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePasswordUsesCurrentAndNewPasswordFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
argsPath := filepath.Join(dir, "args")
|
||||
currentPath := filepath.Join(dir, "current")
|
||||
newPath := filepath.Join(dir, "new")
|
||||
script := filepath.Join(dir, "restic")
|
||||
body := fmt.Sprintf(`#!/bin/sh
|
||||
printf '%%s\n' "$@" > '%s'
|
||||
cat "$RESTIC_PASSWORD_FILE" > '%s'
|
||||
previous=''
|
||||
for value in "$@"; do
|
||||
if [ "$previous" = "--new-password-file" ]; then
|
||||
cat "$value" > '%s'
|
||||
fi
|
||||
previous="$value"
|
||||
done
|
||||
`, argsPath, currentPath, newPath)
|
||||
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runner := &Runner{Binary: script, RuntimeDir: dir, Secrets: fakeSecrets{"password": "stored-old"}}
|
||||
repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo", PasswordRef: "password"}
|
||||
if err := runner.ChangePassword(context.Background(), repo, "current-secret", "new-secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args, _ := os.ReadFile(argsPath)
|
||||
if !strings.Contains(string(args), "key\npasswd\n--new-password-file") {
|
||||
t.Fatalf("unexpected arguments: %s", args)
|
||||
}
|
||||
current, _ := os.ReadFile(currentPath)
|
||||
newPassword, _ := os.ReadFile(newPath)
|
||||
if string(current) != "current-secret" || string(newPassword) != "new-secret" {
|
||||
t.Fatalf("password files current=%q new=%q", current, newPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupLimitsCPUForThisJob(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envPath := filepath.Join(dir, "gomaxprocs")
|
||||
|
||||
Reference in New Issue
Block a user