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 }