From 8b7003aff4c81f124851041eafb8899ea7e83ffd Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Wed, 12 May 2021 11:20:30 +0200 Subject: [PATCH] Add SeccompDefault feature This adds the gate `SeccompDefault` as new alpha feature. Seccomp path and field fallbacks are now passed to the helper functions, whereas unit tests covering those code paths have been added as well. Beside enabling the feature gate, the feature has to be enabled by the `SeccompDefault` kubelet configuration or its corresponding `--seccomp-default` CLI flag. Signed-off-by: Sascha Grunert Apply suggestions from code review Co-authored-by: Paulo Gomes Signed-off-by: Sascha Grunert --- cmd/kubelet/app/options/options.go | 8 + cmd/kubelet/app/server.go | 16 +- pkg/features/kube_features.go | 7 + pkg/kubelet/apis/config/helpers_test.go | 1 + .../KubeletConfiguration/after/v1beta1.yaml | 1 + .../roundtrip/default/v1beta1.yaml | 1 + pkg/kubelet/apis/config/types.go | 2 + pkg/kubelet/apis/config/v1beta1/defaults.go | 3 + .../config/v1beta1/zz_generated.conversion.go | 6 + pkg/kubelet/kubelet.go | 5 +- pkg/kubelet/kuberuntime/helpers.go | 40 +- pkg/kubelet/kuberuntime/helpers_test.go | 348 +++++++++++++++++- .../kuberuntime/kuberuntime_manager.go | 5 + pkg/kubelet/kuberuntime/security_context.go | 4 +- .../k8s.io/kubelet/config/v1beta1/types.go | 5 + .../config/v1beta1/zz_generated.deepcopy.go | 5 + 16 files changed, 437 insertions(+), 20 deletions(-) diff --git a/cmd/kubelet/app/options/options.go b/cmd/kubelet/app/options/options.go index 09631068646d..98da4b7b387c 100644 --- a/cmd/kubelet/app/options/options.go +++ b/cmd/kubelet/app/options/options.go @@ -166,6 +166,9 @@ type KubeletFlags struct { // This flag, if set, instructs the kubelet to keep volumes from terminated pods mounted to the node. // This can be useful for debugging volume related issues. KeepTerminatedPodVolumes bool + // SeccompDefault enables the use of `RuntimeDefault` as the default seccomp profile for all workloads on the node. + // To use this flag, the corresponding SeccompDefault feature gate must be enabled. + SeccompDefault bool } // NewKubeletFlags will create a new KubeletFlags with default values @@ -211,6 +214,10 @@ func ValidateKubeletFlags(f *KubeletFlags) error { return fmt.Errorf("unknown 'kubernetes.io' or 'k8s.io' labels specified with --node-labels: %v\n--node-labels in the 'kubernetes.io' namespace must begin with an allowed prefix (%s) or be in the specifically allowed set (%s)", unknownLabels.List(), strings.Join(kubeletapis.KubeletLabelNamespaces(), ", "), strings.Join(kubeletapis.KubeletLabels(), ", ")) } + if f.SeccompDefault && !utilfeature.DefaultFeatureGate.Enabled(features.SeccompDefault) { + return fmt.Errorf("the SeccompDefault feature gate must be enabled in order to use the --seccomp-default flag") + } + return nil } @@ -346,6 +353,7 @@ func (f *KubeletFlags) AddFlags(mainfs *pflag.FlagSet) { fs.Var(&bindableNodeLabels, "node-labels", fmt.Sprintf(" Labels to add when registering the node in the cluster. Labels must be key=value pairs separated by ','. Labels in the 'kubernetes.io' namespace must begin with an allowed prefix (%s) or be in the specifically allowed set (%s)", strings.Join(kubeletapis.KubeletLabelNamespaces(), ", "), strings.Join(kubeletapis.KubeletLabels(), ", "))) fs.StringVar(&f.LockFilePath, "lock-file", f.LockFilePath, " The path to file for kubelet to use as a lock file.") fs.BoolVar(&f.ExitOnLockContention, "exit-on-lock-contention", f.ExitOnLockContention, "Whether kubelet should exit upon lock-file contention.") + fs.BoolVar(&f.SeccompDefault, "seccomp-default", f.SeccompDefault, " Enable the use of `RuntimeDefault` as the default seccomp profile for all workloads. The SeccompDefault feature gate must be enabled to allow this flag, which is disabled per default.") // DEPRECATED FLAGS fs.StringVar(&f.BootstrapKubeconfig, "experimental-bootstrap-kubeconfig", f.BootstrapKubeconfig, "") diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index 59662e425bb2..942cececcd0c 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -1135,6 +1135,10 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie kubeDeps.OSInterface = kubecontainer.RealOS{} } + if kubeServer.KubeletConfiguration.SeccompDefault && !utilfeature.DefaultFeatureGate.Enabled(features.SeccompDefault) { + return fmt.Errorf("the SeccompDefault feature gate must be enabled in order to use the SeccompDefault configuration") + } + k, err := createAndInitKubelet(&kubeServer.KubeletConfiguration, kubeDeps, &kubeServer.ContainerRuntimeOptions, @@ -1164,7 +1168,9 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie kubeServer.KeepTerminatedPodVolumes, kubeServer.NodeLabels, kubeServer.SeccompProfileRoot, - kubeServer.NodeStatusMaxImages) + kubeServer.NodeStatusMaxImages, + kubeServer.KubeletFlags.SeccompDefault || kubeServer.KubeletConfiguration.SeccompDefault, + ) if err != nil { return fmt.Errorf("failed to create kubelet: %w", err) } @@ -1238,7 +1244,9 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, keepTerminatedPodVolumes bool, nodeLabels map[string]string, seccompProfileRoot string, - nodeStatusMaxImages int32) (k kubelet.Bootstrap, err error) { + nodeStatusMaxImages int32, + seccompDefault bool, +) (k kubelet.Bootstrap, err error) { // TODO: block until all sources have delivered at least one update to the channel, or break the sync loop // up into "per source" synchronizations @@ -1271,7 +1279,9 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, keepTerminatedPodVolumes, nodeLabels, seccompProfileRoot, - nodeStatusMaxImages) + nodeStatusMaxImages, + seccompDefault, + ) if err != nil { return nil, err } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index e299fb022a48..1d1b9af58779 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -721,6 +721,12 @@ const ( // // Enables apiserver and kubelet to allow up to 32 DNSSearchPaths and up to 2048 DNSSearchListChars. ExpandedDNSConfig featuregate.Feature = "ExpandedDNSConfig" + + // owner: @saschagrunert + // alpha: v1.22 + // + // Enables the use of `RuntimeDefault` as the default seccomp profile for all workloads. + SeccompDefault featuregate.Feature = "SeccompDefault" ) func init() { @@ -829,6 +835,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS DisableCloudProviders: {Default: false, PreRelease: featuregate.Alpha}, StatefulSetMinReadySeconds: {Default: false, PreRelease: featuregate.Alpha}, ExpandedDNSConfig: {Default: false, PreRelease: featuregate.Alpha}, + SeccompDefault: {Default: false, PreRelease: featuregate.Alpha}, // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/kubelet/apis/config/helpers_test.go b/pkg/kubelet/apis/config/helpers_test.go index 2ec98df77f57..72a4d59b34fc 100644 --- a/pkg/kubelet/apis/config/helpers_test.go +++ b/pkg/kubelet/apis/config/helpers_test.go @@ -234,6 +234,7 @@ var ( "ReservedSystemCPUs", "RuntimeRequestTimeout.Duration", "RunOnce", + "SeccompDefault", "SerializeImagePulls", "ShowHiddenMetricsForVersion", "StreamingConnectionIdleTimeout.Duration", diff --git a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml index 344f85437376..6429d5ad253b 100644 --- a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml +++ b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml @@ -69,6 +69,7 @@ registryBurst: 10 registryPullQPS: 5 resolvConf: /etc/resolv.conf runtimeRequestTimeout: 2m0s +seccompDefault: false serializeImagePulls: true shutdownGracePeriod: 0s shutdownGracePeriodCriticalPods: 0s diff --git a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml index 344f85437376..6429d5ad253b 100644 --- a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml +++ b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml @@ -69,6 +69,7 @@ registryBurst: 10 registryPullQPS: 5 resolvConf: /etc/resolv.conf runtimeRequestTimeout: 2m0s +seccompDefault: false serializeImagePulls: true shutdownGracePeriod: 0s shutdownGracePeriodCriticalPods: 0s diff --git a/pkg/kubelet/apis/config/types.go b/pkg/kubelet/apis/config/types.go index fcc86830b5a3..7502d24ace1a 100644 --- a/pkg/kubelet/apis/config/types.go +++ b/pkg/kubelet/apis/config/types.go @@ -407,6 +407,8 @@ type KubeletConfiguration struct { EnableProfilingHandler bool // EnableDebugFlagsHandler enables/debug/flags/v handler. EnableDebugFlagsHandler bool + // SeccompDefault enables the use of `RuntimeDefault` as the default seccomp profile for all workloads. + SeccompDefault bool } // KubeletAuthorizationMode denotes the authorization mode for the kubelet diff --git a/pkg/kubelet/apis/config/v1beta1/defaults.go b/pkg/kubelet/apis/config/v1beta1/defaults.go index f16c8b490274..9e313eac50bd 100644 --- a/pkg/kubelet/apis/config/v1beta1/defaults.go +++ b/pkg/kubelet/apis/config/v1beta1/defaults.go @@ -252,4 +252,7 @@ func SetDefaults_KubeletConfiguration(obj *kubeletconfigv1beta1.KubeletConfigura if obj.EnableDebugFlagsHandler == nil { obj.EnableDebugFlagsHandler = utilpointer.BoolPtr(true) } + if obj.SeccompDefault == nil { + obj.SeccompDefault = utilpointer.BoolPtr(false) + } } diff --git a/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go b/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go index 48606111fa43..e200d3a0767e 100644 --- a/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go +++ b/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go @@ -371,6 +371,9 @@ func autoConvert_v1beta1_KubeletConfiguration_To_config_KubeletConfiguration(in if err := v1.Convert_Pointer_bool_To_bool(&in.EnableDebugFlagsHandler, &out.EnableDebugFlagsHandler, s); err != nil { return err } + if err := v1.Convert_Pointer_bool_To_bool(&in.SeccompDefault, &out.SeccompDefault, s); err != nil { + return err + } return nil } @@ -532,6 +535,9 @@ func autoConvert_config_KubeletConfiguration_To_v1beta1_KubeletConfiguration(in if err := v1.Convert_bool_To_Pointer_bool(&in.EnableDebugFlagsHandler, &out.EnableDebugFlagsHandler, s); err != nil { return err } + if err := v1.Convert_bool_To_Pointer_bool(&in.SeccompDefault, &out.SeccompDefault, s); err != nil { + return err + } return nil } diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 4cd869c20a6d..ebf2fbf5e2fe 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -367,7 +367,9 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, keepTerminatedPodVolumes bool, nodeLabels map[string]string, seccompProfileRoot string, - nodeStatusMaxImages int32) (*Kubelet, error) { + nodeStatusMaxImages int32, + seccompDefault bool, +) (*Kubelet, error) { if rootDirectory == "" { return nil, fmt.Errorf("invalid root directory %q", rootDirectory) } @@ -649,6 +651,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps.dockerLegacyService, klet.containerLogManager, klet.runtimeClassManager, + seccompDefault, ) if err != nil { return nil, err diff --git a/pkg/kubelet/kuberuntime/helpers.go b/pkg/kubelet/kuberuntime/helpers.go index 7b4fb7c5d54b..8974888a15fa 100644 --- a/pkg/kubelet/kuberuntime/helpers.go +++ b/pkg/kubelet/kuberuntime/helpers.go @@ -202,8 +202,11 @@ func toKubeRuntimeStatus(status *runtimeapi.RuntimeStatus) *kubecontainer.Runtim return &kubecontainer.RuntimeStatus{Conditions: conditions} } -func fieldProfile(scmp *v1.SeccompProfile, profileRootPath string) string { +func fieldProfile(scmp *v1.SeccompProfile, profileRootPath string, fallbackToRuntimeDefault bool) string { if scmp == nil { + if fallbackToRuntimeDefault { + return v1.SeccompProfileRuntimeDefault + } return "" } if scmp.Type == v1.SeccompProfileTypeRuntimeDefault { @@ -216,6 +219,10 @@ func fieldProfile(scmp *v1.SeccompProfile, profileRootPath string) string { if scmp.Type == v1.SeccompProfileTypeUnconfined { return v1.SeccompProfileNameUnconfined } + + if fallbackToRuntimeDefault { + return v1.SeccompProfileRuntimeDefault + } return "" } @@ -229,10 +236,10 @@ func annotationProfile(profile, profileRootPath string) string { } func (m *kubeGenericRuntimeManager) getSeccompProfilePath(annotations map[string]string, containerName string, - podSecContext *v1.PodSecurityContext, containerSecContext *v1.SecurityContext) string { + podSecContext *v1.PodSecurityContext, containerSecContext *v1.SecurityContext, fallbackToRuntimeDefault bool) string { // container fields are applied first if containerSecContext != nil && containerSecContext.SeccompProfile != nil { - return fieldProfile(containerSecContext.SeccompProfile, m.seccompProfileRoot) + return fieldProfile(containerSecContext.SeccompProfile, m.seccompProfileRoot, fallbackToRuntimeDefault) } // if container field does not exist, try container annotation (deprecated) @@ -244,7 +251,7 @@ func (m *kubeGenericRuntimeManager) getSeccompProfilePath(annotations map[string // when container seccomp is not defined, try to apply from pod field if podSecContext != nil && podSecContext.SeccompProfile != nil { - return fieldProfile(podSecContext.SeccompProfile, m.seccompProfileRoot) + return fieldProfile(podSecContext.SeccompProfile, m.seccompProfileRoot, fallbackToRuntimeDefault) } // as last resort, try to apply pod annotation (deprecated) @@ -252,13 +259,20 @@ func (m *kubeGenericRuntimeManager) getSeccompProfilePath(annotations map[string return annotationProfile(profile, m.seccompProfileRoot) } + if fallbackToRuntimeDefault { + return v1.SeccompProfileRuntimeDefault + } + return "" } -func fieldSeccompProfile(scmp *v1.SeccompProfile, profileRootPath string) *runtimeapi.SecurityProfile { - // TODO: Move to RuntimeDefault as the default instead of Unconfined after discussion - // with sig-node. +func fieldSeccompProfile(scmp *v1.SeccompProfile, profileRootPath string, fallbackToRuntimeDefault bool) *runtimeapi.SecurityProfile { if scmp == nil { + if fallbackToRuntimeDefault { + return &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_RuntimeDefault, + } + } return &runtimeapi.SecurityProfile{ ProfileType: runtimeapi.SecurityProfile_Unconfined, } @@ -281,15 +295,21 @@ func fieldSeccompProfile(scmp *v1.SeccompProfile, profileRootPath string) *runti } func (m *kubeGenericRuntimeManager) getSeccompProfile(annotations map[string]string, containerName string, - podSecContext *v1.PodSecurityContext, containerSecContext *v1.SecurityContext) *runtimeapi.SecurityProfile { + podSecContext *v1.PodSecurityContext, containerSecContext *v1.SecurityContext, fallbackToRuntimeDefault bool) *runtimeapi.SecurityProfile { // container fields are applied first if containerSecContext != nil && containerSecContext.SeccompProfile != nil { - return fieldSeccompProfile(containerSecContext.SeccompProfile, m.seccompProfileRoot) + return fieldSeccompProfile(containerSecContext.SeccompProfile, m.seccompProfileRoot, fallbackToRuntimeDefault) } // when container seccomp is not defined, try to apply from pod field if podSecContext != nil && podSecContext.SeccompProfile != nil { - return fieldSeccompProfile(podSecContext.SeccompProfile, m.seccompProfileRoot) + return fieldSeccompProfile(podSecContext.SeccompProfile, m.seccompProfileRoot, fallbackToRuntimeDefault) + } + + if fallbackToRuntimeDefault { + return &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_RuntimeDefault, + } } return &runtimeapi.SecurityProfile{ diff --git a/pkg/kubelet/kuberuntime/helpers_test.go b/pkg/kubelet/kuberuntime/helpers_test.go index 1628cb3cf24c..90244d50f221 100644 --- a/pkg/kubelet/kuberuntime/helpers_test.go +++ b/pkg/kubelet/kuberuntime/helpers_test.go @@ -210,7 +210,7 @@ func TestFieldProfile(t *testing.T) { expectedProfile: "unconfined", }, { - description: "SeccompProfileTypeLocalhost should return unconfined", + description: "SeccompProfileTypeLocalhost should return localhost", scmpProfile: &v1.SeccompProfile{ Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: utilpointer.StringPtr("profile.json"), @@ -221,7 +221,63 @@ func TestFieldProfile(t *testing.T) { } for i, test := range tests { - seccompProfile := fieldProfile(test.scmpProfile, test.rootPath) + seccompProfile := fieldProfile(test.scmpProfile, test.rootPath, false) + assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) + } +} + +func TestFieldProfileDefaultSeccomp(t *testing.T) { + tests := []struct { + description string + scmpProfile *v1.SeccompProfile + rootPath string + expectedProfile string + }{ + { + description: "no seccompProfile should return runtime/default", + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "type localhost without profile should return runtime/default", + scmpProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeLocalhost, + }, + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "unknown type should return runtime/default", + scmpProfile: &v1.SeccompProfile{ + Type: "", + }, + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "SeccompProfileTypeRuntimeDefault should return runtime/default", + scmpProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeRuntimeDefault, + }, + expectedProfile: "runtime/default", + }, + { + description: "SeccompProfileTypeUnconfined should return unconfined", + scmpProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeUnconfined, + }, + expectedProfile: "unconfined", + }, + { + description: "SeccompProfileTypeLocalhost should return localhost", + scmpProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeLocalhost, + LocalhostProfile: utilpointer.StringPtr("profile.json"), + }, + rootPath: "/test/", + expectedProfile: "localhost//test/profile.json", + }, + } + + for i, test := range tests { + seccompProfile := fieldProfile(test.scmpProfile, test.rootPath, true) assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) } } @@ -411,7 +467,197 @@ func TestGetSeccompProfilePath(t *testing.T) { } for i, test := range tests { - seccompProfile := m.getSeccompProfilePath(test.annotation, test.containerName, test.podSc, test.containerSc) + seccompProfile := m.getSeccompProfilePath(test.annotation, test.containerName, test.podSc, test.containerSc, false) + assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) + } +} + +func TestGetSeccompProfilePathDefaultSeccomp(t *testing.T) { + _, _, m, err := createTestRuntimeManager() + require.NoError(t, err) + + tests := []struct { + description string + annotation map[string]string + podSc *v1.PodSecurityContext + containerSc *v1.SecurityContext + containerName string + expectedProfile string + }{ + { + description: "no seccomp should return runtime/default", + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "annotations: no seccomp with containerName should return runtime/default", + containerName: "container1", + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "annotations: pod runtime/default seccomp profile should return runtime/default", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault, + }, + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "annotations: pod docker/default seccomp profile should return docker/default", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault, + }, + expectedProfile: "docker/default", + }, + { + description: "annotations: pod runtime/default seccomp profile with containerName should return runtime/default", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault, + }, + containerName: "container1", + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "annotations: pod docker/default seccomp profile with containerName should return docker/default", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault, + }, + containerName: "container1", + expectedProfile: "docker/default", + }, + { + description: "annotations: pod unconfined seccomp profile should return unconfined", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileNameUnconfined, + }, + expectedProfile: "unconfined", + }, + { + description: "annotations: pod unconfined seccomp profile with containerName should return unconfined", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileNameUnconfined, + }, + containerName: "container1", + expectedProfile: "unconfined", + }, + { + description: "annotations: pod localhost seccomp profile should return local profile path", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: "localhost/chmod.json", + }, + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "chmod.json"), + }, + { + description: "annotations: pod localhost seccomp profile with containerName should return local profile path", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: "localhost/chmod.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "chmod.json"), + }, + { + description: "annotations: container localhost seccomp profile with containerName should return local profile path", + annotation: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + "container1": "localhost/chmod.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "chmod.json"), + }, + { + description: "annotations: container localhost seccomp profile should override pod profile", + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileNameUnconfined, + v1.SeccompContainerAnnotationKeyPrefix + "container1": "localhost/chmod.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "chmod.json"), + }, + { + description: "annotations: container localhost seccomp profile with unmatched containerName should return runtime/default", + annotation: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + "container1": "localhost/chmod.json", + }, + containerName: "container2", + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "pod seccomp profile set to unconfined returns unconfined", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + expectedProfile: "unconfined", + }, + { + description: "container seccomp profile set to unconfined returns unconfined", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + expectedProfile: "unconfined", + }, + { + description: "pod seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: "runtime/default", + }, + { + description: "container seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: "runtime/default", + }, + { + description: "pod seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename")}}, + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "filename"), + }, + { + description: "pod seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns runtime/default", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "container seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns runtime/default", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, + expectedProfile: v1.SeccompProfileRuntimeDefault, + }, + { + description: "container seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename2")}}, + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "filename2"), + }, + { + description: "prioritise container field over pod field", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: "runtime/default", + }, + { + description: "prioritise container field over container annotation, pod field and pod annotation", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-cont-profile.json")}}, + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: "localhost/annota-pod-profile.json", + v1.SeccompContainerAnnotationKeyPrefix + "container1": "localhost/annota-cont-profile.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "field-cont-profile.json"), + }, + { + description: "prioritise container annotation over pod field", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: "localhost/annota-pod-profile.json", + v1.SeccompContainerAnnotationKeyPrefix + "container1": "localhost/annota-cont-profile.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "annota-cont-profile.json"), + }, + { + description: "prioritise pod field over pod annotation", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, + annotation: map[string]string{ + v1.SeccompPodAnnotationKey: "localhost/annota-pod-profile.json", + }, + containerName: "container1", + expectedProfile: "localhost/" + filepath.Join(fakeSeccompProfileRoot, "field-pod-profile.json"), + }, + } + + for i, test := range tests { + seccompProfile := m.getSeccompProfilePath(test.annotation, test.containerName, test.podSc, test.containerSc, true) assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) } } @@ -505,7 +751,101 @@ func TestGetSeccompProfile(t *testing.T) { } for i, test := range tests { - seccompProfile := m.getSeccompProfile(test.annotation, test.containerName, test.podSc, test.containerSc) + seccompProfile := m.getSeccompProfile(test.annotation, test.containerName, test.podSc, test.containerSc, false) + assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) + } +} + +func TestGetSeccompProfileDefaultSeccomp(t *testing.T) { + _, _, m, err := createTestRuntimeManager() + require.NoError(t, err) + + unconfinedProfile := &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_Unconfined, + } + + runtimeDefaultProfile := &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_RuntimeDefault, + } + + tests := []struct { + description string + annotation map[string]string + podSc *v1.PodSecurityContext + containerSc *v1.SecurityContext + containerName string + expectedProfile *runtimeapi.SecurityProfile + }{ + { + description: "no seccomp should return RuntimeDefault", + expectedProfile: runtimeDefaultProfile, + }, + { + description: "pod seccomp profile set to unconfined returns unconfined", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + expectedProfile: unconfinedProfile, + }, + { + description: "container seccomp profile set to unconfined returns unconfined", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + expectedProfile: unconfinedProfile, + }, + { + description: "pod seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: runtimeDefaultProfile, + }, + { + description: "container seccomp profile set to SeccompProfileTypeRuntimeDefault returns runtime/default", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: runtimeDefaultProfile, + }, + { + description: "pod seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename")}}, + expectedProfile: &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_Localhost, + LocalhostRef: filepath.Join(fakeSeccompProfileRoot, "filename"), + }, + }, + { + description: "pod seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns unconfined", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, + expectedProfile: unconfinedProfile, + }, + { + description: "container seccomp profile set to SeccompProfileTypeLocalhost with empty LocalhostProfile returns unconfined", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost}}, + expectedProfile: unconfinedProfile, + }, + { + description: "container seccomp profile set to SeccompProfileTypeLocalhost returns 'localhost/' + LocalhostProfile", + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("filename2")}}, + expectedProfile: &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_Localhost, + LocalhostRef: filepath.Join(fakeSeccompProfileRoot, "filename2"), + }, + }, + { + description: "prioritise container field over pod field", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeUnconfined}}, + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}}, + expectedProfile: runtimeDefaultProfile, + }, + { + description: "prioritise container field over pod field", + podSc: &v1.PodSecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-pod-profile.json")}}, + containerSc: &v1.SecurityContext{SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeLocalhost, LocalhostProfile: getLocal("field-cont-profile.json")}}, + containerName: "container1", + expectedProfile: &runtimeapi.SecurityProfile{ + ProfileType: runtimeapi.SecurityProfile_Localhost, + LocalhostRef: filepath.Join(fakeSeccompProfileRoot, "field-cont-profile.json"), + }, + }, + } + + for i, test := range tests { + seccompProfile := m.getSeccompProfile(test.annotation, test.containerName, test.podSc, test.containerSc, true) assert.Equal(t, test.expectedProfile, seccompProfile, "TestCase[%d]: %s", i, test.description) } } diff --git a/pkg/kubelet/kuberuntime/kuberuntime_manager.go b/pkg/kubelet/kuberuntime/kuberuntime_manager.go index ad543e485325..c9bf6a847ee3 100644 --- a/pkg/kubelet/kuberuntime/kuberuntime_manager.go +++ b/pkg/kubelet/kuberuntime/kuberuntime_manager.go @@ -141,6 +141,9 @@ type kubeGenericRuntimeManager struct { // PodState provider instance podStateProvider podStateProvider + + // Use RuntimeDefault as the default seccomp profile for all workloads. + seccompDefault bool } // KubeGenericRuntime is a interface contains interfaces for container runtime and command. @@ -182,6 +185,7 @@ func NewKubeGenericRuntimeManager( legacyLogProvider LegacyLogProvider, logManager logs.ContainerLogManager, runtimeClassManager *runtimeclass.Manager, + seccompDefault bool, ) (KubeGenericRuntime, error) { kubeRuntimeManager := &kubeGenericRuntimeManager{ recorder: recorder, @@ -201,6 +205,7 @@ func NewKubeGenericRuntimeManager( logManager: logManager, runtimeClassManager: runtimeClassManager, logReduction: logreduction.NewLogReduction(identicalErrorDelay), + seccompDefault: seccompDefault, } typedVersion, err := kubeRuntimeManager.getTypedVersion() diff --git a/pkg/kubelet/kuberuntime/security_context.go b/pkg/kubelet/kuberuntime/security_context.go index 45f0d5995411..7a43ebf157da 100644 --- a/pkg/kubelet/kuberuntime/security_context.go +++ b/pkg/kubelet/kuberuntime/security_context.go @@ -36,9 +36,9 @@ func (m *kubeGenericRuntimeManager) determineEffectiveSecurityContext(pod *v1.Po // TODO: Deprecated, remove after we switch to Seccomp field // set SeccompProfilePath. - synthesized.SeccompProfilePath = m.getSeccompProfilePath(pod.Annotations, container.Name, pod.Spec.SecurityContext, container.SecurityContext) + synthesized.SeccompProfilePath = m.getSeccompProfilePath(pod.Annotations, container.Name, pod.Spec.SecurityContext, container.SecurityContext, m.seccompDefault) - synthesized.Seccomp = m.getSeccompProfile(pod.Annotations, container.Name, pod.Spec.SecurityContext, container.SecurityContext) + synthesized.Seccomp = m.getSeccompProfile(pod.Annotations, container.Name, pod.Spec.SecurityContext, container.SecurityContext, m.seccompDefault) // set ApparmorProfile. synthesized.ApparmorProfile = apparmor.GetProfileNameFromPodAnnotations(pod.Annotations, container.Name) diff --git a/staging/src/k8s.io/kubelet/config/v1beta1/types.go b/staging/src/k8s.io/kubelet/config/v1beta1/types.go index 5977d855f22c..5ff7435a000d 100644 --- a/staging/src/k8s.io/kubelet/config/v1beta1/types.go +++ b/staging/src/k8s.io/kubelet/config/v1beta1/types.go @@ -864,6 +864,11 @@ type KubeletConfiguration struct { // Default: true // +optional EnableDebugFlagsHandler *bool `json:"enableDebugFlagsHandler,omitempty"` + // SeccompDefault enables the use of `RuntimeDefault` as the default seccomp profile for all workloads. + // This requires the corresponding SeccompDefault feature gate to be enabled as well. + // Default: false + // +optional + SeccompDefault *bool `json:"seccompDefault,omitempty"` } type KubeletAuthorizationMode string diff --git a/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go index cd8b343a9fa1..156bfa6f5b15 100644 --- a/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go @@ -321,6 +321,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { *out = new(bool) **out = **in } + if in.SeccompDefault != nil { + in, out := &in.SeccompDefault, &out.SeccompDefault + *out = new(bool) + **out = **in + } return }