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