diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index c325bb0c43e..88751c68220 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -117,5 +117,6 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: - StreamingProxyRedirects: {Default: true, PreRelease: utilfeature.Beta}, + StreamingProxyRedirects: {Default: true, PreRelease: utilfeature.Beta}, + genericfeatures.AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha}, } diff --git a/pkg/kubeapiserver/server/BUILD b/pkg/kubeapiserver/server/BUILD index 82c48fa0329..0c6f4c43221 100644 --- a/pkg/kubeapiserver/server/BUILD +++ b/pkg/kubeapiserver/server/BUILD @@ -16,8 +16,10 @@ go_library( "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/filters:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/features:go_default_library", "//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", ], ) diff --git a/pkg/kubeapiserver/server/insecure_handler.go b/pkg/kubeapiserver/server/insecure_handler.go index 2e8eee2eb7f..24127f546a8 100644 --- a/pkg/kubeapiserver/server/insecure_handler.go +++ b/pkg/kubeapiserver/server/insecure_handler.go @@ -25,8 +25,10 @@ import ( "k8s.io/apiserver/pkg/authentication/user" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" apirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server" genericfilters "k8s.io/apiserver/pkg/server/filters" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/rest" ) @@ -35,7 +37,12 @@ import ( // InsecureServingInfo *ServingInfo func BuildInsecureHandlerChain(apiHandler http.Handler, c *server.Config) http.Handler { - handler := genericapifilters.WithAudit(apiHandler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicy, c.LongRunningFunc) + handler := apiHandler + if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) { + handler = genericapifilters.WithAudit(handler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc) + } else { + handler = genericapifilters.WithLegacyAudit(handler, c.RequestContextMapper, c.LegacyAuditWriter) + } handler = genericapifilters.WithAuthentication(handler, c.RequestContextMapper, insecureSuperuser{}, nil) handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") handler = genericfilters.WithPanicRecovery(handler) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/audit/helpers.go b/staging/src/k8s.io/apiserver/pkg/apis/audit/helpers.go index 3f54fc1e843..05fe72c0ff9 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/audit/helpers.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/audit/helpers.go @@ -36,11 +36,3 @@ func (a Level) Less(b Level) bool { func (a Level) GreaterOrEqual(b Level) bool { return ordLevel(a) >= ordLevel(b) } - -func NewConstantPolicy(level Level) *Policy { - return &Policy{ - Rules: []PolicyRule{ - {Level: level}, - }, - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/BUILD b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/BUILD new file mode 100644 index 00000000000..63cbc6bacd8 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/BUILD @@ -0,0 +1,27 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library"], +) + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation.go new file mode 100644 index 00000000000..2ceead05f4b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/apis/audit" +) + +func ValidatePolicy(policy *audit.Policy) field.ErrorList { + var allErrs field.ErrorList + rulePath := field.NewPath("rules") + for i, rule := range policy.Rules { + allErrs = append(allErrs, validatePolicyRule(rule, rulePath.Index(i))...) + } + return allErrs +} + +func validatePolicyRule(rule audit.PolicyRule, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, validateLevel(rule.Level, fldPath.Child("level"))...) + + if len(rule.NonResourceURLs) > 0 { + if len(rule.Resources) > 0 || len(rule.Namespaces) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), rule.NonResourceURLs, "rules cannot apply to both regular resources and non-resource URLs")) + } + } + + return allErrs +} + +var validLevels = []string{ + string(audit.LevelNone), + string(audit.LevelMetadata), + string(audit.LevelRequest), + string(audit.LevelRequestResponse), +} + +func validateLevel(level audit.Level, fldPath *field.Path) field.ErrorList { + switch level { + case audit.LevelNone, audit.LevelMetadata, audit.LevelRequest, audit.LevelRequestResponse: + return nil + case "": + return field.ErrorList{field.Required(fldPath, "")} + default: + return field.ErrorList{field.NotSupported(fldPath, level, validLevels)} + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation_test.go b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation_test.go new file mode 100644 index 00000000000..07e354bc7bf --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/audit/validation/validation_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "testing" + + "k8s.io/apiserver/pkg/apis/audit" +) + +func TestValidatePolicy(t *testing.T) { + validRules := []audit.PolicyRule{ + { // Defaulting rule + Level: audit.LevelMetadata, + }, { // Matching non-humans + Level: audit.LevelNone, + UserGroups: []string{"system:serviceaccounts", "system:nodes"}, + }, { // Specific request + Level: audit.LevelRequestResponse, + Verbs: []string{"get"}, + Resources: []audit.GroupResources{{Resources: []string{"secrets"}}}, + Namespaces: []string{"kube-system"}, + }, { // Some non-resource URLs + Level: audit.LevelMetadata, + UserGroups: []string{"developers"}, + NonResourceURLs: []string{ + "/logs*", + "/healthz*", + "/metrics", + }, + }, + } + successCases := []audit.Policy{} + for _, rule := range validRules { + successCases = append(successCases, audit.Policy{Rules: []audit.PolicyRule{rule}}) + } + successCases = append(successCases, audit.Policy{}) // Empty policy is valid. + successCases = append(successCases, audit.Policy{Rules: validRules}) // Multiple rules. + + for i, policy := range successCases { + if errs := ValidatePolicy(&policy); len(errs) != 0 { + t.Errorf("[%d] Expected policy %#v to be valid: %v", i, policy, errs) + } + } + + invalidRules := []audit.PolicyRule{ + {}, // Empty rule (missing Level) + { // Missing level + Verbs: []string{"get"}, + Resources: []audit.GroupResources{{Resources: []string{"secrets"}}}, + Namespaces: []string{"kube-system"}, + }, { // Invalid Level + Level: "FooBar", + }, { // NonResourceURLs + Namespaces + Level: audit.LevelMetadata, + Namespaces: []string{"default"}, + NonResourceURLs: []string{"/logs*"}, + }, { // NonResourceURLs + ResourceKinds + Level: audit.LevelMetadata, + Resources: []audit.GroupResources{{Resources: []string{"secrets"}}}, + NonResourceURLs: []string{"/logs*"}, + }, + } + errorCases := []audit.Policy{} + for _, rule := range invalidRules { + errorCases = append(errorCases, audit.Policy{Rules: []audit.PolicyRule{rule}}) + } + errorCases = append(errorCases, audit.Policy{Rules: append(validRules, audit.PolicyRule{})}) // Multiple rules. + + for i, policy := range errorCases { + if errs := ValidatePolicy(&policy); len(errs) == 0 { + t.Errorf("[%d] Expected policy %#v to be invalid!", i, policy) + } + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/audit/BUILD b/staging/src/k8s.io/apiserver/pkg/audit/BUILD index 298acbed733..9e67642f37a 100644 --- a/staging/src/k8s.io/apiserver/pkg/audit/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/audit/BUILD @@ -11,6 +11,7 @@ go_library( name = "go_default_library", srcs = [ "request.go", + "scheme.go", "types.go", ], tags = ["automanaged"], @@ -20,9 +21,11 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit/v1alpha1:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/authentication/v1:go_default_library", ], diff --git a/staging/src/k8s.io/apiserver/pkg/audit/policy/BUILD b/staging/src/k8s.io/apiserver/pkg/audit/policy/BUILD new file mode 100644 index 00000000000..3ceb86a349c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/policy/BUILD @@ -0,0 +1,44 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "checker_test.go", + "reader_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/github.com/stretchr/testify/assert:go_default_library", + "//vendor/github.com/stretchr/testify/require:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + "checker.go", + "reader.go", + ], + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit/v1alpha1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/audit/validation:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/pkg/audit/policy/checker.go b/staging/src/k8s.io/apiserver/pkg/audit/policy/checker.go new file mode 100644 index 00000000000..526710c2c6f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/policy/checker.go @@ -0,0 +1,177 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + + "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +const ( + // DefaultAuditLevel is the default level to audit at, if no policy rules are matched. + DefaultAuditLevel = audit.LevelNone +) + +// 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 +} + +// NewChecker creates a new policy checker. +func NewChecker(policy *audit.Policy) Checker { + return &policyChecker{*policy} +} + +// FakeChecker creates a checker that returns a constant level for all requests (for testing). +func FakeChecker(level audit.Level) Checker { + return &fakeChecker{level} +} + +type policyChecker struct { + audit.Policy +} + +func (p *policyChecker) Level(attrs authorizer.Attributes) audit.Level { + for _, rule := range p.Rules { + if ruleMatches(&rule, attrs) { + return rule.Level + } + } + return DefaultAuditLevel +} + +// Check whether the rule matches the request attrs. +func ruleMatches(r *audit.PolicyRule, attrs authorizer.Attributes) bool { + if len(r.Users) > 0 { + if !hasString(r.Users, attrs.GetUser().GetName()) { + return false + } + } + if len(r.UserGroups) > 0 { + matched := false + for _, group := range attrs.GetUser().GetGroups() { + if hasString(r.UserGroups, group) { + matched = true + break + } + } + if !matched { + return false + } + } + if len(r.Verbs) > 0 { + if !hasString(r.Verbs, attrs.GetVerb()) { + return false + } + } + + if len(r.Namespaces) > 0 || len(r.Resources) > 0 { + return ruleMatchesResource(r, attrs) + } + + if len(r.NonResourceURLs) > 0 { + return ruleMatchesNonResource(r, attrs) + } + + return true +} + +// Check whether the rule's non-resource URLs match the request attrs. +func ruleMatchesNonResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool { + if attrs.IsResourceRequest() { + return false + } + + path := attrs.GetPath() + for _, spec := range r.NonResourceURLs { + if pathMatches(path, spec) { + return true + } + } + + return false +} + +// Check whether the path matches the path specification. +func pathMatches(path, spec string) bool { + // Allow wildcard match + if spec == "*" { + return true + } + // Allow exact match + if spec == path { + return true + } + // Allow a trailing * subpath match + if strings.HasSuffix(spec, "*") && strings.HasPrefix(path, strings.TrimRight(spec, "*")) { + return true + } + return false +} + +// Check whether the rule's resource fields match the request attrs. +func ruleMatchesResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool { + if !attrs.IsResourceRequest() { + return false + } + + if len(r.Namespaces) > 0 { + if !hasString(r.Namespaces, attrs.GetNamespace()) { // Non-namespaced resources use the empty string. + return false + } + } + if len(r.Resources) == 0 { + return true + } + + apiGroup := attrs.GetAPIGroup() + resource := attrs.GetResource() + for _, gr := range r.Resources { + if gr.Group == apiGroup { + if len(gr.Resources) == 0 { + return true + } + for _, res := range gr.Resources { + if res == resource { + return true + } + } + } + } + return false +} + +// Utility function to check whether a string slice contains a string. +func hasString(slice []string, value string) bool { + for _, s := range slice { + if s == value { + return true + } + } + return false +} + +type fakeChecker struct { + level audit.Level +} + +func (f *fakeChecker) Level(_ authorizer.Attributes) audit.Level { + return f.level +} diff --git a/staging/src/k8s.io/apiserver/pkg/audit/policy/checker_test.go b/staging/src/k8s.io/apiserver/pkg/audit/policy/checker_test.go new file mode 100644 index 00000000000..29cf1a4689a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/policy/checker_test.go @@ -0,0 +1,162 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestChecker(t *testing.T) { + tim := &user.DefaultInfo{ + Name: "tim@k8s.io", + Groups: []string{"humans", "developers"}, + } + attrs := map[string]authorizer.Attributes{ + "namespaced": &authorizer.AttributesRecord{ + User: tim, + Verb: "get", + Namespace: "default", + APIGroup: "", // Core + APIVersion: "v1", + Resource: "pods", + Name: "busybox", + ResourceRequest: true, + Path: "/api/v1/namespaces/default/pods/busybox", + }, + "cluster": &authorizer.AttributesRecord{ + User: tim, + Verb: "get", + APIGroup: "rbac.authorization.k8s.io", // Core + APIVersion: "v1beta1", + Resource: "clusterroles", + Name: "edit", + ResourceRequest: true, + Path: "/apis/rbac.authorization.k8s.io/v1beta1/clusterroles/edit", + }, + "nonResource": &authorizer.AttributesRecord{ + User: tim, + Verb: "get", + ResourceRequest: false, + Path: "/logs/kubelet.log", + }, + } + + rules := map[string]audit.PolicyRule{ + "default": { + Level: audit.LevelMetadata, + }, + "create": { + Level: audit.LevelRequest, + Verbs: []string{"create"}, + }, + "tims": { + Level: audit.LevelMetadata, + Users: []string{"tim@k8s.io"}, + }, + "humans": { + Level: audit.LevelMetadata, + UserGroups: []string{"humans"}, + }, + "serviceAccounts": { + Level: audit.LevelRequest, + UserGroups: []string{"system:serviceaccounts"}, + }, + "getPods": { + Level: audit.LevelRequestResponse, + Verbs: []string{"get"}, + Resources: []audit.GroupResources{{Resources: []string{"pods"}}}, + }, + "getClusterRoles": { + Level: audit.LevelRequestResponse, + Verbs: []string{"get"}, + Resources: []audit.GroupResources{{ + Group: "rbac.authorization.k8s.io", + Resources: []string{"clusterroles"}, + }}, + Namespaces: []string{""}, + }, + "getLogs": { + Level: audit.LevelRequestResponse, + Verbs: []string{"get"}, + NonResourceURLs: []string{ + "/logs*", + }, + }, + "getMetrics": { + Level: audit.LevelRequest, + Verbs: []string{"get"}, + NonResourceURLs: []string{ + "/metrics", + }, + }, + } + + test := func(req string, expected audit.Level, 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, ",")) + } + + 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("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.LevelNone, "getLogs") + test("cluster", audit.LevelNone, "getMetrics") + test("cluster", audit.LevelMetadata, "getMetrics", "serviceAccounts", "default") + test("cluster", audit.LevelRequestResponse, "getMetrics", "getClusterRoles", "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") +} diff --git a/staging/src/k8s.io/apiserver/pkg/audit/policy/reader.go b/staging/src/k8s.io/apiserver/pkg/audit/policy/reader.go new file mode 100644 index 00000000000..afd50152e1e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/policy/reader.go @@ -0,0 +1,57 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "fmt" + "io/ioutil" + + "k8s.io/apimachinery/pkg/runtime" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + auditv1alpha1 "k8s.io/apiserver/pkg/apis/audit/v1alpha1" + "k8s.io/apiserver/pkg/apis/audit/validation" + "k8s.io/apiserver/pkg/audit" +) + +func LoadPolicyFromFile(filePath string) (*auditinternal.Policy, error) { + if filePath == "" { + return nil, fmt.Errorf("file path not specified") + } + policyDef, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file path %q: %+v", filePath, err) + } + if len(policyDef) == 0 { + return nil, fmt.Errorf("file %q was empty", filePath) + } + policyVersioned := &auditv1alpha1.Policy{} + + decoder := audit.Codecs.UniversalDecoder(auditv1alpha1.SchemeGroupVersion) + if err := runtime.DecodeInto(decoder, policyDef, policyVersioned); err != nil { + return nil, fmt.Errorf("failed decoding file %q: %v", filePath, err) + } + + policy := &auditinternal.Policy{} + if err := audit.Scheme.Convert(policyVersioned, policy, nil); err != nil { + return nil, fmt.Errorf("failed converting policy: %v", err) + } + + if err := validation.ValidatePolicy(policy); err != nil { + return nil, err.ToAggregate() + } + return policy, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/audit/policy/reader_test.go b/staging/src/k8s.io/apiserver/pkg/audit/policy/reader_test.go new file mode 100644 index 00000000000..be76364f59c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/policy/reader_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policy + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apiserver/pkg/apis/audit" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const policyDef = ` +rules: + - level: None + nonResourceURLs: + - /healthz* + - /version + - level: RequestResponse + users: ["tim"] + userGroups: ["testers", "developers"] + verbs: ["patch", "delete", "create"] + resources: + - group: "" + - group: "rbac.authorization.k8s.io" + resources: ["clusterroles", "clusterrolebindings"] + namespaces: ["default", "kube-system"] + - level: Metadata +` + +var expectedPolicy = &audit.Policy{ + Rules: []audit.PolicyRule{{ + Level: audit.LevelNone, + NonResourceURLs: []string{"/healthz*", "/version"}, + }, { + Level: audit.LevelRequestResponse, + Users: []string{"tim"}, + UserGroups: []string{"testers", "developers"}, + Verbs: []string{"patch", "delete", "create"}, + Resources: []audit.GroupResources{{}, { + Group: "rbac.authorization.k8s.io", + Resources: []string{"clusterroles", "clusterrolebindings"}, + }}, + Namespaces: []string{"default", "kube-system"}, + }, { + Level: audit.LevelMetadata, + }}, +} + +func TestParser(t *testing.T) { + // Create a policy file. + f, err := ioutil.TempFile("", "policy.yaml") + require.NoError(t, err) + defer os.Remove(f.Name()) + + _, err = f.WriteString(policyDef) + require.NoError(t, err) + require.NoError(t, f.Close()) + + policy, err := LoadPolicyFromFile(f.Name()) + require.NoError(t, err) + + assert.Len(t, policy.Rules, 3) // Sanity check. + if !reflect.DeepEqual(policy, expectedPolicy) { + t.Errorf("Unexpected policy! Diff:\n%s", diff.ObjectDiff(policy, expectedPolicy)) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/audit/request.go b/staging/src/k8s.io/apiserver/pkg/audit/request.go index f233da94f01..4773af244bc 100644 --- a/staging/src/k8s.io/apiserver/pkg/audit/request.go +++ b/staging/src/k8s.io/apiserver/pkg/audit/request.go @@ -40,21 +40,14 @@ import ( authenticationv1 "k8s.io/client-go/pkg/apis/authentication/v1" ) -// NewEventFromRequest generates an audit event for the request. -func NewEventFromRequest(req *http.Request, policy *auditinternal.Policy, attribs authorizer.Attributes) (*auditinternal.Event, error) { +func NewEventFromRequest(req *http.Request, level auditinternal.Level, attribs authorizer.Attributes) (*auditinternal.Event, error) { ev := &auditinternal.Event{ Timestamp: metav1.NewTime(time.Now()), Verb: attribs.GetVerb(), RequestURI: req.URL.RequestURI(), } - // set the level - ev.Level = auditinternal.LevelNone - if policy != nil && len(policy.Rules) > 0 { - // This is just a hack to get through the test without setting a high level by default. - // TODO(audit): add the policy evalutation here - ev.Level = policy.Rules[0].Level - } + ev.Level = level // prefer the id from the headers. If not available, create a new one. // TODO(audit): do we want to forbid the header for non-front-proxy users? diff --git a/staging/src/k8s.io/apiserver/pkg/audit/scheme.go b/staging/src/k8s.io/apiserver/pkg/audit/scheme.go new file mode 100644 index 00000000000..1ab6f5bd7c5 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/audit/scheme.go @@ -0,0 +1,34 @@ +/* +Copyright 2017 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO: Delete this file if we generate a clientset. +package audit + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/apis/audit/v1alpha1" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + v1alpha1.AddToScheme(Scheme) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index 9d43086babc..2bdc93a93ad 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -45,6 +45,7 @@ go_test( "//vendor/k8s.io/apiserver/pkg/apis/example/fuzzer:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/filters:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index eb2abc3b035..7cc02f7b279 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -57,6 +57,7 @@ import ( examplefuzzer "k8s.io/apiserver/pkg/apis/example/fuzzer" examplev1 "k8s.io/apiserver/pkg/apis/example/v1" "k8s.io/apiserver/pkg/audit" + auditpolicy "k8s.io/apiserver/pkg/audit/policy" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" @@ -334,7 +335,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission. } } - handler := genericapifilters.WithAudit(mux, requestContextMapper, auditSink, auditinternal.NewConstantPolicy(auditinternal.LevelRequestResponse), func(r *http.Request, requestInfo *request.RequestInfo) bool { + handler := genericapifilters.WithAudit(mux, requestContextMapper, auditSink, auditpolicy.FakeChecker(auditinternal.LevelRequestResponse), func(r *http.Request, requestInfo *request.RequestInfo) bool { // simplified long-running check return requestInfo.Verb == "watch" || requestInfo.Verb == "proxy" }) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD index f10ee11b57d..3c52d52c28d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/BUILD @@ -24,6 +24,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", @@ -56,6 +57,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go index b64b4034174..9a3083960dc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go @@ -19,16 +19,16 @@ package filters import ( "bufio" "errors" + "fmt" "net" "net/http" "sync" - "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/audit/policy" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" ) @@ -49,8 +49,8 @@ import ( // 2. the response line containing: // - the unique id from 1 // - response code -func WithAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, sink audit.Sink, policy *auditinternal.Policy, longRunningCheck request.LongRunningRequestCheck) http.Handler { - if sink == nil { +func WithAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, sink audit.Sink, policy policy.Checker, longRunningCheck request.LongRunningRequestCheck) http.Handler { + if sink == nil || policy == nil { return handler } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -67,7 +67,13 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext return } - ev, err := audit.NewEventFromRequest(req, policy, attribs) + level := policy.Level(attribs) + if level == auditinternal.LevelNone { + // Don't audit. + handler.ServeHTTP(w, req) + } + + ev, err := audit.NewEventFromRequest(req, level, attribs) if err != nil { utilruntime.HandleError(fmt.Errorf("failed to complete audit event from request: %v", err)) responsewriters.InternalError(w, req, errors.New("failed to update context")) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go index 14dc84da4a9..e312680a3a7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_test.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit/policy" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/request" pluginlog "k8s.io/apiserver/plugin/pkg/audit/log" @@ -321,9 +322,10 @@ func TestAudit(t *testing.T) { } { var buf bytes.Buffer backend := pluginlog.NewBackend(&buf) + policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse) handler := WithAudit(http.HandlerFunc(test.handler), &fakeRequestContextMapper{ user: &user.DefaultInfo{Name: "admin"}, - }, backend, auditinternal.NewConstantPolicy(auditinternal.LevelRequestResponse), func(r *http.Request, ri *request.RequestInfo) bool { + }, backend, policyChecker, func(r *http.Request, ri *request.RequestInfo) bool { // simplified long-running check return ri.Verb == "watch" }) @@ -386,7 +388,8 @@ func (*fakeRequestContextMapper) Update(req *http.Request, context request.Conte } func TestAuditNoPanicOnNilUser(t *testing.T) { - handler := WithAudit(&fakeHTTPHandler{}, &fakeRequestContextMapper{}, &fakeAuditSink{}, auditinternal.NewConstantPolicy(auditinternal.LevelRequestResponse), nil) + policyChecker := policy.FakeChecker(auditinternal.LevelRequestResponse) + handler := WithAudit(&fakeHTTPHandler{}, &fakeRequestContextMapper{}, &fakeAuditSink{}, policyChecker, nil) req, _ := http.NewRequest("GET", "/api/v1/namespaces/default/pods", nil) req.RemoteAddr = "127.0.0.1" handler.ServeHTTP(httptest.NewRecorder(), req) diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index 8ab10fa200c..1b896e1e556 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -33,6 +33,14 @@ const ( // StreamingProxyRedirects controls whether the apiserver should intercept (and follow) // redirects from the backend (Kubelet) for streaming requests (exec/attach/port-forward). StreamingProxyRedirects utilfeature.Feature = "StreamingProxyRedirects" + + // owner: timstclair + // alpha: v1.7 + // + // AdvancedAuditing enables a much more general API auditing pipeline, which includes support for + // pluggable output backends and an audit policy specifying how different requests should be + // audited. + AdvancedAuditing utilfeature.Feature = "AdvancedAuditing" ) func init() { @@ -44,4 +52,5 @@ func init() { // available throughout Kubernetes binaries. var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ StreamingProxyRedirects: {Default: true, PreRelease: utilfeature.Beta}, + AdvancedAuditing: {Default: false, PreRelease: utilfeature.Alpha}, } diff --git a/staging/src/k8s.io/apiserver/pkg/server/BUILD b/staging/src/k8s.io/apiserver/pkg/server/BUILD index 1526f24adbd..45e25f06c7d 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/BUILD @@ -77,8 +77,8 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/version:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/apis/apiserver/install:go_default_library", - "//vendor/k8s.io/apiserver/pkg/apis/audit:go_default_library", "//vendor/k8s.io/apiserver/pkg/audit:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticatorfactory:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/request/union:go_default_library", @@ -92,12 +92,14 @@ go_library( "//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/openapi:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/features:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/healthz:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/mux:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/routes:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/informers:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/util/cert:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index 2475e50be93..30208ba1bac 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io" "net" "net/http" goruntime "runtime" @@ -38,8 +39,8 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/version" "k8s.io/apiserver/pkg/admission" - auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/audit" + auditpolicy "k8s.io/apiserver/pkg/audit/policy" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticatorfactory" authenticatorunion "k8s.io/apiserver/pkg/authentication/request/union" @@ -51,10 +52,12 @@ import ( genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" apiopenapi "k8s.io/apiserver/pkg/endpoints/openapi" apirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" genericregistry "k8s.io/apiserver/pkg/registry/generic" genericfilters "k8s.io/apiserver/pkg/server/filters" "k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/routes" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/informers" restclient "k8s.io/client-go/rest" certutil "k8s.io/client-go/util/cert" @@ -101,11 +104,12 @@ type Config struct { // Version will enable the /version endpoint if non-nil Version *version.Info + // LegacyAuditWriter is the destination for audit logs. If nil, they will not be written. + LegacyAuditWriter io.Writer // AuditBackend is where audit events are sent to. AuditBackend audit.Backend - // AuditPolicy defines rules which determine the audit level for different requests. - AuditPolicy *auditinternal.Policy - + // AuditPolicyChecker makes the decision of whether and how to audit log a request. + AuditPolicyChecker auditpolicy.Checker // SupportsBasicAuth indicates that's at least one Authenticator supports basic auth // If this is true, a basic auth challenge is returned on authentication failure // TODO(roberthbailey): Remove once the server no longer supports http basic auth. @@ -457,8 +461,11 @@ func (c completedConfig) New(delegationTarget DelegationTarget) (*GenericAPIServ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler { handler := genericapifilters.WithAuthorization(apiHandler, c.RequestContextMapper, c.Authorizer) handler = genericapifilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer) - // TODO(audit): use WithLegacyAudit if feature flag is false - handler = genericapifilters.WithAudit(handler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicy, c.LongRunningFunc) + if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) { + handler = genericapifilters.WithAudit(handler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc) + } else { + handler = genericapifilters.WithLegacyAudit(handler, c.RequestContextMapper, c.LegacyAuditWriter) + } handler = genericapifilters.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, genericapifilters.Unauthorized(c.SupportsBasicAuth)) handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") handler = genericfilters.WithPanicRecovery(handler) diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/BUILD b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD index 6d41fea4149..4e5c44a12de 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD @@ -54,6 +54,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library", + "//vendor/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/authenticatorfactory:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library", "//vendor/k8s.io/apiserver/pkg/features:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/audit.go b/staging/src/k8s.io/apiserver/pkg/server/options/audit.go index c9362d2312c..e4ab7da80a6 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/audit.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/audit.go @@ -17,13 +17,17 @@ limitations under the License. package options import ( + "fmt" "io" "os" "github.com/spf13/pflag" "gopkg.in/natefinch/lumberjack.v2" + "k8s.io/apiserver/pkg/audit/policy" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server" + utilfeature "k8s.io/apiserver/pkg/util/feature" pluginlog "k8s.io/apiserver/plugin/pkg/audit/log" ) @@ -32,6 +36,8 @@ type AuditLogOptions struct { MaxAge int MaxBackups int MaxSize int + + PolicyFile string } func NewAuditLogOptions() *AuditLogOptions { @@ -47,9 +53,28 @@ func (o *AuditLogOptions) AddFlags(fs *pflag.FlagSet) { "The maximum number of old audit log files to retain.") fs.IntVar(&o.MaxSize, "audit-log-maxsize", o.MaxSize, "The maximum size in megabytes of the audit log file before it gets rotated.") + + fs.StringVar(&o.PolicyFile, "audit-policy-file", o.PolicyFile, + "Path to the file that defines the audit policy configuration. Requires the 'AdvancedAuditing' feature gate."+ + " With AdvancedAuditing, a profile is required to enable auditing.") } func (o *AuditLogOptions) ApplyTo(c *server.Config) error { + if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) { + if o.PolicyFile != "" { + p, err := policy.LoadPolicyFromFile(o.PolicyFile) + if err != nil { + return err + } + c.AuditPolicyChecker = policy.NewChecker(p) + } + } else { + if o.PolicyFile != "" { + return fmt.Errorf("feature '%s' must be enabled to set an audit policy", features.AdvancedAuditing) + } + } + + // TODO: Generalize for alternative audit backends. if len(o.Path) == 0 { return nil } @@ -63,6 +88,7 @@ func (o *AuditLogOptions) ApplyTo(c *server.Config) error { MaxSize: o.MaxSize, } } + c.LegacyAuditWriter = w c.AuditBackend = pluginlog.NewBackend(w) return nil }