This commit is contained in:
Friedrich Große 2026-05-20 22:33:19 -03:00 committed by GitHub
commit ee768b2c18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 3 deletions

View 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.

View file

@ -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

View file

@ -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

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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] == '/' {