Merge pull request #1496 from prymitive/silence-acl

feat(backend): add configuration options for silence ACL rules
This commit is contained in:
Łukasz Mierzwa
2020-03-09 22:50:31 +00:00
committed by GitHub
38 changed files with 1879 additions and 42 deletions
+8
View File
@@ -10,6 +10,9 @@ requires Alertmanager `>=0.17.0`.
---
See [GitHub Releases](https://github.com/prymitive/karma/releases) for release
changelog.
## Feature overview
Alertmanager UI is useful for browsing alerts and managing silences, but it's
@@ -50,6 +53,8 @@ screen space, the silence will also be moved to the footer.
Silence modal allows to create new silences and manage all silences already
present in Alertmanager.
Silence ACL rules can be used to control silence creation and editing, see
[ACLs](/docs/ACLs.md) docs for more details.
![Silence browser](/docs/img/silenceBrowser.png)
@@ -140,6 +145,9 @@ to modify data in Alertmanager instance, then please ensure that:
for all alertmanager instances, this options will disable any UI elements that
could trigger updates (like silence management)
To restrict some users from creating silences or enforce some matcher rules use
[silence ACL rules](/docs/ACLs.md). This feature requires `proxy` to be enabled.
## Metrics
karma process metrics are accessible under `/metrics` path by default.
+230
View File
@@ -0,0 +1,230 @@
package main
import (
"fmt"
"regexp"
"github.com/prymitive/karma/internal/alertmanager"
"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/slices"
)
const (
aclActionRequireMatcher = "requireMatcher"
aclActionBlock = "block"
aclActionAllow = "allow"
)
var (
allACLActions = []string{aclActionAllow, aclActionBlock, aclActionRequireMatcher}
)
type silenceFilter struct {
Name string
NameRegex *regexp.Regexp
Value string
ValueRegex *regexp.Regexp
IsRegex bool
}
func (sf *silenceFilter) isMatch(silence *models.Silence) bool {
for _, m := range silence.Matchers {
var nameMatch bool
if sf.Name != "" && sf.Name == m.Name {
nameMatch = true
} else if sf.NameRegex != nil && sf.NameRegex.MatchString(m.Name) {
nameMatch = true
}
var valueMatch bool
if sf.Value != "" && sf.Value == m.Value {
valueMatch = true
} else if sf.ValueRegex != nil && sf.ValueRegex.MatchString(m.Value) {
valueMatch = true
}
if nameMatch && valueMatch && sf.IsRegex == m.IsRegex {
return true
}
}
return false
}
type silenceMatcher struct {
Name string
Value string
IsRegex bool
}
type aclMatchers struct {
Required []silenceMatcher
}
type silenceACLScope struct {
Groups []string
Alertmanagers []string
Filters []silenceFilter
}
type silenceACL struct {
Action string
Reason string
Scope silenceACLScope
Matchers aclMatchers
}
func (acl *silenceACL) isAllowed(amName string, silence *models.Silence, username string) (bool, error) {
groups := userGroups(username)
groupMatch := len(acl.Scope.Groups) == 0
for _, aclGroup := range acl.Scope.Groups {
if slices.StringInSlice(groups, aclGroup) {
groupMatch = true
break
}
}
amMatch := len(acl.Scope.Alertmanagers) == 0
for _, aclAM := range acl.Scope.Alertmanagers {
if amName == aclAM {
amMatch = true
break
}
}
filterMatch := len(acl.Scope.Filters) == 0
for _, aclFilter := range acl.Scope.Filters {
filterMatch = aclFilter.isMatch(silence)
}
if groupMatch && amMatch && filterMatch {
switch acl.Action {
case aclActionAllow:
return true, nil
case aclActionBlock:
return false, fmt.Errorf("silence blocked by ACL rule: %s", acl.Reason)
case aclActionRequireMatcher:
for _, aclM := range acl.Matchers.Required {
var wasFound bool
for _, m := range silence.Matchers {
if m.Name == aclM.Name && m.Value == aclM.Value && m.IsRegex == aclM.IsRegex {
wasFound = true
break
}
}
if !wasFound {
return false, fmt.Errorf("silence blocked by ACL rule: %s", acl.Reason)
}
}
}
}
return false, nil
}
func newSilenceACLFromConfig(cfg config.SilenceACLRule) (*silenceACL, error) {
acl := silenceACL{
Action: cfg.Action,
Reason: cfg.Reason,
Scope: silenceACLScope{
Groups: []string{},
Alertmanagers: []string{},
Filters: []silenceFilter{},
},
Matchers: aclMatchers{},
}
if !slices.StringInSlice(allACLActions, acl.Action) {
return nil, fmt.Errorf("silence ACL rule requires 'action' to be one of %v, got %q", allACLActions, acl.Action)
}
if acl.Reason == "" {
return nil, fmt.Errorf("silence ACL rule requires 'reason' to be set")
}
for _, groupName := range cfg.Scope.Groups {
var wasFound bool
for _, authGroup := range config.Config.Authorization.Groups {
if authGroup.Name == groupName {
wasFound = true
break
}
}
if !wasFound {
return nil, fmt.Errorf("invalid silence ACL rule, no group with name %q found in authorization.groups configuration", groupName)
}
acl.Scope.Groups = append(acl.Scope.Groups, groupName)
}
for _, amName := range cfg.Scope.Alertmanagers {
am := alertmanager.GetAlertmanagerByName(amName)
if am == nil {
return nil, fmt.Errorf("invalid ACL rule, no alertmanager with name %q found", amName)
}
acl.Scope.Alertmanagers = append(acl.Scope.Alertmanagers, am.Name)
}
for _, filter := range cfg.Scope.Filters {
if filter.Name == "" && filter.NameRegex == "" {
return nil, fmt.Errorf("silence ACL rule filter requires 'name' or 'name_re' to be set")
}
if filter.Name != "" && filter.NameRegex != "" {
return nil, fmt.Errorf("silence ACL rule filter can only have 'name' or 'name_re' set, not both")
}
if filter.Value == "" && filter.ValueRegex == "" {
return nil, fmt.Errorf("silence ACL rule filter requires 'value' or 'value_re' to be set")
}
if filter.Value != "" && filter.ValueRegex != "" {
return nil, fmt.Errorf("silence ACL rule filter can only have 'value' or 'value_re' set, not both")
}
f := silenceFilter{
Name: filter.Name,
Value: filter.Value,
IsRegex: filter.IsRegex,
}
if filter.NameRegex != "" {
re, err := regexp.Compile(filter.NameRegex)
if err != nil {
return nil, fmt.Errorf("invalid ACL rule, failed to parse name_re %q: %s", filter.NameRegex, err)
}
f.NameRegex = re
}
if filter.ValueRegex != "" {
re, err := regexp.Compile(filter.ValueRegex)
if err != nil {
return nil, fmt.Errorf("invalid ACL rule, failed to parse value_re %q: %s", filter.ValueRegex, err)
}
f.ValueRegex = re
}
acl.Scope.Filters = append(acl.Scope.Filters, f)
}
if acl.Action == aclActionRequireMatcher {
for _, matcherConfig := range cfg.Matchers.Required {
if matcherConfig.Name == "" {
return nil, fmt.Errorf("silence ACL rule matcher requires 'name' to be set")
}
if matcherConfig.Value == "" {
return nil, fmt.Errorf("silence ACL rule matcher requires 'value' to be set")
}
m := silenceMatcher{
Name: matcherConfig.Name,
Value: matcherConfig.Value,
IsRegex: matcherConfig.IsRegex,
}
acl.Matchers.Required = append(acl.Matchers.Required, m)
}
}
return &acl, nil
}
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"github.com/prymitive/karma/internal/config"
"github.com/prymitive/karma/internal/slices"
)
func userGroups(username string) []string {
groups := []string{}
for _, authGroup := range config.Config.Authorization.Groups {
if slices.StringInSlice(authGroup.Members, username) {
groups = append(groups, authGroup.Name)
}
}
return groups
}
+19
View File
@@ -50,6 +50,8 @@ var (
staticSrcFileSystem = newBinaryFileSystem("ui/src")
protectedEndpoints *gin.RouterGroup
silenceACLs = []*silenceACL{}
)
func getViewURL(sub string) string {
@@ -291,6 +293,23 @@ func mainSetup(errorHandling pflag.ErrorHandling) (*gin.Engine, error) {
return nil, fmt.Errorf("No valid Alertmanager URIs defined")
}
if config.Config.Authorization.ACL.Silences != "" {
log.Infof("Reading silence ACL config file %s", config.Config.Authorization.ACL.Silences)
aclConfig, err := config.ReadSilenceACLConfig(config.Config.Authorization.ACL.Silences)
if err != nil {
return nil, err
}
for i, cfg := range aclConfig.Rules {
acl, err := newSilenceACLFromConfig(cfg)
if err != nil {
return nil, fmt.Errorf("Invalid silence ACL rule at position %d: %s", i, err)
}
silenceACLs = append(silenceACLs, acl)
}
log.Infof("Parsed %d ACL rule(s)", len(silenceACLs))
}
if *validateConfig {
log.Info("Configuration is valid")
return nil, nil
+45 -21
View File
@@ -75,38 +75,62 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re
func handlePostRequest(alertmanager *alertmanager.Alertmanager, h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
log.Debugf("[%s] Proxy request %s", alertmanager.Name, c.Request.RequestURI)
body, err := ioutil.ReadAll(c.Request.Body)
c.Request.Body.Close()
if err != nil {
log.Errorf("[%s] proxy request '%s %s' body close failed: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
ver := alertmanager.Version()
if ver == "" {
ver = "999.0"
}
m, err := mapper.GetSilenceMapper(ver)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' error: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
silence, err := m.Unmarshal(body)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' failed to unmarshal silence body: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
for i, acl := range silenceACLs {
username := c.GetString(gin.AuthUserKey)
isAllowed, err := acl.isAllowed(alertmanager.Name, silence, username)
log.Debugf("ACL %d: isAllowed=%v err=%v", i, isAllowed, err)
if err != nil {
log.Warningf("[%s] proxy request '%s %s' was blocked by ACL rule: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.String(http.StatusBadRequest, err.Error())
return
}
if isAllowed {
break
}
}
if config.Config.Authentication.Enabled {
body, err := ioutil.ReadAll(c.Request.Body)
c.Request.Body.Close()
if err != nil {
log.Errorf("[%s] proxy request '%s %s' body close failed: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
ver := alertmanager.Version()
if ver == "" {
ver = "999.0"
}
username := c.MustGet(gin.AuthUserKey).(string)
m, err := mapper.GetSilenceMapper(ver)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' error: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
newBody, err := m.RewriteUsername(body, username)
if err != nil {
log.Errorf("[%s] proxy request '%s %s' silence body rewrite error: %s", alertmanager.Name, c.Request.Method, c.Request.RequestURI, err)
c.AbortWithStatus(http.StatusInternalServerError)
c.String(http.StatusInternalServerError, err.Error())
return
}
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(newBody))
c.Request.ContentLength = int64(len(newBody))
c.Request.Header.Set("Content-Length", fmt.Sprintf("%d", c.Request.ContentLength))
} else {
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
h.ServeHTTP(c.Writer, c.Request)
+502 -3
View File
@@ -6,6 +6,8 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
@@ -93,6 +95,14 @@ var proxyTests = []proxyTest{
}
func TestProxy(t *testing.T) {
dummySilence := `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [{ "isRegex": false, "name": "alertname", "value": "Fake Alert" }]
}`
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"dummy",
@@ -116,7 +126,7 @@ func TestProxy(t *testing.T) {
if testCase.upstreamURI != "" {
httpmock.RegisterResponder(testCase.method, testCase.upstreamURI, httpmock.NewStringResponder(testCase.code, testCase.response))
}
req := httptest.NewRequest(testCase.method, testCase.localPath, nil)
req := httptest.NewRequest(testCase.method, testCase.localPath, strings.NewReader(dummySilence))
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != testCase.code {
@@ -182,6 +192,14 @@ var proxyHeaderTests = []proxyHeaderTest{
}
func TestProxyHeaders(t *testing.T) {
dummySilence := `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [{ "isRegex": false, "name": "alertname", "value": "Fake Alert" }]
}`
httpmock.Activate()
defer httpmock.DeactivateAndReset()
@@ -224,7 +242,7 @@ func TestProxyHeaders(t *testing.T) {
return httpmock.NewStringResponse(testCase.code, "ok"), nil
})
req := httptest.NewRequest(testCase.method, testCase.localPath, nil)
req := httptest.NewRequest(testCase.method, testCase.localPath, strings.NewReader(dummySilence))
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != testCase.code {
@@ -277,6 +295,14 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
},
}
dummySilence := `{
"comment": "comment",
"createdBy": "username",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [{ "isRegex": false, "name": "alertname", "value": "Fake Alert" }]
}`
for _, testCase := range proxyTests {
t.Run(fmt.Sprintf("prefix=%s|uri=%s", testCase.listenPrefix, testCase.alertmanagerURI), func(t *testing.T) {
httpmock.Reset()
@@ -301,7 +327,7 @@ func TestProxyToSubURIAlertmanager(t *testing.T) {
return httpmock.NewStringResponse(200, "ok"), nil
})
req := httptest.NewRequest("POST", testCase.requestURI, nil)
req := httptest.NewRequest("POST", testCase.requestURI, strings.NewReader(dummySilence))
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != 200 {
@@ -536,3 +562,476 @@ func TestProxyUserRewrite(t *testing.T) {
})
}
}
func TestProxySilenceACL(t *testing.T) {
type proxyTest struct {
name string
authGroups map[string][]string
silenceACLs []*silenceACL
requestUsername string
frontednRequestBody string
responseCode int
}
defaultBody := `{
"comment": "comment",
"createdBy": "alice",
"startsAt": "2000-02-01T00:00:00.000Z",
"endsAt": "2000-02-01T00:02:03.000Z",
"matchers": [
{ "isRegex": false, "name": "alertname", "value": "Fake Alert" },
{ "isRegex": true, "name": "foo", "value": "(bar|baz)" }
]}`
proxyTests := []proxyTest{
{
name: "no config, allowed",
authGroups: map[string][]string{},
silenceACLs: []*silenceACL{},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "matcher required, no match, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Groups: []string{"foo"},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "matcher required, alertmanager mismatch, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Groups: []string{"admins"},
Alertmanagers: []string{"proxyFoo"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "matcher required, filter mismatch, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Filters: []silenceFilter{
{Name: "foo", Value: "bar"},
},
Groups: []string{"admins"},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "matcher required, match, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Groups: []string{"admins"},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "matcher required, all groups, match, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Groups: []string{},
Alertmanagers: []string{"foo", "proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "matcher required, filter match, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require cluster=dev",
Scope: silenceACLScope{
Filters: []silenceFilter{
{Name: "alertname", Value: "Fake Alert"},
},
Groups: []string{"admins"},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "cluster",
Value: "dev",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "matcher required, filter name regex match, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require foo=bar",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*")},
},
Groups: []string{},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "foo",
Value: "bar",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "matcher required, filter value regex match, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require foo=bar",
Scope: silenceACLScope{
Filters: []silenceFilter{
{Name: "alertname", ValueRegex: regexp.MustCompile(".*")},
},
Groups: []string{},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "foo",
Value: "bar",
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "matcher required, match, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
},
silenceACLs: []*silenceACL{
{
Action: "requireMatcher",
Reason: "require foo=~(bar|baz)",
Scope: silenceACLScope{
Groups: []string{"admins"},
Alertmanagers: []string{"proxyACL"},
},
Matchers: aclMatchers{
Required: []silenceMatcher{
{
Name: "alertname",
Value: "Fake Alert",
},
{
Name: "foo",
Value: "(bar|baz)",
IsRegex: true,
},
},
},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "block all regex silences",
silenceACLs: []*silenceACL{
{
Action: "block",
Reason: "block all regex silences",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
},
Groups: []string{},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 400,
},
{
name: "block all regex silences except for admins, admin user, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
"users": {"alice"},
},
silenceACLs: []*silenceACL{
{
Action: "allow",
Reason: "block all regex silences",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
},
Groups: []string{"admins"},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
{
Action: "block",
Reason: "block all regex silences",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
},
Groups: []string{},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "block all regex silences on alertname=Block, allowed",
authGroups: map[string][]string{
"admins": {"bob"},
"users": {"alice"},
},
silenceACLs: []*silenceACL{
{
Action: "block",
Reason: "block alertname=Block",
Scope: silenceACLScope{
Filters: []silenceFilter{
{Name: "alertname", Value: "Block"},
},
Groups: []string{},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
},
requestUsername: "bob",
frontednRequestBody: defaultBody,
responseCode: 200,
},
{
name: "block all regex silences except for admins, non-admin user, blocked",
authGroups: map[string][]string{
"admins": {"bob"},
"users": {"alice"},
},
silenceACLs: []*silenceACL{
{
Action: "allow",
Reason: "block all regex silences",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
},
Groups: []string{"admins"},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
{
Action: "block",
Reason: "block all regex silences",
Scope: silenceACLScope{
Filters: []silenceFilter{
{NameRegex: regexp.MustCompile(".*"), ValueRegex: regexp.MustCompile(".*"), IsRegex: true},
},
Groups: []string{},
Alertmanagers: []string{},
},
Matchers: aclMatchers{},
},
},
requestUsername: "alice",
frontednRequestBody: defaultBody,
responseCode: 400,
},
}
for _, testCase := range proxyTests {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
log.SetLevel(log.FatalLevel)
t.Run(testCase.name, func(t *testing.T) {
for _, version := range mock.ListAllMocks() {
t.Logf("Testing alerts using mock files from Alertmanager %s", version)
config.Config.Listen.Prefix = "/"
config.Config.Authentication.Header.Name = "X-User"
config.Config.Authentication.Header.ValueRegex = "(.+)"
config.Config.Authorization.Groups = []config.AuthorizationGroup{}
for groupName, members := range testCase.authGroups {
g := config.AuthorizationGroup{Name: groupName, Members: members}
config.Config.Authorization.Groups = append(config.Config.Authorization.Groups, g)
}
silenceACLs = testCase.silenceACLs
r := ginTestEngine()
am, err := alertmanager.NewAlertmanager(
"proxyACL",
"http://localhost",
alertmanager.WithRequestTimeout(time.Second*5),
alertmanager.WithProxy(true),
)
if err != nil {
t.Error(err)
}
err = setupRouterProxyHandlers(r, am)
if err != nil {
t.Errorf("Failed to setup proxy for Alertmanager %s: %s", am.Name, err)
}
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
httpmock.Reset()
mock.RegisterURL("http://localhost/metrics", version, "metrics")
mock.RegisterURL("http://localhost/api/v2/status", version, "api/v2/status")
mock.RegisterURL("http://localhost/api/v2/silences", version, "api/v2/silences")
mock.RegisterURL("http://localhost/api/v2/alerts/groups", version, "api/v2/alerts/groups")
_ = am.Pull()
httpmock.RegisterResponder("POST", "http://localhost/api/v2/silences", func(req *http.Request) (*http.Response, error) {
body, _ := ioutil.ReadAll(req.Body)
return httpmock.NewBytesResponse(200, body), nil
})
req := httptest.NewRequest("POST", "/proxy/alertmanager/proxyACL/api/v2/silences", ioutil.NopCloser(bytes.NewBufferString(testCase.frontednRequestBody)))
req.Header.Set("X-User", testCase.requestUsername)
resp := newCloseNotifyingRecorder()
r.ServeHTTP(resp, req)
if resp.Code != testCase.responseCode {
t.Errorf("Got response code %d instead of %d", resp.Code, testCase.responseCode)
}
}
})
}
}
@@ -0,0 +1,54 @@
# Config is valid with example silence ACL rules
karma.bin-should-work --log.format=text --log.config=false --check-config
! stdout .
stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
! stderr 'level=error'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: requireMatcher
reason: require cluster=~dev|prod for admins
scope:
filters:
- name: cluster
value_re: .+
alertmanagers:
- default
groups:
- admins
matchers:
required:
- name: cluster
value: dev|prod
isRegex: true
- action: block
reason: block cluster=prod for users
scope:
filters:
- name: cluster
value: prod
groups:
- users
@@ -0,0 +1,30 @@
# Raises an error if --authorization.acl points to a file that cannot be parsed
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Failed to parse silence ACL configuration file \\"acl.yaml\\": yaml: unmarshal errors:.*"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
This Is Not yaml
@@ -0,0 +1,27 @@
# Raises an error if --authorization.acl points to a missing file
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Failed to load silence ACL configuration file \\"acl.yaml\\": open acl.yaml: no such file or directory"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
@@ -0,0 +1,32 @@
# Raises an error if silence ACL rule uses invalid 'action' value
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule requires ''action'' to be one of \[allow block requireMatcher\], got \\"foo\\""'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: foo
reason: invalid action
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses invalid 'alertmanagers' value
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, no alertmanager with name \\"unknown\\" found"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: invalid group
scope:
alertmanagers:
- unknown
@@ -0,0 +1,36 @@
# Raises an error if silence ACL rule uses filter with invalid name_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, failed to parse name_re \\"cluster\*\*\*\\": error parsing regexp: invalid nested repetition operator: `\*\*`"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing name
scope:
filters:
- name_re: cluster***
value: prod
@@ -0,0 +1,36 @@
# Raises an error if silence ACL rule uses filter with invalid value_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: invalid ACL rule, failed to parse value_re \\"prod\*\*\*\\": error parsing regexp: invalid nested repetition operator: `\*\*`"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing name
scope:
filters:
- name_re: cluster[0-9]*
value_re: prod***
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses invalid 'groups' value
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: invalid silence ACL rule, no group with name \\"unknown\\" found in authorization.groups configuration"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: invalid group
scope:
groups:
- unknown
@@ -0,0 +1,37 @@
# Raises an error if silence ACL rule uses filter with both name and name_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter can only have ''name'' or ''name_re'' set, not both"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing name
scope:
filters:
- name: cluster
name_re: cluster
value: prod
@@ -0,0 +1,37 @@
# Raises an error if silence ACL rule uses filter with both value and value_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter can only have ''value'' or ''value_re'' set, not both"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing name
scope:
filters:
- name: cluster
value: prod
value_re: prod
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses filter with missing name or name_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter requires ''name'' or ''name_re'' to be set"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing name
scope:
filters:
- value: prod
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses filter with missing value or value_re
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule filter requires ''value'' or ''value_re'' to be set"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
reason: missing value
scope:
filters:
- name: cluster
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses matcher without name
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule matcher requires ''name'' to be set"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: requireMatcher
reason: missing name
matchers:
required:
- value: prod
@@ -0,0 +1,35 @@
# Raises an error if silence ACL rule uses matcher without value
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule matcher requires ''value'' to be set"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: requireMatcher
reason: missing value
matchers:
required:
- name: cluster
@@ -0,0 +1,31 @@
# Raises an error if silence ACL rule is missing 'reason'
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
! stderr 'msg="Configuration is valid"'
stderr 'msg="Reading silence ACL config file acl.yaml"'
stderr 'msg="Invalid silence ACL rule at position 0: silence ACL rule requires ''reason'' to be set"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
acl:
silences: acl.yaml
alertmanager:
servers:
- name: default
uri: https://localhost:9093
-- acl.yaml --
rules:
- action: block
@@ -0,0 +1,17 @@
# Raises an error if authorization group is missing name
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
stderr 'msg="''members'' is required for every authorization group"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
alertmanager:
servers:
- name: default
uri: https://localhost:9093
@@ -0,0 +1,22 @@
# Raises an error if authorization group is missing name
karma.bin-should-fail --log.format=text --log.config=false --check-config
! stdout .
stderr 'msg="''name'' is required for every authorization group"'
-- karma.yaml --
authentication:
header:
name: "X-User"
value_re: "(.+)"
authorization:
groups:
- name: admins
members:
- alice
- bob
- members:
- john
alertmanager:
servers:
- name: default
uri: https://localhost:9093
@@ -75,6 +75,10 @@ level=info msg=" name: \"\""
level=info msg=" value_re: \"\""
level=info msg=" basicAuth:"
level=info msg=" users: []"
level=info msg="authorization:"
level=info msg=" groups: []"
level=info msg=" acl:"
level=info msg=" silences: \"\""
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"
@@ -11,6 +11,12 @@ authentication:
password: 1234
- username: string
password: '1234'
authorization:
groups:
- name: admins
members:
- alice
- bob
alertmanager:
interval: 10s
servers:
@@ -244,6 +250,14 @@ level=info msg=" - username: number"
level=info msg=" password: '***'"
level=info msg=" - username: string"
level=info msg=" password: '***'"
level=info msg="authorization:"
level=info msg=" groups:"
level=info msg=" - name: admins"
level=info msg=" members:"
level=info msg=" - alice"
level=info msg=" - bob"
level=info msg=" acl:"
level=info msg=" silences: \"\""
level=info msg="alertmanager:"
level=info msg=" interval: 10s"
level=info msg=" servers:"
@@ -357,18 +371,14 @@ level=info msg=" - strip2"
level=info msg=" color:"
level=info msg=" custom:"
level=info msg=" region:"
level=info msg=" - value: \"\""
level=info msg=" value_re: .*"
level=info msg=" - value_re: .*"
level=info msg=" color: '#736598'"
level=info msg=" severity:"
level=info msg=" - value: info"
level=info msg=" value_re: \"\""
level=info msg=" color: '#87c4e0'"
level=info msg=" - value: warning"
level=info msg=" value_re: \"\""
level=info msg=" color: '#ffae42'"
level=info msg=" - value: critical"
level=info msg=" value_re: \"\""
level=info msg=" color: '#ff220c'"
level=info msg=" static:"
level=info msg=" - job"
@@ -7,18 +7,14 @@ stderr 'msg=" strip: \[\]"'
stderr 'msg=" color:"'
stderr 'msg=" custom:"'
stderr 'msg=" region:"'
stderr 'msg=" - value: \\"\\""'
stderr 'msg=" value_re: .*"'
stderr 'msg=" - value_re: .*"'
stderr 'msg=" color: ''#736598''"'
stderr 'msg=" severity:"'
stderr 'msg=" - value: P3"'
stderr 'msg=" value_re: \\"\\""'
stderr 'msg=" color: ''#87c4e0''"'
stderr 'msg=" - value: P2"'
stderr 'msg=" value_re: \\"\\""'
stderr 'msg=" color: ''#ffae42''"'
stderr 'msg=" - value: P1"'
stderr 'msg=" value_re: \\"\\""'
stderr 'msg=" color: ''#ff220c''"'
! stderr 'level=error'
+1
View File
@@ -29,6 +29,7 @@ COPY demo/alertmanager.yaml /etc/alertmanager.yaml
COPY demo/generator.py /generator.py
COPY --from=go-builder /src/karma /karma
COPY demo/karma.yaml /etc/karma.yaml
COPY demo/acls.yaml /etc/acls.yaml
COPY demo/custom.js /custom.js
RUN adduser -D karma
USER karma
+7
View File
@@ -0,0 +1,7 @@
rules:
- action: block
reason: dev cluster cannot be silenced
scope:
filters:
- name: cluster
value: dev
+3
View File
@@ -1,3 +1,6 @@
authorization:
acl:
silences: /etc/acls.yaml
alertmanager:
interval: 10s
servers:
+293
View File
@@ -0,0 +1,293 @@
# Silence Access Control Lists
## Intro
Karma provides ability to setup ACLs for silences created by users. This can be
used to limit what kind of silences each user is allowed to create, which can
help to avoid, for example, `Team A` accidentally silencing alerts for `Team B`,
or blocking engineering team from creating any silence at all, leaving that
ability only to the sys admin / SRE team.
Example Alertmanager silence:
```YAML
{
"matchers": [
{
"name": "alertname",
"value": "Test Alert",
"isRegex": false
},
{
"name": "cluster",
"value": "prod",
"isRegex": false
},
{
"name": "instance",
"value": "server1",
"isRegex": false
}
],
"startsAt": "2020-03-09T20:11:00.000Z",
"endsAt": "2020-03-09T21:11:00.000Z",
"createdBy": "me@example.com",
"comment": "Silence Test Alert on server1"
}
```
It would be applied to all alerts with name `Test Alert` and where label
`cluster` is equal to `prod`.
An ACL rule could be used to restrict silence creation based on matched labels,
so for example only selected users would be allowed to silence this specific
alert.
## Requirements
For ACLs to work a few configuring options are required:
- `authorization:acl:silences` is set with acl config file path, there's no
support for configuring ACLs via environment variables
- `proxy` must be enabled in karma configuration for each Alertmanager server
where ACLs will be applied. `proxy: true` tells karma UI to proxy all silence
operation requests (creating, editing & deleting silences) via karma backend.
Since ACLs are applied in the proxy code it needs to be enabled to take
effect. It is recommended to block ability for users to connect directly to
Alertmanager servers to avoid bypassing ACL rules (alertmanager accepts all
silences).
Optional configuration:
- `authentication` if configured user based matching of ACLs can be used,
Header authentication with a frontend authentication proxy that passes
usernames via header is recommended. This can be done with nginx configured
as an authentication reverse proxy or proxy services like Cloudflare Access.
- `authorization:groups` must be configured if group polices will be used.
This configuration maps users into groups, allowing to use those groups in
ACL rules.
## Regex silences
Alertmanager silences allow to use regex rules which can make it tricky to apply
ACLs to those silences.
Silence example using regex:
```YAML
{
"matchers": [
{
"name": "alertname",
"value": "Test Alert",
"isRegex": false
},
{
"name": "cluster",
"value": "staging|prod",
"isRegex": true
}
],
"startsAt": "2020-03-09T20:11:00.000Z",
"endsAt": "2020-03-09T21:11:00.000Z",
"createdBy": "me@example.com",
"comment": "Silence Test Alert in staging & prod cluster"
}
```
The difference compared to the previous example is that the `cluster` label
is now matched using `staging|prod` regex, so any alert with `cluster` label
equal to `staging` or `prod` will be matched.
This is a simple example, regexes allow to create very complex matching
rules.
The effect on ACL rules can be illustrated with this example: let's say we have
a group that should never be allowed to create any silence for `prod` cluster,
so a silence like the one below should be blocked:
```YAML
{
"matchers": [
{
"name": "alertname",
"value": "Test Alert",
"isRegex": false
},
{
"name": "cluster",
"value": "prod",
"isRegex": false
}
],
"startsAt": "2020-03-09T20:11:00.000Z",
"endsAt": "2020-03-09T21:11:00.000Z",
"createdBy": "me@example.com",
"comment": "Silence Test Alert in prod cluster"
}
```
But if we would create an ACL rule that simply blocks silences with matcher:
```YAML
{
"name": "cluster",
"value": "prod",
"isRegex": false
}
```
then any user could bypass that with a regex matcher like:
```YAML
{
"name": "cluster",
"value": "pro[d]",
"isRegex": true
}
```
Because of that it is *highly recommended* to block regex silences, which can
be done with an ACL rule. Since rules are evaluated in the order they are
listed in the config file it is best to set this as the very first rule.
See examples below to learn how to block regex silences.
## Configuration syntax
- `rules` - list of silence ACL rules, rules are evaluated in the order they
appear in this list
Rule syntax:
```YAML
action: string
reason: string
scope:
groups: list of strings
alertmanagers: list of strings
filters: list of filters
matchers:
required: list of silence matchers
```
- `action` - this is the name of the action to take if given ACL matches all
the conditions.
Valid actions are:
- `allow` - skip all other ACLs and allow silence to be created
- `block` - skip all other ACLs and block silences from being created
- `requireMatcher` - block silence if it doesn't have all of matchers
specified in `matchers:required`
- `reason` - message that will be returned to the user if this ACL blocks any
silence
- `scope` - this section contains all conditions required to apply given ACL
rule to specific silence, if it's skipped then ACL rule will be applied to
all users and every silence
- `scope:groups` - list of group names from `authorization:groups`, if no group
is specified here then this ACL will be applied to all users
- `scope:alertmanagers` - list of alertmanager names as specified in
`alertmanager:servers`, if no name is specified here then this ACL will be
applied to silences for all alertmanager servers
- `scope:filters` - list of matcher filters evaluated when checking if this ACL
should be applied to given silence. Those filters can be used to enforce
ACL rules only to some silences and are compared against silence matchers.
Syntax:
```YAML
name: string
name_re: regex
value: string
value_re: regex
isRegex: bool
```
Every rule must have `name` or `name_re` AND `value` or `value_re`, default
value for `isRegex` is `false`.
Filter works by comparing `name` and `name_re` with silence matcher `name`,
`value` and `value_re` with silence matcher `value` and `isRegex` on the
filter with `isRegex` on silence matcher. See examples below.
- `matchers:required` - list of additional matchers that must be part of the
silence if it matches groups, alertmanagers and filters. This is only used
if `action` is set to `requireMatcher`.
Syntax for each matcher:
```YAML
name: string
value: string or regex
isRegex: bool
```
## Examples
### Block silences using regex matchers
This rule will match all silences with any matcher using regexes
(`isRegex: true` on the matcher) and block it.
```YAML
rules:
- action: block
reason: all regex silences are blocked, use only concrete label names and values
scope:
filters:
- name_re: .+
value_re: .+
isRegex: true
```
### Allow group to create any silence
```YAML
rules:
- action: allow
reason: admins are allowed
scope:
groups:
- admins
```
### Allow only admins group to create silences with cluster=prod
First allow all members of the `admins` group to create any silence, then block
silences with `cluster=prod`. Since ACL rules are evaluated in the order
specified and first `allow` or `block` rule stops other rule processing this
will allow `admins` to create `cluster=prod` silences while everyone else is
blocked from it. Disabling regex rules as first steps prevents users from
bypassing those ACLs with regex silences.
```YAML
rules:
- action: block
reason: all regex silences are blocked, use only concrete label names and values
scope:
filters:
- name_re: .+
value_re: .+
isRegex: true
- action: allow
reason: admins are allowed
scope:
groups:
- admins
- action: block
reason: only admins can create silences with cluster=prod
scope:
filters:
- name: cluster
value: prod
```
### Require postgresAdmins group to always specify db=postgres in silences
Block `postgresAdmins` members from creating silences unless they add
`db=postgres` to the list of matchers.
```YAML
rules:
- action: requireMatcher
reason: postgres admins must add db=postgres to all silences
scope:
groups:
- postgresAdmins
matchers:
required:
- name: db
value: postgres
```
+47 -6
View File
@@ -38,6 +38,7 @@ There are currently two supported authentication methods:
Only one method can be enabled in the config.
Enabling authentication will also force silences to be created with usernames
passed from credentials.
Syntax:
```YAML
authentication:
@@ -97,6 +98,52 @@ authentication:
value_re: ^(.+)$
```
### Authorization
`authorization` section allows to configure authorization groups used in
silence ACL rules.
Syntax:
```YAML
authorization:
acl:
silences: string
groups:
- name: string
members: list of strings
```
- `acl:silences` - path to silence ACL configuration file, see
[ACLs](/docs/ACLs.md) for details
- `groups` - list of group definitons, each group must have a `name` and
`members` list. `name` will be used in silence ACL rules, `members` list
should contain list of user names as passed from authentication layer.
Example with two groups using basic auth users and silences ACL config:
```YAML
authentication:
basicAuth:
users:
- username: alice
password: secret
- username: bob
password: secret
- username: john
password: secret
authorization:
acls:
silences: /etc/karma/acls.yaml
groups:
- name: admins
members:
- alice
- bob
- name: users
members:
- john
```
### Alertmanagers
`alertmanager` section allows setting Alertmanager servers that should be
@@ -824,12 +871,6 @@ silences:
uriTemplate: https://jira.example.com/browse/$1
```
Defaults:
```YAML
jira: []
```
### Receivers
`receivers` section allows configuring how alerts from different receivers are
+59
View File
@@ -0,0 +1,59 @@
package config
import (
"fmt"
"io/ioutil"
yaml "gopkg.in/yaml.v2"
)
type SilenceMatcher struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
IsRegex bool `yaml:"isRegex"`
}
type SilenceACLMatchersConfig struct {
Required []SilenceMatcher `yaml:"required"`
}
type SilenceFilters struct {
Name string `yaml:"name,omitempty"`
NameRegex string `yaml:"name_re,omitempty"`
Value string `yaml:"value,omitempty"`
ValueRegex string `yaml:"value_re,omitempty"`
IsRegex bool `yaml:"isRegex"`
}
type SilenceACLRuleScope struct {
Groups []string
Alertmanagers []string
Filters []SilenceFilters
}
type SilenceACLRule struct {
Action string
Reason string
Scope SilenceACLRuleScope
Matchers SilenceACLMatchersConfig
}
type silencesACLSchema struct {
Rules []SilenceACLRule
}
func ReadSilenceACLConfig(path string) (*silencesACLSchema, error) {
cfg := silencesACLSchema{}
f, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("Failed to load silence ACL configuration file %q: %v", path, err)
}
err = yaml.Unmarshal(f, &cfg)
if err != nil {
return nil, fmt.Errorf("Failed to parse silence ACL configuration file %q: %v", path, err)
}
return &cfg, nil
}
+11
View File
@@ -58,6 +58,8 @@ func SetupFlags(f *pflag.FlagSet) {
f.String("alertAcknowledgement.author", "karma", "Default silence author when acknowledging alerts with short lived silences")
f.String("alertAcknowledgement.commentPrefix", "ACK!", "Comment prefix used when acknowledging alerts with short lived silences")
f.String("authorization.acl.silences", "", "Path to silence ACL config file")
f.Bool(
"annotations.default.hidden", false,
"Hide all annotations by default unless explicitly listed in the 'visible' list")
@@ -313,6 +315,15 @@ func (config *configSchema) Read(flags *pflag.FlagSet) string {
}
}
for _, authGroup := range config.Authorization.Groups {
if authGroup.Name == "" {
log.Fatalf("'name' is required for every authorization group")
}
if len(authGroup.Members) == 0 {
log.Fatalf("'members' is required for every authorization group")
}
}
for labelName, customColors := range config.Labels.Color.Custom {
for i, customColor := range customColors {
if customColor.Value == "" && customColor.ValueRegex == "" {
+4
View File
@@ -26,6 +26,10 @@ func testReadConfig(t *testing.T) {
value_re: ""
basicAuth:
users: []
authorization:
groups: []
acl:
silences: ""
alertmanager:
interval: 1s
servers:
+13 -2
View File
@@ -32,8 +32,8 @@ type LinkDetectRules struct {
}
type CustomLabelColor struct {
Value string `yaml:"value"`
ValueRegex string `yaml:"value_re" koanf:"value_re"`
Value string `yaml:"value,omitempty"`
ValueRegex string `yaml:"value_re,omitempty" koanf:"value_re"`
CompiledRegex *regexp.Regexp `yaml:"-"`
Color string `yaml:"color"`
}
@@ -45,6 +45,11 @@ type AuthenticationUser struct {
Password string
}
type AuthorizationGroup struct {
Name string
Members []string
}
type configSchema struct {
Authentication struct {
Enabled bool `yaml:"-" koanf:"-"`
@@ -56,6 +61,12 @@ type configSchema struct {
Users []AuthenticationUser
} `yaml:"basicAuth" koanf:"basicAuth"`
}
Authorization struct {
Groups []AuthorizationGroup
ACL struct {
Silences string
} `yaml:"acl" koanf:"acl"`
}
Alertmanager struct {
Interval time.Duration
Servers []AlertmanagerConfig
+1
View File
@@ -31,6 +31,7 @@ type SilenceMapper interface {
Mapper
Collect(string, map[string]string, time.Duration, http.RoundTripper) ([]models.Silence, error)
RewriteUsername([]byte, string) ([]byte, error)
Unmarshal([]byte) (*models.Silence, error)
}
// StatusMapper handles mapping Alertmanager status information containing cluster config
+27
View File
@@ -130,3 +130,30 @@ func rewriteSilenceUsername(body []byte, username string) ([]byte, error) {
s.CreatedBy = &username
return s.MarshalBinary()
}
func unmarshal(body []byte) (*models.Silence, error) {
s := ammodels.PostableSilence{}
err := s.UnmarshalBinary(body)
if err != nil {
return nil, err
}
us := models.Silence{
ID: s.ID,
StartsAt: time.Time(*s.StartsAt),
EndsAt: time.Time(*s.EndsAt),
CreatedBy: *s.CreatedBy,
Comment: *s.Comment,
}
for _, m := range s.Matchers {
sm := models.SilenceMatcher{
Name: *m.Name,
Value: *m.Value,
IsRegex: *m.IsRegex,
}
us.Matchers = append(us.Matchers, sm)
}
return &us, nil
}
+4
View File
@@ -31,3 +31,7 @@ func (m SilenceMapper) Collect(uri string, headers map[string]string, timeout ti
func (m SilenceMapper) RewriteUsername(body []byte, username string) ([]byte, error) {
return rewriteSilenceUsername(body, username)
}
func (m SilenceMapper) Unmarshal(body []byte) (*models.Silence, error) {
return unmarshal(body)
}