Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app/vmbackup: introduce flags to take basic auth credentials and snapshot authkey #6223

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).

## tip

* FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup/): allow flag based configuration for basic auth credentials and snapshot auth key via `snapshot.basicAuthUsername`, `snapshot.basicAuthPassword` and `snapshot.authKey` options respectively for `snapshot.createURL` and `snapshot.deleteURL`. See [these docs](https://docs.victoriametrics.com/cluster-victoriametrics/#list-of-command-line-flags-for-vmstorage) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5973).
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229): support selecting of multiple instances on the dashboard. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5869) for details.
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229): properly display version in the Stats row for the custom builds of VictoriaMetrics.
* FEATURE: [dashboards/single](https://grafana.com/grafana/dashboards/10229): add `Network Usage` panel to `Resource Usage` row.
Expand Down
2 changes: 1 addition & 1 deletion docs/Cluster-VictoriaMetrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,7 @@ Below is the output for `/path/to/vmstorage -help`:
-smallMergeConcurrency int
Deprecated: this flag does nothing
-snapshotAuthKey value
authKey, which must be passed in query string to /snapshot* pages
authKey, which must be passed in query string or via 'X-AuthKey' http header to /snapshot* pages
Flag value can be read from the given file when using -snapshotAuthKey=file:///abs/path/to/file or -snapshotAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -snapshotAuthKey=http://host/path or -snapshotAuthKey=https://host/path
-snapshotCreateTimeout duration
Deprecated: this flag does nothing
Expand Down
8 changes: 8 additions & 0 deletions docs/vmbackup.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ Run `vmbackup -help` in order to see all the available options:
VictoriaMetrics create snapshot url. When this is given a snapshot will automatically be created during backup. Example: http://victoriametrics:8428/snapshot/create . There is no need in setting -snapshotName if -snapshot.createURL is set
-snapshot.deleteURL string
VictoriaMetrics delete snapshot url. Optional. Will be generated from -snapshot.createURL if not provided. All created snapshots will be automatically deleted. Example: http://victoriametrics:8428/snapshot/delete
-snapshot.basicAuthUsername string
Optional basic auth username to use for connections to -snapshotCreateURL.
Flag value can be read from the given file when using -snapshot.basicAuthUsername=file:///abs/path/to/file or -snapshot.basicAuthUsername=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -snapshot.basicAuthUsername=http://host/path or -snapshot.basicAuthUsername=https://host/path
-snapshot.basicAuthPassword string
Optional basic auth password to use for connections to -snapshotCreateURL.
Flag value can be read from the given file when using -snapshot.basicAuthPassword=file:///abs/path/to/file or -snapshot.basicAuthPassword=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -snapshot.basicAuthPassword=http://host/path or -snapshot.basicAuthPassword=https://host/path
-snapshot.authKey string
Optional authKey to be passed as 'X-AuthKey' HTTP headder for the connections to -snapshotCreateURL and -snapshot.deleteURL. Flag value can be read from the given file when using -snapshot.authKey=file:///abs/path/to/file or -snapshot.authKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -snapshot.authKey=http://host/path or -snapshot.authKey=https://host/path
-snapshot.tlsCAFile string
Optional path to TLS CA file to use for verifying connections to -snapshotCreateURL. By default, system CA is used
-snapshot.tlsCertFile string
Expand Down
7 changes: 7 additions & 0 deletions lib/httpserver/httpserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,13 @@
return CheckBasicAuth(w, r)
}
if r.FormValue("authKey") != flagValue {
// Check and allow the request if the header 'X-AuthKey' matches the flagValue
// Currently, this is only applicable for snapshotAuthKey. Below condition can be modified to remove
// the check for flagName if 'X-AuthKey' header is to be allowed for other authKeys related flags (for instance reloadAuthKey)
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5973
if flagName == "snapshotAuthKey" && r.Header.Get("X-AuthKey") == flagValue {
return true

Check warning on line 439 in lib/httpserver/httpserver.go

View check run for this annotation

Codecov / codecov/patch

lib/httpserver/httpserver.go#L439

Added line #L439 was not covered by tests
}
authKeyRequestErrors.Inc()
http.Error(w, fmt.Sprintf("The provided authKey doesn't match -%s", flagName), http.StatusUnauthorized)
return false
Expand Down
34 changes: 34 additions & 0 deletions lib/httputils/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package httputils

import (
"net/url"
"regexp"
"strings"
)

var secretWordsRe = regexp.MustCompile("auth|pass|key|secret|token")

// RedactedURL redacts sensitive information like basic auth credentials
// and query parameters containing sensitive info from a URL object (*url.URL).
// It searches for query parameter names containing words commonly associated
// with authentication credentials (like "auth", "pass", "key", "secret", or "token").
// These words are matched in a case-insensitive manner. If there is a match, such sensitive information will be mased with 'xxxxx'
func RedactedURL(u *url.URL) string {
if u == nil {
return ""

Check warning on line 18 in lib/httputils/url.go

View check run for this annotation

Codecov / codecov/patch

lib/httputils/url.go#L18

Added line #L18 was not covered by tests
}
ru := *u
values := ru.Query()
for k, vs := range values {
if secretWordsRe.MatchString(strings.ToLower(k)) {
for i := range vs {
vs[i] = "xxxxx"
}
}
}
ru.RawQuery = values.Encode()
if _, has := ru.User.Password(); has {
ru.User = url.UserPassword("xxxxx", "xxxxx")
}
return ru.String()
}
59 changes: 59 additions & 0 deletions lib/httputils/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package httputils

import (
"net/url"
"testing"
)

func TestRedactedURL(t *testing.T) {
tests := []struct {
name string
inputURL string
expected string
}{
{
name: "empty URL",
inputURL: "",
expected: "",
},
{
name: "no secrets",
inputURL: "https://example.com/path/to/resource",
expected: "https://example.com/path/to/resource",
},
{
name: "secret query parameter",
inputURL: "https://example.com/path/to/resource?authKey=foobar",
expected: "https://example.com/path/to/resource?authKey=xxxxx",
},
{
name: "secret query parameters (case insensitive)",
inputURL: "https://example.com/path/to/resource?TOKEN=foobar",
expected: "https://example.com/path/to/resource?TOKEN=xxxxx",
},
{
name: "with basic auth secrets",
inputURL: "https://username:secretPassword@example.com/path/to/resource",
expected: "https://xxxxx:xxxxx@example.com/path/to/resource",
},
{
name: "with basic auth and query parameters secrets",
inputURL: "https://username:secretPassword@example.com/path/to/resource?authKey=foobar",
expected: "https://xxxxx:xxxxx@example.com/path/to/resource?authKey=xxxxx",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parsedURL, err := url.Parse(test.inputURL)
if err != nil {
t.Errorf("Unexpected error %v", err)
return
}
actual := RedactedURL(parsedURL)
if actual != test.expected {
t.Errorf("Expected: %s, Actual: %s", test.expected, actual)
}
})
}
}
47 changes: 39 additions & 8 deletions lib/snapshot/snapshot.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package snapshot

import (
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
Expand All @@ -19,6 +22,9 @@
tlsKeyFile = flag.String("snapshot.tlsKeyFile", "", "Optional path to client-side TLS certificate key to use when connecting to -snapshotCreateURL")
tlsCAFile = flag.String("snapshot.tlsCAFile", "", `Optional path to TLS CA file to use for verifying connections to -snapshotCreateURL. By default, system CA is used`)
tlsServerName = flag.String("snapshot.tlsServerName", "", `Optional TLS server name to use for connections to -snapshotCreateURL. By default, the server name from -snapshotCreateURL is used`)
basicAuthUser = flagutil.NewPassword("snapshot.basicAuthUsername", `Optional basic auth username to use for connections to -snapshotCreateURL`)
basicAuthPassword = flagutil.NewPassword("snapshot.basicAuthPassword", `Optional basic auth password to use for connections to -snapshotCreateURL`)
snapshotAuthKey = flagutil.NewPassword("snapshot.authKey", `Optional authKey to be passed as 'X-AuthKey' HTTP headder for the connections to -snapshotCreateURL`)
)

type snapshot struct {
Expand All @@ -42,7 +48,12 @@
}
hc := &http.Client{Transport: tr}

resp, err := hc.Get(u.String())
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return "", err

Check warning on line 53 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L53

Added line #L53 was not covered by tests
}
addAuthHeaders(req)
resp, err := hc.Do(req)
if err != nil {
return "", err
}
Expand All @@ -51,13 +62,13 @@
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code returned from %q: %d; expecting %d; response body: %q", u.Redacted(), resp.StatusCode, http.StatusOK, body)
return "", fmt.Errorf("unexpected status code returned from %q: %d; expecting %d; response body: %q", httputils.RedactedURL(u), resp.StatusCode, http.StatusOK, body)
}

snap := snapshot{}
err = json.Unmarshal(body, &snap)
if err != nil {
return "", fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", u.Redacted(), err, body)
return "", fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", httputils.RedactedURL(u), err, body)

Check warning on line 71 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L71

Added line #L71 was not covered by tests
}

if snap.Status == "ok" {
Expand All @@ -67,7 +78,7 @@
if snap.Status == "error" {
return "", errors.New(snap.Msg)
}
return "", fmt.Errorf("Unkown status: %v", snap.Status)
return "", fmt.Errorf("Unknown status: %v", snap.Status)

Check warning on line 81 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L81

Added line #L81 was not covered by tests
}

// Delete deletes a snapshot via the provided api endpoint
Expand All @@ -86,7 +97,13 @@
return err
}
hc := &http.Client{Transport: tr}
resp, err := hc.PostForm(u.String(), formData)
req, err := http.NewRequest("POST", u.String(), strings.NewReader(formData.Encode()))
if err != nil {
return err

Check warning on line 102 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L102

Added line #L102 was not covered by tests
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
addAuthHeaders(req)
resp, err := hc.Do(req)
if err != nil {
return err
}
Expand All @@ -95,13 +112,13 @@
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code returned from %q: %d; expecting %d; response body: %q", u.Redacted(), resp.StatusCode, http.StatusOK, body)
return fmt.Errorf("unexpected status code returned from %q: %d; expecting %d; response body: %q", httputils.RedactedURL(u), resp.StatusCode, http.StatusOK, body)
}

snap := snapshot{}
err = json.Unmarshal(body, &snap)
if err != nil {
return fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", u.Redacted(), err, body)
return fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", httputils.RedactedURL(u), err, body)

Check warning on line 121 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L121

Added line #L121 was not covered by tests
}

if snap.Status == "ok" {
Expand All @@ -111,5 +128,19 @@
if snap.Status == "error" {
return errors.New(snap.Msg)
}
return fmt.Errorf("Unkown status: %v", snap.Status)
return fmt.Errorf("Unknown status: %v", snap.Status)

Check warning on line 131 in lib/snapshot/snapshot.go

View check run for this annotation

Codecov / codecov/patch

lib/snapshot/snapshot.go#L131

Added line #L131 was not covered by tests
}

// addBasicAuthHeader adds basic auth and X-AuthKey (for snapshot authkey) header to request
// if their corresponding flags snapshot.basicAuthUsername, snapshot.basicAuthPassword and snapshot.authKey flags are set.
func addAuthHeaders(req *http.Request) {
if basicAuthUser.Get() != "" {
auth := basicAuthUser.Get() + ":" + basicAuthPassword.Get()
authHeader := base64.StdEncoding.EncodeToString([]byte(auth))
req.Header.Set("Authorization", "Basic "+authHeader)
}
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5973
if snapshotAuthKey.Get() != "" {
req.Header.Set("X-AuthKey", snapshotAuthKey.Get())
}
}