From ca69758701447c59958e48ffc63742b9580d5e881995bc229f945d60701cbbb4 Mon Sep 17 00:00:00 2001 From: Priyanka Saggu Date: Thu, 15 Jun 2023 12:59:42 +0000 Subject: [PATCH] Accepting request 1093309 from home:psaggu:branches:devel:kubic Security Patch Fix for CVE-2023-2727 (bsc#1211630) and CVE-2023-2728 (bsc#1211631) - added patch: kube-apiserver-admission-plugin-policy.patch - new kube-apiserver component patch prevents ephemeral containers: ** from using an image that is restricted by ImagePolicyWebhook (CVE-2023-2727) ** from bypassing the mountable secrets policy enforced by the ServiceAccount admission plugin (CVE-2023-2728) OBS-URL: https://build.opensuse.org/request/show/1093309 OBS-URL: https://build.opensuse.org/package/show/devel:kubic/kubernetes1.23?expand=0&rev=21 --- kube-apiserver-admission-plugin-policy.patch | 492 +++++++++++++++++++ kubernetes1.23.changes | 9 + kubernetes1.23.spec | 5 +- 3 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 kube-apiserver-admission-plugin-policy.patch diff --git a/kube-apiserver-admission-plugin-policy.patch b/kube-apiserver-admission-plugin-policy.patch new file mode 100644 index 0000000..8f5b487 --- /dev/null +++ b/kube-apiserver-admission-plugin-policy.patch @@ -0,0 +1,492 @@ +From 64f3b999c3e488ebc73c2d9a628b73ec092a0caf Mon Sep 17 00:00:00 2001 +From: Rita Zhang +Date: Sun, 21 May 2023 16:21:08 -0700 +Subject: [PATCH] Add ephemeralcontainer to imagepolicy securityaccount + admission plugin + +Signed-off-by: Rita Zhang +--- + plugin/pkg/admission/imagepolicy/admission.go | 26 ++-- + .../admission/imagepolicy/admission_test.go | 135 +++++++++++++++++- + .../pkg/admission/serviceaccount/admission.go | 55 ++++++- + .../serviceaccount/admission_test.go | 93 +++++++++++- + 4 files changed, 290 insertions(+), 19 deletions(-) + +Index: kubernetes-1.23.17/plugin/pkg/admission/imagepolicy/admission.go +=================================================================== +--- kubernetes-1.23.17.orig/plugin/pkg/admission/imagepolicy/admission.go ++++ kubernetes-1.23.17/plugin/pkg/admission/imagepolicy/admission.go +@@ -132,8 +132,8 @@ func (a *Plugin) webhookError(pod *api.P + + // Validate makes an admission decision based on the request attributes + func (a *Plugin) Validate(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) (err error) { +- // Ignore all calls to subresources or resources other than pods. +- if attributes.GetSubresource() != "" || attributes.GetResource().GroupResource() != api.Resource("pods") { ++ // Ignore all calls to subresources other than ephemeralcontainers or calls to resources other than pods. ++ if (attributes.GetSubresource() != "" && attributes.GetSubresource() != "ephemeralcontainers") || attributes.GetResource().GroupResource() != api.Resource("pods") { + return nil + } + +@@ -144,13 +144,21 @@ func (a *Plugin) Validate(ctx context.Co + + // Build list of ImageReviewContainerSpec + var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec +- containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) +- containers = append(containers, pod.Spec.Containers...) +- containers = append(containers, pod.Spec.InitContainers...) +- for _, c := range containers { +- imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{ +- Image: c.Image, +- }) ++ if attributes.GetSubresource() == "" { ++ containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) ++ containers = append(containers, pod.Spec.Containers...) ++ containers = append(containers, pod.Spec.InitContainers...) ++ for _, c := range containers { ++ imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{ ++ Image: c.Image, ++ }) ++ } ++ } else if attributes.GetSubresource() == "ephemeralcontainers" { ++ for _, c := range pod.Spec.EphemeralContainers { ++ imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{ ++ Image: c.Image, ++ }) ++ } + } + imageReview := v1alpha1.ImageReview{ + Spec: v1alpha1.ImageReviewSpec{ +Index: kubernetes-1.23.17/plugin/pkg/admission/imagepolicy/admission_test.go +=================================================================== +--- kubernetes-1.23.17.orig/plugin/pkg/admission/imagepolicy/admission_test.go ++++ kubernetes-1.23.17/plugin/pkg/admission/imagepolicy/admission_test.go +@@ -595,17 +595,23 @@ func TestContainerCombinations(t *testin + test string + pod *api.Pod + wantAllowed, wantErr bool ++ subresource string ++ operation admission.Operation + }{ + { + test: "Single container allowed", + pod: goodPod("good"), + wantAllowed: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Single container denied", + pod: goodPod("bad"), + wantAllowed: false, + wantErr: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "One good container, one bad", +@@ -627,6 +633,8 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: false, + wantErr: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Multiple good containers", +@@ -648,6 +656,8 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: true, + wantErr: false, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Multiple bad containers", +@@ -669,6 +679,8 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: false, + wantErr: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Good container, bad init container", +@@ -692,6 +704,8 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: false, + wantErr: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Bad container, good init container", +@@ -715,6 +729,8 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: false, + wantErr: true, ++ subresource: "", ++ operation: admission.Create, + }, + { + test: "Good container, good init container", +@@ -738,6 +754,123 @@ func TestContainerCombinations(t *testin + }, + wantAllowed: true, + wantErr: false, ++ subresource: "", ++ operation: admission.Create, ++ }, ++ { ++ test: "Good container, good init container, bad ephemeral container when updating ephemeralcontainers subresource", ++ pod: &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: "default", ++ SecurityContext: &api.PodSecurityContext{}, ++ Containers: []api.Container{ ++ { ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ InitContainers: []api.Container{ ++ { ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Image: "bad", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ wantAllowed: false, ++ wantErr: true, ++ subresource: "ephemeralcontainers", ++ operation: admission.Update, ++ }, ++ { ++ test: "Good container, good init container, bad ephemeral container when updating subresource=='' which sets initContainer and container only", ++ pod: &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: "default", ++ SecurityContext: &api.PodSecurityContext{}, ++ Containers: []api.Container{ ++ { ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ InitContainers: []api.Container{ ++ { ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Image: "bad", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ wantAllowed: true, ++ wantErr: false, ++ subresource: "", ++ operation: admission.Update, ++ }, ++ ++ { ++ test: "Bad container, good ephemeral container when updating subresource=='ephemeralcontainers' which sets ephemeralcontainers only", ++ pod: &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: "default", ++ SecurityContext: &api.PodSecurityContext{}, ++ Containers: []api.Container{ ++ { ++ Image: "bad", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ wantAllowed: true, ++ wantErr: false, ++ subresource: "ephemeralcontainers", ++ operation: admission.Update, ++ }, ++ { ++ test: "Good ephemeral container", ++ pod: &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: "default", ++ SecurityContext: &api.PodSecurityContext{}, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Image: "good", ++ SecurityContext: &api.SecurityContext{}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ wantAllowed: true, ++ wantErr: false, ++ subresource: "ephemeralcontainers", ++ operation: admission.Update, + }, + } + for _, tt := range tests { +@@ -759,7 +892,7 @@ func TestContainerCombinations(t *testin + return + } + +- attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) ++ attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), tt.subresource, tt.operation, &metav1.CreateOptions{}, false, &user.DefaultInfo{}) + + err = wh.Validate(context.TODO(), attr, nil) + if tt.wantAllowed { +Index: kubernetes-1.23.17/plugin/pkg/admission/serviceaccount/admission.go +=================================================================== +--- kubernetes-1.23.17.orig/plugin/pkg/admission/serviceaccount/admission.go ++++ kubernetes-1.23.17/plugin/pkg/admission/serviceaccount/admission.go +@@ -100,7 +100,7 @@ var _ = genericadmissioninitializer.Want + // 5. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers + func NewServiceAccount() *Plugin { + return &Plugin{ +- Handler: admission.NewHandler(admission.Create), ++ Handler: admission.NewHandler(admission.Create, admission.Update), + // TODO: enable this once we've swept secret usage to account for adding secret references to service accounts + LimitSecretReferences: false, + // Auto mount service account API token secrets +@@ -140,7 +140,10 @@ func (s *Plugin) Admit(ctx context.Conte + if shouldIgnore(a) { + return nil + } +- ++ if a.GetOperation() != admission.Create { ++ // we only mutate pods during create requests ++ return nil ++ } + pod := a.GetObject().(*api.Pod) + + // Don't modify the spec of mirror pods. +@@ -180,6 +183,15 @@ func (s *Plugin) Validate(ctx context.Co + + pod := a.GetObject().(*api.Pod) + ++ if a.GetOperation() == admission.Update && a.GetSubresource() == "ephemeralcontainers" { ++ return s.limitEphemeralContainerSecretReferences(pod, a) ++ } ++ ++ if a.GetOperation() != admission.Create { ++ // we only validate pod specs during create requests ++ return nil ++ } ++ + // Mirror pods have restrictions on what they can reference + if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; isMirrorPod { + if len(pod.Spec.ServiceAccountName) != 0 { +@@ -205,6 +217,10 @@ func (s *Plugin) Validate(ctx context.Co + return nil + } + ++ // Require container pods to have service accounts ++ if len(pod.Spec.ServiceAccountName) == 0 { ++ return admission.NewForbidden(a, fmt.Errorf("no service account specified for pod %s/%s", a.GetNamespace(), pod.Name)) ++ } + // Ensure the referenced service account exists + serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName) + if err != nil { +@@ -221,10 +237,7 @@ func (s *Plugin) Validate(ctx context.Co + } + + func shouldIgnore(a admission.Attributes) bool { +- if a.GetResource().GroupResource() != api.Resource("pods") { +- return true +- } +- if a.GetSubresource() != "" { ++ if a.GetResource().GroupResource() != api.Resource("pods") || (a.GetSubresource() != "" && a.GetSubresource() != "ephemeralcontainers") { + return true + } + obj := a.GetObject() +@@ -348,6 +361,36 @@ func (s *Plugin) limitSecretReferences(s + } + } + return nil ++} ++ ++func (s *Plugin) limitEphemeralContainerSecretReferences(pod *api.Pod, a admission.Attributes) error { ++ // Require ephemeral container pods to have service accounts ++ if len(pod.Spec.ServiceAccountName) == 0 { ++ return admission.NewForbidden(a, fmt.Errorf("no service account specified for pod %s/%s", a.GetNamespace(), pod.Name)) ++ } ++ // Ensure the referenced service account exists ++ serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName) ++ if err != nil { ++ return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err)) ++ } ++ if !s.enforceMountableSecrets(serviceAccount) { ++ return nil ++ } ++ // Ensure all secrets the ephemeral containers reference are allowed by the service account ++ mountableSecrets := sets.NewString() ++ for _, s := range serviceAccount.Secrets { ++ mountableSecrets.Insert(s.Name) ++ } ++ for _, container := range pod.Spec.EphemeralContainers { ++ for _, env := range container.Env { ++ if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { ++ if !mountableSecrets.Has(env.ValueFrom.SecretKeyRef.Name) { ++ return fmt.Errorf("ephemeral container %s with envVar %s referencing secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", container.Name, env.Name, env.ValueFrom.SecretKeyRef.Name, serviceAccount.Name) ++ } ++ } ++ } ++ } ++ return nil + } + + func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) { +Index: kubernetes-1.23.17/plugin/pkg/admission/serviceaccount/admission_test.go +=================================================================== +--- kubernetes-1.23.17.orig/plugin/pkg/admission/serviceaccount/admission_test.go ++++ kubernetes-1.23.17/plugin/pkg/admission/serviceaccount/admission_test.go +@@ -28,7 +28,6 @@ import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +- "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apiserver/pkg/admission" + admissiontesting "k8s.io/apiserver/pkg/admission/testing" + "k8s.io/client-go/informers" +@@ -225,10 +224,10 @@ func TestAssignsDefaultServiceAccountAnd + } + + if !reflect.DeepEqual(expectedVolumes, pod.Spec.Volumes) { +- t.Errorf("unexpected volumes: %s", diff.ObjectReflectDiff(expectedVolumes, pod.Spec.Volumes)) ++ t.Errorf("unexpected volumes: %s", cmp.Diff(expectedVolumes, pod.Spec.Volumes)) + } + if !reflect.DeepEqual(expectedVolumeMounts, pod.Spec.Containers[0].VolumeMounts) { +- t.Errorf("unexpected volumes: %s", diff.ObjectReflectDiff(expectedVolumeMounts, pod.Spec.Containers[0].VolumeMounts)) ++ t.Errorf("unexpected volumes: %s", cmp.Diff(expectedVolumeMounts, pod.Spec.Containers[0].VolumeMounts)) + } + + // ensure result converted to v1 matches defaulted object +@@ -545,6 +544,34 @@ func TestAllowsReferencedSecret(t *testi + if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil { + t.Errorf("Unexpected error: %v", err) + } ++ ++ pod2 = &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: DefaultServiceAccountName, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Name: "container-2", ++ Env: []api.EnvVar{ ++ { ++ Name: "env-1", ++ ValueFrom: &api.EnvVarSource{ ++ SecretKeyRef: &api.SecretKeySelector{ ++ LocalObjectReference: api.LocalObjectReference{Name: "foo"}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ } ++ // validate enforces restrictions on secret mounts when operation==create and subresource=='' or operation==update and subresource==ephemeralcontainers" ++ attrs = admission.NewAttributesRecord(pod2, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "ephemeralcontainers", admission.Update, &metav1.UpdateOptions{}, false, nil) ++ if err := admit.Validate(context.TODO(), attrs, nil); err != nil { ++ t.Errorf("Unexpected error: %v", err) ++ } + } + + func TestRejectsUnreferencedSecretVolumes(t *testing.T) { +@@ -622,6 +649,66 @@ func TestRejectsUnreferencedSecretVolume + if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err == nil || !strings.Contains(err.Error(), "with envVar") { + t.Errorf("Unexpected error: %v", err) + } ++ ++ pod2 = &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: DefaultServiceAccountName, ++ InitContainers: []api.Container{ ++ { ++ Name: "container-1", ++ Env: []api.EnvVar{ ++ { ++ Name: "env-1", ++ ValueFrom: &api.EnvVarSource{ ++ SecretKeyRef: &api.SecretKeySelector{ ++ LocalObjectReference: api.LocalObjectReference{Name: "foo"}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ } ++ attrs = admission.NewAttributesRecord(pod2, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil) ++ if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil { ++ t.Errorf("admit only enforces restrictions on secret mounts when operation==create. Unexpected error: %v", err) ++ } ++ attrs = admission.NewAttributesRecord(pod2, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil) ++ if err := admit.Validate(context.TODO(), attrs, nil); err == nil || !strings.Contains(err.Error(), "with envVar") { ++ t.Errorf("validate only enforces restrictions on secret mounts when operation==create and subresource==''. Unexpected error: %v", err) ++ } ++ ++ pod2 = &api.Pod{ ++ Spec: api.PodSpec{ ++ ServiceAccountName: DefaultServiceAccountName, ++ EphemeralContainers: []api.EphemeralContainer{ ++ { ++ EphemeralContainerCommon: api.EphemeralContainerCommon{ ++ Name: "container-2", ++ Env: []api.EnvVar{ ++ { ++ Name: "env-1", ++ ValueFrom: &api.EnvVarSource{ ++ SecretKeyRef: &api.SecretKeySelector{ ++ LocalObjectReference: api.LocalObjectReference{Name: "foo"}, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ }, ++ } ++ attrs = admission.NewAttributesRecord(pod2, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil) ++ if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil { ++ t.Errorf("admit only enforces restrictions on secret mounts when operation==create and subresource==''. Unexpected error: %v", err) ++ } ++ attrs = admission.NewAttributesRecord(pod2, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "ephemeralcontainers", admission.Update, &metav1.UpdateOptions{}, false, nil) ++ if err := admit.Validate(context.TODO(), attrs, nil); err == nil || !strings.Contains(err.Error(), "with envVar") { ++ t.Errorf("validate enforces restrictions on secret mounts when operation==update and subresource==ephemeralcontainers. Unexpected error: %v", err) ++ } + } + + func TestAllowUnreferencedSecretVolumesForPermissiveSAs(t *testing.T) { diff --git a/kubernetes1.23.changes b/kubernetes1.23.changes index 70be92b..a96141f 100644 --- a/kubernetes1.23.changes +++ b/kubernetes1.23.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Thu Jun 15 11:36:19 UTC 2023 - Priyanka Saggu + +- Security Patch Fix for CVE-2023-2727 (bsc#1211630) and CVE-2023-2728 (bsc#1211631) + * added patch: kube-apiserver-admission-plugin-policy.patch + * this new kube-apiserver component patch prevents ephemeral containers: + ** from using an image that is restricted by ImagePolicyWebhook (CVE-2023-2727) + ** from bypassing the mountable secrets policy enforced by the ServiceAccount admission plugin (CVE-2023-2728) + ------------------------------------------------------------------- Wed Apr 12 12:34:43 UTC 2023 - Priyanka Saggu diff --git a/kubernetes1.23.spec b/kubernetes1.23.spec index d32d9b8..f17f47d 100644 --- a/kubernetes1.23.spec +++ b/kubernetes1.23.spec @@ -49,6 +49,8 @@ Patch3: opensuse-version-checks.patch Patch4: kubeadm-opensuse-flexvolume.patch # Patch to revert renaming of coredns image location to match how it's done on download.opensuse.org Patch5: revert-coredns-image-renaming.patch +# Patch to fix CVE-2023-2727 and CVE-2023-2728, by preventing ephemeral containers from using an image that is restricted by ImagePolicyWebhook and from bypassing the mountable secrets policy enforced by the ServiceAccount admission plugin +Patch6: kube-apiserver-admission-plugin-policy.patch BuildRequires: fdupes BuildRequires: git BuildRequires: go-go-md2man @@ -71,8 +73,8 @@ for management and discovery. -# packages to build containerized control plane +# packages to build containerized control plane %package apiserver Summary: Kubernetes apiserver for container image Group: System/Management @@ -216,6 +218,7 @@ Fish command line completion support for %{name}-client. %patch3 -p1 %patch4 -p0 %patch5 -p1 +%patch6 -p1 %build # This is fixing bug bsc#1065972