Commit 5f9c2f2c authored by ale's avatar ale
Browse files

Merge branch 'repository-file' into 'master'

Use --repository-file to avoid leaking passwords in logs

Closes #3

See merge request !23
parents dbf3ccd1 0e4bde8a
Pipeline #24123 passed with stages
in 3 minutes and 27 seconds
......@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
......@@ -21,22 +20,39 @@ import (
)
type resticRepository struct {
bin string
uri string
passwordFile string
excludes []string
excludeFiles []string
autoPrune bool
cacheMgr cacheManager
bin string
version *version.Version
uri string
tmpDir string
passwordFile string
repositoryFile string
excludes []string
excludeFiles []string
autoPrune bool
cacheMgr cacheManager
initialized sync.Once
}
func (r *resticRepository) resticCmd() string {
args := []string{r.bin, "-r", r.uri}
if r.passwordFile != "" {
args = append(args, "--password-file", r.passwordFile)
func (r *resticRepository) resticCmd(cmd string) string {
args := []string{r.bin, cmd}
// Work around https://github.com/restic/restic/issues/3104
// ("restic init" does not respect --repository-file in Restic
// versions prior to 0.12).
if r.version.LessThan(resticRepositoryFileVersion) || (cmd == "init" && r.version.LessThan(resticFixedIssue3104Version)) {
args = append(args, "--repo", r.uri)
} else {
args = append(args, "--repository-file", r.repositoryFile)
}
args = append(args, "--password-file", r.passwordFile)
return strings.Join(args, " ")
}
func (r *resticRepository) excludeArgs() string {
var args []string
for _, x := range r.excludes {
args = append(args, "--exclude", x)
}
......@@ -49,29 +65,23 @@ func (r *resticRepository) resticCmd() string {
// We need to check that we're running at least restic 0.9, or the
// restore functionality won't work as expected.
var (
resticVersionRx = regexp.MustCompile(`^restic ([0-9.]+)`)
resticMinGoodVersion = "0.9"
resticVersionRx = regexp.MustCompile(`^restic ([0-9.]+)`)
resticMinGoodVersion = version.Must(version.NewVersion("0.9"))
resticRepositoryFileVersion = version.Must(version.NewVersion("0.11"))
resticFixedIssue3104Version = version.Must(version.NewVersion("0.12"))
)
func checkResticVersion(bin string) error {
func getResticVersion(bin string) (*version.Version, error) {
output, err := exec.Command(bin, "version").Output() // #nosec
if err != nil {
return err
return nil, err
}
m := resticVersionRx.FindStringSubmatch(string(output))
if len(m) < 2 {
return errors.New("could not parse restic version")
return nil, errors.New("could not parse restic version")
}
v, err := version.NewVersion(m[1])
if err != nil {
return err
}
log.Printf("detected restic version %s", v)
minV, _ := version.NewVersion(resticMinGoodVersion) // nolint
if v.LessThan(minV) {
return fmt.Errorf("restic should be at least version %s (is %s)", minV, v)
}
return nil
return version.NewVersion(m[1])
}
// newResticRepository returns a restic repository.
......@@ -89,20 +99,29 @@ func newResticRepository(params Params) (Repository, error) {
if s := params.Get("restic_binary"); s != "" {
bin = s
}
if err := checkResticVersion(bin); err != nil {
resticVersion, err := getResticVersion(bin)
if err != nil {
return nil, err
}
if resticVersion.LessThan(resticMinGoodVersion) {
return nil, fmt.Errorf(
"restic version %s is older than the minimum supported version %s",
resticVersion, resticMinGoodVersion)
}
log.Printf("detected restic version %s", resticVersion)
tmpf, err := ioutil.TempFile("", "restic-pw-")
// Create a temporary directory and write repository URL and
// password in files therein.
tmpDir, err := ioutil.TempDir("", "tabacco-restic-")
if err != nil {
return nil, err
}
if _, err := io.WriteString(tmpf, password); err != nil {
os.Remove(tmpf.Name()) // nolint
passwordFile := filepath.Join(tmpDir, "pw")
if err := ioutil.WriteFile(passwordFile, []byte(password), 0600); err != nil {
return nil, err
}
if err := tmpf.Close(); err != nil {
os.Remove(tmpf.Name()) // nolint
repositoryFile := filepath.Join(tmpDir, "repo")
if err := ioutil.WriteFile(repositoryFile, []byte(uri), 0600); err != nil {
return nil, err
}
......@@ -115,19 +134,23 @@ func newResticRepository(params Params) (Repository, error) {
if b, ok := params.GetBool("autoprune"); ok {
autoPrune = b
}
return &resticRepository{
bin: bin,
uri: uri,
passwordFile: tmpf.Name(),
excludes: params.GetList("exclude"),
excludeFiles: params.GetList("exclude_files"),
autoPrune: autoPrune,
cacheMgr: cmgr,
bin: bin,
version: resticVersion,
uri: uri,
tmpDir: tmpDir,
passwordFile: passwordFile,
repositoryFile: repositoryFile,
excludes: params.GetList("exclude"),
excludeFiles: params.GetList("exclude_files"),
autoPrune: autoPrune,
cacheMgr: cmgr,
}, nil
}
func (r *resticRepository) Close() error {
return os.Remove(r.passwordFile)
return os.RemoveAll(r.tmpDir)
}
func (r *resticRepository) Init(ctx context.Context, rctx RuntimeContext) error {
......@@ -135,8 +158,8 @@ func (r *resticRepository) Init(ctx context.Context, rctx RuntimeContext) error
// Restic init will fail if the repository is already
// initialized, ignore errors (but log them).
if err := rctx.Shell().Run(ctx, fmt.Sprintf(
"%s init --quiet || true",
r.resticCmd(),
"%s --quiet || true",
r.resticCmd("init"),
)); err != nil {
GetLogger(ctx).Printf("restic repository init failed (likely harmless): %v", err)
}
......@@ -149,8 +172,8 @@ func (r *resticRepository) Prepare(ctx context.Context, rctx RuntimeContext, bac
return nil
}
return rctx.Shell().Run(ctx, fmt.Sprintf(
"%s forget --host %s --keep-last 10 --prune",
r.resticCmd(),
"%s --host %s --keep-last 10 --prune",
r.resticCmd("forget"),
backup.Host,
))
}
......@@ -161,8 +184,9 @@ func resticBackupTags(backup *Backup, ds *Dataset) string {
func (r *resticRepository) backupCmd(cc cache, backup *Backup, ds *Dataset, inputFile string, exclude []string) string {
cmd := fmt.Sprintf(
"%s backup %s --json --exclude-caches --one-file-system %s --files-from %s",
r.resticCmd(),
"%s %s %s --json --exclude-caches --one-file-system %s --files-from %s",
r.resticCmd("backup"),
r.excludeArgs(),
cc.Args(),
resticBackupTags(backup, ds),
inputFile,
......@@ -180,8 +204,8 @@ func (r *resticRepository) getSnapshotID(ctx context.Context, cc cache, shell *S
// Legacy compatibility: query restic using the dataset ID.
data, err := shell.Output(ctx, fmt.Sprintf(
"%s snapshots %s --no-lock --json %s",
r.resticCmd(),
"%s %s --no-lock --json %s",
r.resticCmd("snapshots"),
cc.Args(),
resticBackupTags(backup, ds),
))
......@@ -205,7 +229,7 @@ func (r *resticRepository) restoreCmd(ctx context.Context, cc cache, shell *Shel
}
cmd := []string{
fmt.Sprintf("%s restore", r.resticCmd()),
r.resticCmd("restore"),
}
for _, path := range paths {
......@@ -239,8 +263,8 @@ func (r *resticRepository) backupStreamCmd(cc cache, backup *Backup, ds *Dataset
fakePath := datasetStdinPath(ds)
return fmt.Sprintf(
// The --force prevents restic from trying to find a previous snapshot.
"%s backup %s %s --json --force --stdin --stdin-filename %s",
r.resticCmd(),
"%s %s %s --json --force --stdin --stdin-filename %s",
r.resticCmd("backup"),
cc.Args(),
resticBackupTags(backup, ds),
fakePath,
......@@ -258,8 +282,8 @@ func (r *resticRepository) restoreStreamCmd(ctx context.Context, cc cache, shell
// Restore the file to a temporary directory, then pipe it.
return fmt.Sprintf(
"(%s restore %s --target %s %s 1>&2 && cat %s)",
r.resticCmd(),
"(%s %s --target %s %s 1>&2 && cat %s)",
r.resticCmd("restore"),
cc.Args(),
target,
snap,
......
......@@ -41,8 +41,14 @@ func createTempDirWithData(t *testing.T) string {
// nolint: gocyclo
func runResticTest(t *testing.T, tmpdir string, source *SourceSpec, restorePattern string, checkFn func(testing.TB, string)) {
// Check that we can actually run restic.
if err := checkResticVersion("restic"); err != nil {
t.Skip("can't run restic: ", err)
resticVersion, err := getResticVersion("restic")
if err != nil {
t.Skip("can't find restic: ", err)
}
if resticVersion.LessThan(resticMinGoodVersion) {
t.Skip("restic version ", resticVersion,
" is older than the minimum supported version ",
resticMinGoodVersion)
}
// Temporary cache dir.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment