diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index cf0ff826a83..dc7d96ddb17 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -20,7 +20,6 @@ import ( "strings" "github.com/google/go-cmp/cmp" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -540,12 +539,21 @@ func dropDisabledFields( podSpec = &api.PodSpec{} } - if !utilfeature.DefaultFeatureGate.Enabled(features.AppArmor) && !appArmorInUse(oldPodAnnotations) { + if !utilfeature.DefaultFeatureGate.Enabled(features.AppArmor) && !appArmorInUse(oldPodAnnotations, oldPodSpec) { for k := range podAnnotations { - if strings.HasPrefix(k, v1.AppArmorBetaContainerAnnotationKeyPrefix) { + if strings.HasPrefix(k, api.AppArmorContainerAnnotationKeyPrefix) { delete(podAnnotations, k) } } + if podSpec.SecurityContext != nil { + podSpec.SecurityContext.AppArmorProfile = nil + } + VisitContainers(podSpec, AllContainers, func(c *api.Container, _ ContainerType) bool { + if c.SecurityContext != nil { + c.SecurityContext.AppArmorProfile = nil + } + return true + }) } // If the feature is disabled and not in use, drop the hostUsers field. @@ -940,13 +948,28 @@ func procMountInUse(podSpec *api.PodSpec) bool { } // appArmorInUse returns true if the pod has apparmor related information -func appArmorInUse(podAnnotations map[string]string) bool { +func appArmorInUse(podAnnotations map[string]string, podSpec *api.PodSpec) bool { + if podSpec == nil { + return false + } + for k := range podAnnotations { - if strings.HasPrefix(k, v1.AppArmorBetaContainerAnnotationKeyPrefix) { + if strings.HasPrefix(k, api.AppArmorContainerAnnotationKeyPrefix) { return true } } - return false + if podSpec.SecurityContext != nil && podSpec.SecurityContext.AppArmorProfile != nil { + return true + } + hasAppArmorContainer := false + VisitContainers(podSpec, AllContainers, func(c *api.Container, _ ContainerType) bool { + if c.SecurityContext != nil && c.SecurityContext.AppArmorProfile != nil { + hasAppArmorContainer = true + return false + } + return true + }) + return hasAppArmorContainer } // restartableInitContainersInUse returns true if the pod spec is non-nil and diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go index 104a007950f..81604556526 100644 --- a/pkg/api/pod/util_test.go +++ b/pkg/api/pod/util_test.go @@ -23,6 +23,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -704,83 +706,84 @@ func TestDropProcMount(t *testing.T) { } func TestDropAppArmor(t *testing.T) { - podWithAppArmor := func() *api.Pod { - return &api.Pod{ - ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1", v1.AppArmorBetaContainerAnnotationKeyPrefix + "foo": "default"}}, - Spec: api.PodSpec{}, - } - } - podWithoutAppArmor := func() *api.Pod { - return &api.Pod{ - ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1"}}, - Spec: api.PodSpec{}, - } - } - - podInfo := []struct { + tests := []struct { description string hasAppArmor bool - pod func() *api.Pod - }{ - { - description: "has AppArmor", - hasAppArmor: true, - pod: podWithAppArmor, + pod api.Pod + }{{ + description: "with AppArmor Annotations", + hasAppArmor: true, + pod: api.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1", v1.AppArmorBetaContainerAnnotationKeyPrefix + "foo": "default"}}, + Spec: api.PodSpec{}, }, - { - description: "does not have AppArmor", - hasAppArmor: false, - pod: podWithoutAppArmor, + }, { + description: "with pod AppArmor profile", + hasAppArmor: true, + pod: api.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1"}}, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + }, }, - { - description: "is nil", - hasAppArmor: false, - pod: func() *api.Pod { return nil }, + }, { + description: "with container AppArmor profile", + hasAppArmor: true, + pod: api.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1"}}, + Spec: api.PodSpec{ + Containers: []api.Container{{ + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + }}, + }, }, - } + }, { + description: "without AppArmor", + hasAppArmor: false, + pod: api.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"a": "1"}}, + Spec: api.PodSpec{}, + }, + }} - for _, enabled := range []bool{true, false} { - for _, oldPodInfo := range podInfo { - for _, newPodInfo := range podInfo { - oldPodHasAppArmor, oldPod := oldPodInfo.hasAppArmor, oldPodInfo.pod() - newPodHasAppArmor, newPod := newPodInfo.hasAppArmor, newPodInfo.pod() - if newPod == nil { - continue + for _, test := range tests { + for _, enabled := range []bool{true, false} { + t.Run(fmt.Sprintf("%v/enabled=%v", test.description, enabled), func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, enabled)() + + newPod := test.pod.DeepCopy() + + if actual := appArmorInUse(newPod.Annotations, &newPod.Spec); actual != test.hasAppArmor { + t.Errorf("appArmorInUse does not match expectation: %t != %t", actual, test.hasAppArmor) } - t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, enabled)() + DropDisabledPodFields(newPod, newPod) + require.Equal(t, &test.pod, newPod, "unchanged pod should never be mutated") - DropDisabledPodFields(newPod, oldPod) + DropDisabledPodFields(newPod, nil) - // old pod should never be changed - if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { - t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) + if enabled { + assert.Equal(t, &test.pod, newPod, "pod should not be mutated when AppArmor is enabled") + } else { + if appArmorInUse(newPod.Annotations, &newPod.Spec) { + t.Errorf("newPod should not be using appArmor after dropping disabled fields") } - switch { - case enabled || oldPodHasAppArmor: - // new pod should not be changed if the feature is enabled, or if the old pod had AppArmor - if !reflect.DeepEqual(newPod, newPodInfo.pod()) { - t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) - } - case newPodHasAppArmor: - // new pod should be changed - if reflect.DeepEqual(newPod, newPodInfo.pod()) { - t.Errorf("new pod was not changed") - } - // new pod should not have AppArmor - if !reflect.DeepEqual(newPod, podWithoutAppArmor()) { - t.Errorf("new pod had EmptyDir SizeLimit: %v", cmp.Diff(newPod, podWithoutAppArmor())) - } - default: - // new pod should not need to be changed - if !reflect.DeepEqual(newPod, newPodInfo.pod()) { - t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) - } + if test.hasAppArmor { + assert.NotEqual(t, &test.pod, newPod, "pod should be mutated to drop AppArmor") + } else { + assert.Equal(t, &test.pod, newPod, "pod without AppArmor should not be mutated") } - }) - } + } + }) } } } diff --git a/pkg/apis/core/annotation_key_constants.go b/pkg/apis/core/annotation_key_constants.go index 60cff22b9d4..de7d73fa9d2 100644 --- a/pkg/apis/core/annotation_key_constants.go +++ b/pkg/apis/core/annotation_key_constants.go @@ -52,6 +52,19 @@ const ( // Deprecated: set a pod or container security context `seccompProfile` of type "RuntimeDefault" instead. DeprecatedSeccompProfileDockerDefault string = "docker/default" + // AppArmorContainerAnnotationKeyPrefix is the prefix to an annotation key specifying a container's apparmor profile. + // Deprecated: use a pod or container security context `appArmorProfile` field instead. + AppArmorContainerAnnotationKeyPrefix = "container.apparmor.security.beta.kubernetes.io/" + + // AppArmorProfileRuntimeDefault is the profile specifying the runtime default. + AppArmorProfileRuntimeDefault = "runtime/default" + + // AppArmorProfileLocalhostPrefix is the prefix for specifying profiles loaded on the node. + AppArmorProfileLocalhostPrefix = "localhost/" + + // AppArmorProfileNameUnconfined is the Unconfined AppArmor profile + AppArmorProfileNameUnconfined = "unconfined" + // PreferAvoidPodsAnnotationKey represents the key of preferAvoidPods data (json serialized) // in the Annotations of a Node. PreferAvoidPodsAnnotationKey string = "scheduler.alpha.kubernetes.io/preferAvoidPods" diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index ddc78e3207e..2330cac7a6c 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -3325,6 +3325,7 @@ type PodSpec struct { // - spec.hostPID // - spec.hostIPC // - spec.hostUsers + // - spec.securityContext.appArmorProfile // - spec.securityContext.seLinuxOptions // - spec.securityContext.seccompProfile // - spec.securityContext.fsGroup @@ -3334,6 +3335,7 @@ type PodSpec struct { // - spec.securityContext.runAsUser // - spec.securityContext.runAsGroup // - spec.securityContext.supplementalGroups + // - spec.containers[*].securityContext.appArmorProfile // - spec.containers[*].securityContext.seLinuxOptions // - spec.containers[*].securityContext.seccompProfile // - spec.containers[*].securityContext.capabilities @@ -3598,6 +3600,10 @@ type PodSecurityContext struct { // Note that this field cannot be set when spec.os.name is windows. // +optional SeccompProfile *SeccompProfile + // appArmorProfile is the AppArmor options to use by the containers in this pod. + // Note that this field cannot be set when spec.os.name is windows. + // +optional + AppArmorProfile *AppArmorProfile } // SeccompProfile defines a pod/container's seccomp profile settings. @@ -3625,6 +3631,37 @@ const ( SeccompProfileTypeLocalhost SeccompProfileType = "Localhost" ) +// AppArmorProfile defines a pod or container's AppArmor settings. +// +union +type AppArmorProfile struct { + // type indicates which kind of AppArmor profile will be applied. + // Valid options are: + // Localhost - a profile pre-loaded on the node. + // RuntimeDefault - the container runtime's default profile. + // Unconfined - no AppArmor enforcement. + // +unionDescriminator + Type AppArmorProfileType + + // localhostProfile indicates a profile loaded on the node that should be used. + // The profile must be preconfigured on the node to work. + // Must match the loaded name of the profile. + // Must be set if and only if type is "Localhost". + // +optional + LocalhostProfile *string +} + +type AppArmorProfileType string + +const ( + // AppArmorProfileTypeUnconfined indicates that no AppArmor profile should be enforced. + AppArmorProfileTypeUnconfined AppArmorProfileType = "Unconfined" + // AppArmorProfileTypeRuntimeDefault indicates that the container runtime's default AppArmor + // profile should be used. + AppArmorProfileTypeRuntimeDefault AppArmorProfileType = "RuntimeDefault" + // AppArmorProfileTypeLocalhost indicates that a profile pre-loaded on the node should be used. + AppArmorProfileTypeLocalhost AppArmorProfileType = "Localhost" +) + // PodQOSClass defines the supported qos classes of Pods. type PodQOSClass string @@ -6028,6 +6065,11 @@ type SecurityContext struct { // Note that this field cannot be set when spec.os.name is windows. // +optional SeccompProfile *SeccompProfile + // appArmorProfile is the AppArmor options to use by this container. If set, this profile + // overrides the pod's appArmorProfile. + // Note that this field cannot be set when spec.os.name is windows. + // +optional + AppArmorProfile *AppArmorProfile } // ProcMountType defines the type of proc mount diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 9457ef47eb4..43583928b48 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -4234,6 +4234,9 @@ func validateWindows(spec *core.PodSpec, fldPath *field.Path) field.ErrorList { securityContext := spec.SecurityContext // validate Pod SecurityContext if securityContext != nil { + if securityContext.AppArmorProfile != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("securityContext").Child("appArmorProfile"), "cannot be set for a windows pod")) + } if securityContext.SELinuxOptions != nil { allErrs = append(allErrs, field.Forbidden(fldPath.Child("securityContext").Child("seLinuxOptions"), "cannot be set for a windows pod")) } @@ -4280,6 +4283,9 @@ func validateWindows(spec *core.PodSpec, fldPath *field.Path) field.ErrorList { // TODO: Think if we need to relax this restriction or some of the restrictions if sc != nil { fldPath := cFldPath.Child("securityContext") + if sc.AppArmorProfile != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("appArmorProfile"), "cannot be set for a windows pod")) + } if sc.SELinuxOptions != nil { allErrs = append(allErrs, field.Forbidden(fldPath.Child("seLinuxOptions"), "cannot be set for a windows pod")) } @@ -4657,6 +4663,43 @@ func validateSeccompProfileType(fldPath *field.Path, seccompProfileType core.Sec } } +func validateAppArmorProfileField(profile *core.AppArmorProfile, fldPath *field.Path) field.ErrorList { + if profile == nil { + return nil + } + + allErrs := field.ErrorList{} + + switch profile.Type { + case core.AppArmorProfileTypeLocalhost: + if profile.LocalhostProfile == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("localhostProfile"), "must be set when AppArmor type is Localhost")) + } else { + localhostProfile := strings.TrimSpace(*profile.LocalhostProfile) + if localhostProfile != *profile.LocalhostProfile { + allErrs = append(allErrs, field.Invalid(fldPath.Child("localhostProfile"), *profile.LocalhostProfile, "must not be padded with whitespace")) + } else if localhostProfile == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("localhostProfile"), "must be set when AppArmor type is Localhost")) + } + } + + case core.AppArmorProfileTypeRuntimeDefault, core.AppArmorProfileTypeUnconfined: + if profile.LocalhostProfile != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("localhostProfile"), profile.LocalhostProfile, "can only be set when AppArmor type is Localhost")) + } + + case "": + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "type is required when appArmorProfile is set")) + + default: + allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), profile.Type, + []core.AppArmorProfileType{core.AppArmorProfileTypeLocalhost, core.AppArmorProfileTypeRuntimeDefault, core.AppArmorProfileTypeUnconfined})) + } + + return allErrs + +} + func ValidateAppArmorPodAnnotations(annotations map[string]string, spec *core.PodSpec, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} for k, p := range annotations { @@ -4799,6 +4842,7 @@ func validatePodSpecSecurityContext(securityContext *core.PodSecurityContext, sp allErrs = append(allErrs, validateSeccompProfileField(securityContext.SeccompProfile, fldPath.Child("seccompProfile"))...) allErrs = append(allErrs, validateWindowsSecurityContextOptions(securityContext.WindowsOptions, fldPath.Child("windowsOptions"))...) + allErrs = append(allErrs, validateAppArmorProfileField(securityContext.AppArmorProfile, fldPath.Child("appArmorProfile"))...) } return allErrs @@ -7084,6 +7128,7 @@ func ValidateSecurityContext(sc *core.SecurityContext, fldPath *field.Path) fiel } allErrs = append(allErrs, validateWindowsSecurityContextOptions(sc.WindowsOptions, fldPath.Child("windowsOptions"))...) + allErrs = append(allErrs, validateAppArmorProfileField(sc.AppArmorProfile, fldPath.Child("appArmorProfile"))...) return allErrs } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 3c7cfe631a8..4c6bd0366f7 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -10289,7 +10289,7 @@ func TestValidatePod(t *testing.T) { DNSPolicy: core.DNSDefault, }, }, - "default AppArmor profile for a container": { + "default AppArmor annotation for a container": { ObjectMeta: metav1.ObjectMeta{ Name: "123", Namespace: "ns", @@ -10299,7 +10299,7 @@ func TestValidatePod(t *testing.T) { }, Spec: validPodSpec(nil), }, - "default AppArmor profile for an init container": { + "default AppArmor annotation for an init container": { ObjectMeta: metav1.ObjectMeta{ Name: "123", Namespace: "ns", @@ -10314,7 +10314,7 @@ func TestValidatePod(t *testing.T) { DNSPolicy: core.DNSClusterFirst, }, }, - "localhost AppArmor profile for a container": { + "localhost AppArmor annotation for a container": { ObjectMeta: metav1.ObjectMeta{ Name: "123", Namespace: "ns", @@ -10324,6 +10324,107 @@ func TestValidatePod(t *testing.T) { }, Spec: validPodSpec(nil), }, + "runtime default AppArmor profile for a pod": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeRuntimeDefault, + }, + }, + }, + }, + "runtime default AppArmor profile for a container": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeRuntimeDefault, + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, + "unconfined AppArmor profile for a pod": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeUnconfined, + }, + }, + }, + }, + "unconfined AppArmor profile for a container": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeUnconfined, + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, + "localhost AppArmor profile for a pod": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To("example-org/application-foo"), + }, + }, + }, + }, + "localhost AppArmor profile for a container field": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To("example-org/application-foo"), + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, "syntactically valid sysctls": { ObjectMeta: metav1.ObjectMeta{ Name: "123", @@ -11880,6 +11981,143 @@ func TestValidatePod(t *testing.T) { Spec: validPodSpec(nil), }, }, + "unsupported pod AppArmor profile type": { + expectedError: `Unsupported value: "test"`, + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: "test", + }, + }, + }, + }, + }, + "unsupported container AppArmor profile type": { + expectedError: `Unsupported value: "test"`, + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: "test", + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, + }, + "missing pod AppArmor profile type": { + expectedError: "Required value: type is required when appArmorProfile is set", + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: "", + }, + }, + }, + }, + }, + "missing AppArmor localhost profile": { + expectedError: "Required value: must be set when AppArmor type is Localhost", + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + }, + }, + }, + }, + }, + "empty AppArmor localhost profile": { + expectedError: "Required value: must be set when AppArmor type is Localhost", + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To(""), + }, + }, + }, + }, + }, + "invalid AppArmor localhost profile type": { + expectedError: `Invalid value: "foo-bar"`, + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeRuntimeDefault, + LocalhostProfile: ptr.To("foo-bar"), + }, + }, + }, + }, + }, + "invalid AppArmor localhost profile": { + expectedError: `Invalid value: "foo-bar "`, + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + SecurityContext: &core.PodSecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To("foo-bar "), + }, + }, + }, + }, + }, "invalid extended resource name in container request": { expectedError: "must be a standard resource for containers", spec: core.Pod{ @@ -21579,6 +21817,12 @@ func TestValidateWindowsSecurityContext(t *testing.T) { expectError: true, errorMsg: "cannot be set for a windows pod", errorType: "FieldValueForbidden", + }, { + name: "pod with AppArmorProfile", + sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{AppArmorProfile: &core.AppArmorProfile{Type: core.AppArmorProfileTypeRuntimeDefault}}}}}, + expectError: true, + errorMsg: "cannot be set for a windows pod", + errorType: "FieldValueForbidden", }, { name: "pod with WindowsOptions, no error", sc: &core.PodSpec{Containers: []core.Container{{SecurityContext: &core.SecurityContext{WindowsOptions: &core.WindowsSecurityContextOptions{RunAsUserName: utilpointer.String("dummy")}}}}}, @@ -21613,6 +21857,7 @@ func TestValidateOSFields(t *testing.T) { // - Add documentation to the os field in the api // - Add validation logic validateLinux, validateWindows functions to make sure the field is only set for eligible OSes osSpecificFields := sets.NewString( + "Containers[*].SecurityContext.AppArmorProfile", "Containers[*].SecurityContext.AllowPrivilegeEscalation", "Containers[*].SecurityContext.Capabilities", "Containers[*].SecurityContext.Privileged", @@ -21623,6 +21868,7 @@ func TestValidateOSFields(t *testing.T) { "Containers[*].SecurityContext.SELinuxOptions", "Containers[*].SecurityContext.SeccompProfile", "Containers[*].SecurityContext.WindowsOptions", + "InitContainers[*].SecurityContext.AppArmorProfile", "InitContainers[*].SecurityContext.AllowPrivilegeEscalation", "InitContainers[*].SecurityContext.Capabilities", "InitContainers[*].SecurityContext.Privileged", @@ -21633,6 +21879,7 @@ func TestValidateOSFields(t *testing.T) { "InitContainers[*].SecurityContext.SELinuxOptions", "InitContainers[*].SecurityContext.SeccompProfile", "InitContainers[*].SecurityContext.WindowsOptions", + "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.AppArmorProfile", "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.AllowPrivilegeEscalation", "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.Capabilities", "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.Privileged", @@ -21644,6 +21891,7 @@ func TestValidateOSFields(t *testing.T) { "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.SeccompProfile", "EphemeralContainers[*].EphemeralContainerCommon.SecurityContext.WindowsOptions", "OS", + "SecurityContext.AppArmorProfile", "SecurityContext.FSGroup", "SecurityContext.FSGroupChangePolicy", "SecurityContext.HostIPC", diff --git a/staging/src/k8s.io/api/core/v1/annotation_key_constants.go b/staging/src/k8s.io/api/core/v1/annotation_key_constants.go index 106ba14c3df..4c6969e9fbb 100644 --- a/staging/src/k8s.io/api/core/v1/annotation_key_constants.go +++ b/staging/src/k8s.io/api/core/v1/annotation_key_constants.go @@ -55,11 +55,8 @@ const ( SeccompLocalhostProfileNamePrefix = "localhost/" // AppArmorBetaContainerAnnotationKeyPrefix is the prefix to an annotation key specifying a container's apparmor profile. + // Deprecated: use a pod or container security context `appArmorProfile` field instead. AppArmorBetaContainerAnnotationKeyPrefix = "container.apparmor.security.beta.kubernetes.io/" - // AppArmorBetaDefaultProfileAnnotationKey is the annotation key specifying the default AppArmor profile. - AppArmorBetaDefaultProfileAnnotationKey = "apparmor.security.beta.kubernetes.io/defaultProfileName" - // AppArmorBetaAllowedProfilesAnnotationKey is the annotation key specifying the allowed AppArmor profiles. - AppArmorBetaAllowedProfilesAnnotationKey = "apparmor.security.beta.kubernetes.io/allowedProfileNames" // AppArmorBetaProfileRuntimeDefault is the profile specifying the runtime default. AppArmorBetaProfileRuntimeDefault = "runtime/default" diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index e1d058f1e30..aceab6bfce2 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -3757,6 +3757,7 @@ type PodSpec struct { // - spec.hostPID // - spec.hostIPC // - spec.hostUsers + // - spec.securityContext.appArmorProfile // - spec.securityContext.seLinuxOptions // - spec.securityContext.seccompProfile // - spec.securityContext.fsGroup @@ -3766,6 +3767,7 @@ type PodSpec struct { // - spec.securityContext.runAsUser // - spec.securityContext.runAsGroup // - spec.securityContext.supplementalGroups + // - spec.containers[*].securityContext.appArmorProfile // - spec.containers[*].securityContext.seLinuxOptions // - spec.containers[*].securityContext.seccompProfile // - spec.containers[*].securityContext.capabilities @@ -4158,6 +4160,10 @@ type PodSecurityContext struct { // Note that this field cannot be set when spec.os.name is windows. // +optional SeccompProfile *SeccompProfile `json:"seccompProfile,omitempty" protobuf:"bytes,10,opt,name=seccompProfile"` + // appArmorProfile is the AppArmor options to use by the containers in this pod. + // Note that this field cannot be set when spec.os.name is windows. + // +optional + AppArmorProfile *AppArmorProfile `json:"appArmorProfile,omitempty"` } // SeccompProfile defines a pod/container's seccomp profile settings. @@ -4194,6 +4200,38 @@ const ( SeccompProfileTypeLocalhost SeccompProfileType = "Localhost" ) +// AppArmorProfile defines a pod or container's AppArmor settings. +// +union +type AppArmorProfile struct { + // type indicates which kind of AppArmor profile will be applied. + // Valid options are: + // Localhost - a profile pre-loaded on the node. + // RuntimeDefault - the container runtime's default profile. + // Unconfined - no AppArmor enforcement. + // +unionDiscriminator + Type AppArmorProfileType `json:"type"` + + // localhostProfile indicates a profile loaded on the node that should be used. + // The profile must be preconfigured on the node to work. + // Must match the loaded name of the profile. + // Must be set if and only if type is "Localhost". + // +optional + LocalhostProfile *string `json:"localhostProfile,omitempty"` +} + +// +enum +type AppArmorProfileType string + +const ( + // AppArmorProfileTypeUnconfined indicates that no AppArmor profile should be enforced. + AppArmorProfileTypeUnconfined AppArmorProfileType = "Unconfined" + // AppArmorProfileTypeRuntimeDefault indicates that the container runtime's default AppArmor + // profile should be used. + AppArmorProfileTypeRuntimeDefault AppArmorProfileType = "RuntimeDefault" + // AppArmorProfileTypeLocalhost indicates that a profile pre-loaded on the node should be used. + AppArmorProfileTypeLocalhost AppArmorProfileType = "Localhost" +) + // PodQOSClass defines the supported qos classes of Pods. // +enum type PodQOSClass string @@ -7213,6 +7251,11 @@ type SecurityContext struct { // Note that this field cannot be set when spec.os.name is windows. // +optional SeccompProfile *SeccompProfile `json:"seccompProfile,omitempty" protobuf:"bytes,11,opt,name=seccompProfile"` + // appArmorProfile is the AppArmor options to use by this container. If set, this profile + // overrides the pod's appArmorProfile. + // Note that this field cannot be set when spec.os.name is windows. + // +optional + AppArmorProfile *AppArmorProfile `json:"appArmorProfile,omitempty"` } // +enum