CVE-2022-46167 (Go, published 2022-12-02T19:15Z)
Description: 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.

Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/clastix/capsule
Files changed:
e2e/sa_prevent_privilege_escalation_test.go
Commit message: test: preventing serviceaccount privilege escalation


Vulnerable code:

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())
	})
})