From 97f7855de3d963cf0f58d38be165a9eed1c19064 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Sun, 30 Aug 2020 23:20:57 +0200 Subject: [PATCH 1/4] Add new option --repository-file (default: $RESTIC_REPOSITORY_FILE) As an alternative to -r, this allows to read the repository URL from a file in order to prevent certain types of information leaks, especially for URLs containing credentials. Fixes #1458, fixes #2900. --- changelog/unreleased/pull-2910 | 10 ++++++++++ cmd/restic/cmd_init.go | 7 ++++++- cmd/restic/global.go | 34 +++++++++++++++++++++++++++++++--- doc/040_backup.rst | 1 + 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/pull-2910 diff --git a/changelog/unreleased/pull-2910 b/changelog/unreleased/pull-2910 new file mode 100644 index 000000000..0a760e47b --- /dev/null +++ b/changelog/unreleased/pull-2910 @@ -0,0 +1,10 @@ +Enhancement: New option --repository-file + +We've added a new command-line option --repository-file as an alternative +to -r. This allows to read the repository URL from a file in order to +prevent certain types of information leaks, especially for URLs containing +credentials. + +https://github.com/restic/restic/issues/1458 +https://github.com/restic/restic/issues/2900 +https://github.com/restic/restic/pull/2910 diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index ae831c4e3..62c0b35ec 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -52,7 +52,12 @@ func runInit(opts InitOptions, gopts GlobalOptions, args []string) error { return err } - be, err := create(gopts.Repo, gopts.extended) + repo, err := ReadRepo(gopts) + if err != nil { + return err + } + + be, err := create(repo, gopts.extended) if err != nil { return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.Repo), err) } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index e3356cc73..58a43ff4e 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -49,6 +49,7 @@ type backendWrapper func(r restic.Backend) (restic.Backend, error) // GlobalOptions hold all global options for restic. type GlobalOptions struct { Repo string + RepositoryFile string PasswordFile string PasswordCommand string KeyHint string @@ -101,6 +102,7 @@ func init() { f := cmdRoot.PersistentFlags() f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "`repository` to backup to or restore from (default: $RESTIC_REPOSITORY)") + f.StringVarP(&globalOptions.RepositoryFile, "repository-file", "", os.Getenv("RESTIC_REPOSITORY_FILE"), "`file` to read the repository location from (default: $RESTIC_REPOSITORY_FILE)") f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)") f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)") f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)") @@ -382,15 +384,41 @@ func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, er return pw1, nil } +func ReadRepo(opts GlobalOptions) (string, error) { + if opts.Repo == "" && opts.RepositoryFile == "" { + return "", errors.Fatal("Please specify repository location (-r or --repository-file)") + } + + repo := opts.Repo + if opts.RepositoryFile != "" { + if repo != "" { + return "", errors.Fatal("Options -r and --repository-file are mutually exclusive, please specify only one") + } + + s, err := textfile.Read(opts.RepositoryFile) + if os.IsNotExist(errors.Cause(err)) { + return "", errors.Fatalf("%s does not exist", opts.RepositoryFile) + } + if err != nil { + return "", err + } + + repo = strings.TrimSpace(string(s)) + } + + return repo, nil +} + const maxKeys = 20 // OpenRepository reads the password and opens the repository. func OpenRepository(opts GlobalOptions) (*repository.Repository, error) { - if opts.Repo == "" { - return nil, errors.Fatal("Please specify repository location (-r)") + repo, err := ReadRepo(opts) + if err != nil { + return nil, err } - be, err := open(opts.Repo, opts, opts.extended) + be, err := open(repo, opts, opts.extended) if err != nil { return nil, err } diff --git a/doc/040_backup.rst b/doc/040_backup.rst index 274949010..3c0152569 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -407,6 +407,7 @@ environment variables. The following lists these environment variables: .. code-block:: console + RESTIC_REPOSITORY_FILE Name of a file containing the location of the repository (replaces --repository-file) RESTIC_REPOSITORY Location of repository (replaces -r) RESTIC_PASSWORD_FILE Location of password file (replaces --password-file) RESTIC_PASSWORD The actual password for the repository From 61035d68bc03c4db3b28526605d7faff2e2b6c3e Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 30 Sep 2020 22:32:14 +0200 Subject: [PATCH 2/4] Add test for --repository-file --- cmd/restic/global_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cmd/restic/global_test.go b/cmd/restic/global_test.go index 7d0477be6..2a0b05ceb 100644 --- a/cmd/restic/global_test.go +++ b/cmd/restic/global_test.go @@ -2,8 +2,11 @@ package main import ( "bytes" + "io/ioutil" + "path/filepath" "testing" + "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test" ) @@ -26,3 +29,33 @@ func Test_PrintFunctionsRespectsGlobalStdout(t *testing.T) { buf.Reset() } } + +func TestReadRepo(t *testing.T) { + tempDir, cleanup := test.TempDir(t) + defer cleanup() + + // test --repo option + var opts GlobalOptions + opts.Repo = tempDir + repo, err := ReadRepo(opts) + rtest.OK(t, err) + rtest.Equals(t, tempDir, repo) + + // test --repository-file option + foo := filepath.Join(tempDir, "foo") + err = ioutil.WriteFile(foo, []byte(tempDir+"\n"), 0666) + rtest.OK(t, err) + + var opts2 GlobalOptions + opts2.RepositoryFile = foo + repo, err = ReadRepo(opts2) + rtest.OK(t, err) + rtest.Equals(t, tempDir, repo) + + var opts3 GlobalOptions + opts3.RepositoryFile = foo + "-invalid" + repo, err = ReadRepo(opts3) + if err == nil { + t.Fatal("must not read repository path from invalid file path") + } +} From c18b119a9b216510906cdddcf09b7f7111b6f97d Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 1 Oct 2020 00:14:34 +0200 Subject: [PATCH 3/4] Document new option --repository-file --- doc/030_preparing_a_new_repo.rst | 6 ++++-- doc/040_backup.rst | 2 +- doc/manual_rest.rst | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst index 28c79217f..d7907645a 100644 --- a/doc/030_preparing_a_new_repo.rst +++ b/doc/030_preparing_a_new_repo.rst @@ -22,8 +22,10 @@ other options. You can skip to the next chapter once you've read the relevant section here. For automated backups, restic accepts the repository location in the -environment variable ``RESTIC_REPOSITORY``. For the password, several options -exist: +environment variable ``RESTIC_REPOSITORY``. Restic can also read the repository +location from a file specified via the ``--repository-file`` option or the +environment variable ``RESTIC_REPOSITORY_FILE``. For the password, several +options exist: * Setting the environment variable ``RESTIC_PASSWORD`` diff --git a/doc/040_backup.rst b/doc/040_backup.rst index 3c0152569..970038930 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -407,7 +407,7 @@ environment variables. The following lists these environment variables: .. code-block:: console - RESTIC_REPOSITORY_FILE Name of a file containing the location of the repository (replaces --repository-file) + RESTIC_REPOSITORY_FILE Name of file containing the repository location (replaces --repository-file) RESTIC_REPOSITORY Location of repository (replaces -r) RESTIC_PASSWORD_FILE Location of password file (replaces --password-file) RESTIC_PASSWORD The actual password for the repository diff --git a/doc/manual_rest.rst b/doc/manual_rest.rst index ea9e0673a..b0fd44a60 100644 --- a/doc/manual_rest.rst +++ b/doc/manual_rest.rst @@ -60,6 +60,7 @@ Usage help is available: -p, --password-file file file to read the repository password from (default: $RESTIC_PASSWORD_FILE) -q, --quiet do not output comprehensive progress report -r, --repo repository repository to backup to or restore from (default: $RESTIC_REPOSITORY) + --repository-file file file to read the repository location from (default: $RESTIC_REPOSITORY_FILE) --tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key -v, --verbose n be verbose (specify --verbose multiple times or level --verbose=n) @@ -122,6 +123,7 @@ command: -p, --password-file file file to read the repository password from (default: $RESTIC_PASSWORD_FILE) -q, --quiet do not output comprehensive progress report -r, --repo repository repository to backup to or restore from (default: $RESTIC_REPOSITORY) + --repository-file file file to read the repository location from (default: $RESTIC_REPOSITORY_FILE) --tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key -v, --verbose n be verbose (specify --verbose multiple times or level --verbose=n) From 7b50a6549200740bb1a0c5738d5b0c05be00b0e1 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 1 Oct 2020 00:15:10 +0200 Subject: [PATCH 4/4] Update backup help output in documentation --- doc/manual_rest.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manual_rest.rst b/doc/manual_rest.rst index b0fd44a60..224e20ea5 100644 --- a/doc/manual_rest.rst +++ b/doc/manual_rest.rst @@ -93,6 +93,7 @@ command: --exclude-caches excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard --exclude-file file read exclude patterns from a file (can be specified multiple times) --exclude-if-present filename[:header] takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times) + --exclude-larger-than size max size of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T) --files-from file read the files to backup from file (can be combined with file args/can be specified multiple times) -f, --force force re-reading the target files/directories (overrides the "parent" flag) -h, --help help for backup