Skip to content

Commit 89a0232

Browse files
authored
Added limit for size of configuration file that tenant can upload to Alertmanager. (#4201)
* Added limit for size of configuration file that tenant can upload to Alertmanager. Signed-off-by: Peter Štibraný <[email protected]> * CHANGELOG.md Signed-off-by: Peter Štibraný <[email protected]> * Update help message. Signed-off-by: Peter Štibraný <[email protected]> * Address review feedback. Signed-off-by: Peter Štibraný <[email protected]>
1 parent 58a18e2 commit 89a0232

File tree

7 files changed

+86
-9
lines changed

7 files changed

+86
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [CHANGE] Change default value of `-server.grpc.keepalive.min-time-between-pings` to `10s` and `-server.grpc.keepalive.ping-without-stream-allowed` to `true`. #4168
1010
* [FEATURE] Querier: Added new `-querier.max-fetched-series-per-query` flag. When Cortex is running with blocks storage, the max series per query limit is enforced in the querier and applies to unique series received from ingesters and store-gateway (long-term storage). #4179
1111
* [FEATURE] Alertmanager: Added rate-limits to notifiers. Rate limits used by all integrations can be configured using `-alertmanager.notification-rate-limit`, while per-integration rate limits can be specified via `-alertmanager.notification-rate-limit-per-integration` parameter. Both shared and per-integration limits can be overwritten using overrides mechanism. These limits are applied on individual (per-tenant) alertmanagers. Rate-limited notifications are failed notifications. It is possible to monitor rate-limited notifications via new `cortex_alertmanager_notification_rate_limited_total` metric. #4135 #4163
12+
* [FEATURE] Alertmanager: Added `-alertmanager.max-config-size-bytes` limit to control size of configuration files that Cortex users can upload to Alertmanager via API. This limit is configurable per-tenant. #4201
1213
* [FEATURE] Added flag `-debug.block-profile-rate` to enable goroutine blocking events profiling. #4217
1314
* [ENHANCEMENT] Alertmanager: introduced new metrics to monitor operation when using `-alertmanager.sharding-enabled`: #4149
1415
* `cortex_alertmanager_state_fetch_replica_state_total`

docs/configuration/config-file-reference.md

+5
Original file line numberDiff line numberDiff line change
@@ -4128,6 +4128,11 @@ The `limits_config` configures default and per-tenant limits imposed by Cortex s
41284128
# wechat, slack, victorops, pushover.
41294129
# CLI flag: -alertmanager.notification-rate-limit-per-integration
41304130
[alertmanager_notification_rate_limit_per_integration: <map of string to float64> | default = {}]
4131+
4132+
# Maximum size of configuration file for Alertmanager that tenant can upload via
4133+
# Alertmanager API. 0 = no limit.
4134+
# CLI flag: -alertmanager.max-config-size-bytes
4135+
[alertmanager_max_config_size_bytes: <int> | default = 0]
41314136
```
41324137

41334138
### `redis_config`

pkg/alertmanager/api.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package alertmanager
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"io/ioutil"
78
"net/http"
89
"os"
@@ -32,6 +33,7 @@ const (
3233
errDeletingConfiguration = "unable to delete the Alertmanager config"
3334
errNoOrgID = "unable to determine the OrgID"
3435
errListAllUser = "unable to list the Alertmanager users"
36+
errConfigurationTooBig = "Alertmanager configuration is too big, limit: %d bytes"
3537

3638
fetchConcurrency = 16
3739
)
@@ -98,13 +100,30 @@ func (am *MultitenantAlertmanager) SetUserConfig(w http.ResponseWriter, r *http.
98100
return
99101
}
100102

101-
payload, err := ioutil.ReadAll(r.Body)
103+
var input io.Reader
104+
maxConfigSize := am.limits.AlertmanagerMaxConfigSize(userID)
105+
if maxConfigSize > 0 {
106+
// LimitReader will return EOF after reading specified number of bytes. To check if
107+
// we have read too many bytes, allow one extra byte.
108+
input = io.LimitReader(r.Body, int64(maxConfigSize)+1)
109+
} else {
110+
input = r.Body
111+
}
112+
113+
payload, err := ioutil.ReadAll(input)
102114
if err != nil {
103115
level.Error(logger).Log("msg", errReadingConfiguration, "err", err.Error())
104116
http.Error(w, fmt.Sprintf("%s: %s", errReadingConfiguration, err.Error()), http.StatusBadRequest)
105117
return
106118
}
107119

120+
if maxConfigSize > 0 && len(payload) > maxConfigSize {
121+
msg := fmt.Sprintf(errConfigurationTooBig, maxConfigSize)
122+
level.Warn(logger).Log("msg", msg)
123+
http.Error(w, msg, http.StatusBadRequest)
124+
return
125+
}
126+
108127
cfg := &UserConfig{}
109128
err = yaml.Unmarshal(payload, cfg)
110129
if err != nil {

pkg/alertmanager/api_test.go

+40-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import (
3333

3434
func TestAMConfigValidationAPI(t *testing.T) {
3535
testCases := []struct {
36-
name string
37-
cfg string
36+
name string
37+
cfg string
38+
maxConfigSize int
39+
3840
response string
3941
err error
4042
}{
@@ -452,14 +454,50 @@ template_files:
452454
`,
453455
err: fmt.Errorf(`error validating Alertmanager config: template: test.tmpl:1: function "invalid" not defined`),
454456
},
457+
{
458+
name: "config too big",
459+
cfg: `
460+
alertmanager_config: |
461+
route:
462+
receiver: 'default-receiver'
463+
group_wait: 30s
464+
group_interval: 5m
465+
repeat_interval: 4h
466+
group_by: [cluster, alertname]
467+
receivers:
468+
- name: default-receiver
469+
`,
470+
maxConfigSize: 10,
471+
err: fmt.Errorf(errConfigurationTooBig, 10),
472+
},
473+
{
474+
name: "config size OK",
475+
cfg: `
476+
alertmanager_config: |
477+
route:
478+
receiver: 'default-receiver'
479+
group_wait: 30s
480+
group_interval: 5m
481+
repeat_interval: 4h
482+
group_by: [cluster, alertname]
483+
receivers:
484+
- name: default-receiver
485+
`,
486+
maxConfigSize: 1000,
487+
err: nil,
488+
},
455489
}
456490

491+
limits := &mockAlertManagerLimits{}
457492
am := &MultitenantAlertmanager{
458493
store: prepareInMemoryAlertStore(),
459494
logger: util_log.Logger,
495+
limits: limits,
460496
}
461497
for _, tc := range testCases {
462498
t.Run(tc.name, func(t *testing.T) {
499+
limits.maxConfigSize = tc.maxConfigSize
500+
463501
req := httptest.NewRequest(http.MethodPost, "http://alertmanager/api/v1/alerts", bytes.NewReader([]byte(tc.cfg)))
464502
ctx := user.InjectOrgID(req.Context(), "testing")
465503
w := httptest.NewRecorder()

pkg/alertmanager/multitenant.go

+3
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ type Limits interface {
246246
// NotificationBurstSize returns burst-size for rate limiter for given integration type. If 0, no notifications are allowed except
247247
// when limit == rate.Inf.
248248
NotificationBurstSize(tenant string, integration string) int
249+
250+
// AlertmanagerMaxConfigSize returns max size of configuration file that user is allowed to upload. If 0, there is no limit.
251+
AlertmanagerMaxConfigSize(tenant string) int
249252
}
250253

251254
// A MultitenantAlertmanager manages Alertmanager instances for multiple

pkg/alertmanager/multitenant_test.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -1867,7 +1867,7 @@ receivers:
18671867

18681868
reg := prometheus.NewPedanticRegistry()
18691869
cfg := mockAlertmanagerConfig(t)
1870-
am, err := createMultitenantAlertmanager(cfg, nil, nil, store, nil, limits, log.NewNopLogger(), reg)
1870+
am, err := createMultitenantAlertmanager(cfg, nil, nil, store, nil, &limits, log.NewNopLogger(), reg)
18711871
require.NoError(t, err)
18721872

18731873
err = am.loadAndSyncConfigs(context.Background(), reasonPeriodic)
@@ -1942,20 +1942,25 @@ func (f *passthroughAlertmanagerClientPool) GetClientFor(addr string) (Client, e
19421942
type mockAlertManagerLimits struct {
19431943
emailNotificationRateLimit rate.Limit
19441944
emailNotificationBurst int
1945+
maxConfigSize int
19451946
}
19461947

1947-
func (m mockAlertManagerLimits) AlertmanagerReceiversBlockCIDRNetworks(user string) []flagext.CIDR {
1948+
func (m *mockAlertManagerLimits) AlertmanagerMaxConfigSize(tenant string) int {
1949+
return m.maxConfigSize
1950+
}
1951+
1952+
func (m *mockAlertManagerLimits) AlertmanagerReceiversBlockCIDRNetworks(user string) []flagext.CIDR {
19481953
panic("implement me")
19491954
}
19501955

1951-
func (m mockAlertManagerLimits) AlertmanagerReceiversBlockPrivateAddresses(user string) bool {
1956+
func (m *mockAlertManagerLimits) AlertmanagerReceiversBlockPrivateAddresses(user string) bool {
19521957
panic("implement me")
19531958
}
19541959

1955-
func (m mockAlertManagerLimits) NotificationRateLimit(_ string, integration string) rate.Limit {
1960+
func (m *mockAlertManagerLimits) NotificationRateLimit(_ string, integration string) rate.Limit {
19561961
return m.emailNotificationRateLimit
19571962
}
19581963

1959-
func (m mockAlertManagerLimits) NotificationBurstSize(_ string, integration string) int {
1964+
func (m *mockAlertManagerLimits) NotificationBurstSize(_ string, integration string) int {
19601965
return m.emailNotificationBurst
19611966
}

pkg/util/validation/limits.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,10 @@ type Limits struct {
104104
AlertmanagerReceiversBlockCIDRNetworks flagext.CIDRSliceCSV `yaml:"alertmanager_receivers_firewall_block_cidr_networks" json:"alertmanager_receivers_firewall_block_cidr_networks"`
105105
AlertmanagerReceiversBlockPrivateAddresses bool `yaml:"alertmanager_receivers_firewall_block_private_addresses" json:"alertmanager_receivers_firewall_block_private_addresses"`
106106

107-
// Alertmanager limits
108107
NotificationRateLimit float64 `yaml:"alertmanager_notification_rate_limit" json:"alertmanager_notification_rate_limit"`
109108
NotificationRateLimitPerIntegration NotificationRateLimitMap `yaml:"alertmanager_notification_rate_limit_per_integration" json:"alertmanager_notification_rate_limit_per_integration"`
109+
110+
AlertmanagerMaxConfigSizeBytes int `yaml:"alertmanager_max_config_size_bytes" json:"alertmanager_max_config_size_bytes"`
110111
}
111112

112113
// RegisterFlags adds the flags required to config this to the given FlagSet
@@ -175,6 +176,7 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) {
175176
l.NotificationRateLimitPerIntegration = NotificationRateLimitMap{}
176177
}
177178
f.Var(&l.NotificationRateLimitPerIntegration, "alertmanager.notification-rate-limit-per-integration", "Per-integration notification rate limits. Value is a map, where each key is integration name and value is a rate-limit (float). On command line, this map is given in JSON format. Rate limit has the same meaning as -alertmanager.notification-rate-limit, but only applies for specific integration. Allowed integration names: "+strings.Join(allowedIntegrationNames, ", ")+".")
179+
f.IntVar(&l.AlertmanagerMaxConfigSizeBytes, "alertmanager.max-config-size-bytes", 0, "Maximum size of configuration file for Alertmanager that tenant can upload via Alertmanager API. 0 = no limit.")
178180
}
179181

180182
// Validate the limits config and returns an error if the validation
@@ -581,6 +583,10 @@ func (o *Overrides) NotificationBurstSize(user string, integration string) int {
581583
return int(l)
582584
}
583585

586+
func (o *Overrides) AlertmanagerMaxConfigSize(userID string) int {
587+
return o.getOverridesForUser(userID).AlertmanagerMaxConfigSizeBytes
588+
}
589+
584590
func (o *Overrides) getOverridesForUser(userID string) *Limits {
585591
if o.tenantLimits != nil {
586592
l := o.tenantLimits.ByUserID(userID)

0 commit comments

Comments
 (0)