Commit 47ba9145 authored by Cao Shufeng's avatar Cao Shufeng

Provide a way to omit Event stages in audit policy

Updates https://github.com/kubernetes/kubernetes/issues/48561 This provide a way to omit some stages for each audit policy rule. For example: apiVersion: audit.k8s.io/v1beta1 kind: Policy - level: Metadata resources: - group: "rbac.authorization.k8s.io" resources: ["roles"] omitStages: - "RequestReceived" RequestReceived stage will not be emitted to audit backends with previous config.
parent 156c4b44
......@@ -207,6 +207,10 @@ type PolicyRule struct {
// "/healthz*" - Log all health checks
// +optional
NonResourceURLs []string
// OmitStages specify events generated in which stages will not be emitted to backend.
// An empty list means no restrictions will apply.
OmitStages []Stage
}
// GroupResources represents resource kinds in an API group.
......
......@@ -208,6 +208,10 @@ type PolicyRule struct {
// "/healthz*" - Log all health checks
// +optional
NonResourceURLs []string `json:"nonResourceURLs,omitempty" protobuf:"bytes,7,rep,name=nonResourceURLs"`
// OmitStages specify events generated in which stages will not be emitted to backend.
// An empty list means no restrictions will apply.
OmitStages []Stage `json:"omitStages,omitempty" protobuf:"bytes,8,rep,name=omitStages"`
}
// GroupResources represents resource kinds in an API group.
......
......@@ -201,6 +201,10 @@ type PolicyRule struct {
// "/healthz*" - Log all health checks
// +optional
NonResourceURLs []string `json:"nonResourceURLs,omitempty" protobuf:"bytes,7,rep,name=nonResourceURLs"`
// OmitStages specify events generated in which stages will not be emitted to backend.
// An empty list means no restrictions will apply.
OmitStages []Stage `json:"omitStages,omitempty" protobuf:"bytes,8,rep,name=omitStages"`
}
// GroupResources represents resource kinds in an API group.
......
......@@ -38,6 +38,7 @@ func validatePolicyRule(rule audit.PolicyRule, fldPath *field.Path) field.ErrorL
allErrs = append(allErrs, validateLevel(rule.Level, fldPath.Child("level"))...)
allErrs = append(allErrs, validateNonResourceURLs(rule.NonResourceURLs, fldPath.Child("nonResourceURLs"))...)
allErrs = append(allErrs, validateResources(rule.Resources, fldPath.Child("resources"))...)
allErrs = append(allErrs, validateOmitStages(rule.OmitStages, fldPath.Child("omitStages"))...)
if len(rule.NonResourceURLs) > 0 {
if len(rule.Resources) > 0 || len(rule.Namespaces) > 0 {
......@@ -55,6 +56,13 @@ var validLevels = []string{
string(audit.LevelRequestResponse),
}
var validOmitStages = []string{
string(audit.StageRequestReceived),
string(audit.StageResponseStarted),
string(audit.StageResponseComplete),
string(audit.StagePanic),
}
func validateLevel(level audit.Level, fldPath *field.Path) field.ErrorList {
switch level {
case audit.LevelNone, audit.LevelMetadata, audit.LevelRequest, audit.LevelRequestResponse:
......@@ -104,3 +112,20 @@ func validateResources(groupResources []audit.GroupResources, fldPath *field.Pat
}
return allErrs
}
func validateOmitStages(omitStages []audit.Stage, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for i, stage := range omitStages {
valid := false
for _, validOmitStage := range validOmitStages {
if string(stage) == validOmitStage {
valid = true
break
}
}
if !valid {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), string(stage), "allowed stages are "+strings.Join(validOmitStages, ",")))
}
}
return allErrs
}
......@@ -43,6 +43,11 @@ func TestValidatePolicy(t *testing.T) {
"/metrics",
"*",
},
}, { // Omit RequestReceived stage
Level: audit.LevelMetadata,
OmitStages: []audit.Stage{
audit.Stage("RequestReceived"),
},
},
}
successCases := []audit.Policy{}
......@@ -108,6 +113,12 @@ func TestValidatePolicy(t *testing.T) {
Resources: []audit.GroupResources{{ResourceNames: []string{"leader"}}},
Namespaces: []string{"kube-system"},
},
{ // invalid omitStages
Level: audit.LevelMetadata,
OmitStages: []audit.Stage{
audit.Stage("foo"),
},
},
}
errorCases := []audit.Policy{}
for _, rule := range invalidRules {
......
......@@ -31,7 +31,7 @@ const (
// Checker exposes methods for checking the policy rules.
type Checker interface {
// Check the audit level for a request with the given authorizer attributes.
Level(authorizer.Attributes) audit.Level
LevelAndStages(authorizer.Attributes) (audit.Level, []audit.Stage)
}
// NewChecker creates a new policy checker.
......@@ -40,21 +40,21 @@ func NewChecker(policy *audit.Policy) Checker {
}
// FakeChecker creates a checker that returns a constant level for all requests (for testing).
func FakeChecker(level audit.Level) Checker {
return &fakeChecker{level}
func FakeChecker(level audit.Level, stage []audit.Stage) Checker {
return &fakeChecker{level, stage}
}
type policyChecker struct {
audit.Policy
}
func (p *policyChecker) Level(attrs authorizer.Attributes) audit.Level {
func (p *policyChecker) LevelAndStages(attrs authorizer.Attributes) (audit.Level, []audit.Stage) {
for _, rule := range p.Rules {
if ruleMatches(&rule, attrs) {
return rule.Level
return rule.Level, rule.OmitStages
}
}
return DefaultAuditLevel
return DefaultAuditLevel, nil
}
// Check whether the rule matches the request attrs.
......@@ -181,8 +181,9 @@ func hasString(slice []string, value string) bool {
type fakeChecker struct {
level audit.Level
stage []audit.Stage
}
func (f *fakeChecker) Level(_ authorizer.Attributes) audit.Level {
return f.level
func (f *fakeChecker) LevelAndStages(_ authorizer.Attributes) (audit.Level, []audit.Stage) {
return f.level, f.stage
}
......@@ -136,59 +136,80 @@ func TestChecker(t *testing.T) {
ResourceNames: []string{"edit"},
}},
},
"omit RequestReceived": {
Level: audit.LevelRequest,
OmitStages: []audit.Stage{
audit.StageRequestReceived,
},
},
"only audit panic": {
Level: audit.LevelRequest,
OmitStages: []audit.Stage{
audit.StageRequestReceived,
audit.StageResponseStarted,
audit.StageResponseComplete,
},
},
}
test := func(req string, expected audit.Level, ruleNames ...string) {
test := func(req string, expLevel audit.Level, expOmitStages []audit.Stage, ruleNames ...string) {
policy := audit.Policy{}
for _, rule := range ruleNames {
require.Contains(t, rules, rule)
policy.Rules = append(policy.Rules, rules[rule])
}
require.Contains(t, attrs, req)
actual := NewChecker(&policy).Level(attrs[req])
assert.Equal(t, expected, actual, "request:%s rules:%s", req, strings.Join(ruleNames, ","))
actualLevel, actualOmitStages := NewChecker(&policy).LevelAndStages(attrs[req])
assert.Equal(t, expLevel, actualLevel, "request:%s rules:%s", req, strings.Join(ruleNames, ","))
assert.Equal(t, expOmitStages, actualOmitStages, "request:%s rules:%s", req, strings.Join(ruleNames, ","))
}
test("namespaced", audit.LevelMetadata, "default")
test("namespaced", audit.LevelNone, "create")
test("namespaced", audit.LevelMetadata, "tims")
test("namespaced", audit.LevelMetadata, "humans")
test("namespaced", audit.LevelNone, "serviceAccounts")
test("namespaced", audit.LevelRequestResponse, "getPods")
test("namespaced", audit.LevelNone, "getClusterRoles")
test("namespaced", audit.LevelNone, "getLogs")
test("namespaced", audit.LevelNone, "getMetrics")
test("namespaced", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("namespaced", audit.LevelRequestResponse, "getMetrics", "getPods", "default")
test("namespaced", audit.LevelRequestResponse, "getPodLogs", "getPods")
test("namespaced", audit.LevelMetadata, nil, "default")
test("namespaced", audit.LevelNone, nil, "create")
test("namespaced", audit.LevelMetadata, nil, "tims")
test("namespaced", audit.LevelMetadata, nil, "humans")
test("namespaced", audit.LevelNone, nil, "serviceAccounts")
test("namespaced", audit.LevelRequestResponse, nil, "getPods")
test("namespaced", audit.LevelNone, nil, "getClusterRoles")
test("namespaced", audit.LevelNone, nil, "getLogs")
test("namespaced", audit.LevelNone, nil, "getMetrics")
test("namespaced", audit.LevelMetadata, nil, "getMetrics", "serviceAccounts", "default")
test("namespaced", audit.LevelRequestResponse, nil, "getMetrics", "getPods", "default")
test("namespaced", audit.LevelRequestResponse, nil, "getPodLogs", "getPods")
test("namespaced", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived}, "omit RequestReceived", "getPods", "default")
test("namespaced", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived, audit.StageResponseStarted, audit.StageResponseComplete}, "only audit panic", "getPods", "default")
test("cluster", audit.LevelMetadata, "default")
test("cluster", audit.LevelNone, "create")
test("cluster", audit.LevelMetadata, "tims")
test("cluster", audit.LevelMetadata, "humans")
test("cluster", audit.LevelNone, "serviceAccounts")
test("cluster", audit.LevelNone, "getPods")
test("cluster", audit.LevelRequestResponse, "getClusterRoles")
test("cluster", audit.LevelRequest, "clusterRoleEdit", "getClusterRoles")
test("cluster", audit.LevelNone, "getLogs")
test("cluster", audit.LevelNone, "getMetrics")
test("cluster", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("cluster", audit.LevelRequestResponse, "getMetrics", "getClusterRoles", "default")
test("cluster", audit.LevelNone, "getPodLogs", "getPods")
test("cluster", audit.LevelMetadata, nil, "default")
test("cluster", audit.LevelNone, nil, "create")
test("cluster", audit.LevelMetadata, nil, "tims")
test("cluster", audit.LevelMetadata, nil, "humans")
test("cluster", audit.LevelNone, nil, "serviceAccounts")
test("cluster", audit.LevelNone, nil, "getPods")
test("cluster", audit.LevelRequestResponse, nil, "getClusterRoles")
test("cluster", audit.LevelRequest, nil, "clusterRoleEdit", "getClusterRoles")
test("cluster", audit.LevelNone, nil, "getLogs")
test("cluster", audit.LevelNone, nil, "getMetrics")
test("cluster", audit.LevelMetadata, nil, "getMetrics", "serviceAccounts", "default")
test("cluster", audit.LevelRequestResponse, nil, "getMetrics", "getClusterRoles", "default")
test("cluster", audit.LevelNone, nil, "getPodLogs", "getPods")
test("cluster", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived}, "omit RequestReceived", "getPods", "default")
test("cluster", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived, audit.StageResponseStarted, audit.StageResponseComplete}, "only audit panic", "getPods", "default")
test("nonResource", audit.LevelMetadata, "default")
test("nonResource", audit.LevelNone, "create")
test("nonResource", audit.LevelMetadata, "tims")
test("nonResource", audit.LevelMetadata, "humans")
test("nonResource", audit.LevelNone, "serviceAccounts")
test("nonResource", audit.LevelNone, "getPods")
test("nonResource", audit.LevelNone, "getClusterRoles")
test("nonResource", audit.LevelRequestResponse, "getLogs")
test("nonResource", audit.LevelNone, "getMetrics")
test("nonResource", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default")
test("nonResource", audit.LevelRequestResponse, "getLogs", "getClusterRoles", "default")
test("nonResource", audit.LevelNone, "getPodLogs", "getPods")
test("nonResource", audit.LevelMetadata, nil, "default")
test("nonResource", audit.LevelNone, nil, "create")
test("nonResource", audit.LevelMetadata, nil, "tims")
test("nonResource", audit.LevelMetadata, nil, "humans")
test("nonResource", audit.LevelNone, nil, "serviceAccounts")
test("nonResource", audit.LevelNone, nil, "getPods")
test("nonResource", audit.LevelNone, nil, "getClusterRoles")
test("nonResource", audit.LevelRequestResponse, nil, "getLogs")
test("nonResource", audit.LevelNone, nil, "getMetrics")
test("nonResource", audit.LevelMetadata, nil, "getMetrics", "serviceAccounts", "default")
test("nonResource", audit.LevelRequestResponse, nil, "getLogs", "getClusterRoles", "default")
test("nonResource", audit.LevelNone, nil, "getPodLogs", "getPods")
test("nonResource", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived}, "omit RequestReceived", "getPods", "default")
test("nonResource", audit.LevelRequest, []audit.Stage{audit.StageRequestReceived, audit.StageResponseStarted, audit.StageResponseComplete}, "only audit panic", "getPods", "default")
test("subresource", audit.LevelRequest, "getPodLogs", "getPods")
test("subresource", audit.LevelRequest, "getPods", "getPodLogs")
test("subresource", audit.LevelRequest, nil, "getPodLogs", "getPods")
test("subresource", audit.LevelRequest, nil, "getPods", "getPodLogs")
}
......@@ -331,7 +331,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
}
}
handler := genericapifilters.WithAudit(mux, requestContextMapper, auditSink, auditpolicy.FakeChecker(auditinternal.LevelRequestResponse), func(r *http.Request, requestInfo *request.RequestInfo) bool {
handler := genericapifilters.WithAudit(mux, requestContextMapper, auditSink, auditpolicy.FakeChecker(auditinternal.LevelRequestResponse, nil), func(r *http.Request, requestInfo *request.RequestInfo) bool {
// simplified long-running check
return requestInfo.Verb == "watch" || requestInfo.Verb == "proxy"
})
......
......@@ -42,7 +42,7 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx, ev, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
ctx, ev, omitStages, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err))
responsewriters.InternalError(w, req, errors.New("failed to create audit event"))
......@@ -54,7 +54,7 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
}
ev.Stage = auditinternal.StageRequestReceived
processAuditEvent(sink, ev)
processAuditEvent(sink, ev, omitStages)
// intercept the status code
var longRunningSink audit.Sink
......@@ -64,7 +64,7 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
longRunningSink = sink
}
}
respWriter := decorateResponseWriter(w, ev, longRunningSink)
respWriter := decorateResponseWriter(w, ev, longRunningSink, omitStages)
// send audit event when we leave this func, either via a panic or cleanly. In the case of long
// running requests, this will be the second audit event.
......@@ -78,7 +78,7 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
Reason: metav1.StatusReasonInternalError,
Message: fmt.Sprintf("APIServer panic'd: %v", r),
}
processAuditEvent(sink, ev)
processAuditEvent(sink, ev, omitStages)
return
}
......@@ -92,14 +92,14 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
if ev.ResponseStatus == nil && longRunningSink != nil {
ev.ResponseStatus = fakedSuccessStatus
ev.Stage = auditinternal.StageResponseStarted
processAuditEvent(longRunningSink, ev)
processAuditEvent(longRunningSink, ev, omitStages)
}
ev.Stage = auditinternal.StageResponseComplete
if ev.ResponseStatus == nil {
ev.ResponseStatus = fakedSuccessStatus
}
processAuditEvent(sink, ev)
processAuditEvent(sink, ev, omitStages)
}()
handler.ServeHTTP(respWriter, req)
})
......@@ -110,47 +110,53 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext
// - context with audit event attached to it
// - created audit event
// - error if anything bad happened
func createAuditEventAndAttachToContext(requestContextMapper request.RequestContextMapper, req *http.Request, policy policy.Checker) (request.Context, *auditinternal.Event, error) {
func createAuditEventAndAttachToContext(requestContextMapper request.RequestContextMapper, req *http.Request, policy policy.Checker) (request.Context, *auditinternal.Event, []auditinternal.Stage, error) {
ctx, ok := requestContextMapper.Get(req)
if !ok {
return nil, nil, fmt.Errorf("no context found for request")
return nil, nil, nil, fmt.Errorf("no context found for request")
}
attribs, err := GetAuthorizerAttributes(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
return nil, nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
}
level := policy.Level(attribs)
level, omitStages := policy.LevelAndStages(attribs)
audit.ObservePolicyLevel(level)
if level == auditinternal.LevelNone {
// Don't audit.
return nil, nil, nil
return nil, nil, nil, nil
}
ev, err := audit.NewEventFromRequest(req, level, attribs)
if err != nil {
return nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
return nil, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
}
ctx = request.WithAuditEvent(ctx, ev)
if err := requestContextMapper.Update(req, ctx); err != nil {
return nil, nil, fmt.Errorf("failed to attach audit event to context: %v", err)
return nil, nil, nil, fmt.Errorf("failed to attach audit event to context: %v", err)
}
return ctx, ev, nil
return ctx, ev, omitStages, nil
}
func processAuditEvent(sink audit.Sink, ev *auditinternal.Event) {
func processAuditEvent(sink audit.Sink, ev *auditinternal.Event, omitStages []auditinternal.Stage) {
for _, stage := range omitStages {
if ev.Stage == stage {
return
}
}
audit.ObserveEvent()
sink.ProcessEvents(ev)
}
func decorateResponseWriter(responseWriter http.ResponseWriter, ev *auditinternal.Event, sink audit.Sink) http.ResponseWriter {
func decorateResponseWriter(responseWriter http.ResponseWriter, ev *auditinternal.Event, sink audit.Sink, omitStages []auditinternal.Stage) http.ResponseWriter {
delegate := &auditResponseWriter{
ResponseWriter: responseWriter,
event: ev,
sink: sink,
omitStages: omitStages,
}
// check if the ResponseWriter we're wrapping is the fancy one we need
......@@ -170,9 +176,10 @@ var _ http.ResponseWriter = &auditResponseWriter{}
// create immediately an event (for long running requests).
type auditResponseWriter struct {
http.ResponseWriter
event *auditinternal.Event
once sync.Once
sink audit.Sink
event *auditinternal.Event
once sync.Once
sink audit.Sink
omitStages []auditinternal.Stage
}
func (a *auditResponseWriter) setHttpHeader() {
......@@ -188,7 +195,7 @@ func (a *auditResponseWriter) processCode(code int) {
a.event.Stage = auditinternal.StageResponseStarted
if a.sink != nil {
processAuditEvent(a.sink, a.event)
processAuditEvent(a.sink, a.event, a.omitStages)
}
})
}
......
......@@ -38,7 +38,7 @@ func WithFailedAuthenticationAudit(failedHandler http.Handler, requestContextMap
return failedHandler
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, ev, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
_, ev, omitStages, err := createAuditEventAndAttachToContext(requestContextMapper, req, policy)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err))
responsewriters.InternalError(w, req, errors.New("failed to create audit event"))
......@@ -53,7 +53,7 @@ func WithFailedAuthenticationAudit(failedHandler http.Handler, requestContextMap
ev.ResponseStatus.Message = getAuthMethods(req)
ev.Stage = auditinternal.StageResponseStarted
rw := decorateResponseWriter(w, ev, sink)
rw := decorateResponseWriter(w, ev, sink, omitStages)
failedHandler.ServeHTTP(rw, req)
})
}
......
......@@ -30,7 +30,7 @@ import (
func TestFailedAuthnAudit(t *testing.T) {
sink := &fakeAuditSink{}
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse)
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, nil)
handler := WithFailedAuthenticationAudit(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
......@@ -61,7 +61,7 @@ func TestFailedAuthnAudit(t *testing.T) {
func TestFailedMultipleAuthnAudit(t *testing.T) {
sink := &fakeAuditSink{}
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse)
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, nil)
handler := WithFailedAuthenticationAudit(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
......@@ -93,7 +93,7 @@ func TestFailedMultipleAuthnAudit(t *testing.T) {
func TestFailedAuthnAuditWithoutAuthorization(t *testing.T) {
sink := &fakeAuditSink{}
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse)
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, nil)
handler := WithFailedAuthenticationAudit(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
......@@ -120,3 +120,20 @@ func TestFailedAuthnAuditWithoutAuthorization(t *testing.T) {
t.Errorf("Unexpected user, expected /api/v1/namespaces/default/pods, got %s", ev.RequestURI)
}
}
func TestFailedAuthnAuditOmitted(t *testing.T) {
sink := &fakeAuditSink{}
policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse, []auditinternal.Stage{auditinternal.StageResponseStarted})
handler := WithFailedAuthenticationAudit(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}),
&fakeRequestContextMapper{}, sink, policyChecker)
req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil)
req.RemoteAddr = "127.0.0.1"
handler.ServeHTTP(httptest.NewRecorder(), req)
if len(sink.events) != 0 {
t.Fatalf("Unexpected number of audit events generated, expected 0, got: %d", len(sink.events))
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment