From d609f4ebca3882da1cc96846495828b936f47f7c Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Fri, 5 May 2017 02:14:38 -0400 Subject: [PATCH 1/3] Add pod util for extracting referenced configmaps --- pkg/api/pod/util.go | 61 ++++++++++++++++++++++++++++++++++++++++-- pkg/api/v1/pod/util.go | 7 +++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index ec91c03276b..45e935b90aa 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -21,11 +21,14 @@ import ( "k8s.io/kubernetes/pkg/api" ) +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + // VisitPodSecretNames invokes the visitor function with the name of every secret // referenced by the pod spec. If visitor returns false, visiting is short-circuited. // Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. // Returns true if visiting completed, false if visiting was short-circuited. -func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool { +func VisitPodSecretNames(pod *api.Pod, visitor Visitor) bool { for _, reference := range pod.Spec.ImagePullSecrets { if !visitor(reference.Name) { return false @@ -86,7 +89,7 @@ func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool { return true } -func visitContainerSecretNames(container *api.Container, visitor func(string) bool) bool { +func visitContainerSecretNames(container *api.Container, visitor Visitor) bool { for _, env := range container.EnvFrom { if env.SecretRef != nil { if !visitor(env.SecretRef.Name) { @@ -104,6 +107,60 @@ func visitContainerSecretNames(container *api.Container, visitor func(string) bo return true } +// VisitPodConfigmapNames invokes the visitor function with the name of every configmap +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPodConfigmapNames(pod *api.Pod, visitor Visitor) bool { + for i := range pod.Spec.InitContainers { + if !visitContainerConfigmapNames(&pod.Spec.InitContainers[i], visitor) { + return false + } + } + for i := range pod.Spec.Containers { + if !visitContainerConfigmapNames(&pod.Spec.Containers[i], visitor) { + return false + } + } + var source *api.VolumeSource + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].ConfigMap != nil { + if !visitor(source.Projected.Sources[j].ConfigMap.Name) { + return false + } + } + } + case source.ConfigMap != nil: + if !visitor(source.ConfigMap.Name) { + return false + } + } + } + return true +} + +func visitContainerConfigmapNames(container *api.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.ConfigMapRef != nil { + if !visitor(env.ConfigMapRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.ConfigMapKeyRef != nil { + if !visitor(envVar.ValueFrom.ConfigMapKeyRef.Name) { + return false + } + } + } + return true +} + // IsPodReady returns true if a pod is ready; false otherwise. func IsPodReady(pod *api.Pod) bool { return IsPodReadyConditionTrue(pod.Status) diff --git a/pkg/api/v1/pod/util.go b/pkg/api/v1/pod/util.go index bb24a3c7626..85eb5338337 100644 --- a/pkg/api/v1/pod/util.go +++ b/pkg/api/v1/pod/util.go @@ -107,11 +107,14 @@ func SetInitContainersStatusesAnnotations(pod *v1.Pod) error { return nil } +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + // VisitPodSecretNames invokes the visitor function with the name of every secret // referenced by the pod spec. If visitor returns false, visiting is short-circuited. // Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. // Returns true if visiting completed, false if visiting was short-circuited. -func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool { +func VisitPodSecretNames(pod *v1.Pod, visitor Visitor) bool { for _, reference := range pod.Spec.ImagePullSecrets { if !visitor(reference.Name) { return false @@ -173,7 +176,7 @@ func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool { return true } -func visitContainerSecretNames(container *v1.Container, visitor func(string) bool) bool { +func visitContainerSecretNames(container *v1.Container, visitor Visitor) bool { for _, env := range container.EnvFrom { if env.SecretRef != nil { if !visitor(env.SecretRef.Name) { From 0c516c3ac216cca8be8b033a712069d4d0336ec0 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 16 May 2017 16:09:00 -0400 Subject: [PATCH 2/3] Add NodeIdentifier interface and default implementation --- hack/.linted_packages | 1 + pkg/BUILD | 1 + pkg/auth/nodeidentifier/BUILD | 40 +++++++++++++++ pkg/auth/nodeidentifier/default.go | 64 +++++++++++++++++++++++ pkg/auth/nodeidentifier/default_test.go | 68 +++++++++++++++++++++++++ pkg/auth/nodeidentifier/interfaces.go | 30 +++++++++++ 6 files changed, 204 insertions(+) create mode 100644 pkg/auth/nodeidentifier/BUILD create mode 100644 pkg/auth/nodeidentifier/default.go create mode 100644 pkg/auth/nodeidentifier/default_test.go create mode 100644 pkg/auth/nodeidentifier/interfaces.go diff --git a/hack/.linted_packages b/hack/.linted_packages index f3ef847cdf6..71fc2db171b 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -86,6 +86,7 @@ pkg/apis/settings/install pkg/apis/settings/validation pkg/apis/storage/install pkg/apis/storage/validation +pkg/auth/nodeidentifier pkg/bootstrap/api pkg/client/conditions pkg/client/informers/informers_generated/externalversions diff --git a/pkg/BUILD b/pkg/BUILD index 83cd2a523df..2f5f692342c 100644 --- a/pkg/BUILD +++ b/pkg/BUILD @@ -31,6 +31,7 @@ filegroup( "//pkg/apis/settings:all-srcs", "//pkg/apis/storage:all-srcs", "//pkg/auth/authorizer/abac:all-srcs", + "//pkg/auth/nodeidentifier:all-srcs", "//pkg/auth/user:all-srcs", "//pkg/bootstrap/api:all-srcs", "//pkg/capabilities:all-srcs", diff --git a/pkg/auth/nodeidentifier/BUILD b/pkg/auth/nodeidentifier/BUILD new file mode 100644 index 00000000000..cc228ad79c1 --- /dev/null +++ b/pkg/auth/nodeidentifier/BUILD @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = [ + "default.go", + "interfaces.go", + ], + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["default_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/auth/nodeidentifier/default.go b/pkg/auth/nodeidentifier/default.go new file mode 100644 index 00000000000..80df38ba4f3 --- /dev/null +++ b/pkg/auth/nodeidentifier/default.go @@ -0,0 +1,64 @@ +/* +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 nodeidentifier + +import ( + "strings" + + "k8s.io/apiserver/pkg/authentication/user" +) + +// NewDefaultNodeIdentifier returns a default NodeIdentifier implementation, +// which returns isNode=true if the user groups contain the system:nodes group, +// and populates nodeName if isNode is true, and the user name is in the format system:node: +func NewDefaultNodeIdentifier() NodeIdentifier { + return defaultNodeIdentifier{} +} + +// defaultNodeIdentifier implements NodeIdentifier +type defaultNodeIdentifier struct{} + +// nodeUserNamePrefix is the prefix for usernames in the form `system:node:` +const nodeUserNamePrefix = "system:node:" + +// NodeIdentity returns isNode=true if the user groups contain the system:nodes group, +// and populates nodeName if isNode is true, and the user name is in the format system:node: +func (defaultNodeIdentifier) NodeIdentity(u user.Info) (string, bool) { + // Make sure we're a node, and can parse the node name + if u == nil { + return "", false + } + + isNode := false + for _, g := range u.GetGroups() { + if g == user.NodesGroup { + isNode = true + break + } + } + if !isNode { + return "", false + } + + userName := u.GetName() + nodeName := "" + if strings.HasPrefix(userName, nodeUserNamePrefix) { + nodeName = strings.TrimPrefix(userName, nodeUserNamePrefix) + } + + return nodeName, isNode +} diff --git a/pkg/auth/nodeidentifier/default_test.go b/pkg/auth/nodeidentifier/default_test.go new file mode 100644 index 00000000000..fee38d57296 --- /dev/null +++ b/pkg/auth/nodeidentifier/default_test.go @@ -0,0 +1,68 @@ +/* +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 nodeidentifier + +import ( + "testing" + + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestDefaultNodeIdentifier_NodeIdentity(t *testing.T) { + tests := []struct { + name string + user user.Info + expectNodeName string + expectIsNode bool + }{ + { + name: "nil user", + user: nil, + expectNodeName: "", + expectIsNode: false, + }, + { + name: "node username without group", + user: &user.DefaultInfo{Name: "system:node:foo"}, + expectNodeName: "", + expectIsNode: false, + }, + { + name: "node group without username", + user: &user.DefaultInfo{Name: "foo", Groups: []string{"system:nodes"}}, + expectNodeName: "", + expectIsNode: true, + }, + { + name: "node group and username", + user: &user.DefaultInfo{Name: "system:node:foo", Groups: []string{"system:nodes"}}, + expectNodeName: "foo", + expectIsNode: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nodeName, isNode := NewDefaultNodeIdentifier().NodeIdentity(tt.user) + if nodeName != tt.expectNodeName { + t.Errorf("DefaultNodeIdentifier.NodeIdentity() got = %v, want %v", nodeName, tt.expectNodeName) + } + if isNode != tt.expectIsNode { + t.Errorf("DefaultNodeIdentifier.NodeIdentity() got1 = %v, want %v", isNode, tt.expectIsNode) + } + }) + } +} diff --git a/pkg/auth/nodeidentifier/interfaces.go b/pkg/auth/nodeidentifier/interfaces.go new file mode 100644 index 00000000000..917bebaf9d9 --- /dev/null +++ b/pkg/auth/nodeidentifier/interfaces.go @@ -0,0 +1,30 @@ +/* +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 nodeidentifier + +import ( + "k8s.io/apiserver/pkg/authentication/user" +) + +// NodeIdentifier determines node information from a given user +type NodeIdentifier interface { + // IdentifyNode determines node information from the given user.Info. + // nodeName is the name of the Node API object associated with the user.Info, + // and may be empty if a specific node cannot be determined. + // isNode is true if the user.Info represents an identity issued to a node. + NodeIdentity(user.Info) (nodeName string, isNode bool) +} From 6fd36792f185a2f846d08ff5547460b25435b413 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 16 May 2017 16:09:28 -0400 Subject: [PATCH 3/3] Add NodeRestriction admission plugin --- cmd/kube-apiserver/app/BUILD | 1 + cmd/kube-apiserver/app/plugins.go | 1 + hack/local-up-cluster.sh | 3 + plugin/BUILD | 1 + plugin/pkg/admission/noderestriction/BUILD | 54 ++ plugin/pkg/admission/noderestriction/OWNERS | 8 + .../admission/noderestriction/admission.go | 203 ++++++++ .../noderestriction/admission_test.go | 476 ++++++++++++++++++ 8 files changed, 747 insertions(+) create mode 100644 plugin/pkg/admission/noderestriction/BUILD create mode 100644 plugin/pkg/admission/noderestriction/OWNERS create mode 100644 plugin/pkg/admission/noderestriction/admission.go create mode 100644 plugin/pkg/admission/noderestriction/admission_test.go diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index b671a78ee65..d93e5b64aac 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -54,6 +54,7 @@ go_library( "//plugin/pkg/admission/namespace/autoprovision:go_default_library", "//plugin/pkg/admission/namespace/exists:go_default_library", "//plugin/pkg/admission/namespace/lifecycle:go_default_library", + "//plugin/pkg/admission/noderestriction:go_default_library", "//plugin/pkg/admission/persistentvolume/label:go_default_library", "//plugin/pkg/admission/podnodeselector:go_default_library", "//plugin/pkg/admission/podpreset:go_default_library", diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index f5d524ab6d8..0ddd92566ad 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -37,6 +37,7 @@ import ( _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle" + _ "k8s.io/kubernetes/plugin/pkg/admission/noderestriction" _ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label" _ "k8s.io/kubernetes/plugin/pkg/admission/podnodeselector" _ "k8s.io/kubernetes/plugin/pkg/admission/podpreset" diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index 61fed3bfff2..955cb3857e8 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -388,6 +388,9 @@ function start_apiserver { if [[ -n "${PSP_ADMISSION}" ]]; then security_admission=",PodSecurityPolicy" fi + if [[ -n "${NODE_ADMISSION}" ]]; then + security_admission=",NodeRestriction" + fi # Admission Controllers to invoke prior to persisting objects in cluster ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},ResourceQuota,DefaultStorageClass,DefaultTolerationSeconds diff --git a/plugin/BUILD b/plugin/BUILD index cf8c9c5be76..9f7368ddf2b 100644 --- a/plugin/BUILD +++ b/plugin/BUILD @@ -27,6 +27,7 @@ filegroup( "//plugin/pkg/admission/namespace/autoprovision:all-srcs", "//plugin/pkg/admission/namespace/exists:all-srcs", "//plugin/pkg/admission/namespace/lifecycle:all-srcs", + "//plugin/pkg/admission/noderestriction:all-srcs", "//plugin/pkg/admission/persistentvolume/label:all-srcs", "//plugin/pkg/admission/podnodeselector:all-srcs", "//plugin/pkg/admission/podpreset:all-srcs", diff --git a/plugin/pkg/admission/noderestriction/BUILD b/plugin/pkg/admission/noderestriction/BUILD new file mode 100644 index 00000000000..83fcdd19b9f --- /dev/null +++ b/plugin/pkg/admission/noderestriction/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["admission.go"], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/api/pod:go_default_library", + "//pkg/auth/nodeidentifier:go_default_library", + "//pkg/client/clientset_generated/internalclientset:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", + "//pkg/kubeapiserver/admission:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["admission_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/auth/nodeidentifier:go_default_library", + "//pkg/client/clientset_generated/internalclientset/fake:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/plugin/pkg/admission/noderestriction/OWNERS b/plugin/pkg/admission/noderestriction/OWNERS new file mode 100644 index 00000000000..e58cadf54d4 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/OWNERS @@ -0,0 +1,8 @@ +approvers: +- deads2k +- liggitt +- timstclair +reviewers: +- deads2k +- liggitt +- timstclair diff --git a/plugin/pkg/admission/noderestriction/admission.go b/plugin/pkg/admission/noderestriction/admission.go new file mode 100644 index 00000000000..7eabfdf7a73 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/admission.go @@ -0,0 +1,203 @@ +/* +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 node + +import ( + "fmt" + "io" + + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/kubernetes/pkg/api" + podutil "k8s.io/kubernetes/pkg/api/pod" + "k8s.io/kubernetes/pkg/auth/nodeidentifier" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" +) + +const ( + PluginName = "NodeRestriction" +) + +func init() { + kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { + return NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), false), nil + }) +} + +// NewPlugin creates a new NodeRestriction admission plugin. +// This plugin identifies requests from nodes +func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier, strict bool) *nodePlugin { + return &nodePlugin{ + Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), + nodeIdentifier: nodeIdentifier, + strict: strict, + } +} + +// nodePlugin holds state for and implements the admission plugin. +type nodePlugin struct { + *admission.Handler + strict bool + nodeIdentifier nodeidentifier.NodeIdentifier + podsGetter coreinternalversion.PodsGetter +} + +var ( + _ = admission.Interface(&nodePlugin{}) + _ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{}) +) + +func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) { + p.podsGetter = f.Core() +} + +func (p *nodePlugin) Validate() error { + if p.nodeIdentifier == nil { + return fmt.Errorf("%s requires a node identifier", PluginName) + } + if p.podsGetter == nil { + return fmt.Errorf("%s requires a pod getter", PluginName) + } + return nil +} + +var ( + podResource = api.Resource("pods") + nodeResource = api.Resource("nodes") +) + +func (c *nodePlugin) Admit(a admission.Attributes) error { + nodeName, isNode := c.nodeIdentifier.NodeIdentity(a.GetUserInfo()) + + // Our job is just to restrict nodes + if !isNode { + return nil + } + + if len(nodeName) == 0 { + if c.strict { + // In strict mode, disallow requests from nodes we cannot match to a particular node + return admission.NewForbidden(a, fmt.Errorf("could not determine node identity from user")) + } + // Our job is just to restrict identifiable nodes + return nil + } + + switch a.GetResource().GroupResource() { + case podResource: + switch a.GetSubresource() { + case "": + return c.admitPod(nodeName, a) + case "status": + return c.admitPodStatus(nodeName, a) + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected pod subresource %s", a.GetSubresource())) + } + + case nodeResource: + return c.admitNode(nodeName, a) + + default: + return nil + } +} + +func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error { + switch a.GetOperation() { + case admission.Create: + // require a pod object + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) + } + + // only allow nodes to create mirror pods + if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; !isMirrorPod { + return admission.NewForbidden(a, fmt.Errorf("pod does not have %q annotation, node %s can only create mirror pods", api.MirrorPodAnnotationKey, nodeName)) + } + + // only allow nodes to create a pod bound to itself + if pod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only create pods with spec.nodeName set to itself", nodeName)) + } + + // don't allow a node to create a pod that references any other API objects + if pod.Spec.ServiceAccountName != "" { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference a service account", nodeName)) + } + hasSecrets := false + podutil.VisitPodSecretNames(pod, func(name string) (shouldContinue bool) { hasSecrets = true; return false }) + if hasSecrets { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference secrets", nodeName)) + } + hasConfigMaps := false + podutil.VisitPodConfigmapNames(pod, func(name string) (shouldContinue bool) { hasConfigMaps = true; return false }) + if hasConfigMaps { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference configmaps", nodeName)) + } + for _, v := range pod.Spec.Volumes { + if v.PersistentVolumeClaim != nil { + return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference persistentvolumeclaims", nodeName)) + } + } + + return nil + + case admission.Delete: + // get the existing pod + existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{ResourceVersion: "0"}) + if err != nil { + return admission.NewForbidden(a, err) + } + // only allow a node to delete a pod bound to itself + if existingPod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only delete pods with spec.nodeName set to itself", nodeName)) + } + return nil + + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation())) + } +} + +func (c *nodePlugin) admitPodStatus(nodeName string, a admission.Attributes) error { + switch a.GetOperation() { + case admission.Update: + // require an existing pod + pod, ok := a.GetOldObject().(*api.Pod) + if !ok { + return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject())) + } + // only allow a node to update status of a pod bound to itself + if pod.Spec.NodeName != nodeName { + return admission.NewForbidden(a, fmt.Errorf("node %s can only update pod status for pods with spec.nodeName set to itself", nodeName)) + } + return nil + + default: + return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation())) + } +} + +func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error { + if a.GetName() != nodeName { + return admission.NewForbidden(a, fmt.Errorf("cannot modify other nodes")) + } + return nil +} diff --git a/plugin/pkg/admission/noderestriction/admission_test.go b/plugin/pkg/admission/noderestriction/admission_test.go new file mode 100644 index 00000000000..5cb0eb23bb8 --- /dev/null +++ b/plugin/pkg/admission/noderestriction/admission_test.go @@ -0,0 +1,476 @@ +/* +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 node + +import ( + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/auth/nodeidentifier" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" +) + +func makeTestPod(namespace, name, node string, mirror bool) *api.Pod { + pod := &api.Pod{} + pod.Namespace = namespace + pod.Name = name + pod.Spec.NodeName = node + if mirror { + pod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"} + } + return pod +} + +func Test_nodePlugin_Admit(t *testing.T) { + var ( + mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} + bob = &user.DefaultInfo{Name: "bob"} + + mynodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode"}} + othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}} + + mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true) + othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true) + unboundmirrorpod = makeTestPod("ns", "unboundmirrorpod", "", true) + mypod = makeTestPod("ns", "mypod", "mynode", false) + otherpod = makeTestPod("ns", "otherpod", "othernode", false) + unboundpod = makeTestPod("ns", "unboundpod", "", false) + + configmapResource = api.Resource("configmap").WithVersion("v1") + configmapKind = api.Kind("ConfigMap").WithVersion("v1") + + podResource = api.Resource("pods").WithVersion("v1") + podKind = api.Kind("Pod").WithVersion("v1") + + nodeResource = api.Resource("nodes").WithVersion("v1") + nodeKind = api.Kind("Node").WithVersion("v1") + + noExistingPods = fake.NewSimpleClientset().Core() + existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core() + ) + + sapod := makeTestPod("ns", "mysapod", "mynode", true) + sapod.Spec.ServiceAccountName = "foo" + + secretpod := makeTestPod("ns", "mysecretpod", "mynode", true) + secretpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}} + + configmappod := makeTestPod("ns", "myconfigmappod", "mynode", true) + configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}} + + pvcpod := makeTestPod("ns", "mypvcpod", "mynode", true) + pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}} + + tests := []struct { + name string + strict bool + podsGetter coreinternalversion.PodsGetter + attributes admission.Attributes + err string + }{ + // Mirror pods bound to us + { + name: "allow creating a mirror pod bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Create, mynode), + err: "", + }, + { + name: "forbid update of mirror pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow delete of mirror pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "forbid create of mirror pod status bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow update of mirror pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "", + }, + { + name: "forbid delete of mirror pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Mirror pods bound to another node + { + name: "forbid creating a mirror pod bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Create, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid update of mirror pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of mirror pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of mirror pod status bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of mirror pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of mirror pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Mirror pods not bound to any node + { + name: "forbid creating a mirror pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Create, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid update of mirror pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of mirror pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of mirror pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of mirror pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of mirror pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods bound to us + { + name: "forbid creating a normal pod bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow delete of normal pod bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "forbid create of normal pod status bound to self", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "allow update of normal pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Update, mynode), + err: "", + }, + { + name: "forbid delete of normal pod status bound to self", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods bound to another + { + name: "forbid creating a normal pod bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of normal pod bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of normal pod status bound to another", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of normal pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of normal pod status bound to another", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Normal pods not bound to any node + { + name: "forbid creating a normal pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, mynode), + err: "can only create mirror pods", + }, + { + name: "forbid update of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid delete of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid create of normal pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, mynode), + err: "forbidden: unexpected operation", + }, + { + name: "forbid update of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, mynode), + err: "spec.nodeName set to itself", + }, + { + name: "forbid delete of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, mynode), + err: "forbidden: unexpected operation", + }, + + // Missing pod + { + name: "forbid delete of unknown pod", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode), + err: "not found", + }, + + // Resource pods + { + name: "forbid create of pod referencing service account", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(sapod, nil, podKind, sapod.Namespace, sapod.Name, podResource, "", admission.Create, mynode), + err: "reference a service account", + }, + { + name: "forbid create of pod referencing secret", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(secretpod, nil, podKind, secretpod.Namespace, secretpod.Name, podResource, "", admission.Create, mynode), + err: "reference secrets", + }, + { + name: "forbid create of pod referencing configmap", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, mynode), + err: "reference configmaps", + }, + { + name: "forbid create of pod referencing persistentvolumeclaim", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(pvcpod, nil, podKind, pvcpod.Namespace, pvcpod.Name, podResource, "", admission.Create, mynode), + err: "reference persistentvolumeclaims", + }, + + // My node object + { + name: "allow create of my node", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, mynode), + err: "", + }, + { + name: "allow update of my node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode), + err: "", + }, + { + name: "allow delete of my node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Delete, mynode), + err: "", + }, + { + name: "allow update of my node status", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "status", admission.Update, mynode), + err: "", + }, + + // Other node object + { + name: "forbid create of other node", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Create, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid update of other node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Update, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid delete of other node", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Delete, mynode), + err: "cannot modify other nodes", + }, + { + name: "forbid update of other node status", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "status", admission.Update, mynode), + err: "cannot modify other nodes", + }, + + // Unrelated objects + { + name: "allow create of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(&api.ConfigMap{}, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Create, mynode), + err: "", + }, + { + name: "allow update of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(&api.ConfigMap{}, &api.ConfigMap{}, configmapKind, "myns", "mycm", configmapResource, "", admission.Update, mynode), + err: "", + }, + { + name: "allow delete of unrelated object", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Delete, mynode), + err: "", + }, + + // Unrelated user + { + name: "allow unrelated user creating a normal pod unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, bob), + err: "", + }, + { + name: "allow unrelated user update of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, bob), + err: "", + }, + { + name: "allow unrelated user delete of normal pod unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, bob), + err: "", + }, + { + name: "allow unrelated user create of normal pod status unbound", + podsGetter: noExistingPods, + attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, bob), + err: "", + }, + { + name: "allow unrelated user update of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, bob), + err: "", + }, + { + name: "allow unrelated user delete of normal pod status unbound", + podsGetter: existingPods, + attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, bob), + err: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), tt.strict) + c.podsGetter = tt.podsGetter + err := c.Admit(tt.attributes) + if (err == nil) != (len(tt.err) == 0) { + t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err) + return + } + if len(tt.err) > 0 && !strings.Contains(err.Error(), tt.err) { + t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err) + } + }) + } +}