Skip to content

Commit cc28dcb

Browse files
mhenriksawels
andauthored
Address CVE-2024-1725: Restrict access to infrastructure PVCs by requiring matching infraClusterLabels on tenant PVCs (#103)
The CVE describes how an attacker may create a PV/PVC in a guest cluster to access any PVC in the infra cluster namespace. The infra clusters may belong to other guest clusters or have been created out of band from the kubevirt-csi driver. This PR addresses the issue by: 1. infraClusterLabels are required (but is up to admin to make sure they are unique per tenant) 2. guest may only access infra PVCs with matching labels 3. guest can only access PVCs with specific prefix (default is "pvc-") Shoutout to awels who actually implemented this based on input from davidvossel. Signed-off-by: Michael Henriksen <[email protected]> Co-authored-by: Alexander Wels <[email protected]>
1 parent a722f94 commit cc28dcb

File tree

8 files changed

+296
-33
lines changed

8 files changed

+296
-33
lines changed

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ include $(addprefix ./vendor/github.com/openshift/build-machinery-go/make/, \
4646
# You can list all codegen related variables by:
4747
# $ make -n --print-data-base | grep ^CODEGEN
4848
.PHONY: image-build
49-
image-build: generate
49+
# let's disable generate for for now
50+
# it updates libs and I think it is better to do that manually
51+
# especially when changes will be backported
52+
#image-build: generate
53+
image-build:
5054
source ./hack/cri-bin.sh && \
5155
$$CRI_BIN build -t $(IMAGE_REF) --build-arg git_sha=$(SHA) .
5256

@@ -116,4 +120,4 @@ sanity-test:
116120

117121
.PHONY: generate
118122
generate:
119-
./hack/generate_clients.sh
123+
./hack/generate_clients.sh

cmd/kubevirt-csi-driver/kubevirt-csi-driver.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var (
2525
infraClusterNamespace = flag.String("infra-cluster-namespace", "", "The infra-cluster namespace")
2626
infraClusterKubeconfig = flag.String("infra-cluster-kubeconfig", "", "the infra-cluster kubeconfig file. If not set, defaults to in cluster config.")
2727
infraClusterLabels = flag.String("infra-cluster-labels", "", "The infra-cluster labels to use when creating resources in infra cluster. 'name=value' fields separated by a comma")
28+
volumePrefix = flag.String("volume-prefix", "pvc", "The prefix expected for persistent volumes")
2829
// infraStorageClassEnforcement = flag.String("infra-storage-class-enforcement", "", "A string encoded yaml that represents the policy of enforcing which infra storage classes are allowed in persistentVolume of type kubevirt")
2930
infraStorageClassEnforcement = os.Getenv("INFRA_STORAGE_CLASS_ENFORCEMENT")
3031

@@ -54,6 +55,13 @@ func handle() {
5455
}
5556
klog.V(2).Infof("Driver vendor %v %v", service.VendorName, service.VendorVersion)
5657

58+
if (infraClusterLabels == nil || *infraClusterLabels == "") && !*runNodeService {
59+
klog.Fatal("infra-cluster-labels must be set")
60+
}
61+
if volumePrefix == nil || *volumePrefix == "" {
62+
klog.Fatal("volume-prefix must be set")
63+
}
64+
5765
inClusterConfig, err := rest.InClusterConfig()
5866
if err != nil {
5967
klog.Fatalf("Failed to build in cluster config: %v", err)
@@ -86,7 +94,7 @@ func handle() {
8694
infraClusterLabelsMap := parseLabels()
8795
storageClassEnforcement := configureStorageClassEnforcement(infraStorageClassEnforcement)
8896

89-
virtClient, err := kubevirt.NewClient(infraRestConfig, infraClusterLabelsMap, storageClassEnforcement)
97+
virtClient, err := kubevirt.NewClient(infraRestConfig, infraClusterLabelsMap, storageClassEnforcement, *volumePrefix)
9098
if err != nil {
9199
klog.Fatal(err)
92100
}

e2e/common_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"k8s.io/client-go/tools/clientcmd"
2323
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2424
"k8s.io/klog/v2"
25+
cdicli "kubevirt.io/csi-driver/pkg/generated/containerized-data-importer/client-go/clientset/versioned"
2526
)
2627

2728
// RunCmd function executes a command, and returns STDOUT and STDERR bytes
@@ -192,6 +193,15 @@ func generateInfraClient() (*kubernetes.Clientset, error) {
192193
return kubernetes.NewForConfig(restConfig)
193194
}
194195

196+
func generateInfraCdiClient() (*cdicli.Clientset, error) {
197+
restConfig, err := generateInfraRestConfig()
198+
if err != nil {
199+
return nil, err
200+
}
201+
202+
return cdicli.NewForConfig(restConfig)
203+
}
204+
195205
func generateInfraSnapClient() (*snapcli.Clientset, error) {
196206
restConfig, err := generateInfraRestConfig()
197207
if err != nil {

e2e/create-pvc_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,22 @@ import (
1010

1111
"k8s.io/apimachinery/pkg/api/errors"
1212
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
13+
"k8s.io/klog/v2"
1314

1415
"k8s.io/client-go/tools/clientcmd"
16+
cdicli "kubevirt.io/csi-driver/pkg/generated/containerized-data-importer/client-go/clientset/versioned"
1517
kubecli "kubevirt.io/csi-driver/pkg/generated/kubevirt/client-go/clientset/versioned"
1618

1719
. "github.com/onsi/ginkgo/v2"
1820
. "github.com/onsi/gomega"
21+
1922
"github.com/spf13/pflag"
2023
k8sv1 "k8s.io/api/core/v1"
2124
"k8s.io/apimachinery/pkg/api/resource"
2225
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2326
"k8s.io/apimachinery/pkg/util/rand"
2427
"k8s.io/client-go/kubernetes"
28+
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
2529
)
2630

2731
var virtClient *kubecli.Clientset
@@ -282,6 +286,136 @@ var _ = Describe("CreatePVC", func() {
282286
Entry("Filesystem volume mode", k8sv1.PersistentVolumeFilesystem, attacherPodFs),
283287
Entry("Block volume mode", k8sv1.PersistentVolumeBlock, attacherPodBlock),
284288
)
289+
290+
Context("Should prevent access to volumes from infra cluster", func() {
291+
var tenantPVC *k8sv1.PersistentVolumeClaim
292+
var tenantPV *k8sv1.PersistentVolume
293+
var infraDV *cdiv1.DataVolume
294+
var infraCdiClient *cdicli.Clientset
295+
BeforeEach(func() {
296+
var err error
297+
infraCdiClient, err = generateInfraCdiClient()
298+
Expect(err).ToNot(HaveOccurred())
299+
})
300+
301+
AfterEach(func() {
302+
By("Cleaning up resources for test")
303+
if tenantPVC != nil {
304+
err := tenantClient.CoreV1().PersistentVolumeClaims(tenantPVC.Namespace).Delete(context.Background(), tenantPVC.Name, metav1.DeleteOptions{})
305+
Expect(err).ToNot(HaveOccurred())
306+
Eventually(func() bool {
307+
_, err := tenantClient.CoreV1().PersistentVolumeClaims(tenantPVC.Namespace).Get(context.Background(), tenantPVC.Name, metav1.GetOptions{})
308+
return errors.IsNotFound(err)
309+
}, 1*time.Minute, 2*time.Second).Should(BeTrue(), "tenant pvc should disappear")
310+
tenantPVC = nil
311+
}
312+
if tenantPV != nil {
313+
err := tenantClient.CoreV1().PersistentVolumes().Delete(context.Background(), tenantPV.Name, metav1.DeleteOptions{})
314+
Expect(err).ToNot(HaveOccurred())
315+
// For some reason this takes about 2 minutes.
316+
Eventually(func() bool {
317+
_, err := tenantClient.CoreV1().PersistentVolumes().Get(context.Background(), tenantPV.Name, metav1.GetOptions{})
318+
return errors.IsNotFound(err)
319+
}, 3*time.Minute, 2*time.Second).Should(BeTrue(), "tenant pv should disappear")
320+
tenantPV = nil
321+
}
322+
if infraDV != nil {
323+
_, err := infraCdiClient.CdiV1beta1().DataVolumes(InfraClusterNamespace).Get(context.Background(), infraDV.Name, metav1.GetOptions{})
324+
Expect(err).ToNot(HaveOccurred())
325+
err = infraCdiClient.CdiV1beta1().DataVolumes(InfraClusterNamespace).Delete(context.Background(), infraDV.Name, metav1.DeleteOptions{})
326+
Expect(err).ToNot(HaveOccurred())
327+
infraDV = nil
328+
}
329+
})
330+
331+
It("should not be able to create a PV and access a volume from the infra cluster that is not labeled", func() {
332+
infraDV = &cdiv1.DataVolume{
333+
ObjectMeta: metav1.ObjectMeta{
334+
Name: "infra-pvc",
335+
Namespace: InfraClusterNamespace,
336+
},
337+
Spec: cdiv1.DataVolumeSpec{
338+
Source: &cdiv1.DataVolumeSource{
339+
Blank: &cdiv1.DataVolumeBlankImage{},
340+
},
341+
Storage: &cdiv1.StorageSpec{
342+
Resources: k8sv1.ResourceRequirements{
343+
Requests: k8sv1.ResourceList{
344+
k8sv1.ResourceStorage: resource.MustParse("1Gi"),
345+
},
346+
},
347+
},
348+
},
349+
}
350+
var err error
351+
infraDV, err = infraCdiClient.CdiV1beta1().DataVolumes(InfraClusterNamespace).Create(context.Background(), infraDV, metav1.CreateOptions{})
352+
Expect(err).ToNot(HaveOccurred())
353+
354+
By("Creating a specially crafted PV, attempt to access volume from infra cluster that should not be accessed")
355+
tenantPV = &k8sv1.PersistentVolume{
356+
ObjectMeta: metav1.ObjectMeta{
357+
Name: "tenant-pv",
358+
},
359+
Spec: k8sv1.PersistentVolumeSpec{
360+
AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce},
361+
Capacity: k8sv1.ResourceList{k8sv1.ResourceStorage: resource.MustParse("1Gi")},
362+
PersistentVolumeSource: k8sv1.PersistentVolumeSource{
363+
CSI: &k8sv1.CSIPersistentVolumeSource{
364+
Driver: "csi.kubevirt.io",
365+
VolumeHandle: infraDV.Name,
366+
VolumeAttributes: map[string]string{
367+
"bus": "scsi",
368+
"serial": "abcd",
369+
"storage.kubernetes.io/csiProvisionerIdentity": "1708112628060-923-csi.kubevirt.io",
370+
},
371+
FSType: "ext4",
372+
},
373+
},
374+
StorageClassName: "kubevirt",
375+
PersistentVolumeReclaimPolicy: k8sv1.PersistentVolumeReclaimDelete,
376+
},
377+
}
378+
_, err = tenantClient.CoreV1().PersistentVolumes().Create(context.Background(), tenantPV, metav1.CreateOptions{})
379+
Expect(err).ToNot(HaveOccurred())
380+
tenantPVC = &k8sv1.PersistentVolumeClaim{
381+
ObjectMeta: metav1.ObjectMeta{
382+
Name: "tenant-pvc",
383+
},
384+
Spec: k8sv1.PersistentVolumeClaimSpec{
385+
AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce},
386+
Resources: k8sv1.ResourceRequirements{
387+
Requests: k8sv1.ResourceList{
388+
k8sv1.ResourceStorage: resource.MustParse("1Gi"),
389+
},
390+
},
391+
VolumeName: tenantPV.Name,
392+
},
393+
}
394+
tenantPVC, err = tenantClient.CoreV1().PersistentVolumeClaims(namespace).Create(context.Background(), tenantPVC, metav1.CreateOptions{})
395+
Expect(err).ToNot(HaveOccurred())
396+
pod := writerPodFs(tenantPVC.Name)
397+
By("Creating pod that attempts to use the specially crafted PVC")
398+
pod, err = tenantClient.CoreV1().Pods(namespace).Create(context.Background(), pod, metav1.CreateOptions{})
399+
Expect(err).ToNot(HaveOccurred())
400+
defer deletePod(tenantClient.CoreV1(), namespace, pod.Name)
401+
402+
involvedObject := fmt.Sprintf("involvedObject.name=%s", pod.Name)
403+
By("Waiting for error event to show up in pod event log")
404+
Eventually(func() bool {
405+
list, err := tenantClient.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{
406+
FieldSelector: involvedObject, TypeMeta: metav1.TypeMeta{Kind: "Pod"},
407+
})
408+
Expect(err).ToNot(HaveOccurred())
409+
for _, event := range list.Items {
410+
klog.Infof("Event: %s [%s]", event.Message, event.Reason)
411+
if event.Reason == "FailedAttachVolume" && strings.Contains(event.Message, "invalid volume name") {
412+
return true
413+
}
414+
}
415+
return false
416+
}, 30*time.Second, time.Second).Should(BeTrue(), "error event should show up in pod event log")
417+
})
418+
})
285419
})
286420

287421
func writerPodFs(volumeName string) *k8sv1.Pod {

hack/cluster-sync.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ INFRA_STORAGE_CLASS=${INFRA_STORAGE_CLASS:-rook-ceph-block}
88
REGISTRY=${REGISTRY:-192.168.66.2:5000}
99
TARGET_NAME=${TARGET_NAME:-kubevirt-csi-driver}
1010
TAG=${TAG:-latest}
11+
export INFRACLUSTER_LABELS=${INFRACLUSTER_LABELS:-"tenant-cluster=${TENANT_CLUSTER_NAMESPACE}"}
1112

1213
function tenant::deploy_kubeconfig_secret() {
1314
TOKEN=$(_kubectl create token kubevirt-csi -n $TENANT_CLUSTER_NAMESPACE)

pkg/kubevirt/client.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
goerrors "errors"
77
"fmt"
8+
"strings"
89
"time"
910

1011
snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
@@ -62,10 +63,11 @@ type client struct {
6263
restClient *rest.RESTClient
6364
storageClassEnforcement util.StorageClassEnforcement
6465
infraLabelMap map[string]string
66+
volumePrefix string
6567
}
6668

6769
// NewClient New creates our client wrapper object for the actual kubeVirt and kubernetes clients we use.
68-
func NewClient(config *rest.Config, infraClusterLabelMap map[string]string, storageClassEnforcement util.StorageClassEnforcement) (Client, error) {
70+
func NewClient(config *rest.Config, infraClusterLabelMap map[string]string, storageClassEnforcement util.StorageClassEnforcement, prefix string) (Client, error) {
6971
result := &client{}
7072

7173
Scheme := runtime.NewScheme()
@@ -108,6 +110,7 @@ func NewClient(config *rest.Config, infraClusterLabelMap map[string]string, stor
108110
result.restClient = restClient
109111
result.snapClient = snapClient
110112
result.infraLabelMap = infraClusterLabelMap
113+
result.volumePrefix = fmt.Sprintf("%s-", prefix)
111114
result.storageClassEnforcement = storageClassEnforcement
112115
return result, nil
113116
}
@@ -211,7 +214,11 @@ func (c *client) GetVirtualMachine(ctx context.Context, namespace, name string)
211214

212215
// CreateDataVolume creates a new DataVolume under a namespace
213216
func (c *client) CreateDataVolume(ctx context.Context, namespace string, dataVolume *cdiv1.DataVolume) (*cdiv1.DataVolume, error) {
214-
return c.cdiClient.CdiV1beta1().DataVolumes(namespace).Create(ctx, dataVolume, metav1.CreateOptions{})
217+
if !strings.HasPrefix(dataVolume.GetName(), c.volumePrefix) {
218+
return nil, ErrInvalidVolume
219+
} else {
220+
return c.cdiClient.CdiV1beta1().DataVolumes(namespace).Create(ctx, dataVolume, metav1.CreateOptions{})
221+
}
215222
}
216223

217224
// Ping performs a minimal request to the infra-cluster k8s api
@@ -222,11 +229,27 @@ func (c *client) Ping(ctx context.Context) error {
222229

223230
// DeleteDataVolume deletes a DataVolume from a namespace by name
224231
func (c *client) DeleteDataVolume(ctx context.Context, namespace string, name string) error {
225-
return c.cdiClient.CdiV1beta1().DataVolumes(namespace).Delete(ctx, name, metav1.DeleteOptions{})
232+
if dv, err := c.GetDataVolume(ctx, namespace, name); errors.IsNotFound(err) {
233+
return nil
234+
} else if err != nil {
235+
return err
236+
} else if dv != nil {
237+
return c.cdiClient.CdiV1beta1().DataVolumes(namespace).Delete(ctx, dv.Name, metav1.DeleteOptions{})
238+
}
239+
return nil
226240
}
227241

228242
func (c *client) GetDataVolume(ctx context.Context, namespace string, name string) (*cdiv1.DataVolume, error) {
229-
return c.cdiClient.CdiV1beta1().DataVolumes(namespace).Get(ctx, name, metav1.GetOptions{})
243+
dv, err := c.cdiClient.CdiV1beta1().DataVolumes(namespace).Get(ctx, name, metav1.GetOptions{})
244+
if err != nil {
245+
return nil, err
246+
}
247+
if dv != nil {
248+
if !containsLabels(dv.Labels, c.infraLabelMap) || !strings.HasPrefix(dv.GetName(), c.volumePrefix) {
249+
return nil, ErrInvalidVolume
250+
}
251+
}
252+
return dv, nil
230253
}
231254

232255
func (c *client) CreateVolumeSnapshot(ctx context.Context, namespace, name, claimName, snapshotClassName string) (*snapshotv1.VolumeSnapshot, error) {
@@ -407,3 +430,4 @@ func (c *client) ListVolumeSnapshots(ctx context.Context, namespace string) (*sn
407430
}
408431

409432
var ErrInvalidSnapshot = goerrors.New("invalid snapshot name")
433+
var ErrInvalidVolume = goerrors.New("invalid volume name")

0 commit comments

Comments
 (0)