AppArmor API changes

This commit is contained in:
Tim Allclair 2024-02-20 17:14:52 -08:00
parent b0ee334374
commit 94927afb50
8 changed files with 491 additions and 77 deletions

View File

@ -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

View File

@ -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")
}
})
}
}
})
}
}
}

View File

@ -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"

View File

@ -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

View File

@ -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
}

View File

@ -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",

View File

@ -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"

View File

@ -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