Merge pull request #46009 from timstclair/audit-policy

Automatic merge from submit-queue (batch tested with PRs 45949, 46009, 46320, 46423, 46437)

Implement audit policy logic

Includes https://github.com/kubernetes/kubernetes/pull/45315#discussion_r117115932 (ignore the first commit)

Feature: https://github.com/kubernetes/features/issues/22

Remaining work:

- [x] Load the policy into the `server.Config`
- [x] Rebase on https://github.com/kubernetes/kubernetes/pull/45315
- [x] Establish shared code for audit api scheme (with https://github.com/kubernetes/kubernetes/pull/45919)
- [x] Once https://github.com/kubernetes/kubernetes/pull/45766 is merged, call the policy checker in the audit path

/cc @sttts @soltysh @ericchiang @ihmccreery @pweil- @deads2k
This commit is contained in:
Kubernetes Submit Queue 2017-05-25 19:40:59 -07:00 committed by GitHub
commit ae03f22c65
25 changed files with 828 additions and 34 deletions

View File

@ -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},
}

View File

@ -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",
],
)

View File

@ -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)

View File

@ -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},
},
}
}

View File

@ -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",
],
)

View File

@ -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)}
}
}

View File

@ -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)
}
}
}

View File

@ -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",
],

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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?

View File

@ -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)
}

View File

@ -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",

View File

@ -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"
})

View File

@ -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",

View File

@ -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"))

View File

@ -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)

View File

@ -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},
}

View File

@ -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",

View File

@ -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)

View File

@ -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",

View File

@ -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
}