Merge pull request #4808 from MichaelEischer/insecure-no-password

Implement `--insecure-no-password` option.
This commit is contained in:
Michael Eischer 2024-05-24 22:58:25 +02:00 committed by GitHub
commit 80132e71d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 211 additions and 51 deletions

View file

@ -0,0 +1,19 @@
Enhancement: Support repositories with empty password
Restic refused to create or operate on repositories with an emtpy password.
Using the new option `--insecure-no-password` it is now possible to disable
this check. Restic will not prompt for a password when using this option.
For security reasons, the option must always be specified when operating on
repositories with an empty password.
Specifying `--insecure-no-password` while also passing a password to restic
via a CLI option or via environment variable results in an error.
The `init` and `copy` command also support the option `--from-insecure-no-password`
which applies to the source repository. The `key add` and `key passwd` comands
include the `--new-insecure-no-password` option to add or set an emtpy password.
https://github.com/restic/restic/issues/1786
https://github.com/restic/restic/issues/4326
https://github.com/restic/restic/pull/4698
https://github.com/restic/restic/pull/4808

View file

@ -257,7 +257,7 @@ func readFilenamesRaw(r io.Reader) (names []string, err error) {
// Check returns an error when an invalid combination of options was set.
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
if gopts.password == "" {
if gopts.password == "" && !gopts.InsecureNoPassword {
if opts.Stdin {
return errors.Fatal("cannot read both password and data from stdin")
}

View file

@ -627,3 +627,17 @@ func TestStdinFromCommandFailNoOutputAndExitCode(t *testing.T) {
testRunCheck(t, env.gopts)
}
func TestBackupEmptyPassword(t *testing.T) {
// basic sanity test that empty passwords work
env, cleanup := withTestEnvironment(t)
defer cleanup()
env.gopts.password = ""
env.gopts.InsecureNoPassword = true
testSetupBackupData(t, env)
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
testListSnapshots(t, env.gopts, 1)
testRunCheck(t, env.gopts)
}

View file

@ -13,10 +13,12 @@ func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
gopts := srcGopts
gopts.Repo = dstGopts.Repo
gopts.password = dstGopts.password
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
copyOpts := CopyOptions{
secondaryRepoOptions: secondaryRepoOptions{
Repo: srcGopts.Repo,
password: srcGopts.password,
Repo: srcGopts.Repo,
password: srcGopts.password,
InsecureNoPassword: srcGopts.InsecureNoPassword,
},
}
@ -134,3 +136,22 @@ func TestCopyUnstableJSON(t *testing.T) {
testRunCheck(t, env2.gopts)
testListSnapshots(t, env2.gopts, 1)
}
func TestCopyToEmptyPassword(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
env2, cleanup2 := withTestEnvironment(t)
defer cleanup2()
env2.gopts.password = ""
env2.gopts.InsecureNoPassword = true
testSetupBackupData(t, env)
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, BackupOptions{}, env.gopts)
testRunInit(t, env2.gopts)
testRunCopy(t, env.gopts, env2.gopts)
testListSnapshots(t, env.gopts, 1)
testListSnapshots(t, env2.gopts, 1)
testRunCheck(t, env2.gopts)
}

View file

@ -9,6 +9,7 @@ import (
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var cmdKeyAdd = &cobra.Command{
@ -23,26 +24,30 @@ EXIT STATUS
Exit status is 0 if the command is successful, and non-zero if there was any error.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runKeyAdd(cmd.Context(), globalOptions, keyAddOpts, args)
},
}
type KeyAddOptions struct {
NewPasswordFile string
Username string
Hostname string
NewPasswordFile string
InsecureNoPassword bool
Username string
Hostname string
}
var keyAddOpts KeyAddOptions
func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
flags.StringVarP(&opts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
flags.BoolVar(&opts.InsecureNoPassword, "new-insecure-no-password", false, "add an empty password for the repository (insecure)")
flags.StringVarP(&opts.Username, "user", "", "", "the username for new key")
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
}
func init() {
cmdKey.AddCommand(cmdKeyAdd)
flags := cmdKeyAdd.Flags()
flags.StringVarP(&keyAddOpts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
flags.StringVarP(&keyAddOpts.Username, "user", "", "", "the username for new key")
flags.StringVarP(&keyAddOpts.Hostname, "host", "", "", "the hostname for new key")
var keyAddOpts KeyAddOptions
keyAddOpts.Add(cmdKeyAdd.Flags())
cmdKeyAdd.RunE = func(cmd *cobra.Command, args []string) error {
return runKeyAdd(cmd.Context(), globalOptions, keyAddOpts, args)
}
}
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
@ -60,7 +65,7 @@ func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, arg
}
func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile)
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
}
@ -83,19 +88,35 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
// testKeyNewPassword is used to set a new password during integration testing.
var testKeyNewPassword string
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string) (string, error) {
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) {
if testKeyNewPassword != "" {
return testKeyNewPassword, nil
}
if insecureNoPassword {
if newPasswordFile != "" {
return "", fmt.Errorf("only either --new-password-file or --new-insecure-no-password may be specified")
}
return "", nil
}
if newPasswordFile != "" {
return loadPasswordFromFile(newPasswordFile)
password, err := loadPasswordFromFile(newPasswordFile)
if err != nil {
return "", err
}
if password == "" {
return "", fmt.Errorf("an empty password is not allowed by default. Pass the flag `--new-insecure-no-password` to restic to disable this check")
}
return password, nil
}
// Since we already have an open repository, temporary remove the password
// to prompt the user for the passwd.
newopts := gopts
newopts.password = ""
// empty passwords are already handled above
newopts.InsecureNoPassword = false
return ReadPasswordTwice(ctx, newopts,
"enter new password: ",

View file

@ -3,6 +3,8 @@ package main
import (
"bufio"
"context"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
@ -109,6 +111,43 @@ func TestKeyAddRemove(t *testing.T) {
testRunKeyAddNewKeyUserHost(t, env.gopts)
}
func TestKeyAddInvalid(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testRunInit(t, env.gopts)
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
NewPasswordFile: "some-file",
InsecureNoPassword: true,
}, []string{})
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
pwfile := filepath.Join(t.TempDir(), "pwfile")
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
NewPasswordFile: pwfile,
}, []string{})
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
}
func TestKeyAddEmpty(t *testing.T) {
env, cleanup := withTestEnvironment(t)
// must list keys more than once
env.gopts.backendTestHook = nil
defer cleanup()
testRunInit(t, env.gopts)
rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
InsecureNoPassword: true,
}, []string{}))
env.gopts.password = ""
env.gopts.InsecureNoPassword = true
testRunCheck(t, env.gopts)
}
type emptySaveBackend struct {
backend.Backend
}

View file

@ -22,24 +22,20 @@ EXIT STATUS
Exit status is 0 if the command is successful, and non-zero if there was any error.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runKeyPasswd(cmd.Context(), globalOptions, keyPasswdOpts, args)
},
}
type KeyPasswdOptions struct {
KeyAddOptions
}
var keyPasswdOpts KeyPasswdOptions
func init() {
cmdKey.AddCommand(cmdKeyPasswd)
flags := cmdKeyPasswd.Flags()
flags.StringVarP(&keyPasswdOpts.NewPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
flags.StringVarP(&keyPasswdOpts.Username, "user", "", "", "the username for new key")
flags.StringVarP(&keyPasswdOpts.Hostname, "host", "", "", "the hostname for new key")
var keyPasswdOpts KeyPasswdOptions
keyPasswdOpts.KeyAddOptions.Add(cmdKeyPasswd.Flags())
cmdKeyPasswd.RunE = func(cmd *cobra.Command, args []string) error {
return runKeyPasswd(cmd.Context(), globalOptions, keyPasswdOpts, args)
}
}
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {
@ -57,7 +53,7 @@ func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOption
}
func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error {
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile)
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
}

View file

@ -52,22 +52,23 @@ type backendWrapper func(r backend.Backend) (backend.Backend, error)
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
Repo string
RepositoryFile string
PasswordFile string
PasswordCommand string
KeyHint string
Quiet bool
Verbose int
NoLock bool
RetryLock time.Duration
JSON bool
CacheDir string
NoCache bool
CleanupCache bool
Compression repository.CompressionMode
PackSize uint
NoExtraVerify bool
Repo string
RepositoryFile string
PasswordFile string
PasswordCommand string
KeyHint string
Quiet bool
Verbose int
NoLock bool
RetryLock time.Duration
JSON bool
CacheDir string
NoCache bool
CleanupCache bool
Compression repository.CompressionMode
PackSize uint
NoExtraVerify bool
InsecureNoPassword bool
backend.TransportOptions
limiter.Limits
@ -125,6 +126,7 @@ func init() {
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
f.StringSliceVar(&globalOptions.RootCertFilenames, "cacert", nil, "`file` to load root certificates from (default: use system certificates or $RESTIC_CACERT)")
f.StringVar(&globalOptions.TLSClientCertKeyFilename, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key (default: $RESTIC_TLS_CLIENT_CERT)")
f.BoolVar(&globalOptions.InsecureNoPassword, "insecure-no-password", false, "use an empty password for the repository, must be passed to every restic command (insecure)")
f.BoolVar(&globalOptions.InsecureTLS, "insecure-tls", false, "skip TLS certificate verification when connecting to the repository (insecure)")
f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
f.Var(&globalOptions.Compression, "compression", "compression mode (only available for repository format version 2), one of (auto|off|max) (default: $RESTIC_COMPRESSION)")
@ -327,6 +329,13 @@ func readPasswordTerminal(ctx context.Context, in *os.File, out *os.File, prompt
// variable RESTIC_PASSWORD or prompts the user. If the context is canceled,
// the function leaks the password reading goroutine.
func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (string, error) {
if opts.InsecureNoPassword {
if opts.password != "" {
return "", errors.Fatal("--insecure-no-password must not be specified together with providing a password via a cli option or environment variable")
}
return "", nil
}
if opts.password != "" {
return opts.password, nil
}
@ -348,7 +357,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin
}
if len(password) == 0 {
return "", errors.Fatal("an empty password is not a password")
return "", errors.Fatal("an empty password is not allowed by default. Pass the flag `--insecure-no-password` to restic to disable this check")
}
return password, nil
@ -445,7 +454,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
}
passwordTriesLeft := 1
if stdinIsTerminal() && opts.password == "" {
if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
passwordTriesLeft = 3
}

View file

@ -1,8 +1,10 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
rtest "github.com/restic/restic/internal/test"
@ -50,3 +52,14 @@ func TestReadRepo(t *testing.T) {
t.Fatal("must not read repository path from invalid file path")
}
}
func TestReadEmptyPassword(t *testing.T) {
opts := GlobalOptions{InsecureNoPassword: true}
password, err := ReadPassword(context.TODO(), opts, "test")
rtest.OK(t, err)
rtest.Equals(t, "", password, "got unexpected password")
opts.password = "invalid"
_, err = ReadPassword(context.TODO(), opts, "test")
rtest.Assert(t, strings.Contains(err.Error(), "must not be specified together with providing a password via a cli option or environment variable"), "unexpected error message, got %v", err)
}

View file

@ -11,11 +11,12 @@ import (
type secondaryRepoOptions struct {
password string
// from-repo options
Repo string
RepositoryFile string
PasswordFile string
PasswordCommand string
KeyHint string
Repo string
RepositoryFile string
PasswordFile string
PasswordCommand string
KeyHint string
InsecureNoPassword bool
// repo2 options
LegacyRepo string
LegacyRepositoryFile string
@ -49,6 +50,7 @@ func initSecondaryRepoOptions(f *pflag.FlagSet, opts *secondaryRepoOptions, repo
f.StringVarP(&opts.PasswordFile, "from-password-file", "", "", "`file` to read the source repository password from (default: $RESTIC_FROM_PASSWORD_FILE)")
f.StringVarP(&opts.KeyHint, "from-key-hint", "", "", "key ID of key to try decrypting the source repository first (default: $RESTIC_FROM_KEY_HINT)")
f.StringVarP(&opts.PasswordCommand, "from-password-command", "", "", "shell `command` to obtain the source repository password from (default: $RESTIC_FROM_PASSWORD_COMMAND)")
f.BoolVar(&opts.InsecureNoPassword, "from-insecure-no-password", false, "use an empty password for the source repository, must be passed to every restic command (insecure)")
opts.Repo = os.Getenv("RESTIC_FROM_REPOSITORY")
opts.RepositoryFile = os.Getenv("RESTIC_FROM_REPOSITORY_FILE")
@ -63,7 +65,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
}
hasFromRepo := opts.Repo != "" || opts.RepositoryFile != "" || opts.PasswordFile != "" ||
opts.KeyHint != "" || opts.PasswordCommand != ""
opts.KeyHint != "" || opts.PasswordCommand != "" || opts.InsecureNoPassword
hasRepo2 := opts.LegacyRepo != "" || opts.LegacyRepositoryFile != "" || opts.LegacyPasswordFile != "" ||
opts.LegacyKeyHint != "" || opts.LegacyPasswordCommand != ""
@ -85,6 +87,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
dstGopts.PasswordFile = opts.PasswordFile
dstGopts.PasswordCommand = opts.PasswordCommand
dstGopts.KeyHint = opts.KeyHint
dstGopts.InsecureNoPassword = opts.InsecureNoPassword
pwdEnv = "RESTIC_FROM_PASSWORD"
repoPrefix = "source"
@ -98,6 +101,8 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
dstGopts.PasswordFile = opts.LegacyPasswordFile
dstGopts.PasswordCommand = opts.LegacyPasswordCommand
dstGopts.KeyHint = opts.LegacyKeyHint
// keep existing bevhaior for legacy options
dstGopts.InsecureNoPassword = false
pwdEnv = "RESTIC_PASSWORD2"
}

View file

@ -852,3 +852,26 @@ and then grants read/write permissions for group access.
.. note:: To manage who has access to the repository you can use
``usermod`` on Linux systems, to change which group controls
repository access ``chgrp -R`` is your friend.
Repositories with empty password
********************************
Restic by default refuses to create or operate on repositories that use an
empty password. Since restic 0.17.0, the option ``--insecure-no-password`` allows
disabling this check. Restic will not prompt for a password when using this option.
Specifying ``--insecure-no-password`` while also passing a password to restic
via a CLI option or via environment variable results in an error.
For security reasons, the option must always be specified when operating on
repositories with an empty password. For example to create a new repository
with an empty password, use the following command.
.. code-block:: console
restic init --insecure-no-password
The ``init`` and ``copy`` command also support the option ``--from-insecure-no-password``
which applies to the source repository. The ``key add`` and ``key passwd`` comands
include the ``--new-insecure-no-password`` option to add or set and emtpy password.