From f06833bd427e7b2ff8fe82e74230e1934d8981d4 Mon Sep 17 00:00:00 2001 From: Nalin Dahyabhai Date: Thu, 10 Mar 2022 08:51:46 -0500 Subject: [PATCH] build controller: set privileged=false If the BUILD_PRIVILEGED variable is set to "false" or 0 as an integer, set the "Privileged" flag on docker/sti/custom build pod SecurityContexts to false, and put CAP_MKNOD and CAP_KILL in the list of capabilities to drop. If it is set to "true" or non-zero, the "Privileged" flag will be set to true. If it is not set, we still default to privileged. Stop setting BUILD_STORAGE_DRIVER, since we're going to expect the builder image to figure this out on its own. Mount an emptyDir volume at /var/run/containers in setupContainersStorage(), so that we can ensure its permissions will be set so that the build user will always be able to write to it. Move the emptyDir volume for layers from /var/lib/containers/storage to /var/lib/containers, so that the build user can create the storage directory inside of it with exactly the permissions that the build user wants. When the pod security context is not privileged, configure the build pod by setting annotations that should instruct CRI-O to run the pod in a user namespace using mappings that CRI-O selects, and to provide the build pod with a working /dev/fuse. Set the seccomp profile for build pods to "unconfined", since we depend on unshare() and CRI-O is attempting to move away from allowing it to be used in its default seccomp profile. Signed-off-by: Nalin Dahyabhai --- .../controller/build/build_controller.go | 2 +- pkg/build/controller/strategy/custom.go | 19 +-- pkg/build/controller/strategy/custom_test.go | 24 +++- pkg/build/controller/strategy/docker.go | 37 +++--- pkg/build/controller/strategy/docker_test.go | 27 ++++- pkg/build/controller/strategy/sti.go | 37 +++--- pkg/build/controller/strategy/sti_test.go | 28 ++++- pkg/build/controller/strategy/util.go | 110 ++++++++++++------ pkg/build/controller/strategy/util_test.go | 105 +++++++++++++++++ 9 files changed, 288 insertions(+), 101 deletions(-) diff --git a/pkg/build/controller/build/build_controller.go b/pkg/build/controller/build/build_controller.go index 4be35fa22..8dff8ac36 100644 --- a/pkg/build/controller/build/build_controller.go +++ b/pkg/build/controller/build/build_controller.go @@ -1321,7 +1321,7 @@ func (bc *BuildController) createBuildPod(build *buildv1.Build) (*buildUpdate, e return update, fmt.Errorf("could not find registry config for build: %v", err) } if !hasRegistryConf { - // Create the registry config ConfigMap to mount the regsitry config to the existing build pod + // Create the registry config ConfigMap to mount the registry config to the existing build pod update, err = bc.createBuildSystemConfConfigMap(build, existingPod, update) if err != nil { return update, err diff --git a/pkg/build/controller/strategy/custom.go b/pkg/build/controller/strategy/custom.go index ca0159917..1ea162358 100644 --- a/pkg/build/controller/strategy/custom.go +++ b/pkg/build/controller/strategy/custom.go @@ -88,7 +88,7 @@ func (bs *CustomBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA serviceAccount = buildutil.BuilderServiceAccountName } - privileged := true + securityContext := securityContextForBuild(strategy.Env) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: buildutil.GetBuildPodName(build), @@ -99,13 +99,10 @@ func (bs *CustomBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA ServiceAccountName: serviceAccount, Containers: []corev1.Container{ { - Name: CustomBuild, - Image: strategy.From.Name, - Env: containerEnv, - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, + Name: CustomBuild, + Image: strategy.From.Name, + Env: containerEnv, + SecurityContext: securityContext, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, }, }, @@ -138,6 +135,10 @@ func (bs *CustomBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA setupAdditionalSecrets(pod, &pod.Spec.Containers[0], build.Spec.Strategy.CustomStrategy.Secrets) setupContainersConfigs(build, pod) setupBuildCAs(build, pod, additionalCAs, internalRegistryHost) - setupContainersStorage(pod, &pod.Spec.Containers[0]) // for unprivileged builds + setupContainersStorage(pod, &pod.Spec.Containers[0]) + if securityContext == nil || securityContext.Privileged == nil || !*securityContext.Privileged { + setupBuilderAutonsUser(build, strategy.Env, pod) + setupBuilderDeviceFUSE(pod) + } return pod, nil } diff --git a/pkg/build/controller/strategy/custom_test.go b/pkg/build/controller/strategy/custom_test.go index c5f6b0d7e..b5a5c336c 100644 --- a/pkg/build/controller/strategy/custom_test.go +++ b/pkg/build/controller/strategy/custom_test.go @@ -65,9 +65,10 @@ func TestCustomCreateBuildPod(t *testing.T) { // build-system-configmap // certificate authorities // container storage + // container run // global CA injection configmap - if len(container.VolumeMounts) != 8 { - t.Fatalf("Expected 8 volumes in container, got %d", len(container.VolumeMounts)) + if len(container.VolumeMounts) != 9 { + t.Fatalf("Expected 9 volumes in container, got %d", len(container.VolumeMounts)) } expectedMounts := []string{"/var/run/docker.sock", DockerPushSecretMountPath, @@ -76,7 +77,8 @@ func TestCustomCreateBuildPod(t *testing.T) { ConfigMapBuildSystemConfigsMountPath, ConfigMapCertsMountPath, ConfigMapBuildGlobalCAMountPath, - "/var/lib/containers/storage", + "/var/lib/containers", + "/var/run/containers", } for i, expected := range expectedMounts { if container.VolumeMounts[i].MountPath != expected { @@ -94,8 +96,8 @@ func TestCustomCreateBuildPod(t *testing.T) { if !kapihelper.Semantic.DeepEqual(container.Resources, build.Spec.Resources) { t.Fatalf("Expected actual=expected, %v != %v", container.Resources, build.Spec.Resources) } - if len(actual.Spec.Volumes) != 8 { - t.Fatalf("Expected 8 volumes in Build pod, got %d", len(actual.Spec.Volumes)) + if len(actual.Spec.Volumes) != 9 { + t.Fatalf("Expected 9 volumes in Build pod, got %d", len(actual.Spec.Volumes)) } buildJSON, _ := runtime.Encode(customBuildEncodingCodecFactory.LegacyCodec(buildv1.GroupVersion), build) errorCases := map[int][]string{ @@ -267,3 +269,15 @@ func mockCustomBuild(forcePull, emptySource bool) *buildv1.Build { }, } } + +func TestCustomCreateBuildPodAutonsUser(t *testing.T) { + strategy := CustomBuildStrategy{} + + build := mockCustomBuild(false, false) + + testCreateBuildPodAutonsUser(t, build, &strategy, + func(build *buildv1.Build, env corev1.EnvVar) { + build.Spec.Strategy.CustomStrategy.Env = append(build.Spec.Strategy.CustomStrategy.Env, env) + }, + ) +} diff --git a/pkg/build/controller/strategy/docker.go b/pkg/build/controller/strategy/docker.go index 98e0308dd..45b60de40 100644 --- a/pkg/build/controller/strategy/docker.go +++ b/pkg/build/controller/strategy/docker.go @@ -38,8 +38,8 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA return nil, fmt.Errorf("failed to encode the build: %v", err) } - privileged := true strategy := build.Spec.Strategy.DockerStrategy + securityContext := securityContextForBuild(strategy.Env) hostPathFile := v1.HostPathFile containerEnv := []v1.EnvVar{ @@ -69,14 +69,11 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA ServiceAccountName: serviceAccount, Containers: []v1.Container{ { - Name: DockerBuild, - Image: bs.Image, - Args: []string{"openshift-docker-build"}, - Env: copyEnvVarSlice(containerEnv), - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 - SecurityContext: &v1.SecurityContext{ - Privileged: &privileged, - }, + Name: DockerBuild, + Image: bs.Image, + Args: []string{"openshift-docker-build"}, + Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []v1.VolumeMount{ { @@ -133,6 +130,7 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA Image: bs.Image, Args: []string{"openshift-git-clone"}, Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []v1.VolumeMount{ { @@ -152,14 +150,11 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA } if len(build.Spec.Source.Images) > 0 { extractImageContentContainer := v1.Container{ - Name: ExtractImageContentContainer, - Image: bs.Image, - Args: []string{"openshift-extract-image-content"}, - Env: copyEnvVarSlice(containerEnv), - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 - SecurityContext: &v1.SecurityContext{ - Privileged: &privileged, - }, + Name: ExtractImageContentContainer, + Image: bs.Image, + Args: []string{"openshift-extract-image-content"}, + Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []v1.VolumeMount{ { @@ -188,6 +183,7 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA Image: bs.Image, Args: []string{"openshift-manage-dockerfile"}, Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []v1.VolumeMount{ { @@ -212,8 +208,11 @@ func (bs *DockerBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA setupInputConfigMaps(pod, &pod.Spec.Containers[0], build.Spec.Source.ConfigMaps) setupContainersConfigs(build, pod) setupBuildCAs(build, pod, additionalCAs, internalRegistryHost) - setupContainersStorage(pod, &pod.Spec.Containers[0]) // for unprivileged builds - // setupContainersNodeStorage(pod, &pod.Spec.Containers[0]) // for privileged builds + setupContainersStorage(pod, &pod.Spec.Containers[0]) + if securityContext == nil || securityContext.Privileged == nil || !*securityContext.Privileged { + setupBuilderAutonsUser(build, strategy.Env, pod) + setupBuilderDeviceFUSE(pod) + } setupBlobCache(pod) if err := setupBuildVolumes(pod, build.Spec.Strategy.DockerStrategy.Volumes, bs.BuildCSIVolumesEnabled); err != nil { return pod, err diff --git a/pkg/build/controller/strategy/docker_test.go b/pkg/build/controller/strategy/docker_test.go index 4608fd989..bc46423a4 100644 --- a/pkg/build/controller/strategy/docker_test.go +++ b/pkg/build/controller/strategy/docker_test.go @@ -65,7 +65,6 @@ func TestDockerCreateBuildPod(t *testing.T) { "BUILD_REGISTRIES_DIR_PATH": "", "BUILD_SIGNATURE_POLICY_PATH": "", "BUILD_STORAGE_CONF_PATH": "", - "BUILD_STORAGE_DRIVER": "", "BUILD_BLOBCACHE_DIR": "", "BUILD_MOUNT_ETC_PKI_CATRUST": "", } @@ -87,11 +86,12 @@ func TestDockerCreateBuildPod(t *testing.T) { // build-system-config // certificate authorities // container storage + // container run // blobs content cache // global CA injection configmap // node pull secrets - if len(container.VolumeMounts) != 12 { - t.Fatalf("Expected 12 volumes in container, got %d", len(container.VolumeMounts)) + if len(container.VolumeMounts) != 13 { + t.Fatalf("Expected 13 volumes in container, got %d", len(container.VolumeMounts)) } if *actual.Spec.ActiveDeadlineSeconds != 60 { t.Errorf("Expected ActiveDeadlineSeconds 60, got %d", *actual.Spec.ActiveDeadlineSeconds) @@ -106,7 +106,8 @@ func TestDockerCreateBuildPod(t *testing.T) { ConfigMapBuildSystemConfigsMountPath, ConfigMapCertsMountPath, ConfigMapBuildGlobalCAMountPath, - "/var/lib/containers/storage", + "/var/lib/containers", + "/var/run/containers", buildutil.BuildBlobsContentCache, } for i, expected := range expectedMounts { @@ -115,8 +116,8 @@ func TestDockerCreateBuildPod(t *testing.T) { } } // build pod has an extra volume: the git clone source secret - if len(actual.Spec.Volumes) != 13 { - t.Fatalf("Expected 13 volumes in Build pod, got %d", len(actual.Spec.Volumes)) + if len(actual.Spec.Volumes) != 14 { + t.Fatalf("Expected 14 volumes in Build pod, got %d", len(actual.Spec.Volumes)) } if !kapihelper.Semantic.DeepEqual(container.Resources, build.Spec.Resources) { t.Fatalf("Expected actual=expected, %v != %v", container.Resources, build.Spec.Resources) @@ -238,3 +239,17 @@ func mockDockerBuild() *buildv1.Build { }, } } + +func TestDockerCreateBuildPodAutonsUser(t *testing.T) { + strategy := DockerBuildStrategy{ + Image: "docker-test-image", + } + + build := mockDockerBuild() + + testCreateBuildPodAutonsUser(t, build, &strategy, + func(build *buildv1.Build, env corev1.EnvVar) { + build.Spec.Strategy.DockerStrategy.Env = append(build.Spec.Strategy.DockerStrategy.Env, env) + }, + ) +} diff --git a/pkg/build/controller/strategy/sti.go b/pkg/build/controller/strategy/sti.go index 2fe09ce18..efcff1d21 100644 --- a/pkg/build/controller/strategy/sti.go +++ b/pkg/build/controller/strategy/sti.go @@ -68,7 +68,7 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA } hostPathFile := corev1.HostPathFile - privileged := true + securityContext := securityContextForBuild(strategy.Env) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: buildutil.GetBuildPodName(build), @@ -79,14 +79,11 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA ServiceAccountName: serviceAccount, Containers: []corev1.Container{ { - Name: StiBuild, - Image: bs.Image, - Args: []string{"openshift-sti-build"}, - Env: copyEnvVarSlice(containerEnv), - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, + Name: StiBuild, + Image: bs.Image, + Args: []string{"openshift-sti-build"}, + Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []corev1.VolumeMount{ { @@ -140,6 +137,7 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA Image: bs.Image, Args: []string{"openshift-git-clone"}, Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []corev1.VolumeMount{ { @@ -159,14 +157,11 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA } if len(build.Spec.Source.Images) > 0 { extractImageContentContainer := corev1.Container{ - Name: ExtractImageContentContainer, - Image: bs.Image, - Args: []string{"openshift-extract-image-content"}, - Env: copyEnvVarSlice(containerEnv), - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 - SecurityContext: &corev1.SecurityContext{ - Privileged: &privileged, - }, + Name: ExtractImageContentContainer, + Image: bs.Image, + Args: []string{"openshift-extract-image-content"}, + Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []corev1.VolumeMount{ { @@ -195,6 +190,7 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA Image: bs.Image, Args: []string{"openshift-manage-dockerfile"}, Env: copyEnvVarSlice(containerEnv), + SecurityContext: securityContext, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, VolumeMounts: []corev1.VolumeMount{ { @@ -219,8 +215,11 @@ func (bs *SourceBuildStrategy) CreateBuildPod(build *buildv1.Build, additionalCA setupInputConfigMaps(pod, &pod.Spec.Containers[0], build.Spec.Source.ConfigMaps) setupContainersConfigs(build, pod) setupBuildCAs(build, pod, additionalCAs, internalRegistryHost) - setupContainersStorage(pod, &pod.Spec.Containers[0]) // for unprivileged builds - // setupContainersNodeStorage(pod, &pod.Spec.Containers[0]) // for privileged builds + setupContainersStorage(pod, &pod.Spec.Containers[0]) + if securityContext == nil || securityContext.Privileged == nil || !*securityContext.Privileged { + setupBuilderAutonsUser(build, strategy.Env, pod) + setupBuilderDeviceFUSE(pod) + } setupBlobCache(pod) if err := setupBuildVolumes(pod, build.Spec.Strategy.SourceStrategy.Volumes, bs.BuildCSIVolumeseEnabled); err != nil { return pod, err diff --git a/pkg/build/controller/strategy/sti_test.go b/pkg/build/controller/strategy/sti_test.go index 749e0499c..7d389c999 100644 --- a/pkg/build/controller/strategy/sti_test.go +++ b/pkg/build/controller/strategy/sti_test.go @@ -101,7 +101,6 @@ func testSTICreateBuildPod(t *testing.T, rootAllowed bool) { "BUILD_REGISTRIES_DIR_PATH": "", "BUILD_SIGNATURE_POLICY_PATH": "", "BUILD_STORAGE_CONF_PATH": "", - "BUILD_STORAGE_DRIVER": "", "BUILD_BLOBCACHE_DIR": "", "BUILD_MOUNT_ETC_PKI_CATRUST": "", } @@ -128,10 +127,11 @@ func testSTICreateBuildPod(t *testing.T, rootAllowed bool) { // build-system-configmap // certificate authorities // container storage + // container run // blobs content cache // global CA injection configmap - if len(container.VolumeMounts) != 12 { - t.Fatalf("Expected 12 volumes in container, got %d %v", len(container.VolumeMounts), container.VolumeMounts) + if len(container.VolumeMounts) != 13 { + t.Fatalf("Expected 13 volumes in container, got %d %v", len(container.VolumeMounts), container.VolumeMounts) } expectedMounts := []string{buildutil.NodePullSecretsPath, buildutil.BuildWorkDirMount, @@ -143,7 +143,8 @@ func testSTICreateBuildPod(t *testing.T, rootAllowed bool) { ConfigMapBuildSystemConfigsMountPath, ConfigMapCertsMountPath, ConfigMapBuildGlobalCAMountPath, - "/var/lib/containers/storage", + "/var/lib/containers", + "/var/run/containers", buildutil.BuildBlobsContentCache, } for i, expected := range expectedMounts { @@ -152,8 +153,8 @@ func testSTICreateBuildPod(t *testing.T, rootAllowed bool) { } } // build pod has an extra volume: the git clone source secret - if len(actual.Spec.Volumes) != 13 { - t.Fatalf("Expected 13 volumes in Build pod, got %d", len(actual.Spec.Volumes)) + if len(actual.Spec.Volumes) != 14 { + t.Fatalf("Expected 14 volumes in Build pod, got %d", len(actual.Spec.Volumes)) } if *actual.Spec.ActiveDeadlineSeconds != 60 { t.Errorf("Expected ActiveDeadlineSeconds 60, got %d", *actual.Spec.ActiveDeadlineSeconds) @@ -303,3 +304,18 @@ func mockSTIBuild() *buildv1.Build { }, } } + +func TestS2ICreateBuildPodAutonsUser(t *testing.T) { + strategy := SourceBuildStrategy{ + Image: "sti-test-image", + SecurityClient: newFakeSecurityClient(true), + } + + build := mockSTIBuild() + + testCreateBuildPodAutonsUser(t, build, &strategy, + func(build *buildv1.Build, env corev1.EnvVar) { + build.Spec.Strategy.SourceStrategy.Env = append(build.Spec.Strategy.SourceStrategy.Env, env) + }, + ) +} diff --git a/pkg/build/controller/strategy/util.go b/pkg/build/controller/strategy/util.go index 5be5b65e1..abb9bfed1 100644 --- a/pkg/build/controller/strategy/util.go +++ b/pkg/build/controller/strategy/util.go @@ -148,7 +148,10 @@ func mountVolume(pod *corev1.Pod, container *corev1.Container, objName, mountPat break } } - mode := int32(0600) + mode := int32(0o600) + if container.SecurityContext == nil || container.SecurityContext.Privileged == nil || !*container.SecurityContext.Privileged { + mode = int32(0o644) // make sure unprivileged builders can read them + } if !volumeExists { volume := makeVolume(volumeName, objName, mode, fsType, volumeSource) pod.Spec.Volumes = append(pod.Spec.Volumes, volume) @@ -465,17 +468,19 @@ func updateConfigsForContainer(c corev1.Container, volumeName string, configDir return c } -// setupContainersStorage creates a volume that we'll use for holding images and working +// setupContainersStorage creates volumes that we'll use for holding images and working // root filesystems for building images. func setupContainersStorage(pod *corev1.Pod, container *corev1.Container) { - exists := false + rootExists, runExists := false, false for _, v := range pod.Spec.Volumes { if v.Name == "container-storage-root" { - exists = true - break + rootExists = true + } + if v.Name == "container-storage-run" { + runExists = true } } - if !exists { + if !rootExists { pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ Name: "container-storage-root", @@ -485,46 +490,26 @@ func setupContainersStorage(pod *corev1.Pod, container *corev1.Container) { }, ) } - container.VolumeMounts = append(container.VolumeMounts, - corev1.VolumeMount{ - Name: "container-storage-root", - MountPath: "/var/lib/containers/storage", - }, - ) - container.Env = append(container.Env, corev1.EnvVar{Name: "BUILD_STORAGE_DRIVER", Value: "overlay"}) -} - -// setupContainersNodeStorage borrows the appropriate storage directories from the node so -// that we can share layers that we're using with the node -func setupContainersNodeStorage(pod *corev1.Pod, container *corev1.Container) { - exists := false - for _, v := range pod.Spec.Volumes { - if v.Name == "node-storage-root" { - exists = true - break - } - } - if !exists { + if !runExists { pod.Spec.Volumes = append(pod.Spec.Volumes, - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 corev1.Volume{ - Name: "node-storage-root", + Name: "container-storage-run", VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: "/var/lib/containers/storage", - }, + EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, ) } container.VolumeMounts = append(container.VolumeMounts, - // TODO: run unprivileged https://github.com/openshift/origin/issues/662 corev1.VolumeMount{ - Name: "node-storage-root", - MountPath: "/var/lib/containers/storage", + Name: "container-storage-root", + MountPath: "/var/lib/containers", + }, + corev1.VolumeMount{ + Name: "container-storage-run", + MountPath: "/var/run/containers", }, ) - container.Env = append(container.Env, corev1.EnvVar{Name: "BUILD_STORAGE_DRIVER", Value: "overlay"}) } func addVolumeMountToContainers(conts []corev1.Container, mount corev1.VolumeMount) []corev1.Container { @@ -536,6 +521,60 @@ func addVolumeMountToContainers(conts []corev1.Container, mount corev1.VolumeMou return containers } +// securityContextForBuild returns a security context that limits the requested +// privileges because the build spec wanted an unprivileged build, or that sets +// the pod to be privileged otherwise. +func securityContextForBuild(vars []corev1.EnvVar) *corev1.SecurityContext { + privileged := true + for _, env := range vars { + if env.Name == "BUILD_PRIVILEGED" { + if b, err := strconv.ParseBool(env.Value); err == nil { + privileged = b + } else if i, err := strconv.Atoi(env.Value); err == nil { + privileged = i != 0 + } + } + } + securityContext := &corev1.SecurityContext{ + Privileged: &privileged, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeUnconfined, // default profile might ban unshare() + }, + } + if privileged { + uid, gid := int64(0), int64(0) + securityContext.RunAsUser = &uid + securityContext.RunAsGroup = &gid + } else { + securityContext.Capabilities = &corev1.Capabilities{ + Add: []corev1.Capability{ + "CAP_SETFCAP", + }, + Drop: []corev1.Capability{ + "CAP_KILL", + "CAP_MKNOD", + }, + } + } + return securityContext +} + +// Add annotations that should tell CRI-O to provide /dev/fuse in the pod's +// containers' device control group and in the /dev that the runtime will set +// up for them. +// Reference: https://github.com/openshift/machine-config-operator/pull/2805 +func setupBuilderDeviceFUSE(pod *corev1.Pod) { + metav1.SetMetaDataAnnotation(&pod.ObjectMeta, "io.openshift.builder", "") + metav1.SetMetaDataAnnotation(&pod.ObjectMeta, "io.kubernetes.cri-o.Devices", "/dev/fuse:rwm") +} + +// Request that the builder be run in a user namespace, mapped from host ID +// ranges chosen by the node. +func setupBuilderAutonsUser(build *buildv1.Build, vars []corev1.EnvVar, pod *corev1.Pod) { + metav1.SetMetaDataAnnotation(&pod.ObjectMeta, "io.openshift.builder", "") + metav1.SetMetaDataAnnotation(&pod.ObjectMeta, "io.kubernetes.cri-o.userns-mode", "auto:size=65536") +} + // setupBuildCAs mounts certificate authorities for the build from a predetermined ConfigMap. func setupBuildCAs(build *buildv1.Build, pod *corev1.Pod, additionalCAs map[string]string, internalRegistryHost string) { casExist := false @@ -721,7 +760,6 @@ func setupBuildVolumes(pod *corev1.Pod, buildVolumes []buildv1.BuildVolume, csiV } volumeSource.CSI = buildVolume.Source.CSI mountCSIVolume(pod, &pod.Spec.Containers[0], strings.ToLower(buildVolume.Name), PathForBuildVolume(buildVolume.Name), buildVolumeSuffix, &volumeSource) - default: return fmt.Errorf("encountered unsupported build volume source type %q", buildVolume.Source.Type) } diff --git a/pkg/build/controller/strategy/util_test.go b/pkg/build/controller/strategy/util_test.go index 222be33bc..68e94993a 100644 --- a/pkg/build/controller/strategy/util_test.go +++ b/pkg/build/controller/strategy/util_test.go @@ -4,10 +4,12 @@ import ( "fmt" "path/filepath" "reflect" + "strings" "testing" "unsafe" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" buildv1 "github.com/openshift/api/build/v1" ) @@ -813,3 +815,106 @@ func TestSetupBuildVolumes(t *testing.T) { }) } } + +type buildPodCreator interface { + CreateBuildPod(build *buildv1.Build, additionalCAs map[string]string, internalRegistryHost string) (*corev1.Pod, error) +} + +func testCreateBuildPodAutonsUser(t *testing.T, build *buildv1.Build, strategy buildPodCreator, addEnv func(build *buildv1.Build, env corev1.EnvVar)) { + for _, testCase := range []struct { + env string + expectError bool + privileged bool + annotations map[string]string + noAnnotations []string + }{ + { + env: "", + privileged: true, + noAnnotations: []string{ + "io.openshift.builder", + "io.kubernetes.cri-o.Devices", + "io.kubernetes.cri-o.userns-mode", + }, + }, + { + env: "BUILD_PRIVILEGED=0", + privileged: false, + annotations: map[string]string{ + "io.openshift.builder": "", + "io.kubernetes.cri-o.Devices": "/dev/fuse:rwm", + "io.kubernetes.cri-o.userns-mode": "auto:size=65536", + }, + }, + { + env: "BUILD_PRIVILEGED=42", + privileged: true, + noAnnotations: []string{ + "io.openshift.builder", + "io.kubernetes.cri-o.Devices", + "io.kubernetes.cri-o.userns-mode", + }, + }, + { + env: "BUILD_PRIVILEGED=false", + privileged: false, + annotations: map[string]string{ + "io.openshift.builder": "", + "io.kubernetes.cri-o.Devices": "/dev/fuse:rwm", + "io.kubernetes.cri-o.userns-mode": "auto:size=65536", + }, + }, + { + env: "BUILD_PRIVILEGED=true", + privileged: true, + noAnnotations: []string{ + "io.openshift.builder", + "io.kubernetes.cri-o.Devices", + "io.kubernetes.cri-o.userns-mode", + }, + }, + } { + t.Run(testCase.env, func(t *testing.T) { + build := build.DeepCopy() + for _, envVar := range strings.Split(testCase.env, ":") { + if env := strings.SplitN(envVar, "=", 2); len(env) > 1 { + addEnv(build, corev1.EnvVar{Name: env[0], Value: env[1]}) + } + } + actual, err := strategy.CreateBuildPod(build, nil, testInternalRegistryHost) + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + for ctrIndex, ctr := range append(actual.Spec.Containers, actual.Spec.InitContainers...) { + sc := ctr.SecurityContext + if sc == nil { + t.Errorf("Container %d in pod spec has no SecurityContext", ctrIndex) + continue + } + if sc.Privileged == nil { + t.Errorf("Container %d in pod spec has no privileged field", ctrIndex) + continue + } + if *sc.Privileged != testCase.privileged { + t.Errorf("Expected privileged: %q to produce privileged=%v, got %v", testCase.env, testCase.privileged, *sc.Privileged) + } + } + for annotation, value := range testCase.annotations { + if !metav1.HasAnnotation(actual.ObjectMeta, annotation) { + t.Errorf("Expected a %q annotation, but don't see one", annotation) + continue + } + annotations := actual.ObjectMeta.GetAnnotations() + if val := annotations[annotation]; val != value { + t.Errorf("Annotation %q was expected to be %q, but was actually %q", annotation, value, val) + } + } + for _, annotation := range testCase.noAnnotations { + if metav1.HasAnnotation(actual.ObjectMeta, annotation) { + t.Errorf("Expected no %q annotation, but got one", annotation) + } + } + }) + } +}