diff --git a/app/vmauth/README.md b/app/vmauth/README.md index e6befcc85..cc0640762 100644 --- a/app/vmauth/README.md +++ b/app/vmauth/README.md @@ -32,6 +32,38 @@ Pass `-help` to `vmauth` in order to see all the supported command-line flags wi Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, accounting and rate limiting such as [vmgateway](https://docs.victoriametrics.com/vmgateway.html). +## Dropping request path prefix + +By default `vmauth` doesn't drop the path prefix from the original request when proxying the request to the matching backend. +Sometimes it is needed to drop path prefix before routing the request to the backend. This can be done by specifying the number of `/`-delimited +prefix parts to drop from the request path via `drop_src_path_prefix_parts` option at `url_map` level or at `user` level. + +For example, if you need to serve requests to [vmalert](https://docs.victoriametrics.com/vmalert.html) at `/vmalert/` path prefix, +while serving requests to [vmagent](https://docs.victoriametrics.com/vmagent.html) at `/vmagent/` path prefix for a particular user, +then the following [-auth.config](#auth-config) can be used: + +```yml +users: +- username: foo + url_map: + + # proxy all the requests, which start with `/vmagent/`, to vmagent backend + - src_paths: + - "/vmagent/.+" + + # drop /vmagent/ path prefix from the original request before proxying it to url_prefix. + drop_src_path_prefix_parts: 1 + url_prefix: "http://vmagent-backend:8429/" + + # proxy all the requests, which start with `/vmalert`, to vmalert backend + - src_paths: + - "/vmalert/.+" + + # drop /vmalert/ path prefix from the original request before proxying it to url_prefix. + drop_src_path_prefix_parts: 1 + url_prefix: "http://vmalert-backend:8880/" +``` + ## Load balancing Each `url_prefix` in the [-auth.config](#auth-config) may contain either a single url or a list of urls. diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go index 3b11f2c6c..9f12b9a42 100644 --- a/app/vmauth/auth_config.go +++ b/app/vmauth/auth_config.go @@ -40,18 +40,19 @@ type AuthConfig struct { // UserInfo is user information read from authConfigPath type UserInfo struct { - Name string `yaml:"name,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` - URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"` - URLMaps []URLMap `yaml:"url_map,omitempty"` - HeadersConf HeadersConf `yaml:",inline"` - MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"` - DefaultURL *URLPrefix `yaml:"default_url,omitempty"` - RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"` - TLSInsecureSkipVerify *bool `yaml:"tls_insecure_skip_verify,omitempty"` - TLSCAFile string `yaml:"tls_ca_file,omitempty"` + Name string `yaml:"name,omitempty"` + BearerToken string `yaml:"bearer_token,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"` + URLMaps []URLMap `yaml:"url_map,omitempty"` + HeadersConf HeadersConf `yaml:",inline"` + MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"` + DefaultURL *URLPrefix `yaml:"default_url,omitempty"` + RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"` + DropSrcPathPrefixParts int `yaml:"drop_src_path_prefix_parts,omitempty"` + TLSInsecureSkipVerify *bool `yaml:"tls_insecure_skip_verify,omitempty"` + TLSCAFile string `yaml:"tls_ca_file,omitempty"` concurrencyLimitCh chan struct{} concurrencyLimitReached *metrics.Counter @@ -119,10 +120,11 @@ func (h *Header) MarshalYAML() (interface{}, error) { // URLMap is a mapping from source paths to target urls. type URLMap struct { - SrcPaths []*SrcPath `yaml:"src_paths,omitempty"` - URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"` - HeadersConf HeadersConf `yaml:",inline"` - RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"` + SrcPaths []*SrcPath `yaml:"src_paths,omitempty"` + URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"` + HeadersConf HeadersConf `yaml:",inline"` + RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"` + DropSrcPathPrefixParts int `yaml:"drop_src_path_prefix_parts,omitempty"` } // SrcPath represents an src path diff --git a/app/vmauth/auth_config_test.go b/app/vmauth/auth_config_test.go index 8f24b6030..4d2882b2a 100644 --- a/app/vmauth/auth_config_test.go +++ b/app/vmauth/auth_config_test.go @@ -249,6 +249,8 @@ users: - http://node1:343/bbb - http://node2:343/bbb tls_insecure_skip_verify: false + retry_status_codes: [500, 501] + drop_src_path_prefix_parts: 1 `, map[string]*UserInfo{ getAuthToken("", "foo", "bar"): { Username: "foo", @@ -257,7 +259,9 @@ users: "http://node1:343/bbb", "http://node2:343/bbb", }), - TLSInsecureSkipVerify: &insecureSkipVerifyFalse, + TLSInsecureSkipVerify: &insecureSkipVerifyFalse, + RetryStatusCodes: []int{500, 501}, + DropSrcPathPrefixParts: 1, }, }) diff --git a/app/vmauth/main.go b/app/vmauth/main.go index a63be3719..d11c8dfc9 100644 --- a/app/vmauth/main.go +++ b/app/vmauth/main.go @@ -164,7 +164,7 @@ func processUserRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) { func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) { u := normalizeURL(r.URL) - up, hc, retryStatusCodes := ui.getURLPrefixAndHeaders(u) + up, hc, retryStatusCodes, dropSrcPathPrefixParts := ui.getURLPrefixAndHeaders(u) isDefault := false if up == nil { if ui.DefaultURL == nil { @@ -198,7 +198,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) { query.Set("request_path", u.Path) targetURL.RawQuery = query.Encode() } else { // Update path for regular routes. - targetURL = mergeURLs(targetURL, u) + targetURL = mergeURLs(targetURL, u, dropSrcPathPrefixParts) } ok := tryProcessingRequest(w, r, targetURL, hc, retryStatusCodes, ui.httpTransport) bu.put() diff --git a/app/vmauth/target_url.go b/app/vmauth/target_url.go index 02022eba0..46bb4f395 100644 --- a/app/vmauth/target_url.go +++ b/app/vmauth/target_url.go @@ -6,12 +6,13 @@ import ( "strings" ) -func mergeURLs(uiURL, requestURI *url.URL) *url.URL { +func mergeURLs(uiURL, requestURI *url.URL, dropSrcPathPrefixParts int) *url.URL { targetURL := *uiURL - if strings.HasPrefix(requestURI.Path, "/") { + srcPath := dropPrefixParts(requestURI.Path, dropSrcPathPrefixParts) + if strings.HasPrefix(srcPath, "/") { targetURL.Path = strings.TrimSuffix(targetURL.Path, "/") } - targetURL.Path += requestURI.Path + targetURL.Path += srcPath requestParams := requestURI.Query() // fast path if len(requestParams) == 0 { @@ -32,18 +33,34 @@ func mergeURLs(uiURL, requestURI *url.URL) *url.URL { return &targetURL } -func (ui *UserInfo) getURLPrefixAndHeaders(u *url.URL) (*URLPrefix, HeadersConf, []int) { +func dropPrefixParts(path string, parts int) string { + if parts <= 0 { + return path + } + for parts > 0 { + path = strings.TrimPrefix(path, "/") + n := strings.IndexByte(path, '/') + if n < 0 { + return "" + } + path = path[n:] + parts-- + } + return path +} + +func (ui *UserInfo) getURLPrefixAndHeaders(u *url.URL) (*URLPrefix, HeadersConf, []int, int) { for _, e := range ui.URLMaps { for _, sp := range e.SrcPaths { if sp.match(u.Path) { - return e.URLPrefix, e.HeadersConf, e.RetryStatusCodes + return e.URLPrefix, e.HeadersConf, e.RetryStatusCodes, e.DropSrcPathPrefixParts } } } if ui.URLPrefix != nil { - return ui.URLPrefix, ui.HeadersConf, ui.RetryStatusCodes + return ui.URLPrefix, ui.HeadersConf, ui.RetryStatusCodes, ui.DropSrcPathPrefixParts } - return nil, HeadersConf{}, nil + return nil, HeadersConf{}, nil, 0 } func normalizeURL(uOrig *url.URL) *url.URL { diff --git a/app/vmauth/target_url_test.go b/app/vmauth/target_url_test.go index 89b18bada..b8aa3eb94 100644 --- a/app/vmauth/target_url_test.go +++ b/app/vmauth/target_url_test.go @@ -7,20 +7,91 @@ import ( "testing" ) +func TestDropPrefixParts(t *testing.T) { + f := func(path string, parts int, expectedResult string) { + t.Helper() + + result := dropPrefixParts(path, parts) + if result != expectedResult { + t.Fatalf("unexpected result; got %q; want %q", result, expectedResult) + } + } + + f("", 0, "") + f("", 1, "") + f("", 10, "") + f("foo", 0, "foo") + f("foo", -1, "foo") + f("foo", 1, "") + + f("/foo", 0, "/foo") + f("/foo/bar", 0, "/foo/bar") + f("/foo/bar/baz", 0, "/foo/bar/baz") + + f("foo", 0, "foo") + f("foo/bar", 0, "foo/bar") + f("foo/bar/baz", 0, "foo/bar/baz") + + f("/foo/", 0, "/foo/") + f("/foo/bar/", 0, "/foo/bar/") + f("/foo/bar/baz/", 0, "/foo/bar/baz/") + + f("/foo", 1, "") + f("/foo/bar", 1, "/bar") + f("/foo/bar/baz", 1, "/bar/baz") + + f("foo", 1, "") + f("foo/bar", 1, "/bar") + f("foo/bar/baz", 1, "/bar/baz") + + f("/foo/", 1, "/") + f("/foo/bar/", 1, "/bar/") + f("/foo/bar/baz/", 1, "/bar/baz/") + + f("/foo", 2, "") + f("/foo/bar", 2, "") + f("/foo/bar/baz", 2, "/baz") + + f("foo", 2, "") + f("foo/bar", 2, "") + f("foo/bar/baz", 2, "/baz") + + f("/foo/", 2, "") + f("/foo/bar/", 2, "/") + f("/foo/bar/baz/", 2, "/baz/") + + f("/foo", 3, "") + f("/foo/bar", 3, "") + f("/foo/bar/baz", 3, "") + + f("foo", 3, "") + f("foo/bar", 3, "") + f("foo/bar/baz", 3, "") + + f("/foo/", 3, "") + f("/foo/bar/", 3, "") + f("/foo/bar/baz/", 3, "/") + + f("/foo/", 4, "") + f("/foo/bar/", 4, "") + f("/foo/bar/baz/", 4, "") +} + func TestCreateTargetURLSuccess(t *testing.T) { - f := func(ui *UserInfo, requestURI, expectedTarget, expectedRequestHeaders, expectedResponseHeaders string, expectedRetryStatusCodes []int) { + f := func(ui *UserInfo, requestURI, expectedTarget, expectedRequestHeaders, expectedResponseHeaders string, + expectedRetryStatusCodes []int, expectedDropSrcPathPrefixParts int) { t.Helper() u, err := url.Parse(requestURI) if err != nil { t.Fatalf("cannot parse %q: %s", requestURI, err) } u = normalizeURL(u) - up, hc, retryStatusCodes := ui.getURLPrefixAndHeaders(u) + up, hc, retryStatusCodes, dropSrcPathPrefixParts := ui.getURLPrefixAndHeaders(u) if up == nil { t.Fatalf("cannot determie backend: %s", err) } bu := up.getLeastLoadedBackendURL() - target := mergeURLs(bu.url, u) + target := mergeURLs(bu.url, u, dropSrcPathPrefixParts) bu.put() if target.String() != expectedTarget { t.Fatalf("unexpected target; got %q; want %q", target, expectedTarget) @@ -32,11 +103,14 @@ func TestCreateTargetURLSuccess(t *testing.T) { if !reflect.DeepEqual(retryStatusCodes, expectedRetryStatusCodes) { t.Fatalf("unexpected retryStatusCodes; got %d; want %d", retryStatusCodes, expectedRetryStatusCodes) } + if dropSrcPathPrefixParts != expectedDropSrcPathPrefixParts { + t.Fatalf("unexpected dropSrcPathPrefixParts; got %d; want %d", dropSrcPathPrefixParts, expectedDropSrcPathPrefixParts) + } } // Simple routing with `url_prefix` f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar"), - }, "", "http://foo.bar/.", "[]", "[]", nil) + }, "", "http://foo.bar/.", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar"), HeadersConf: HeadersConf{ @@ -45,29 +119,30 @@ func TestCreateTargetURLSuccess(t *testing.T) { Value: "aaa", }}, }, - RetryStatusCodes: []int{503, 501}, - }, "/", "http://foo.bar", `[{"bb" "aaa"}]`, `[]`, []int{503, 501}) + RetryStatusCodes: []int{503, 501}, + DropSrcPathPrefixParts: 2, + }, "/a/b/c", "http://foo.bar/c", `[{"bb" "aaa"}]`, `[]`, []int{503, 501}, 2) f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar/federate"), - }, "/", "http://foo.bar/federate", "[]", "[]", nil) + }, "/", "http://foo.bar/federate", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar"), - }, "a/b?c=d", "http://foo.bar/a/b?c=d", "[]", "[]", nil) + }, "a/b?c=d", "http://foo.bar/a/b?c=d", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("https://sss:3894/x/y"), - }, "/z", "https://sss:3894/x/y/z", "[]", "[]", nil) + }, "/z", "https://sss:3894/x/y/z", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("https://sss:3894/x/y"), - }, "/../../aaa", "https://sss:3894/x/y/aaa", "[]", "[]", nil) + }, "/../../aaa", "https://sss:3894/x/y/aaa", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("https://sss:3894/x/y"), - }, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd", "[]", "[]", nil) + }, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd", "[]", "[]", nil, 0) // Complex routing with `url_map` ui := &UserInfo{ URLMaps: []URLMap{ { - SrcPaths: getSrcPaths([]string{"/api/v1/query"}), + SrcPaths: getSrcPaths([]string{"/vmsingle/api/v1/query"}), URLPrefix: mustParseURL("http://vmselect/0/prometheus"), HeadersConf: HeadersConf{ RequestHeaders: []Header{ @@ -87,7 +162,8 @@ func TestCreateTargetURLSuccess(t *testing.T) { }, }, }, - RetryStatusCodes: []int{503, 500, 501}, + RetryStatusCodes: []int{503, 500, 501}, + DropSrcPathPrefixParts: 1, }, { SrcPaths: getSrcPaths([]string{"/api/v1/write"}), @@ -105,11 +181,12 @@ func TestCreateTargetURLSuccess(t *testing.T) { Value: "y", }}, }, - RetryStatusCodes: []int{502}, + RetryStatusCodes: []int{502}, + DropSrcPathPrefixParts: 2, } - f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", `[{"xx" "aa"} {"yy" "asdf"}]`, `[{"qwe" "rty"}]`, []int{503, 500, 501}) - f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]", "[]", nil) - f(ui, "/api/v1/query_range", "http://default-server/api/v1/query_range", `[{"bb" "aaa"}]`, `[{"x" "y"}]`, []int{502}) + f(ui, "/vmsingle/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", `[{"xx" "aa"} {"yy" "asdf"}]`, `[{"qwe" "rty"}]`, []int{503, 500, 501}, 1) + f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]", "[]", nil, 0) + f(ui, "/foo/bar/api/v1/query_range", "http://default-server/api/v1/query_range", `[{"bb" "aaa"}]`, `[{"x" "y"}]`, []int{502}, 2) // Complex routing regexp paths in `url_map` ui = &UserInfo{ @@ -125,17 +202,17 @@ func TestCreateTargetURLSuccess(t *testing.T) { }, URLPrefix: mustParseURL("http://default-server"), } - f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", "[]", "[]", nil) - f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up", "[]", "[]", nil) - f(ui, "/api/v1/label/foo/values", "http://vmselect/0/prometheus/api/v1/label/foo/values", "[]", "[]", nil) - f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]", "[]", nil) - f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar", "[]", "[]", nil) + f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", "[]", "[]", nil, 0) + f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up", "[]", "[]", nil, 0) + f(ui, "/api/v1/label/foo/values", "http://vmselect/0/prometheus/api/v1/label/foo/values", "[]", "[]", nil, 0) + f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]", "[]", nil, 0) + f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar?extra_label=team=dev"), - }, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev", "[]", "[]", nil) + }, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev", "[]", "[]", nil, 0) f(&UserInfo{ URLPrefix: mustParseURL("http://foo.bar?extra_label=team=mobile"), - }, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile", "[]", "[]", nil) + }, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile", "[]", "[]", nil, 0) } func TestCreateTargetURLFailure(t *testing.T) { @@ -146,7 +223,7 @@ func TestCreateTargetURLFailure(t *testing.T) { t.Fatalf("cannot parse %q: %s", requestURI, err) } u = normalizeURL(u) - up, hc, retryStatusCodes := ui.getURLPrefixAndHeaders(u) + up, hc, retryStatusCodes, dropSrcPathPrefixParts := ui.getURLPrefixAndHeaders(u) if up != nil { t.Fatalf("unexpected non-empty up=%#v", up) } @@ -159,6 +236,9 @@ func TestCreateTargetURLFailure(t *testing.T) { if retryStatusCodes != nil { t.Fatalf("unexpected non-empty retryStatusCodes=%d", retryStatusCodes) } + if dropSrcPathPrefixParts != 0 { + t.Fatalf("unexpected non-zero dropSrcPathPrefixParts=%d", dropSrcPathPrefixParts) + } } f(&UserInfo{}, "/foo/bar") f(&UserInfo{ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 68f02b0f6..4142c15a3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -79,6 +79,7 @@ The sandbox cluster installation is running under the constant load generated by * FEATURE: [vmalert-tool](https://docs.victoriametrics.com/#vmalert-tool): add `unittest` command to run unittest for alerting and recording rules. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4789) for details. * FEATURE: dashboards/vmalert: add new panel `Missed evaluations` for indicating alerting groups that miss their evaluations. * FEATURE: all: track requests with wrong auth key and wrong basic auth at `vm_http_request_errors_total` [metric](https://docs.victoriametrics.com/#monitoring) with `reason="wrong_auth_key"` and `reason="wrong_basic_auth"`. See [this issue](https://github.com/victoriaMetrics/victoriaMetrics/issues/4590). Thanks to @venkatbvc for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5166). +* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to drop the specified number of `/`-delimited prefix parts from the request path before proxying the request to the matching backend. See [these docs](https://docs.victoriametrics.com/vmauth.html#dropping-request-path-prefix). * FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to skip TLS verification and to specify TLS Root CA when connecting to backends. See [these docs](https://docs.victoriametrics.com/vmauth.html#backend-tls-setup) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5240). * FEATURE: `vmstorage`: add `-blockcache.missesBeforeCaching` command-line flag, which can be used for fine-tuning RAM usage for `indexdb/dataBlocks` cache when queries touching big number of time series are executed. * FEATURE: add `-loggerMaxArgLen` command-line flag for fine-tuning the maximum lengths of logged args. diff --git a/docs/vmauth.md b/docs/vmauth.md index 6375ed511..867a57cd9 100644 --- a/docs/vmauth.md +++ b/docs/vmauth.md @@ -43,6 +43,38 @@ Pass `-help` to `vmauth` in order to see all the supported command-line flags wi Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, accounting and rate limiting such as [vmgateway](https://docs.victoriametrics.com/vmgateway.html). +## Dropping request path prefix + +By default `vmauth` doesn't drop the path prefix from the original request when proxying the request to the matching backend. +Sometimes it is needed to drop path prefix before routing the request to the backend. This can be done by specifying the number of `/`-delimited +prefix parts to drop from the request path via `drop_src_path_prefix_parts` option at `url_map` level or at `user` level. + +For example, if you need to serve requests to [vmalert](https://docs.victoriametrics.com/vmalert.html) at `/vmalert/` path prefix, +while serving requests to [vmagent](https://docs.victoriametrics.com/vmagent.html) at `/vmagent/` path prefix for a particular user, +then the following [-auth.config](#auth-config) can be used: + +```yml +users: +- username: foo + url_map: + + # proxy all the requests, which start with `/vmagent/`, to vmagent backend + - src_paths: + - "/vmagent/.+" + + # drop /vmagent/ path prefix from the original request before proxying it to url_prefix. + drop_src_path_prefix_parts: 1 + url_prefix: "http://vmagent-backend:8429/" + + # proxy all the requests, which start with `/vmalert`, to vmalert backend + - src_paths: + - "/vmalert/.+" + + # drop /vmalert/ path prefix from the original request before proxying it to url_prefix. + drop_src_path_prefix_parts: 1 + url_prefix: "http://vmalert-backend:8880/" +``` + ## Load balancing Each `url_prefix` in the [-auth.config](#auth-config) may contain either a single url or a list of urls.