From f175a224436c38a32979c4629fc85cb85dcccf61 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 18 Dec 2015 16:48:49 -0500 Subject: [PATCH] Add admission controller to force image pulls Add an admission controller that forces every container's image pull policy to Always when a pod is created. --- cmd/kube-apiserver/app/plugins.go | 1 + docs/admin/admission-controllers.md | 11 ++ docs/admin/kube-apiserver.md | 4 +- .../admission/alwayspullimages/admission.go | 70 +++++++++++ .../alwayspullimages/admission_test.go | 118 ++++++++++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 plugin/pkg/admission/alwayspullimages/admission.go create mode 100644 plugin/pkg/admission/alwayspullimages/admission_test.go diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index f998568b964..ac2bf84943e 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -25,6 +25,7 @@ import ( // Admission policies _ "k8s.io/kubernetes/plugin/pkg/admission/admit" + _ "k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages" _ "k8s.io/kubernetes/plugin/pkg/admission/deny" _ "k8s.io/kubernetes/plugin/pkg/admission/exec" _ "k8s.io/kubernetes/plugin/pkg/admission/initialresources" diff --git a/docs/admin/admission-controllers.md b/docs/admin/admission-controllers.md index 520fc4ec2b9..473c71e2869 100644 --- a/docs/admin/admission-controllers.md +++ b/docs/admin/admission-controllers.md @@ -42,6 +42,7 @@ Documentation for other releases can be found at - [How do I turn on an admission control plug-in?](#how-do-i-turn-on-an-admission-control-plug-in) - [What does each plug-in do?](#what-does-each-plug-in-do) - [AlwaysAdmit](#alwaysadmit) + - [AlwaysPullImages](#alwayspullimages) - [AlwaysDeny](#alwaysdeny) - [DenyExecOnPrivileged (deprecated)](#denyexeconprivileged-deprecated) - [DenyEscalatingExec](#denyescalatingexec) @@ -90,6 +91,16 @@ ordered list of admission control choices to invoke prior to modifying objects i Use this plugin by itself to pass-through all requests. +### AlwaysPullImages + +This plug-in modifies every new Pod to force the image pull policy to Always. This is useful in a +multitenant cluster so that users can be assured that their private images can only be used by those +who have the credentials to pull them. Without this plug-in, once an image has been pulled to a +node, any pod from any user can use it simply by knowing the image's name (assuming the Pod is +scheduled onto the right node), without any authorization check against the image. When this plug-in +is enabled, images are always pulled prior to starting containers, which means valid credentials are +required. + ### AlwaysDeny Rejects all requests. Used for testing. diff --git a/docs/admin/kube-apiserver.md b/docs/admin/kube-apiserver.md index 1d627e1a0c8..306f835263d 100644 --- a/docs/admin/kube-apiserver.md +++ b/docs/admin/kube-apiserver.md @@ -50,7 +50,7 @@ kube-apiserver ### Options ``` - --admission-control="AlwaysAdmit": Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: AlwaysAdmit, AlwaysDeny, DenyEscalatingExec, DenyExecOnPrivileged, InitialResources, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, ResourceQuota, SecurityContextDeny, ServiceAccount + --admission-control="AlwaysAdmit": Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, DenyEscalatingExec, DenyExecOnPrivileged, InitialResources, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, ResourceQuota, SecurityContextDeny, ServiceAccount --admission-control-config-file="": File with admission control configuration. --advertise-address=: The IP address on which to advertise the apiserver to members of the cluster. This address must be reachable by the rest of the cluster. If blank, the --bind-address will be used. If --bind-address is unspecified, the host's default interface will be used. --allow-privileged[=false]: If true, allow privileged containers. @@ -106,7 +106,7 @@ kube-apiserver --watch-cache[=true]: Enable watch caching in the apiserver ``` -###### Auto generated by spf13/cobra on 18-Dec-2015 +###### Auto generated by spf13/cobra on 22-Dec-2015 diff --git a/plugin/pkg/admission/alwayspullimages/admission.go b/plugin/pkg/admission/alwayspullimages/admission.go new file mode 100644 index 00000000000..7702b155d31 --- /dev/null +++ b/plugin/pkg/admission/alwayspullimages/admission.go @@ -0,0 +1,70 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 alwayspullimages contains an admission controller that modifies every new Pod to force +// the image pull policy to Always. This is useful in a multitenant cluster so that users can be +// assured that their private images can only be used by those who have the credentials to pull +// them. Without this admission controller, once an image has been pulled to a node, any pod from +// any user can use it simply by knowing the image's name (assuming the Pod is scheduled onto the +// right node), without any authorization check against the image. With this admission controller +// enabled, images are always pulled prior to starting containers, which means valid credentials are +// required. +package alwayspullimages + +import ( + "io" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + apierrors "k8s.io/kubernetes/pkg/api/errors" + client "k8s.io/kubernetes/pkg/client/unversioned" +) + +func init() { + admission.RegisterPlugin("AlwaysPullImages", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewAlwaysPullImages(), nil + }) +} + +// alwaysPullImages is an implementation of admission.Interface. +// It looks at all new pods and overrides each container's image pull policy to Always. +type alwaysPullImages struct { + *admission.Handler +} + +func (a *alwaysPullImages) Admit(attributes admission.Attributes) (err error) { + // Ignore all calls to subresources or resources other than pods. + if len(attributes.GetSubresource()) != 0 || attributes.GetResource() != api.Resource("pods") { + return nil + } + pod, ok := attributes.GetObject().(*api.Pod) + if !ok { + return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + + for i := range pod.Spec.Containers { + pod.Spec.Containers[i].ImagePullPolicy = api.PullAlways + } + + return nil +} + +// NewAlwaysPullImages creates a new always pull images admission control handler +func NewAlwaysPullImages() admission.Interface { + return &alwaysPullImages{ + Handler: admission.NewHandler(admission.Create, admission.Update), + } +} diff --git a/plugin/pkg/admission/alwayspullimages/admission_test.go b/plugin/pkg/admission/alwayspullimages/admission_test.go new file mode 100644 index 00000000000..ba7d88b54bd --- /dev/null +++ b/plugin/pkg/admission/alwayspullimages/admission_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 alwayspullimages + +import ( + "testing" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/runtime" +) + +// TestAdmission verifies all create requests for pods result in every container's image pull policy +// set to Always +func TestAdmission(t *testing.T) { + namespace := "test" + handler := &alwaysPullImages{} + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr1", Image: "image"}, + {Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever}, + {Name: "ctr3", Image: "image", ImagePullPolicy: api.PullIfNotPresent}, + {Name: "ctr4", Image: "image", ImagePullPolicy: api.PullAlways}, + }, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, api.Kind("Pod"), pod.Namespace, pod.Name, api.Resource("pods"), "", admission.Create, nil)) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + for _, c := range pod.Spec.Containers { + if c.ImagePullPolicy != api.PullAlways { + t.Errorf("Container %s: expected pull always, got %v", c.ImagePullPolicy) + } + } +} + +// TestOtherResources ensures that this admission controller is a no-op for other resources, +// subresources, and non-pods. +func TestOtherResources(t *testing.T) { + namespace := "testnamespace" + name := "testname" + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespace}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever}, + }, + }, + } + tests := []struct { + name string + kind string + resource string + subresource string + object runtime.Object + expectError bool + }{ + { + name: "non-pod resource", + kind: "Foo", + resource: "foos", + object: pod, + }, + { + name: "pod subresource", + kind: "Pod", + resource: "pods", + subresource: "exec", + object: pod, + }, + { + name: "non-pod object", + kind: "Pod", + resource: "pods", + object: &api.Service{}, + expectError: true, + }, + } + + for _, tc := range tests { + handler := &alwaysPullImages{} + + err := handler.Admit(admission.NewAttributesRecord(tc.object, api.Kind(tc.kind), namespace, name, api.Resource(tc.resource), tc.subresource, admission.Create, nil)) + + if tc.expectError { + if err == nil { + t.Errorf("%s: unexpected nil error", tc.name) + } + continue + } + + if err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + continue + } + + if e, a := api.PullNever, pod.Spec.Containers[0].ImagePullPolicy; e != a { + t.Errorf("%s: image pull policy was changed to %s", tc.name, a) + } + } +}