Yuvipanda has submitted this change and it was merged.

Change subject: Add uidenforcer admission controller
......................................................................


Add uidenforcer admission controller

Enforces that:

  - All namespaces have a RunAsUser annotation
  - Namespaced users can't touch annotations on their own namespace
  - Pods in a namespace will have RunAsUser set in all the containers
    to the RunAsUser annotation
  - Requires whitelisting of the resources that each user can
    access in an ABAC file
  - Does not let non-empty SecurityContexts pass through at all, since
    they can have capabilities or other dangerous things in the future

The previous uidenforcer was an obselete version that was
accidentally cherry-picked, instead of this version.

Change-Id: Ie9fc11c5f8849225b04ac3e581e2eaafaf36bc44
---
M plugin/pkg/admission/uidenforcer/admission.go
M plugin/pkg/admission/uidenforcer/admission_test.go
2 files changed, 92 insertions(+), 30 deletions(-)

Approvals:
  Yuvipanda: Verified; Looks good to me, approved



diff --git a/plugin/pkg/admission/uidenforcer/admission.go 
b/plugin/pkg/admission/uidenforcer/admission.go
index ba99bbb..dbe36d5 100644
--- a/plugin/pkg/admission/uidenforcer/admission.go
+++ b/plugin/pkg/admission/uidenforcer/admission.go
@@ -19,34 +19,59 @@
 import (
        "io"
        "strconv"
+       "time"
 
        "k8s.io/kubernetes/pkg/admission"
        "k8s.io/kubernetes/pkg/api"
        apierrors "k8s.io/kubernetes/pkg/api/errors"
+       "k8s.io/kubernetes/pkg/client/cache"
        clientset 
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
+       "k8s.io/kubernetes/pkg/runtime"
+       "k8s.io/kubernetes/pkg/watch"
 )
 
 func init() {
-       admission.RegisterPlugin("UidEnforcer", func(client 
clientset.Interface, config io.Reader) (admission.Interface, error) {
-               return NewUidEnforcer(client), nil
+       admission.RegisterPlugin("UidEnforcer", func(clientset 
clientset.Interface, config io.Reader) (admission.Interface, error) {
+               return NewUidEnforcer(clientset), nil
        })
 }
 
 // plugin contains the client used by the uidenforcer admin controller
 type plugin struct {
        *admission.Handler
-       client clientset.Interface
+       clientset clientset.Interface
+       store     cache.Store
 }
 
-// NewSecurityContextDeny creates a new instance of the SecurityContextDeny 
admission controller
-func NewUidEnforcer(client clientset.Interface) admission.Interface {
+// NewUidEnforcer creates a new instance of the UidEnforcer admission 
controller
+func NewUidEnforcer(clientset clientset.Interface) admission.Interface {
+       store := cache.NewStore(cache.MetaNamespaceKeyFunc)
+       reflector := cache.NewReflector(
+               &cache.ListWatch{
+                       ListFunc: func(options api.ListOptions) 
(runtime.Object, error) {
+                               return 
clientset.Core().Namespaces().List(options)
+                       },
+                       WatchFunc: func(options api.ListOptions) 
(watch.Interface, error) {
+                               return 
clientset.Core().Namespaces().Watch(options)
+                       },
+               },
+               &api.Namespace{},
+               store,
+               5*time.Minute,
+       )
+       reflector.Run()
        return &plugin{
-               Handler: admission.NewHandler(admission.Create, 
admission.Update),
-               client:  client,
+               Handler:   admission.NewHandler(admission.Create, 
admission.Update),
+               clientset: clientset,
+               store:     store,
        }
 }
 
-// Admit will deny pods that have a RunAsUser set that isn't the uid of the 
user requesting it
+// This will verify the following:
+//  - User object has a numeric uid
+//  - Namespace object has an annotation called RunAsUser that's an integer
+//
+// If after all this there's no SecurityContext on each Container with a 
RunAsUser set to the same RunAsUser, it'll be set
 func (p *plugin) Admit(a admission.Attributes) (err error) {
        if a.GetResource() != api.Resource("pods") {
                return nil
@@ -56,24 +81,40 @@
        if !ok {
                return apierrors.NewBadRequest("Resource was marked with kind 
Pod but was unable to be converted")
        }
-       user := a.GetUserInfo()
-       if user == nil {
-               return apierrors.NewBadRequest("uidenforcer admission 
controller can not be used if there is no user set")
+
+       namespaceObj, exists, err := p.store.Get(&api.Namespace{
+               ObjectMeta: api.ObjectMeta{
+                       Name:      a.GetNamespace(),
+                       Namespace: "",
+               },
+       })
+
+       if !exists {
+               return apierrors.NewBadRequest("Namespace " + a.GetNamespace() 
+ " not found")
+       }
+
+       if err != nil {
+               return apierrors.NewBadRequest("Everything must be in a 
namespace!")
+       }
+       namespace := namespaceObj.(*api.Namespace)
+
+       if _, ok := namespace.Annotations["RunAsUser"]; !ok {
+               return apierrors.NewBadRequest("Namespace does not have a 
RunAsUser annotation!")
        }
 
        for i := 0; i < len(pod.Spec.Containers); i++ {
                container := &pod.Spec.Containers[i]
-               uid, ok := strconv.ParseInt(user.GetUID(), 10, 32)
+               uid, ok := strconv.ParseInt(namespace.Annotations["RunAsUser"], 
10, 32)
                if ok == nil {
                        if container.SecurityContext == nil {
                                container.SecurityContext = 
&api.SecurityContext{
                                        RunAsUser: &uid,
                                }
                        } else {
-                               container.SecurityContext.RunAsUser = &uid
+                               return apierrors.NewBadRequest("Must have an 
empty SecuriyContext to pass!")
                        }
                } else {
-                       return apierrors.NewBadRequest("Requesting user's uid 
is not an integer")
+                       return apierrors.NewBadRequest("Namespace's RunAsUser 
not an integer")
                }
        }
        return nil
diff --git a/plugin/pkg/admission/uidenforcer/admission_test.go 
b/plugin/pkg/admission/uidenforcer/admission_test.go
index 81c01de..3b99cd1 100644
--- a/plugin/pkg/admission/uidenforcer/admission_test.go
+++ b/plugin/pkg/admission/uidenforcer/admission_test.go
@@ -22,7 +22,7 @@
 
        "k8s.io/kubernetes/pkg/admission"
        "k8s.io/kubernetes/pkg/api"
-       "k8s.io/kubernetes/pkg/auth/user"
+       "k8s.io/kubernetes/pkg/client/cache"
        
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
 )
 
@@ -39,36 +39,57 @@
        return pod
 }
 
-func TestFailWithNoUserInfo(t *testing.T) {
-       client := fake.NewSimpleClientset()
-
-       enforcer := NewUidEnforcer(client)
+func TestNoRunUser(t *testing.T) {
+       clientset := fake.NewSimpleClientset()
+       store := cache.NewStore(cache.MetaNamespaceKeyFunc)
        testPod := validPod("test", 2)
-       err := enforcer.Admit(admission.NewAttributesRecord(&testPod, 
api.Kind("Pod"), "test", "testPod", api.Resource("pods"), "", admission.Update, 
nil))
+
+       namespace := &api.Namespace{
+               ObjectMeta: api.ObjectMeta{
+                       Name: testPod.Namespace,
+               },
+       }
+       store.Add(namespace)
+
+       handler := &plugin{
+               clientset: clientset,
+               store:     store,
+       }
+
+       err := handler.Admit(admission.NewAttributesRecord(&testPod, 
api.Kind("Pod"), testPod.Namespace, testPod.Name, api.Resource("pods"), "", 
admission.Update, nil))
        if err == nil {
-               t.Errorf("Expected an error since the pod did not specify 
resource limits in its update call")
+               t.Errorf("Expected admission to fail but it passed!")
        }
 }
 
 func TestPodWithUID(t *testing.T) {
-       client := fake.NewSimpleClientset()
-
-       enforcer := NewUidEnforcer(client)
+       clientset := fake.NewSimpleClientset()
+       store := cache.NewStore(cache.MetaNamespaceKeyFunc)
        testPod := validPod("test", 2)
-       userInfo := &user.DefaultInfo{
-               Name:   "test",
-               UID:    "50",
-               Groups: nil,
+
+       namespace := &api.Namespace{
+               ObjectMeta: api.ObjectMeta{
+                       Name: testPod.Namespace,
+                       Annotations: map[string]string{
+                               "RunAsUser": "100",
+                       },
+               },
+       }
+       store.Add(namespace)
+
+       handler := &plugin{
+               clientset: clientset,
+               store:     store,
        }
 
-       err := enforcer.Admit(admission.NewAttributesRecord(&testPod, 
api.Kind("Pod"), "test", "testPod", api.Resource("pods"), "", admission.Update, 
userInfo))
+       err := handler.Admit(admission.NewAttributesRecord(&testPod, 
api.Kind("Pod"), testPod.Namespace, testPod.Name, api.Resource("pods"), "", 
admission.Update, nil))
        if err != nil {
                t.Errorf("%+v", err)
        }
 
        for _, v := range testPod.Spec.Containers {
                if v.SecurityContext != nil {
-                       if *v.SecurityContext.RunAsUser != 50 {
+                       if *v.SecurityContext.RunAsUser != 100 {
                                t.Errorf("WTF!")
                        }
                } else {

-- 
To view, visit https://gerrit.wikimedia.org/r/281731
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie9fc11c5f8849225b04ac3e581e2eaafaf36bc44
Gerrit-PatchSet: 4
Gerrit-Project: operations/software/kubernetes
Gerrit-Branch: master
Gerrit-Owner: Yuvipanda <[email protected]>
Gerrit-Reviewer: Yuvipanda <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to