Skip to content

Commit d620b04

Browse files
Merge commit from fork
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
1 parent 1d9fcc7 commit d620b04

File tree

3 files changed

+152
-5
lines changed

3 files changed

+152
-5
lines changed

charts/capsule/templates/configuration-default.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ spec:
2525
nodeMetadata:
2626
{{- toYaml . | nindent 4 }}
2727
{{- end }}
28-
{{- end }}
28+
{{- end }}
29+

e2e/namespace_hijacking_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//go:build e2e
2+
3+
// Copyright 2020-2023 Project Capsule Authors.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package e2e
7+
8+
import (
9+
"context"
10+
"fmt"
11+
corev1 "k8s.io/api/core/v1"
12+
"math/rand"
13+
14+
. "github.com/onsi/ginkgo/v2"
15+
. "github.com/onsi/gomega"
16+
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/types"
19+
)
20+
21+
var _ = Describe("creating several Namespaces for a Tenant", func() {
22+
tnt := &capsulev1beta2.Tenant{
23+
ObjectMeta: metav1.ObjectMeta{
24+
Name: "capsule-ns-attack-1",
25+
},
26+
Spec: capsulev1beta2.TenantSpec{
27+
Owners: capsulev1beta2.OwnerListSpec{
28+
{
29+
Name: "charlie",
30+
Kind: "User",
31+
},
32+
{
33+
Kind: "ServiceAccount",
34+
Name: "system:serviceaccount:attacker-system:attacker",
35+
},
36+
},
37+
},
38+
}
39+
40+
kubeSystem := &corev1.Namespace{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: "kube-system",
43+
},
44+
}
45+
JustBeforeEach(func() {
46+
EventuallyCreation(func() (err error) {
47+
tnt.ResourceVersion = ""
48+
err = k8sClient.Create(context.TODO(), tnt)
49+
50+
return
51+
}).Should(Succeed())
52+
})
53+
JustAfterEach(func() {
54+
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
55+
56+
})
57+
58+
It("Can't hijack offlimits namespace", func() {
59+
tenant := &capsulev1beta2.Tenant{}
60+
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed())
61+
62+
// Get the namespace
63+
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: kubeSystem.GetName()}, kubeSystem)).Should(Succeed())
64+
65+
for _, owner := range tnt.Spec.Owners {
66+
cs := ownerClient(owner)
67+
68+
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()))
69+
70+
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), kubeSystem.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
71+
Expect(err).To(HaveOccurred())
72+
73+
}
74+
})
75+
76+
It("Owners can create and attempt to patch new namespaces but patches should not be applied", func() {
77+
for _, owner := range tnt.Spec.Owners {
78+
cs := ownerClient(owner)
79+
80+
// Each owner creates a new namespace
81+
ns := NewNamespace("")
82+
NamespaceCreation(ns, owner, defaultTimeoutInterval).Should(Succeed())
83+
84+
// Attempt to patch the owner references of the new namespace
85+
tenant := &capsulev1beta2.Tenant{}
86+
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tenant)).Should(Succeed())
87+
88+
randomUID := types.UID(fmt.Sprintf("%d", rand.Int()))
89+
randomName := fmt.Sprintf("random-tenant-%d", rand.Int())
90+
patch := []byte(fmt.Sprintf(`{"metadata":{"ownerReferences":[{"apiVersion":"%s/%s","kind":"Tenant","name":"%s","uid":"%s"}]}}`, capsulev1beta2.GroupVersion.Group, capsulev1beta2.GroupVersion.Version, randomName, randomUID))
91+
92+
_, err := cs.CoreV1().Namespaces().Patch(context.TODO(), ns.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
93+
Expect(err).ToNot(HaveOccurred())
94+
95+
retrievedNs := &corev1.Namespace{}
96+
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.Name}, retrievedNs)).Should(Succeed())
97+
98+
// Check if the namespace has an owner reference with the specific UID and name
99+
hasSpecificOwnerRef := false
100+
for _, ownerRef := range retrievedNs.OwnerReferences {
101+
if ownerRef.UID == randomUID && ownerRef.Name == randomName {
102+
hasSpecificOwnerRef = true
103+
break
104+
}
105+
}
106+
Expect(hasSpecificOwnerRef).To(BeFalse(), "Namespace should not have owner reference with UID %s and name %s", randomUID, randomName)
107+
108+
hasOriginReference := false
109+
for _, ownerRef := range retrievedNs.OwnerReferences {
110+
if ownerRef.UID == tenant.GetUID() && ownerRef.Name == tenant.GetName() {
111+
hasOriginReference = true
112+
break
113+
}
114+
}
115+
Expect(hasOriginReference).To(BeTrue(), "Namespace should have origin reference", tenant.GetUID(), tenant.GetName())
116+
}
117+
})
118+
119+
})

pkg/webhook/ownerreference/patching.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/json"
99
"fmt"
10+
"k8s.io/apimachinery/pkg/fields"
1011
"net/http"
1112
"sort"
1213
"strings"
@@ -49,15 +50,26 @@ func (h *handler) OnDelete(client.Client, admission.Decoder, record.EventRecorde
4950
}
5051
}
5152

52-
func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
53-
return func(_ context.Context, req admission.Request) *admission.Response {
53+
func (h *handler) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
54+
return func(ctx context.Context, req admission.Request) *admission.Response {
5455
oldNs := &corev1.Namespace{}
5556
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
5657
return utils.ErroredResponse(err)
5758
}
5859

59-
if len(oldNs.OwnerReferences) == 0 {
60-
return nil
60+
tntList := &capsulev1beta2.TenantList{}
61+
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
62+
Selector: fields.OneTermEqualSelector(".status.namespaces", oldNs.Name),
63+
}); err != nil {
64+
return utils.ErroredResponse(err)
65+
}
66+
67+
if !h.namespaceIsOwned(oldNs, tntList, req) {
68+
recorder.Eventf(oldNs, corev1.EventTypeWarning, "OfflimitNamespace", "Namespace %s can not be patched", oldNs.GetName())
69+
70+
response := admission.Denied("Denied patch request for this namespace")
71+
72+
return &response
6173
}
6274

6375
newNs := &corev1.Namespace{}
@@ -101,6 +113,21 @@ func (h *handler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.
101113
}
102114
}
103115

116+
func (h *handler) namespaceIsOwned(ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) bool {
117+
for _, tenant := range tenantList.Items {
118+
for _, ownerRef := range ns.OwnerReferences {
119+
if !capsuleutils.IsTenantOwnerReference(ownerRef) {
120+
continue
121+
}
122+
if ownerRef.UID == tenant.UID && utils.IsTenantOwner(tenant.Spec.Owners, req.UserInfo) {
123+
return true
124+
}
125+
}
126+
}
127+
128+
return false
129+
}
130+
104131
func (h *handler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response {
105132
ns := &corev1.Namespace{}
106133
if err := decoder.Decode(req, ns); err != nil {

0 commit comments

Comments
 (0)