Skip to content

Commit

Permalink
adds dynamic audit policy class
Browse files Browse the repository at this point in the history
  • Loading branch information
pbarker committed Jan 11, 2019
1 parent f4487a0 commit ecc9c60
Show file tree
Hide file tree
Showing 4 changed files with 475 additions and 12 deletions.
146 changes: 137 additions & 9 deletions pkg/apis/auditregistration/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2018 The Kubernetes Authors.
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -64,7 +64,6 @@ const (
// AuditSink represents a cluster level sink for audit data
type AuditSink struct {
metav1.TypeMeta

// +optional
metav1.ObjectMeta

Expand All @@ -74,7 +73,7 @@ type AuditSink struct {

// AuditSinkSpec is the spec for the audit sink object
type AuditSinkSpec struct {
// Policy defines the policy for selecting which events should be sent to the backend
// Policy defines the policy for selecting which events should be sent to the webhook
// required
Policy Policy

Expand All @@ -85,10 +84,9 @@ type AuditSinkSpec struct {

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// AuditSinkList is a list of a audit sink items.
// AuditSinkList is a list of AuditSink items.
type AuditSinkList struct {
metav1.TypeMeta

// +optional
metav1.ListMeta

Expand All @@ -104,11 +102,37 @@ type Policy struct {
Level Level

// Stages is a list of stages for which events are created.
// Must be non-empty if level != none
// +optional
Stages []Stage

// Rules define how classes should be handled.
// A request may fall under multiple audit classes.
// Rules are evaluated in order (first matching wins).
// Rules override the top-level & stage.
// Unmatched requests use the top-level rule.
// +optional
Rules []PolicyRule
}

// PolicyRule defines how a class is handled per sink
type PolicyRule struct {
// ClassName of the AuditClass object. This rule matches requests that are
// classified with this AuditClass
ClassName string

// The Level that all requests are recorded at.
// available options: None, Metadata, Request, RequestResponse
// required
Level Level

// Stages is a list of stages for which events are created.
// If no stages are given nothing will be logged
// +optional
Stages []Stage
}

// Webhook holds the configuration of the webhooks
// Webhook holds the configuration of the webhook
type Webhook struct {
// Throttle holds the options for throttling the webhook
// +optional
Expand All @@ -119,14 +143,14 @@ type Webhook struct {
ClientConfig WebhookClientConfig
}

// WebhookThrottleConfig holds the configuration for throttling
// WebhookThrottleConfig holds the configuration for throttling events
type WebhookThrottleConfig struct {
// QPS maximum number of batches per second
// ThrottleQPS maximum number of batches per second
// default 10 QPS
// +optional
QPS *int64

// Burst is the maximum number of events sent at the same moment
// ThrottleBurst is the maximum number of events sent at the same moment
// default 15 QPS
// +optional
Burst *int64
Expand Down Expand Up @@ -194,3 +218,107 @@ type ServiceReference struct {
// +optional
Path *string
}

// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// AuditClass is a set of rules that categorize requests
//
// This should be considered a highly privileged object, as modifying it
// will change what is logged.
type AuditClass struct {
metav1.TypeMeta
// +optional
metav1.ObjectMeta

// Spec is the spec for the audit class
Spec AuditClassSpec
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// AuditClassList is a list of AuditClass items
type AuditClassList struct {
metav1.TypeMeta
// +optional
metav1.ListMeta

// List of audit classes
Items []AuditClass
}

// AuditClassSpec is the spec for the audit class
type AuditClassSpec struct {
// RequestSelectors defines a list of RequestSelectors
RequestSelectors []RequestSelector
}

// RequestSelector selects requests by matching on the given attributes. Selectors are
// used to compose audit classes.
type RequestSelector struct {
// The users (by authenticated user name) in this attribute group.
// An empty list implies every user.
// +optional
Users []string
// The user groups in this attribute group. A user is considered matching
// if it is a member of any of the UserGroups.
// An empty list implies every user group.
// +optional
UserGroups []string

// The verbs included in this attribute group.
// An empty list implies every verb.
// +optional
Verbs []string

// Attribute groups can apply to API resources (such as "pods" or "secrets"),
// non-resource URL paths (such as "/api"), or neither, but not both.
// If neither is specified, the attribute group is treated as a default for all URLs.

// Resources in this attribute group. An empty list implies all kinds in all API groups.
// +optional
Resources []GroupResources
// Namespaces in this attribute group.
// The empty string "" matches non-namespaced resources.
// An empty list implies every namespace.
// Non-namespaced resources will only be matched if the empty string is present in the list
// +optional
Namespaces []string

// NonResourceURLs is a set of URL paths that should be audited.
// *s are allowed, but only as the full, final step in the path, and are delimited by the path separator
// Examples:
// "/metrics" - Log requests for apiserver metrics
// "/healthz/*" - Log all health checks
// +optional
NonResourceURLs []string
}

// GroupResources represents resource kinds in an API group.
type GroupResources struct {
// Group is the name of the API group that contains the resources.
// The empty string represents the core API group.
// +optional
Group string
// Resources is a list of resources in this group.
//
// For example:
// 'pods' matches pods.
// 'pods/log' matches the log subresource of pods.
// '*' matches all resources and their subresources.
// 'pods/*' matches all subresources of pods.
// '*/scale' matches all scale subresources.
//
// If wildcard is present, the validation rule will ensure resources do not
// overlap with each other.
//
// An empty list implies all resources and subresources in this API groups apply.
// +optional
Resources []string
// ObjectNames is a list of resource instance names in this group.
// Using this field requires Resources to be specified.
// An empty list implies that every instance of the resource is matched.
// +optional
ObjectNames []string
}
78 changes: 78 additions & 0 deletions pkg/apis/auditregistration/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package validation
import (
"strings"

"k8s.io/apimachinery/pkg/api/validation"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -80,6 +81,44 @@ func ValidatePolicy(policy auditregistration.Policy, fldPath *field.Path) field.
if policy.Level != auditregistration.LevelNone && len(policy.Stages) == 0 {
return field.ErrorList{field.Required(fldPath.Child("stages"), "")}
}
for _, rule := range policy.Rules {
allErrs = append(allErrs, validatePolicyRule(rule, fldPath.Child("rules"))...)
}
return allErrs
}

func validatePolicyRule(policyRule auditregistration.PolicyRule, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if policyRule.ClassName == "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("className"), policyRule.ClassName, "className cannot be blank"))
}
allErrs = append(allErrs, validateLevel(policyRule.Level, fldPath.Child("level"))...)
allErrs = append(allErrs, validateStages(policyRule.Stages, fldPath.Child("stages"))...)

return allErrs
}

// ValidateAuditClass validates the AuditClass
func ValidateAuditClass(class auditregistration.AuditClass) field.ErrorList {
var allErrs field.ErrorList
fldPath := field.NewPath("spec")
for _, requestSelector := range class.Spec.RequestSelectors {
allErrs = append(allErrs, validateRequestSelector(requestSelector, fldPath.Child("requestSelectors"))...)
}
return allErrs
}

func validateRequestSelector(requestSelector auditregistration.RequestSelector, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateNonResourceURLs(requestSelector.NonResourceURLs, fldPath.Child("nonResourceURLs"))...)
allErrs = append(allErrs, validateResources(requestSelector.Resources, fldPath.Child("resources"))...)

if len(requestSelector.NonResourceURLs) > 0 {
if len(requestSelector.Resources) > 0 || len(requestSelector.Namespaces) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), requestSelector.NonResourceURLs, "groups cannot contain to both regular resources and non-resource URLs"))
}
}

return allErrs
}

Expand Down Expand Up @@ -117,6 +156,45 @@ func validateStages(stages []auditregistration.Stage, fldPath *field.Path) field
return allErrs
}

func validateNonResourceURLs(urls []string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for i, url := range urls {
if url == "*" {
continue
}

if !strings.HasPrefix(url, "/") {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), url, "non-resource URL rules must begin with a '/' character"))
}

if url != "" && strings.ContainsRune(url[:len(url)-1], '*') {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), url, "non-resource URL wildcards '*' must be the final character of the rule"))
}
}
return allErrs
}

func validateResources(groupResources []auditregistration.GroupResources, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for _, groupResource := range groupResources {
// The empty string represents the core API group.
if len(groupResource.Group) != 0 {
// Group names must be lower case and be valid DNS subdomains.
// reference: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md
// an error is returned for group name like rbac.authorization.k8s.io/v1beta1
// rbac.authorization.k8s.io is the valid one
if msgs := validation.NameIsDNSSubdomain(groupResource.Group, false); len(msgs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), groupResource.Group, strings.Join(msgs, ",")))
}
}

if len(groupResource.ObjectNames) > 0 && len(groupResource.Resources) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("objectNames"), groupResource.ObjectNames, "using objectNames requires at least one resource"))
}
}
return allErrs
}

// ValidateAuditSinkUpdate validates an update to the object
func ValidateAuditSinkUpdate(newC, oldC *auditregistration.AuditSink) field.ErrorList {
return ValidateAuditSink(newC)
Expand Down
Loading

0 comments on commit ecc9c60

Please sign in to comment.