CVE-2020-1701
Description
A flaw was found in the KubeVirt main virt-handler versions before 0.26.0 regarding the access permissions of virt-handler. An attacker with access to create VMs could attach any secret within their namespace, allowing them to read the contents of that secret.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
KubeVirt virt-handler before 0.26.0 had overly broad secret access, allowing attackers with VM creation privileges to read any secret in the namespace.
Vulnerability
A flaw in KubeVirt's virt-handler component, versions before 0.26.0, granted the handler excessive Kubernetes API permissions to GET secrets across all namespaces. This was required because virt-handler directly fetched cloud-init user data from secrets via the API to inject into virtual machines. The vulnerability is rooted in the design where virt-handler needed to resolve secret references for CloudInitNoCloud volumes, which it did by calling the Kubernetes API rather than relying on volume mounts [1][2].
Exploitation
An attacker with the ability to create virtual machines (VMs) within a namespace can exploit this flaw. By attaching any secret in the same namespace to a VM (e.g., via a CloudInitNoCloud volume with a UserDataSecretRef), the attacker triggers virt-handler to read that secret's contents. No additional authentication or network position is required beyond the ability to create VMs [4].
Impact
Successful exploitation allows the attacker to read the full contents of any secret in the namespace, leading to unauthorized disclosure of sensitive data such as passwords, API tokens, SSH keys, or other credentials. This compromises the confidentiality of secrets managed within the Kubernetes namespace [2].
Mitigation
The vulnerability is fixed in KubeVirt version 0.26.0. The fix changes the approach: virt-controller now mounts secrets as volumes to the virt-launcher pod, and virt-handler reads the secret data from the mounted filesystem instead of via the API. This removes the need for virt-handler to have GET secrets permission across the cluster. Users should upgrade to 0.26.0 or later. No workaround is available other than restricting VM creation permissions or upgrading [1][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
kubevirt.io/kubevirtGo | < 0.26.0 | 0.26.0 |
Affected products
2- KubeVirt/virt-handlerdescription
Patches
19efa8d7388d4Merge pull request #3001 from danielBelenky/cloud-init-secrets
13 files changed · +312 −366
manifests/generated/operator-csv.yaml.in+1 −1 modified@@ -503,7 +503,6 @@ spec: - apiGroups: - "" resources: - - secrets - persistentvolumeclaims verbs: - get @@ -542,6 +541,7 @@ spec: - secrets verbs: - create + - get - apiGroups: - subresources.kubevirt.io resources:
manifests/generated/rbac-kubevirt.authorization.k8s.yaml.in+1 −1 modified@@ -311,7 +311,6 @@ rules: - apiGroups: - "" resources: - - secrets - persistentvolumeclaims verbs: - get @@ -374,6 +373,7 @@ rules: - secrets verbs: - create + - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding
manifests/generated/rbac-operator.authorization.k8s.yaml.in+1 −1 modified@@ -358,7 +358,6 @@ rules: - apiGroups: - "" resources: - - secrets - persistentvolumeclaims verbs: - get @@ -397,6 +396,7 @@ rules: - secrets verbs: - create + - get - apiGroups: - subresources.kubevirt.io resources:
pkg/cloud-init/BUILD.bazel+0 −7 modified@@ -9,11 +9,8 @@ go_library( "//pkg/ephemeral-disk-utils:go_default_library", "//pkg/util/net/dns:go_default_library", "//staging/src/kubevirt.io/client-go/api/v1:go_default_library", - "//staging/src/kubevirt.io/client-go/kubecli:go_default_library", "//staging/src/kubevirt.io/client-go/log:go_default_library", "//staging/src/kubevirt.io/client-go/precond:go_default_library", - "//vendor/k8s.io/api/core/v1:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", ], ) @@ -27,14 +24,10 @@ go_test( deps = [ "//pkg/ephemeral-disk-utils:go_default_library", "//staging/src/kubevirt.io/client-go/api/v1:go_default_library", - "//staging/src/kubevirt.io/client-go/kubecli:go_default_library", "//staging/src/kubevirt.io/client-go/log:go_default_library", "//staging/src/kubevirt.io/client-go/precond:go_default_library", - "//vendor/github.com/golang/mock/gomock:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", - "//vendor/k8s.io/client-go/kubernetes/fake:go_default_library", ], )
pkg/cloud-init/cloud-init.go+97 −92 modified@@ -25,13 +25,10 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "time" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "kubevirt.io/client-go/api/v1" - "kubevirt.io/client-go/kubecli" "kubevirt.io/client-go/log" "kubevirt.io/client-go/precond" diskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" @@ -72,7 +69,7 @@ func IsValidCloudInitData(cloudInitData *CloudInitData) bool { // ReadCloudInitVolumeDataSource scans the given VMI for CloudInit volumes and // reads their content into a CloudInitData struct. Does not resolve secret refs. -// To ensure that secrets are read correctly, call InjectCloudInitSecrets beforehand. +// To ensure that secrets are read correctly, call ResolveNoCloudSecrets beforehand. func ReadCloudInitVolumeDataSource(vmi *v1.VirtualMachineInstance) (cloudInitData *CloudInitData, err error) { precond.MustNotBeNil(vmi) hostname := dns.SanitizeHostname(vmi) @@ -92,6 +89,101 @@ func ReadCloudInitVolumeDataSource(vmi *v1.VirtualMachineInstance) (cloudInitDat return nil, nil } +// ResolveNoCloudSecrets is looking for CloudInitNoCloud volumes with UserDataSecretRef +// requests. It reads the `userdata` secret the corresponds to the given CloudInitNoCloud +// volume and sets the UserData field on that volume. +// +// Note: when using this function, make sure that your code can access the secret volumes. +func ResolveNoCloudSecrets(vmi *v1.VirtualMachineInstance, secretSourceDir string) error { + volume := findCloudInitNoCloudSecretVolume(vmi.Spec.Volumes) + if volume == nil { + return nil + } + + baseDir := filepath.Join(secretSourceDir, volume.Name) + userData, userDataError := readFileFromDir(baseDir, "userdata") + networkData, networkDataError := readFileFromDir(baseDir, "networkdata") + if userDataError != nil && networkDataError != nil { + return fmt.Errorf("no cloud-init data-source found at volume: %s", volume.Name) + } + + if userData != "" { + volume.CloudInitNoCloud.UserData = userData + } + if networkData != "" { + volume.CloudInitNoCloud.NetworkData = networkData + } + + return nil +} + +// ResolveConfigDriveSecrets is looking for CloudInitConfigDriveSource volume source with +// UserDataSecretRef and NetworkDataSecretRef and resolves the secret from the corresponding +// VolumeMount. +// +// Note: when using this function, make sure that your code can access the secret volumes. +func ResolveConfigDriveSecrets(vmi *v1.VirtualMachineInstance, secretSourceDir string) error { + volume := findCloudInitConfigDriveSecretVolume(vmi.Spec.Volumes) + if volume == nil { + return nil + } + + baseDir := filepath.Join(secretSourceDir, volume.Name) + userData, userDataError := readFileFromDir(baseDir, "userdata") + networkData, networkDataError := readFileFromDir(baseDir, "networkdata") + if userDataError != nil && networkDataError != nil { + return fmt.Errorf("no cloud-init data-source found at volume: %s", volume.Name) + } + + if userData != "" { + volume.CloudInitConfigDrive.UserData = userData + } + if networkData != "" { + volume.CloudInitConfigDrive.NetworkData = networkData + } + + return nil +} + +// findCloudInitConfigDriveSecretVolume loops over a given list of volumes and return a pointer +// to the first volume with a CloudInitConfigDrive source and UserDataSecretRef field set. +func findCloudInitConfigDriveSecretVolume(volumes []v1.Volume) *v1.Volume { + for _, volume := range volumes { + if volume.CloudInitConfigDrive == nil { + continue + } + if volume.CloudInitConfigDrive.UserDataSecretRef != nil || + volume.CloudInitConfigDrive.NetworkDataSecretRef != nil { + return &volume + } + } + + return nil +} + +func readFileFromDir(basedir, secretFile string) (string, error) { + userDataSecretFile := filepath.Join(basedir, secretFile) + userDataSecret, err := ioutil.ReadFile(userDataSecretFile) + if err != nil { + log.Log.V(2).Reason(err). + Errorf("could not read secret data from source: %s", userDataSecretFile) + return "", err + } + return string(userDataSecret), nil +} + +// findCloudInitNoCloudSecretVolume loops over a given list of volumes and return a pointer +// to the first CloudInitNoCloud volume with a UserDataSecretRef field set. +func findCloudInitNoCloudSecretVolume(volumes []v1.Volume) *v1.Volume { + for _, volume := range volumes { + if volume.CloudInitNoCloud != nil && volume.CloudInitNoCloud.UserDataSecretRef != nil { + return &volume + } + } + + return nil +} + func readRawOrBase64Data(rawData, base64Data string) (string, error) { if rawData != "" { return rawData, nil @@ -242,93 +334,6 @@ func removeLocalData(domain string, namespace string) error { return err } -// InjectCloudInitSecrets inspects cloud-init volumes in the given VMI and -// resolves any userdata and networkdata secret refs it may find. The resolved -// cloud-init secrets are then injected into the VMI. -func InjectCloudInitSecrets(vmi *v1.VirtualMachineInstance, clientset kubecli.KubevirtClient) error { - precond.MustNotBeNil(vmi) - namespace := precond.MustNotBeEmpty(vmi.GetObjectMeta().GetNamespace()) - - var err error - for _, volume := range vmi.Spec.Volumes { - if volume.CloudInitNoCloud != nil { - err = resolveNoCloudSecrets(volume.CloudInitNoCloud, namespace, clientset) - break - } - if volume.CloudInitConfigDrive != nil { - err = resolveConfigDriveSecrets(volume.CloudInitConfigDrive, namespace, clientset) - break - } - } - if err != nil { - return err - } - return nil -} - -func resolveNoCloudSecrets(source *v1.CloudInitNoCloudSource, namespace string, clientset kubecli.KubevirtClient) error { - precond.CheckNotNil(source) - - secretRefs := []*corev1.LocalObjectReference{source.UserDataSecretRef, source.NetworkDataSecretRef} - dataKeys := []string{"userdata", "networkdata"} - resolvedData, err := resolveSecrets(secretRefs, dataKeys, namespace, clientset) - if err != nil { - return err - } - - if userData, ok := resolvedData["userdata"]; ok { - source.UserData = userData - } - if networkData, ok := resolvedData["networkdata"]; ok { - source.NetworkData = networkData - } - return nil -} - -func resolveConfigDriveSecrets(source *v1.CloudInitConfigDriveSource, namespace string, clientset kubecli.KubevirtClient) error { - precond.CheckNotNil(source) - - secretRefs := []*corev1.LocalObjectReference{source.UserDataSecretRef, source.NetworkDataSecretRef} - dataKeys := []string{"userdata", "networkdata"} - resolvedData, err := resolveSecrets(secretRefs, dataKeys, namespace, clientset) - if err != nil { - return err - } - - if userData, ok := resolvedData["userdata"]; ok { - source.UserData = userData - } - if networkData, ok := resolvedData["networkdata"]; ok { - source.NetworkData = networkData - } - return nil -} - -func resolveSecrets(secretRefs []*corev1.LocalObjectReference, dataKeys []string, namespace string, clientset kubecli.KubevirtClient) (map[string]string, error) { - precond.CheckNotEmpty(namespace) - precond.CheckNotNil(clientset) - resolvedData := make(map[string]string, len(secretRefs)) - - for i, secretRef := range secretRefs { - if secretRef == nil { - continue - } - - secretID := secretRef.Name - secret, err := clientset.CoreV1().Secrets(namespace).Get(secretID, metav1.GetOptions{}) - if err != nil { - return resolvedData, err - } - data, ok := secret.Data[dataKeys[i]] - if !ok { - return resolvedData, fmt.Errorf("%s key not found in k8s secret %s %v", dataKeys[i], secretID, err) - } - resolvedData[dataKeys[i]] = string(data) - } - - return resolvedData, nil -} - func GenerateLocalData(vmiName string, namespace string, data *CloudInitData) error { precond.MustNotBeEmpty(vmiName) precond.MustNotBeNil(data)
pkg/cloud-init/cloud-init_test.go+86 −239 modified@@ -25,41 +25,53 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "time" - "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" k8sv1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" v1 "kubevirt.io/client-go/api/v1" - "kubevirt.io/client-go/kubecli" "kubevirt.io/client-go/precond" ) var _ = Describe("CloudInit", func() { var ( - ctrl *gomock.Controller - virtClient *kubecli.MockKubevirtClient isoCreationFunc IsoCreationFunc + tmpDir string ) - tmpDir, _ := ioutil.TempDir("", "cloudinittest") + createEmptyVMIWithVolumes := func(volumes []v1.Volume) *v1.VirtualMachineInstance { + return &v1.VirtualMachineInstance{ + Spec: v1.VirtualMachineInstanceSpec{ + Volumes: volumes, + }, + } + } + + fakeVolumeMountDir := func(dirName string, files map[string]string) string { + volumeDir := filepath.Join(tmpDir, dirName) + err := os.Mkdir(volumeDir, 0700) + Expect(err).To(Not(HaveOccurred()), "could not create volume dir: ", volumeDir) + for fileName, content := range files { + err = ioutil.WriteFile( + filepath.Join(volumeDir, fileName), + []byte(content), + 0644) + Expect(err).To(Not(HaveOccurred()), "could not create file: ", fileName) + } + return volumeDir + } - BeforeSuite(func() { + BeforeEach(func() { + tmpDir, _ = ioutil.TempDir("", "cloudinittest") err := SetLocalDirectory(tmpDir) if err != nil { panic(err) } - }) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - virtClient = kubecli.NewMockKubevirtClient(ctrl) isoCreationFunc = func(isoOutFile, volumeID string, inDir string) error { switch volumeID { case "cidata", "config-2": @@ -79,7 +91,7 @@ var _ = Describe("CloudInit", func() { SetIsoCreationFunction(isoCreationFunc) }) - AfterSuite(func() { + AfterEach(func() { os.RemoveAll(tmpDir) }) @@ -279,127 +291,44 @@ var _ = Describe("CloudInit", func() { }) Context("with secretRefs", func() { - userDataSecretName := "userDataSecretName" - networkDataSecretName := "networkDataSecretName" - namespace := "testing" - - createSecret := func(name, dataKey, dataValue string) *k8sv1.Secret { - return &k8sv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Type: "Opaque", - Data: map[string][]byte{ - dataKey: []byte(dataValue), + createCloudInitSecretRefVolume := func(name, secret string) *v1.Volume { + return &v1.Volume{ + Name: name, + VolumeSource: v1.VolumeSource{ + CloudInitNoCloud: &v1.CloudInitNoCloudSource{ + UserDataSecretRef: &k8sv1.LocalObjectReference{ + Name: secret, + }, + }, }, } } - createUserDataSecret := func(data string) *k8sv1.Secret { - return createSecret(userDataSecretName, "userdata", data) - } - createBadUserDataSecret := func(data string) *k8sv1.Secret { - return createSecret(userDataSecretName, "baduserdara", data) - } - createNetworkDataSecret := func(data string) *k8sv1.Secret { - return createSecret(networkDataSecretName, "networkdata", data) - } - createBadNetworkDataSecret := func(data string) *k8sv1.Secret { - return createSecret(networkDataSecretName, "badnetworkdata", data) - } - - It("should succeed to verify userDataSecretRef", func() { - userSecret := createUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()).AnyTimes() - - cloudInitData := &v1.CloudInitNoCloudSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) - Expect(cloudInitData.UserData).To(Equal("secretUserData")) + It("should resolve no-cloud data from volume", func() { + testVolume := createCloudInitSecretRefVolume("test-volume", "test-secret") + vmi := createEmptyVMIWithVolumes([]v1.Volume{*testVolume}) + fakeVolumeMountDir("test-volume", map[string]string{ + "userdata": "secret-userdata", + "networkdata": "secret-networkdata", + }) + err := ResolveNoCloudSecrets(vmi, tmpDir) + Expect(err).To(Not(HaveOccurred()), "could not resolve secret volume") + Expect(testVolume.CloudInitNoCloud.UserData).To(Equal("secret-userdata")) + Expect(testVolume.CloudInitNoCloud.NetworkData).To(Equal("secret-networkdata")) }) - It("should succeed to verify userDataSecretRef and networkDataSecretRef", func() { - userSecret := createUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - networkSecret := createNetworkDataSecret("secretNetworkData") - networkClient := fake.NewSimpleClientset(networkSecret) - - gomock.InOrder( - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()), - virtClient.EXPECT().CoreV1().Return(networkClient.CoreV1()), - ) - - cloudInitData := &v1.CloudInitNoCloudSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) - Expect(cloudInitData.UserData).To(Equal("secretUserData")) - Expect(cloudInitData.NetworkData).To(Equal("secretNetworkData")) + It("should resolve empty no-cloud volume and do nothing", func() { + vmi := createEmptyVMIWithVolumes([]v1.Volume{}) + err := ResolveNoCloudSecrets(vmi, tmpDir) + Expect(err).To(Not(HaveOccurred()), "failed to resolve empty volumes") }) - It("should succeed to verify nothing", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - cloudInitData := &v1.CloudInitNoCloudSource{} - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) - }) - - It("should fail to verify UserDataSecretRef without a secret", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - - cloudInitData := &v1.CloudInitNoCloudSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } - - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("secrets \"%s\" not found", userDataSecretName))) - }) - - It("should fail to verify NetworkDataSecretRef without a secret", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - - cloudInitData := &v1.CloudInitNoCloudSource{ - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("secrets \"%s\" not found", networkDataSecretName))) - }) - - It("should fail to verify UserDataSecretRef with a misnamed secret", func() { - userSecret := createBadUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()).AnyTimes() - - cloudInitData := &v1.CloudInitNoCloudSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } - - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("userdata key not found in k8s secret %s <nil>", userDataSecretName))) - }) - - It("should fail to verify NetworkDataSecretRef with a misnamed secret", func() { - networkSecret := createBadNetworkDataSecret("secretNetworkData") - networkClient := fake.NewSimpleClientset(networkSecret) - virtClient.EXPECT().CoreV1().Return(networkClient.CoreV1()) - - cloudInitData := &v1.CloudInitNoCloudSource{ - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveNoCloudSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("networkdata key not found in k8s secret %s <nil>", networkDataSecretName))) + It("should fail if both userdata and network data does not exist", func() { + testVolume := createCloudInitSecretRefVolume("test-volume", "test-secret") + vmi := createEmptyVMIWithVolumes([]v1.Volume{*testVolume}) + err := ResolveNoCloudSecrets(vmi, tmpDir) + Expect(err).To(HaveOccurred(), "expected a failure when no sources found") + Expect(err.Error()).To(Equal("no cloud-init data-source found at volume: test-volume")) }) }) }) @@ -473,128 +402,46 @@ var _ = Describe("CloudInit", func() { }) Context("with secretRefs", func() { - userDataSecretName := "userDataSecretName" - networkDataSecretName := "networkDataSecretName" - namespace := "testing" - - createSecret := func(name, dataKey, dataValue string) *k8sv1.Secret { - return &k8sv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Type: "Opaque", - Data: map[string][]byte{ - dataKey: []byte(dataValue), + createCloudInitConfigDriveVolume := func(name, secret string) *v1.Volume { + return &v1.Volume{ + Name: name, + VolumeSource: v1.VolumeSource{ + CloudInitConfigDrive: &v1.CloudInitConfigDriveSource{ + UserDataSecretRef: &k8sv1.LocalObjectReference{ + Name: secret, + }, + }, }, } } - createUserDataSecret := func(data string) *k8sv1.Secret { - return createSecret(userDataSecretName, "userdata", data) - } - createBadUserDataSecret := func(data string) *k8sv1.Secret { - return createSecret(userDataSecretName, "baduserdara", data) - } - createNetworkDataSecret := func(data string) *k8sv1.Secret { - return createSecret(networkDataSecretName, "networkdata", data) - } - createBadNetworkDataSecret := func(data string) *k8sv1.Secret { - return createSecret(networkDataSecretName, "badnetworkdata", data) - } - - It("should succeed to verify userDataSecretRef", func() { - userSecret := createUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()).AnyTimes() - - cloudInitData := &v1.CloudInitConfigDriveSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } - - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) - Expect(cloudInitData.UserData).To(Equal("secretUserData")) - }) - - It("should succeed to verify userDataSecretRef and networkDataSecretRef", func() { - userSecret := createUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - networkSecret := createNetworkDataSecret("secretNetworkData") - networkClient := fake.NewSimpleClientset(networkSecret) - - gomock.InOrder( - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()), - virtClient.EXPECT().CoreV1().Return(networkClient.CoreV1()), - ) - - cloudInitData := &v1.CloudInitConfigDriveSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) - Expect(cloudInitData.UserData).To(Equal("secretUserData")) - Expect(cloudInitData.NetworkData).To(Equal("secretNetworkData")) + It("should resolve config-drive data from volume", func() { + testVolume := createCloudInitConfigDriveVolume("test-volume", "test-secret") + vmi := createEmptyVMIWithVolumes([]v1.Volume{*testVolume}) + fakeVolumeMountDir("test-volume", map[string]string{ + "userdata": "secret-userdata", + "networkdata": "secret-networkdata", + }) + err := ResolveConfigDriveSecrets(vmi, tmpDir) + Expect(err).To(Not(HaveOccurred()), "could not resolve secret volume") + Expect(testVolume.CloudInitConfigDrive.UserData).To(Equal("secret-userdata")) + Expect(testVolume.CloudInitConfigDrive.NetworkData).To(Equal("secret-networkdata")) }) - It("should succeed to verify nothing", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - cloudInitData := &v1.CloudInitConfigDriveSource{} - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err).To(BeNil()) + It("should resolve empty config-drive volume and do nothing", func() { + vmi := createEmptyVMIWithVolumes([]v1.Volume{}) + err := ResolveConfigDriveSecrets(vmi, tmpDir) + Expect(err).To(Not(HaveOccurred()), "failed to resolve empty volumes") }) - It("should fail to verify UserDataSecretRef without a secret", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - - cloudInitData := &v1.CloudInitConfigDriveSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } + It("should fail if both userdata and network data does not exist", func() { + testVolume := createCloudInitConfigDriveVolume("test-volume", "test-secret") + vmi := createEmptyVMIWithVolumes([]v1.Volume{*testVolume}) + err := ResolveConfigDriveSecrets(vmi, tmpDir) + Expect(err).To(HaveOccurred(), "expected a failure when no sources found") + Expect(err.Error()).To(Equal("no cloud-init data-source found at volume: test-volume")) - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("secrets \"%s\" not found", userDataSecretName))) }) - It("should fail to verify NetworkDataSecretRef without a secret", func() { - fakeClient := fake.NewSimpleClientset() - virtClient.EXPECT().CoreV1().Return(fakeClient.CoreV1()) - - cloudInitData := &v1.CloudInitConfigDriveSource{ - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("secrets \"%s\" not found", networkDataSecretName))) - }) - - It("should fail to verify UserDataSecretRef with a misnamed secret", func() { - userSecret := createBadUserDataSecret("secretUserData") - userClient := fake.NewSimpleClientset(userSecret) - virtClient.EXPECT().CoreV1().Return(userClient.CoreV1()).AnyTimes() - - cloudInitData := &v1.CloudInitConfigDriveSource{ - UserDataSecretRef: &k8sv1.LocalObjectReference{Name: userDataSecretName}, - } - - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("userdata key not found in k8s secret %s <nil>", userDataSecretName))) - }) - - It("should fail to verify NetworkDataSecretRef with a misnamed secret", func() { - networkSecret := createBadNetworkDataSecret("secretNetworkData") - networkClient := fake.NewSimpleClientset(networkSecret) - virtClient.EXPECT().CoreV1().Return(networkClient.CoreV1()) - - cloudInitData := &v1.CloudInitConfigDriveSource{ - NetworkDataSecretRef: &k8sv1.LocalObjectReference{Name: networkDataSecretName}, - } - - err := resolveConfigDriveSecrets(cloudInitData, namespace, virtClient) - Expect(err.Error()).To(Equal(fmt.Sprintf("networkdata key not found in k8s secret %s <nil>", networkDataSecretName))) - }) }) }) })
pkg/virt-controller/services/template.go+34 −0 modified@@ -504,6 +504,40 @@ func (t *templateService) RenderLaunchManifest(vmi *v1.VirtualMachineInstance) ( if volume.ServiceAccount != nil { serviceAccountName = volume.ServiceAccount.ServiceAccountName } + + if volume.CloudInitNoCloud != nil && volume.CloudInitNoCloud.UserDataSecretRef != nil { + // attach a secret referenced by the user + volumes = append(volumes, k8sv1.Volume{ + Name: volume.Name, + VolumeSource: k8sv1.VolumeSource{ + Secret: &k8sv1.SecretVolumeSource{ + SecretName: volume.CloudInitNoCloud.UserDataSecretRef.Name, + }, + }, + }) + volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ + Name: volume.Name, + MountPath: filepath.Join(config.SecretSourceDir, volume.Name), + ReadOnly: true, + }) + } + + if volume.CloudInitConfigDrive != nil && volume.CloudInitConfigDrive.UserDataSecretRef != nil { + // attach a secret referenced by the user + volumes = append(volumes, k8sv1.Volume{ + Name: volume.Name, + VolumeSource: k8sv1.VolumeSource{ + Secret: &k8sv1.SecretVolumeSource{ + SecretName: volume.CloudInitConfigDrive.UserDataSecretRef.Name, + }, + }, + }) + volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ + Name: volume.Name, + MountPath: filepath.Join(config.SecretSourceDir, volume.Name), + ReadOnly: true, + }) + } } if t.imagePullSecret != "" {
pkg/virt-controller/services/template_test.go+44 −0 modified@@ -198,6 +198,50 @@ var _ = Describe("Template", func() { Expect(debugLogsValue).To(Equal("1")) }) }) + Context("with cloud-init secret", func() { + It("should add volume with secret referenced by cloud-init user secret ref", func() { + vmi := v1.VirtualMachineInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testvmi", + Namespace: "default", + UID: "1234", + }, + Spec: v1.VirtualMachineInstanceSpec{ + Volumes: []v1.Volume{ + { + Name: "cloud-init-user-data-secret-ref", + VolumeSource: v1.VolumeSource{ + CloudInitNoCloud: &v1.CloudInitNoCloudSource{ + UserDataSecretRef: &kubev1.LocalObjectReference{ + Name: "some-secret", + }, + }, + }, + }, + }, + }, + } + + pod, err := svc.RenderLaunchManifest(&vmi) + Expect(err).ToNot(HaveOccurred()) + + cloudInitVolumeFound := false + for _, volume := range pod.Spec.Volumes { + if volume.Name == "cloud-init-user-data-secret-ref" { + cloudInitVolumeFound = true + } + } + Expect(cloudInitVolumeFound).To(BeTrue(), "could not find cloud init secret volume") + + cloudInitVolumeMountFound := false + for _, volumeMount := range pod.Spec.Containers[0].VolumeMounts { + if volumeMount.Name == "cloud-init-user-data-secret-ref" { + cloudInitVolumeMountFound = true + } + } + Expect(cloudInitVolumeMountFound).To(BeTrue(), "could not find cloud init secret volume mount") + }) + }) Context("with multus annotation", func() { It("should add multus networks in the pod annotation", func() { vmi := v1.VirtualMachineInstance{
pkg/virt-handler/BUILD.bazel+0 −1 modified@@ -6,7 +6,6 @@ go_library( importpath = "kubevirt.io/kubevirt/pkg/virt-handler", visibility = ["//visibility:public"], deps = [ - "//pkg/cloud-init:go_default_library", "//pkg/controller:go_default_library", "//pkg/handler-launcher-com/cmd/v1:go_default_library", "//pkg/host-disk:go_default_library",
pkg/virt-handler/vm.go+0 −6 modified@@ -46,7 +46,6 @@ import ( v1 "kubevirt.io/client-go/api/v1" "kubevirt.io/client-go/kubecli" "kubevirt.io/client-go/log" - cloudinit "kubevirt.io/kubevirt/pkg/cloud-init" "kubevirt.io/kubevirt/pkg/controller" cmdv1 "kubevirt.io/kubevirt/pkg/handler-launcher-com/cmd/v1" hostdisk "kubevirt.io/kubevirt/pkg/host-disk" @@ -1396,11 +1395,6 @@ func (d *VirtualMachineController) processVmUpdate(origVMI *v1.VirtualMachineIns return err } - err = cloudinit.InjectCloudInitSecrets(vmi, d.clientset) - if err != nil { - return err - } - client, err := d.getLauncherClient(vmi) if err != nil { return fmt.Errorf("unable to create virt-launcher client connection: %v", err)
pkg/virt-launcher/virtwrap/manager.go+10 −0 modified@@ -774,6 +774,16 @@ func (l *LibvirtDomainManager) preStartHook(vmi *v1.VirtualMachineInstance, doma logger.Info("Executing PreStartHook on VMI pod environment") + err := cloudinit.ResolveNoCloudSecrets(vmi, config.SecretSourceDir) + if err != nil { + return nil, err + } + + err = cloudinit.ResolveConfigDriveSecrets(vmi, config.SecretSourceDir) + if err != nil { + return nil, err + } + // generate cloud-init data cloudInitData, err := cloudinit.ReadCloudInitVolumeDataSource(vmi) if err != nil {
pkg/virt-operator/creation/rbac/handler.go+2 −1 modified@@ -84,7 +84,7 @@ func newHandlerClusterRole() *rbacv1.ClusterRole { "", }, Resources: []string{ - "secrets", "persistentvolumeclaims", + "persistentvolumeclaims", }, Verbs: []string{ "get", @@ -190,6 +190,7 @@ func newHandlerRole(namespace string) *rbacv1.Role { }, Verbs: []string{ "create", + "get", }, }, },
tests/vmi_lifecycle_test.go+36 −17 modified@@ -313,8 +313,26 @@ var _ = Describe("[rfe_id:273][crit:high][vendor:cnv-qe@redhat.com][level:compon }) Context("with user-data", func() { + + findLauncherForVMI := func(vmi *v1.VirtualMachineInstance) *k8sv1.Pod { + By("Finding a launcher for VMI: " + vmi.Name) + launchers, err := virtClient. + CoreV1(). + Pods(tests.NamespaceTestDefault). + List(metav1.ListOptions{LabelSelector: "kubevirt.io=virt-launcher"}) + Expect(err).To(BeNil(), "Should list virt-launchers") + var launcher k8sv1.Pod + for _, launcherPod := range launchers.Items { + if domain, ok := launcherPod.ObjectMeta.Annotations[v1.DomainAnnotation]; ok && domain == vmi.Name { + return &launcherPod + } + } + Expect(launcher).ToNot(BeNil(), "Should find virt-launcher pod for created VMI") + return nil + } + Context("without k8s secret", func() { - It("[test_id:1629]should retry starting the VirtualMachineInstance", func() { + It("[test_id:1629]should not be able to start virt-launcher pod", func() { userData := fmt.Sprintf("#!/bin/sh\n\necho 'hi'\n") vmi = tests.NewRandomVMIWithEphemeralDiskAndUserdata(tests.ContainerDiskFor(tests.ContainerDiskCirros), userData) @@ -327,23 +345,21 @@ var _ = Describe("[rfe_id:273][crit:high][vendor:cnv-qe@redhat.com][level:compon } } By("Starting a VirtualMachineInstance") - obj, err := virtClient.VirtualMachineInstance(tests.NamespaceTestDefault).Create(vmi) + _, err := virtClient.VirtualMachineInstance(tests.NamespaceTestDefault).Create(vmi) Expect(err).To(BeNil(), "Should create VMI successfully") - - By("Checking that VirtualMachineInstance was restarted twice") - retryCount := 0 stopChan := make(chan struct{}) defer close(stopChan) - tests.NewObjectEventWatcher(obj).SinceWatchedObjectResourceVersion().Timeout(60*time.Second).Watch(stopChan, func(event *k8sv1.Event) bool { - if event.Type == "Warning" && event.Reason == v1.SyncFailed.String() { - retryCount++ - if retryCount >= 2 { - // Done, two retries is enough + launcher := findLauncherForVMI(vmi) + tests.NewObjectEventWatcher(launcher). + SinceWatchedObjectResourceVersion(). + Timeout(60*time.Second). + Watch(stopChan, func(event *k8sv1.Event) bool { + if event.Type == "Warning" && event.Reason == "FailedMount" { return true } - } - return false - }, fmt.Sprintf("two events of type Warning, reason = %s", v1.SyncFailed.String())) + return false + }, + "event of type Warning, reason = FailedMount") }) It("[test_id:1630]should log warning and proceed once the secret is there", func() { @@ -363,13 +379,16 @@ var _ = Describe("[rfe_id:273][crit:high][vendor:cnv-qe@redhat.com][level:compon By("Starting a VirtualMachineInstance") createdVMI, err := virtClient.VirtualMachineInstance(tests.NamespaceTestDefault).Create(vmi) Expect(err).To(BeNil(), "Should create VMI successfully") - + launcher := findLauncherForVMI(vmi) // Wait until we see that starting the VirtualMachineInstance is failing By("Checking that VirtualMachineInstance start failed") stopChan := make(chan struct{}) defer close(stopChan) - event := tests.NewObjectEventWatcher(createdVMI).Timeout(60*time.Second).SinceWatchedObjectResourceVersion().WaitFor(stopChan, tests.WarningEvent, v1.SyncFailed) - Expect(event.Message).To(ContainSubstring("nonexistent"), "VMI should not be started") + event := tests.NewObjectEventWatcher(launcher).Timeout(60*time.Second).SinceWatchedObjectResourceVersion().WaitFor(stopChan, tests.WarningEvent, "FailedMount") + Expect(event.Message).To(SatisfyAny( + ContainSubstring(`secret "nonexistent" not found`), + ContainSubstring(`secrets "nonexistent" not found`), // for k8s 1.11.x + ), "VMI should not be started") // Creat nonexistent secret, so that the VirtualMachineInstance can recover By("Creating a user-data secret") @@ -391,7 +410,7 @@ var _ = Describe("[rfe_id:273][crit:high][vendor:cnv-qe@redhat.com][level:compon // Wait for the VirtualMachineInstance to be started, allow warning events to occur By("Checking that VirtualMachineInstance start succeeded") - tests.NewObjectEventWatcher(createdVMI).SinceWatchedObjectResourceVersion().Timeout(30*time.Second).WaitFor(stopChan, tests.NormalEvent, v1.Started) + tests.NewObjectEventWatcher(createdVMI).SinceWatchedObjectResourceVersion().Timeout(60*time.Second).WaitFor(stopChan, tests.NormalEvent, v1.Started) }) }) })
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-849r-8wvp-4wwgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-1701ghsaADVISORY
- bugzilla.redhat.com/show_bug.cgighsax_refsource_MISCWEB
- github.com/kubevirt/containerized-data-importer/pull/1098ghsaWEB
- github.com/kubevirt/kubevirt/commit/9efa8d7388d4fe1c698c6980aa7122c06bd141beghsaWEB
- github.com/kubevirt/kubevirt/issues/2967ghsaWEB
- github.com/kubevirt/kubevirt/pull/3001ghsaWEB
News mentions
0No linked articles in our index yet.