mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Merge b947431bf6 into f000da3b35
This commit is contained in:
commit
ee768b2c18
6 changed files with 101 additions and 3 deletions
6
changelog/unreleased/pull-REST-password-file
Normal file
6
changelog/unreleased/pull-REST-password-file
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Enhancement: Support loading REST server password from a file
|
||||
|
||||
The new environment variable ``RESTIC_REST_PASSWORD_FILE`` allows loading
|
||||
the HTTP password for the REST backend from a file instead of specifying
|
||||
it directly via ``RESTIC_REST_PASSWORD``. If both variables are set,
|
||||
``RESTIC_REST_PASSWORD_FILE`` takes precedence.
|
||||
|
|
@ -220,6 +220,16 @@ variables as well:
|
|||
$ export RESTIC_REST_USERNAME=<MY_REST_SERVER_USERNAME>
|
||||
$ export RESTIC_REST_PASSWORD=<MY_REST_SERVER_PASSWORD>
|
||||
|
||||
To avoid storing the password in plain text in the environment, it can
|
||||
be read from a file instead:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export RESTIC_REST_PASSWORD_FILE=/path/to/password_file
|
||||
|
||||
If both ``RESTIC_REST_PASSWORD`` and ``RESTIC_REST_PASSWORD_FILE`` are
|
||||
set, the file takes precedence.
|
||||
|
||||
If you use TLS, restic will use the system's CA certificates to verify the
|
||||
server certificate. When the verification fails, restic refuses to proceed and
|
||||
exits with an error. If you have your own self-signed certificate, or a custom
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ environment variables, which are listed below.
|
|||
|
||||
RESTIC_REST_USERNAME Restic REST Server username
|
||||
RESTIC_REST_PASSWORD Restic REST Server password
|
||||
RESTIC_REST_PASSWORD_FILE Location of file containing the Restic REST Server password (takes precedence over RESTIC_REST_PASSWORD)
|
||||
|
||||
ST_AUTH Auth URL for keystone v1 authentication
|
||||
ST_USER Username for keystone v1 authentication
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import (
|
|||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/options"
|
||||
"github.com/restic/restic/internal/textfile"
|
||||
)
|
||||
|
||||
// Config contains all configuration necessary to connect to a REST server.
|
||||
type Config struct {
|
||||
URL *url.URL
|
||||
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
|
||||
URL *url.URL
|
||||
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
|
||||
PasswordFile string // path to file containing the HTTP password, set via RESTIC_REST_PASSWORD_FILE
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -84,7 +86,17 @@ func (cfg *Config) ApplyEnvironment(prefix string) {
|
|||
if username == "" && !pwdSet {
|
||||
envName := os.Getenv(prefix + "RESTIC_REST_USERNAME")
|
||||
envPwd := os.Getenv(prefix + "RESTIC_REST_PASSWORD")
|
||||
|
||||
cfg.PasswordFile = os.Getenv(prefix + "RESTIC_REST_PASSWORD_FILE")
|
||||
cfg.URL.User = url.UserPassword(envName, envPwd)
|
||||
}
|
||||
}
|
||||
|
||||
// loadPasswordFromFile reads the password from the given file, stripping a BOM
|
||||
// and converting to UTF-8 if necessary.
|
||||
func loadPasswordFromFile(path string) (string, error) {
|
||||
s, err := textfile.Read(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", errors.Fatalf("%s does not exist", path)
|
||||
}
|
||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/backend/test"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func parseURL(s string) *url.URL {
|
||||
|
|
@ -111,3 +115,60 @@ func TestStripPassword(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvironmentPasswordFile(t *testing.T) {
|
||||
cfg, err := ParseConfig("rest:http://localhost:1234/")
|
||||
rtest.OK(t, err)
|
||||
|
||||
t.Setenv("RESTIC_REST_PASSWORD_FILE", "/some/path/password")
|
||||
cfg.ApplyEnvironment("")
|
||||
|
||||
rtest.Equals(t, "/some/path/password", cfg.PasswordFile)
|
||||
}
|
||||
|
||||
func TestOpenPasswordFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pwdFile := filepath.Join(dir, "password")
|
||||
rtest.OK(t, os.WriteFile(pwdFile, []byte("secret\n"), 0600))
|
||||
|
||||
cfg, err := ParseConfig("rest:http://localhost:1234/")
|
||||
rtest.OK(t, err)
|
||||
cfg.PasswordFile = pwdFile
|
||||
|
||||
be, err := Open(t.Context(), *cfg, http.DefaultTransport, nil)
|
||||
rtest.OK(t, err)
|
||||
|
||||
pwd, set := be.url.User.Password()
|
||||
rtest.Assert(t, set, "expected password to be set")
|
||||
rtest.Equals(t, "secret", pwd)
|
||||
}
|
||||
|
||||
func TestOpenPasswordFileMissing(t *testing.T) {
|
||||
cfg, err := ParseConfig("rest:http://localhost:1234/")
|
||||
rtest.OK(t, err)
|
||||
cfg.PasswordFile = "/nonexistent/path/password"
|
||||
|
||||
_, err = Open(t.Context(), *cfg, http.DefaultTransport, nil)
|
||||
rtest.Assert(t, err != nil, "expected error for missing password file")
|
||||
}
|
||||
|
||||
func TestOpenPasswordFilePreferredOverPassword(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pwdFile := filepath.Join(dir, "password")
|
||||
rtest.OK(t, os.WriteFile(pwdFile, []byte("from-file\n"), 0600))
|
||||
|
||||
cfg, err := ParseConfig("rest:http://localhost:1234/")
|
||||
rtest.OK(t, err)
|
||||
|
||||
// Simulate both env vars being set: ApplyEnvironment stores the direct password
|
||||
// in the URL and the file path in PasswordFile. Open should prefer the file.
|
||||
cfg.URL.User = url.UserPassword("", "from-env")
|
||||
cfg.PasswordFile = pwdFile
|
||||
|
||||
be, err := Open(t.Context(), *cfg, http.DefaultTransport, nil)
|
||||
rtest.OK(t, err)
|
||||
|
||||
pwd, set := be.url.User.Password()
|
||||
rtest.Assert(t, set, "expected password to be set")
|
||||
rtest.Equals(t, "from-file", pwd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ const (
|
|||
|
||||
// Open opens the REST backend with the given config.
|
||||
func Open(_ context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (*Backend, error) {
|
||||
if cfg.PasswordFile != "" {
|
||||
pwd, err := loadPasswordFromFile(cfg.PasswordFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Loading password from file failed")
|
||||
}
|
||||
cfg.URL.User = url.UserPassword(cfg.URL.User.Username(), pwd)
|
||||
}
|
||||
|
||||
// use url without trailing slash for layout
|
||||
url := cfg.URL.String()
|
||||
if url[len(url)-1] == '/' {
|
||||
|
|
|
|||
Loading…
Reference in a new issue