CVE-2022-46167: Incorrect Authorization (CWE-863)
value: Capsule is a multi-tenancy and policy-based framework for Kubernetes. Prior to version 0.1.3, a ServiceAccount deployed in a Tenant Namespace, when granted with `PATCH` capabilities on its own Namespace, is able to edit it and remove the Owner Reference, breaking the reconciliation of the Capsule Operator and removing all the enforcement like Pod Security annotations, Network Policies, Limit Range and Resource Quota items. An attacker could detach the Namespace from a Tenant that is forbidding starting privileged Pods using the Pod Security labels by removing the OwnerReference, removing the enforcement labels, and being able to start privileged containers that would be able to start a generic Kubernetes privilege escalation. Patches have been released for version 0.1.3. No known workarounds are available.

CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 8.8
Published: 2022-12-02T19:15Z
Repository: https://github.com/clastix/capsule
Language: Go
Files changed: e2e/sa_prevent_privilege_escalation_test.go
Commit date: 2022-12-02 15:19:06 +0100
Commit message: test: preventing serviceaccount privilege escalation

Diff: diff --git a/e2e/sa_prevent_privilege_escalation_test.go b/e2e/sa_prevent_privilege_escalation_test.go
new file mode 100644
index 0000000..59649e5
--- /dev/null
+++ b/e2e/sa_prevent_privilege_escalation_test.go
@@ -0,0 +1,146 @@
+//go:build e2e
+
+// Copyright 2020-2021 Clastix Labs
+// SPDX-License-Identifier: Apache-2.0
+
+package e2e
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	rbacv1 "k8s.io/api/rbac/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/uuid"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+
+	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
+)
+
+var _ = Describe("trying to escalate from a Tenant Namespace ServiceAccount", func() {
+	tnt := &capsulev1beta1.Tenant{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "sa-privilege-escalation",
+		},
+		Spec: capsulev1beta1.TenantSpec{
+			Owners: capsulev1beta1.OwnerListSpec{
+				{
+					Name: "mario",
+					Kind: "User",
+				},
+			},
+			NodeSelector: map[string]string{
+				"kubernetes.io/os": "linux",
+			},
+		},
+	}
+
+	ns := NewNamespace("attack")
+
+	JustBeforeEach(func() {
+		EventuallyCreation(func() error {
+			return k8sClient.Create(context.TODO(), tnt)
+		}).Should(Succeed())
+
+		NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
+		TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+	})
+
+	JustAfterEach(func() {
+		Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
+	})
+
+	It("should block Namespace changes", func() {
+		role := rbacv1.Role{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "ns-update-role",
+				Namespace: ns.GetName(),
+			},
+			Rules: []rbacv1.PolicyRule{
+				{
+					Verbs:         []string{"update"},
+					APIGroups:     []string{""},
+					Resources:     []string{"namespaces"},
+					ResourceNames: []string{ns.GetName()},
+				},
+			},
+		}
+
+		EventuallyCreation(func() error {
+			return k8sClient.Create(context.Background(), &role)
+		}).Should(Succeed())
+
+		rolebinding := rbacv1.RoleBinding{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "attacker-rolebinding",
+				Namespace: ns.GetName(),
+			},
+			Subjects: []rbacv1.Subject{
+				{
+					Kind:      "ServiceAccount",
+					Name:      "attacker",
+					Namespace: ns.GetName(),
+				},
+			},
+			RoleRef: rbacv1.RoleRef{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "Role",
+				Name:     role.GetName(),
+			},
+		}
+
+		EventuallyCreation(func() error {
+			return k8sClient.Create(context.Background(), &rolebinding)
+		}).Should(Succeed())
+
+		c, err := config.GetConfig()
+		Expect(err).ToNot(HaveOccurred())
+		c.Impersonate.Groups = []string{"system:serviceaccounts"}
+		c.Impersonate.UserName = fmt.Sprintf("system:serviceaccount:%s:%s", rolebinding.Subjects[0].Namespace, rolebinding.Subjects[0].Name)
+		saClient, err := kubernetes.NewForConfig(c)
+		Expect(err).ToNot(HaveOccurred())
+		// Changing Owner Reference is forbidden
+		Consistently(func() error {
+			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
+				return err
+			}
+
+			ns.OwnerReferences[0].UID = uuid.NewUUID()
+
+			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
+
+			return err
+		}, 10*time.Second, time.Second).ShouldNot(Succeed())
+		// Removing Owner Reference is forbidden
+		Consistently(func() error {
+			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
+				return err
+			}
+
+			ns.SetOwnerReferences(nil)
+
+			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
+
+			return err
+		}, 10*time.Second, time.Second).ShouldNot(Succeed())
+		// Breaking nodeSelector is forbidden
+		Consistently(func() error {
+			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
+				return err
+			}
+
+			ns.SetAnnotations(map[string]string{
+				"scheduler.alpha.kubernetes.io/node-selector": "kubernetes.io/os=forbidden",
+			})
+
+			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})
+
+			return err
+		}, 10*time.Second, time.Second).ShouldNot(Succeed())
+	})
+})

Fixed code: //go:build e2e

// Copyright 2020-2021 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package e2e

import (
	"context"
	"fmt"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/uuid"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/controller-runtime/pkg/client/config"

	capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)

var _ = Describe("trying to escalate from a Tenant Namespace ServiceAccount", func() {
	tnt := &capsulev1beta1.Tenant{
		ObjectMeta: metav1.ObjectMeta{
			Name: "sa-privilege-escalation",
		},
		Spec: capsulev1beta1.TenantSpec{
			Owners: capsulev1beta1.OwnerListSpec{
				{
					Name: "mario",
					Kind: "User",
				},
			},
			NodeSelector: map[string]string{
				"kubernetes.io/os": "linux",
			},
		},
	}

	ns := NewNamespace("attack")

	JustBeforeEach(func() {
		EventuallyCreation(func() error {
			return k8sClient.Create(context.TODO(), tnt)
		}).Should(Succeed())

		NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
		TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
	})

	JustAfterEach(func() {
		Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
	})

	It("should block Namespace changes", func() {
		role := rbacv1.Role{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "ns-update-role",
				Namespace: ns.GetName(),
			},
			Rules: []rbacv1.PolicyRule{
				{
					Verbs:         []string{"update"},
					APIGroups:     []string{""},
					Resources:     []string{"namespaces"},
					ResourceNames: []string{ns.GetName()},
				},
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &role)
		}).Should(Succeed())

		rolebinding := rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "attacker-rolebinding",
				Namespace: ns.GetName(),
			},
			Subjects: []rbacv1.Subject{
				{
					Kind:      "ServiceAccount",
					Name:      "attacker",
					Namespace: ns.GetName(),
				},
			},
			RoleRef: rbacv1.RoleRef{
				APIGroup: "rbac.authorization.k8s.io",
				Kind:     "Role",
				Name:     role.GetName(),
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &rolebinding)
		}).Should(Succeed())

		c, err := config.GetConfig()
		Expect(err).ToNot(HaveOccurred())
		c.Impersonate.Groups = []string{"system:serviceaccounts"}
		c.Impersonate.UserName = fmt.Sprintf("system:serviceaccount:%s:%s", rolebinding.Subjects[0].Namespace, rolebinding.Subjects[0].Name)
		saClient, err := kubernetes.NewForConfig(c)
		Expect(err).ToNot(HaveOccurred())
		// Changing Owner Reference is forbidden
		Consistently(func() error {
			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
				return err
			}

			ns.OwnerReferences[0].UID = uuid.NewUUID()

			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})

			return err
		}, 10*time.Second, time.Second).ShouldNot(Succeed())
		// Removing Owner Reference is forbidden
		Consistently(func() error {
			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
				return err
			}

			ns.SetOwnerReferences(nil)

			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})

			return err
		}, 10*time.Second, time.Second).ShouldNot(Succeed())
		// Breaking nodeSelector is forbidden
		Consistently(func() error {
			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: ns.GetName()}, ns); err != nil {
				return err
			}

			ns.SetAnnotations(map[string]string{
				"scheduler.alpha.kubernetes.io/node-selector": "kubernetes.io/os=forbidden",
			})

			_, err = saClient.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{})

			return err
		}, 10*time.Second, time.Second).ShouldNot(Succeed())
	})
})