diff --git a/charts/capsule/templates/configuration-default.yaml b/charts/capsule/templates/configuration-default.yaml index e4a7754c..bc3b07ec 100644 --- a/charts/capsule/templates/configuration-default.yaml +++ b/charts/capsule/templates/configuration-default.yaml @@ -25,4 +25,5 @@ spec: nodeMetadata: {{- toYaml . | nindent 4 }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} + diff --git a/e2e/namespace_hijacking_test.go b/e2e/namespace_hijacking_test.go new file mode 100644 index 00000000..7b7540e8 --- /dev/null +++ b/e2e/namespace_hijacking_test.go @@ -0,0 +1,119 @@ +//go:build e2e + +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "fmt" + corev1 "k8s.io/api/core/v1" + "math/rand" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("creating several Namespaces for a Tenant", func() { + tnt := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "capsule-ns-attack-1", + }, + Spec: capsulev1beta2.TenantSpec{ + Owners: capsulev1beta2.OwnerListSpec{ + { + Name: "charlie", + Kind: "User", + }, + { + Kind: "ServiceAccount", + Name: "system:serviceaccount:attacker-system:attacker", + }, + }, + }, + } + + kubeSystem := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + } + JustBeforeEach(func() { + EventuallyCreation(func() (err error) { + tnt.ResourceVersion = "" + err = k8sClient.Create(context.TODO(), tnt) + + return + }).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + + }) + + It("Can't hijack offlimits namespace", func() { + tenant := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed()) + + // Get the namespace + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed()) + + for _, owner := range tnt.Spec.Owners { + cs := ownerClient(owner) + + patch := []byte(fmt.Sprintf(`{"metadata":{"ownerReferences":[{"apiVersion":"%s/%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`, capsulev1beta2.GroupVersion.Group, capsulev1beta2.GroupVersion.Version, tenant.GetName(), tenant.GetUID())) + + _, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + Expect(err).To(HaveOccurred()) + + } + }) + + It("Owners can create and attempt to patch new namespaces but patches should not be applied", func() { + for _, owner := range tnt.Spec.Owners { + cs := ownerClient(owner) + + // Each owner creates a new namespace + ns := NewNamespace("") + NamespaceCreation(ns, owner, defaultTimeoutInterval).Should(Succeed()) + + // Attempt to patch the owner references of the new namespace + tenant := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed()) + + randomUID := types.UID(fmt.Sprintf("%d", rand.Int())) + randomName := fmt.Sprintf("random-tenant-%d", rand.Int()) + patch := []byte(fmt.Sprintf(`{"metadata":{"ownerReferences":[{"apiVersion":"%s/%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`, capsulev1beta2.GroupVersion.Group, capsulev1beta2.GroupVersion.Version, randomName, randomUID)) + + _, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + Expect(err).ToNot(HaveOccurred()) + + retrievedNs := &corev1.Namespace{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.Name}, retrievedNs)).Should(Succeed()) + + // Check if the namespace has an owner reference with the specific UID and name + hasSpecificOwnerRef := false + for _, ownerRef := range retrievedNs.OwnerReferences { + if ownerRef.UID == randomUID && ownerRef.Name == randomName { + hasSpecificOwnerRef = true + break + } + } + Expect(hasSpecificOwnerRef).To(BeFalse(), "Namespace should not have owner reference with UID %s and name %s", randomUID, randomName) + + hasOriginReference := false + for _, ownerRef := range retrievedNs.OwnerReferences { + if ownerRef.UID == tenant.GetUID() && ownerRef.Name == tenant.GetName() { + hasOriginReference = true + break + } + } + Expect(hasOriginReference).To(BeTrue(), "Namespace should have origin reference", tenant.GetUID(), tenant.GetName()) + } + }) + +}) diff --git a/pkg/webhook/ownerreference/patching.go b/pkg/webhook/ownerreference/patching.go index 9b32a793..8dde2816 100644 --- a/pkg/webhook/ownerreference/patching.go +++ b/pkg/webhook/ownerreference/patching.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "k8s.io/apimachinery/pkg/fields" "net/http" "sort" "strings" @@ -49,15 +50,26 @@ func (h *handler) OnDelete(client.Client, admission.Decoder, record.EventRecorde } } -func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { - return func(_ context.Context, req admission.Request) *admission.Response { +func (h *handler) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { oldNs := &corev1.Namespace{} if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil { return utils.ErroredResponse(err) } - if len(oldNs.OwnerReferences) == 0 { - return nil + tntList := &capsulev1beta2.TenantList{} + if err := c.List(ctx, tntList, client.MatchingFieldsSelector{ + Selector: fields.OneTermEqualSelector(".status.namespaces", oldNs.Name), + }); err != nil { + return utils.ErroredResponse(err) + } + + if !h.namespaceIsOwned(oldNs, tntList, req) { + recorder.Eventf(oldNs, corev1.EventTypeWarning, "OfflimitNamespace", "Namespace %s can not be patched", oldNs.GetName()) + + response := admission.Denied("Denied patch request for this namespace") + + return &response } newNs := &corev1.Namespace{} @@ -101,6 +113,21 @@ func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record. } } +func (h *handler) namespaceIsOwned(ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) bool { + for _, tenant := range tenantList.Items { + for _, ownerRef := range ns.OwnerReferences { + if !capsuleutils.IsTenantOwnerReference(ownerRef) { + continue + } + if ownerRef.UID == tenant.UID && utils.IsTenantOwner(tenant.Spec.Owners, req.UserInfo) { + return true + } + } + } + + return false +} + func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response { ns := &corev1.Namespace{} if err := decoder.Decode(req, ns); err != nil {