diff --git a/README.md b/README.md index 83c27466..f4fcf1d5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/Shopify/kubeaudit)](https://goreportcard.com/report/github.com/Shopify/kubeaudit) [![GoDoc](https://godoc.org/github.com/Shopify/kubeaudit?status.png)](https://godoc.org/github.com/Shopify/kubeaudit) -> Kubeaudit no longer supports APIs deprecated as of [Kubernetes v.1.16 release](https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/). So, it is now a requirement for clusters to run Kubernetes >=1.16 +> It is now a requirement for clusters to run Kubernetes >=1.19. # kubeaudit :cloud: :lock: :muscle: diff --git a/auditors/all/all_test.go b/auditors/all/all_test.go index 8a94562b..3b4d689e 100644 --- a/auditors/all/all_test.go +++ b/auditors/all/all_test.go @@ -43,7 +43,7 @@ func TestAuditAll(t *testing.T) { privesc.AllowPrivilegeEscalationNil, privileged.PrivilegedNil, rootfs.ReadOnlyRootFilesystemNil, - seccomp.SeccompAnnotationMissing, + seccomp.SeccompProfileMissing, } allAuditors, err := Auditors( @@ -86,7 +86,7 @@ func TestAllWithConfig(t *testing.T) { } expectedErrors := []string{ apparmor.AppArmorAnnotationMissing, - seccomp.SeccompAnnotationMissing, + seccomp.SeccompProfileMissing, } conf := config.KubeauditConfig{ diff --git a/auditors/apparmor/apparmor.go b/auditors/apparmor/apparmor.go index 24e820d8..e794fabe 100644 --- a/auditors/apparmor/apparmor.go +++ b/auditors/apparmor/apparmor.go @@ -117,8 +117,8 @@ func auditPodAnnotations(resource k8s.Resource, containerNames []string) []*kube "Container": containerName, "Annotation": fmt.Sprintf("%s: %s", annotationKey, annotationValue), }, - PendingFix: &fix.ByRemovingPodAnnotation{ - Key: annotationKey, + PendingFix: &fix.ByRemovingPodAnnotations{ + Keys: []string{annotationKey}, }, }) } diff --git a/auditors/image/fixtures/image-tag-missing.yml b/auditors/image/fixtures/image-tag-missing.yml index f12994ac..7c94a264 100644 --- a/auditors/image/fixtures/image-tag-missing.yml +++ b/auditors/image/fixtures/image-tag-missing.yml @@ -13,8 +13,10 @@ spec: name: deployment annotations: container.apparmor.security.beta.kubernetes.io/container: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default spec: + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: container image: scratch diff --git a/auditors/seccomp/fix.go b/auditors/seccomp/fix.go new file mode 100644 index 00000000..4932a707 --- /dev/null +++ b/auditors/seccomp/fix.go @@ -0,0 +1,59 @@ +package seccomp + +import ( + "fmt" + + "github.com/Shopify/kubeaudit/pkg/k8s" + apiv1 "k8s.io/api/core/v1" +) + +type BySettingSeccompProfile struct { + seccompProfileType apiv1.SeccompProfileType +} + +func (pending *BySettingSeccompProfile) Plan() string { + return fmt.Sprintf("Set SeccompProfile type to '%s' in pod SecurityContext", pending.seccompProfileType) +} + +func (pending *BySettingSeccompProfile) Apply(resource k8s.Resource) []k8s.Resource { + podSpec := k8s.GetPodSpec(resource) + if podSpec.SecurityContext == nil { + podSpec.SecurityContext = &apiv1.PodSecurityContext{} + } + podSpec.SecurityContext.SeccompProfile = &apiv1.SeccompProfile{Type: pending.seccompProfileType} + + return nil +} + +type BySettingSeccompProfileInContainer struct { + container *k8s.ContainerV1 + seccompProfileType apiv1.SeccompProfileType +} + +func (pending *BySettingSeccompProfileInContainer) Plan() string { + return fmt.Sprintf("Set SeccompProfile type to '%s' in SecurityContext for container `%s`", pending.seccompProfileType, pending.container.Name) +} + +func (pending *BySettingSeccompProfileInContainer) Apply(resource k8s.Resource) []k8s.Resource { + if pending.container.SecurityContext == nil { + pending.container.SecurityContext = &apiv1.SecurityContext{} + } + pending.container.SecurityContext.SeccompProfile = &apiv1.SeccompProfile{Type: pending.seccompProfileType} + return nil +} + +type ByRemovingSeccompProfileInContainer struct { + container *k8s.ContainerV1 +} + +func (pending *ByRemovingSeccompProfileInContainer) Plan() string { + return fmt.Sprintf("Remove SeccompProfile in SecurityContext for container `%s`", pending.container.Name) +} + +func (pending *ByRemovingSeccompProfileInContainer) Apply(resource k8s.Resource) []k8s.Resource { + if pending.container.SecurityContext == nil { + return nil + } + pending.container.SecurityContext.SeccompProfile = nil + return nil +} diff --git a/auditors/seccomp/fix_test.go b/auditors/seccomp/fix_test.go new file mode 100644 index 00000000..4ce84820 --- /dev/null +++ b/auditors/seccomp/fix_test.go @@ -0,0 +1,83 @@ +package seccomp + +import ( + "strings" + "testing" + + "github.com/Shopify/kubeaudit/internal/test" + "github.com/Shopify/kubeaudit/pkg/k8s" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apiv1 "k8s.io/api/core/v1" +) + +const fixtureDir = "fixtures" +const emptyProfile = apiv1.SeccompProfileType("EMPTY") +const defaultProfile = apiv1.SeccompProfileTypeRuntimeDefault +const localhostProfile = apiv1.SeccompProfileTypeLocalhost + +func TestFixSeccomp(t *testing.T) { + cases := []struct { + file string + expectedPodSeccompProfile apiv1.SeccompProfileType + expectedContainerSeccompProfiles []apiv1.SeccompProfileType + }{ + {"seccomp-profile-missing.yml", defaultProfile, []apiv1.SeccompProfileType{emptyProfile}}, + {"seccomp-profile-missing-disabled-container.yml", defaultProfile, []apiv1.SeccompProfileType{emptyProfile}}, + {"seccomp-profile-missing-annotations.yml", defaultProfile, []apiv1.SeccompProfileType{emptyProfile}}, + {"seccomp-disabled-pod.yml", defaultProfile, []apiv1.SeccompProfileType{defaultProfile}}, + {"seccomp-disabled.yml", defaultProfile, []apiv1.SeccompProfileType{emptyProfile, emptyProfile}}, + {"seccomp-disabled-localhost.yml", localhostProfile, []apiv1.SeccompProfileType{defaultProfile, emptyProfile}}, + } + + for _, tc := range cases { + // This line is needed because of how scopes work with parallel tests (see https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca008721) + tc := tc + t.Run(tc.file, func(t *testing.T) { + resources, _ := test.FixSetup(t, fixtureDir, tc.file, New()) + require.Len(t, resources, 1) + resource := resources[0] + + updatedPodSpec := k8s.GetPodSpec(resource) + checkPodSeccompProfile(t, updatedPodSpec, tc.expectedPodSeccompProfile) + checkContainerSeccompProfiles(t, updatedPodSpec, tc.expectedContainerSeccompProfiles) + checkNoSeccompAnnotations(t, resource) + }) + } +} + +func checkPodSeccompProfile(t *testing.T, podSpec *apiv1.PodSpec, expectedPodSeccompProfile apiv1.SeccompProfileType) { + securityContext := podSpec.SecurityContext + if expectedPodSeccompProfile == emptyProfile { + require.Nil(t, securityContext) + } else { + assert.Equal(t, expectedPodSeccompProfile, securityContext.SeccompProfile.Type) + } +} + +func checkContainerSeccompProfiles(t *testing.T, podSpec *apiv1.PodSpec, expectedContainerSeccompProfiles []apiv1.SeccompProfileType) { + for i, container := range podSpec.Containers { + securityContext := container.SecurityContext + expectedProfile := expectedContainerSeccompProfiles[i] + if expectedProfile == emptyProfile { + require.True(t, securityContext == nil || securityContext.SeccompProfile == nil) + } else { + assert.Equal(t, expectedProfile, securityContext.SeccompProfile.Type) + } + } +} + +func checkNoSeccompAnnotations(t *testing.T, resource k8s.Resource) { + annotations := k8s.GetAnnotations(resource) + if annotations == nil { + return + } + + seccompAnnotations := []string{} + for annotation := range annotations { + if annotation == PodAnnotationKey || strings.HasPrefix(annotation, ContainerAnnotationKeyPrefix) { + seccompAnnotations = append(seccompAnnotations, annotation) + } + } + assert.Empty(t, seccompAnnotations) +} diff --git a/auditors/seccomp/fixtures/seccomp-deprecated.yml b/auditors/seccomp/fixtures/seccomp-deprecated.yml deleted file mode 100644 index 6e5bced6..00000000 --- a/auditors/seccomp/fixtures/seccomp-deprecated.yml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: pod - namespace: seccomp-deprecated - annotations: - container.seccomp.security.alpha.kubernetes.io/container: docker/default -spec: - containers: - - name: container - image: scratch diff --git a/auditors/seccomp/fixtures/seccomp-disabled-localhost.yml b/auditors/seccomp/fixtures/seccomp-disabled-localhost.yml new file mode 100644 index 00000000..d94ff0c3 --- /dev/null +++ b/auditors/seccomp/fixtures/seccomp-disabled-localhost.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod + namespace: seccomp-disabled-localhost +spec: + securityContext: + seccompProfile: + type: Localhost + localhostProfile: my-seccomp-profile.json + containers: + - name: container1 + image: scratch + securityContext: + seccompProfile: + type: Unconfined + - name: container2 + image: scratch diff --git a/auditors/seccomp/fixtures/seccomp-disabled-pod.yml b/auditors/seccomp/fixtures/seccomp-disabled-pod.yml index 4c10db3c..78a45d24 100644 --- a/auditors/seccomp/fixtures/seccomp-disabled-pod.yml +++ b/auditors/seccomp/fixtures/seccomp-disabled-pod.yml @@ -3,10 +3,13 @@ kind: Pod metadata: name: pod namespace: seccomp-disabled-pod - annotations: - seccomp.security.alpha.kubernetes.io/pod: unconfined - container.seccomp.security.alpha.kubernetes.io/container: runtime/default spec: + securityContext: + seccompProfile: + type: Unconfined containers: - name: container image: scratch + securityContext: + seccompProfile: + type: RuntimeDefault diff --git a/auditors/seccomp/fixtures/seccomp-disabled.yml b/auditors/seccomp/fixtures/seccomp-disabled.yml index 49e17dfd..c019bb35 100644 --- a/auditors/seccomp/fixtures/seccomp-disabled.yml +++ b/auditors/seccomp/fixtures/seccomp-disabled.yml @@ -3,15 +3,15 @@ kind: Pod metadata: name: pod namespace: seccomp-disabled - annotations: - seccomp.security.alpha.kubernetes.io/pod: runtime/default - container.seccomp.security.alpha.kubernetes.io/container1: badval - container.seccomp.security.alpha.kubernetes.io/container2: unconfined spec: + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: container1 image: scratch + securityContext: + seccompProfile: + type: Unconfined - name: container2 image: scratch - - name: container3 - image: scratch diff --git a/auditors/seccomp/fixtures/seccomp-enabled-pod.yml b/auditors/seccomp/fixtures/seccomp-enabled-pod.yml index b9996ba5..50c6c149 100644 --- a/auditors/seccomp/fixtures/seccomp-enabled-pod.yml +++ b/auditors/seccomp/fixtures/seccomp-enabled-pod.yml @@ -3,9 +3,11 @@ kind: Pod metadata: name: pod namespace: seccomp-enabled-pod - annotations: - seccomp.security.alpha.kubernetes.io/pod: localhost/bla spec: + securityContext: + seccompProfile: + type: Localhost + localhostProfile: my-seccomp-profile.json containers: - name: container image: scratch diff --git a/auditors/seccomp/fixtures/seccomp-enabled.yml b/auditors/seccomp/fixtures/seccomp-enabled.yml index 4684c3cf..90e4fea5 100644 --- a/auditors/seccomp/fixtures/seccomp-enabled.yml +++ b/auditors/seccomp/fixtures/seccomp-enabled.yml @@ -3,9 +3,10 @@ kind: Pod metadata: name: pod namespace: seccomp-enabled - annotations: - container.seccomp.security.alpha.kubernetes.io/container: runtime/default spec: containers: - name: container image: scratch + securityContext: + seccompProfile: + type: RuntimeDefault diff --git a/auditors/seccomp/fixtures/seccomp-deprecated-pod.yml b/auditors/seccomp/fixtures/seccomp-profile-missing-annotations.yml similarity index 64% rename from auditors/seccomp/fixtures/seccomp-deprecated-pod.yml rename to auditors/seccomp/fixtures/seccomp-profile-missing-annotations.yml index 52cce65c..c47c762b 100644 --- a/auditors/seccomp/fixtures/seccomp-deprecated-pod.yml +++ b/auditors/seccomp/fixtures/seccomp-profile-missing-annotations.yml @@ -2,9 +2,9 @@ apiVersion: v1 kind: Pod metadata: name: pod - namespace: seccomp-deprecated-pod + namespace: seccomp-profile-missing-annotations annotations: - seccomp.security.alpha.kubernetes.io/pod: docker/default + seccomp.security.alpha.kubernetes.io/pod: runtime/default container.seccomp.security.alpha.kubernetes.io/container: localhost/bla spec: containers: diff --git a/auditors/seccomp/fixtures/seccomp-profile-missing-disabled-container.yml b/auditors/seccomp/fixtures/seccomp-profile-missing-disabled-container.yml new file mode 100644 index 00000000..1e32f4d8 --- /dev/null +++ b/auditors/seccomp/fixtures/seccomp-profile-missing-disabled-container.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod + namespace: seccomp-profile-missing-disabled-container +spec: + containers: + - name: container + image: scratch + securityContext: + seccompProfile: + type: Unconfined diff --git a/auditors/seccomp/fixtures/seccomp-annotation-missing.yml b/auditors/seccomp/fixtures/seccomp-profile-missing.yml similarity index 73% rename from auditors/seccomp/fixtures/seccomp-annotation-missing.yml rename to auditors/seccomp/fixtures/seccomp-profile-missing.yml index 62a7708f..d5da6d2a 100644 --- a/auditors/seccomp/fixtures/seccomp-annotation-missing.yml +++ b/auditors/seccomp/fixtures/seccomp-profile-missing.yml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Pod metadata: name: pod - namespace: seccomp-annotation-missing + namespace: seccomp-profile-missing spec: containers: - name: container diff --git a/auditors/seccomp/seccomp.go b/auditors/seccomp/seccomp.go index 3f01d319..02ab493b 100644 --- a/auditors/seccomp/seccomp.go +++ b/auditors/seccomp/seccomp.go @@ -13,30 +13,25 @@ import ( const Name = "seccomp" const ( - // SeccompAnnotationMissing occurs when there are no seccomp annotations (pod nor container level) - SeccompAnnotationMissing = "SeccompAnnotationMissing" - // SeccompDeprecatedPod occurs when the pod-level seccomp annotation is set to a deprecated value - SeccompDeprecatedPod = "SeccompDeprecatedPod" - // SeccompDisabledPod occurs when the pod-level seccomp annotation is set to a value which disables seccomp + // SeccompDeprecatedAnnotations occurs when deprecated seccomp annotations are present + SeccompDeprecatedAnnotations = "SeccompDeprecatedAnnotations" + // SeccompProfileMissing occurs when there are no seccomp profiles (pod nor container level) + SeccompProfileMissing = "SeccompProfileMissing" + // SeccompDisabledPod occurs when the pod-level seccomp profile is set to a value which disables seccomp SeccompDisabledPod = "SeccompDisabledPod" - // SeccompDeprecatedContainer occurs when the container-level seccomp annotation is set to a deprecated value - SeccompDeprecatedContainer = "SeccompDeprecatedContainer" - // SeccompDisabledContainer occurs when the container-level seccomp annotation is set to a value which disables seccomp + // SeccompDisabledContainer occurs when the container-level seccomp profile is set to a value which disables seccomp SeccompDisabledContainer = "SeccompDisabledContainer" ) const ( + // ProfileRuntimeDefault represents the default seccomp profile used by container runtime + ProfileRuntimeDefault = apiv1.SeccompProfileTypeRuntimeDefault + // ProfileLocalhost represents the localhost seccomp profile used by container runtime + ProfileLocalhost = apiv1.SeccompProfileTypeLocalhost // ContainerAnnotationKeyPrefix represents the key of a seccomp profile applied to one container of a pod ContainerAnnotationKeyPrefix = apiv1.SeccompContainerAnnotationKeyPrefix // PodAnnotationKey represents the key of a seccomp profile applied to all containers of a pod PodAnnotationKey = apiv1.SeccompPodAnnotationKey - // ProfileRuntimeDefault represents the default seccomp profile used by container runtime - ProfileRuntimeDefault = apiv1.SeccompProfileRuntimeDefault - // ProfileNamePrefix is the prefix for a custom seccomp profile - ProfileNamePrefix = "localhost/" - // DeprecatedProfileRuntimeDefault represents the default seccomp profile used by docker. - // This is now deprecated and should be replaced by SeccompProfileRuntimeDefault - DeprecatedProfileRuntimeDefault = apiv1.DeprecatedSeccompProfileDockerDefault ) // Seccomp implements Auditable @@ -50,135 +45,130 @@ func New() *Seccomp { func (a *Seccomp) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudit.AuditResult, error) { var auditResults []*kubeaudit.AuditResult + annotationAuditResult := auditAnnotations(resource) + auditResults = appendNotNil(auditResults, annotationAuditResult) + auditResult := auditPod(resource) - if auditResult != nil { - auditResults = append(auditResults, auditResult) - } + auditResults = appendNotNil(auditResults, auditResult) for _, container := range k8s.GetContainers(resource) { auditResult := auditContainer(container, resource) - if auditResult != nil { - auditResults = append(auditResults, auditResult) - } + auditResults = appendNotNil(auditResults, auditResult) } return auditResults, nil } -func auditPod(resource k8s.Resource) *kubeaudit.AuditResult { - annotations := k8s.GetAnnotations(resource) - PodAnnotationKey := apiv1.SeccompPodAnnotationKey +func appendNotNil(auditResults []*kubeaudit.AuditResult, auditResult *kubeaudit.AuditResult) []*kubeaudit.AuditResult { + if auditResult != nil { + return append(auditResults, auditResult) + } + return auditResults +} - if isSeccompAnnotationMissing(annotations, PodAnnotationKey) { - // If all the containers have container-level seccomp annotations then we don't need a pod-level annotation - if isSeccompEnabledForContainers(annotations, resource) { - return nil - } +func auditAnnotations(resource k8s.Resource) *kubeaudit.AuditResult { + podSpec := k8s.GetPodSpec(resource) + if podSpec == nil { + return nil + } - return &kubeaudit.AuditResult{ - Auditor: Name, - Rule: SeccompAnnotationMissing, - Severity: kubeaudit.Error, - Message: fmt.Sprintf("Seccomp annotation is missing. The annotation %s: %s should be added.", PodAnnotationKey, ProfileRuntimeDefault), - PendingFix: &fix.ByAddingPodAnnotation{ - Key: PodAnnotationKey, - Value: ProfileRuntimeDefault, - }, - Metadata: kubeaudit.Metadata{ - "MissingAnnotation": PodAnnotationKey, - }, - } + // We check annotations only when seccomp profile is missing for both pod and all containers + // This way we ensure that we're in Manifest mode. + // In Local and Cluster mode Kubernetes automatically populates seccomp profile in Security context when seccomp annotations are provided. + if !isPodSeccompProfileMissing(podSpec.SecurityContext) || !isSeccompProfileMissingForAllContainers(resource) { + return nil } - podSeccompProfile := annotations[PodAnnotationKey] + seccompAnnotations := findSeccompAnnotations(resource) - if isSeccompProfileDeprecated(podSeccompProfile) { - return &kubeaudit.AuditResult{ - Auditor: Name, - Rule: SeccompDeprecatedPod, - Severity: kubeaudit.Error, - Message: fmt.Sprintf("Seccomp pod annotation is set to deprecated value %s. It should be set to %s instead.", podSeccompProfile, ProfileRuntimeDefault), - PendingFix: &fix.BySettingPodAnnotation{ - Key: PodAnnotationKey, - Value: ProfileRuntimeDefault, - }, - Metadata: kubeaudit.Metadata{ - "AnnotationKey": PodAnnotationKey, - "AnnotationValue": podSeccompProfile, - }, - } - } + if len(seccompAnnotations) > 0 { - if !isSeccompEnabled(podSeccompProfile) { return &kubeaudit.AuditResult{ - Auditor: Name, - Rule: SeccompDisabledPod, - Severity: kubeaudit.Error, - Message: fmt.Sprintf("Seccomp pod annotation is set to %s which disables Seccomp. It should be set to the default profile %s or should start with %s.", podSeccompProfile, ProfileRuntimeDefault, ProfileNamePrefix), - PendingFix: &fix.BySettingPodAnnotation{ - Key: PodAnnotationKey, - Value: ProfileRuntimeDefault, - }, - Metadata: kubeaudit.Metadata{ - "AnnotationKey": PodAnnotationKey, - "AnnotationValue": podSeccompProfile, - }, + Auditor: Name, + Rule: SeccompDeprecatedAnnotations, + Severity: kubeaudit.Warn, + Message: "Pod Seccomp annotations are deprecated. Seccomp profile should be added to the pod SecurityContext.", + PendingFix: &fix.ByRemovingPodAnnotations{Keys: seccompAnnotations}, + Metadata: kubeaudit.Metadata{"AnnotationKeys": strings.Join(seccompAnnotations, ", ")}, } } return nil } -func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudit.AuditResult { - annotations := k8s.GetAnnotations(resource) - containerAnnotationKey := getContainerAnnotationKey(container) - PodAnnotationKey := apiv1.SeccompPodAnnotationKey - - // Assume that the container will be covered by the pod-level seccomp annotation. If there is no pod-level - // seccomp annotation, assume that it will be added as part of the pod annotation audit / autofix - if isSeccompAnnotationMissing(annotations, containerAnnotationKey) { +func auditPod(resource k8s.Resource) *kubeaudit.AuditResult { + podSpec := k8s.GetPodSpec(resource) + if podSpec == nil { return nil } - // If the pod seccomp profile is a custom profile, and the container seccomp profile is set to a bad value, - // then set the container annotation to the default profile. Otherwise, if the container annotation is set to a - // bad value, then remove the container annotation in favour of the pod annotation (assumes the pod annotation is - // the default profile because even if the pod annotation is set to a bad value, it will be autofixed to be the - // default profile) - var pendingFix kubeaudit.PendingFix - podSeccompProfile := annotations[PodAnnotationKey] - if isSeccompProfileCustom(podSeccompProfile) { - pendingFix = &fix.BySettingPodAnnotation{Key: containerAnnotationKey, Value: ProfileRuntimeDefault} - } else { - pendingFix = &fix.ByRemovingPodAnnotation{Key: containerAnnotationKey} + if isPodSeccompProfileMissing(podSpec.SecurityContext) { + // If all the containers have container-level seccomp profiles then we don't need a pod-level profile + if isSeccompEnabledForAllContainers(resource) { + return nil + } + + return &kubeaudit.AuditResult{ + Auditor: Name, + Rule: SeccompProfileMissing, + Severity: kubeaudit.Error, + Message: "Pod Seccomp profile is missing. Seccomp profile should be added to the pod SecurityContext.", + PendingFix: &BySettingSeccompProfile{seccompProfileType: ProfileRuntimeDefault}, + Metadata: kubeaudit.Metadata{}, + } } - containerSeccompProfile := annotations[containerAnnotationKey] + podSeccompProfileType := podSpec.SecurityContext.SeccompProfile.Type - if isSeccompProfileDeprecated(containerSeccompProfile) { + if !isSeccompEnabled(podSeccompProfileType) { return &kubeaudit.AuditResult{ Auditor: Name, - Rule: SeccompDeprecatedContainer, + Rule: SeccompDisabledPod, Severity: kubeaudit.Error, - Message: fmt.Sprintf("Seccomp container annotation is set to deprecated value %s. It should be set to %s instead.", containerSeccompProfile, ProfileRuntimeDefault), - PendingFix: pendingFix, - Metadata: kubeaudit.Metadata{ - "AnnotationKey": containerAnnotationKey, - "AnnotationValue": containerSeccompProfile, - }, + Message: fmt.Sprintf("Pod Seccomp profile is set to %s which disables Seccomp. It should be set to the `%s` or `%s`.", podSeccompProfileType, ProfileRuntimeDefault, ProfileLocalhost), + PendingFix: &BySettingSeccompProfile{seccompProfileType: ProfileRuntimeDefault}, + Metadata: kubeaudit.Metadata{"SeccompProfileType": string(podSeccompProfileType)}, } } + return nil +} + +func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudit.AuditResult { + // Assume that the container will be covered by the pod-level seccomp profile. If there is no pod-level + // seccomp profile, assume that it will be added as part of the pod seccomp profile audit / autofix + if isContainerSeccompProfileMissing(container.SecurityContext) { + return nil + } + + containerSeccompProfile := container.SecurityContext.SeccompProfile.Type if !isSeccompEnabled(containerSeccompProfile) { + + // If the pod seccomp profile is set to Localhost, and the container seccomp profile is disabled, + // then set the container seccomp profile to the default profile. + // Otherwise, remove the container seccomp profile in favour of the pod profile. + var pendingFix kubeaudit.PendingFix + var msg string + + podSpec := k8s.GetPodSpec(resource) + if isPodSeccompProfileMissing(podSpec.SecurityContext) || isSeccompProfileDefault(podSpec.SecurityContext.SeccompProfile.Type) { + pendingFix = &ByRemovingSeccompProfileInContainer{container: container} + msg = fmt.Sprintf("Container Seccomp profile is set to %s which disables Seccomp. It should be removed from the container SecurityContext, as the pod SeccompProfile is set.", containerSeccompProfile) + + } else { + pendingFix = &BySettingSeccompProfileInContainer{container: container, seccompProfileType: ProfileRuntimeDefault} + msg = fmt.Sprintf("Container Seccomp profile is set to %s which disables Seccomp. It should be set to the `%s` or `%s`.", containerSeccompProfile, ProfileRuntimeDefault, ProfileLocalhost) + } + return &kubeaudit.AuditResult{ Auditor: Name, Rule: SeccompDisabledContainer, Severity: kubeaudit.Error, - Message: fmt.Sprintf("Seccomp container annotation is set to %s which disables Seccomp. It should be set to the default profile %s or should start with %s.", containerSeccompProfile, ProfileRuntimeDefault, ProfileNamePrefix), + Message: msg, PendingFix: pendingFix, Metadata: kubeaudit.Metadata{ - "AnnotationKey": containerAnnotationKey, - "AnnotationValue": containerSeccompProfile, + "Container": container.Name, + "SeccompProfileType": string(containerSeccompProfile), }, } } @@ -186,21 +176,24 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi return nil } -func isSeccompAnnotationMissing(annotations map[string]string, annotationKey string) bool { - _, ok := annotations[annotationKey] - return !ok +func isPodSeccompProfileMissing(securityContext *apiv1.PodSecurityContext) bool { + return securityContext == nil || securityContext.SeccompProfile == nil +} + +func isContainerSeccompProfileMissing(securityContext *apiv1.SecurityContext) bool { + return securityContext == nil || securityContext.SeccompProfile == nil } // returns false if there is at least one container that is not covered by a container-level seccomp annotation -func isSeccompEnabledForContainers(annotations map[string]string, resource k8s.Resource) bool { +func isSeccompEnabledForAllContainers(resource k8s.Resource) bool { for _, container := range k8s.GetContainers(resource) { - containerAnnotationKey := getContainerAnnotationKey(container) - if isSeccompAnnotationMissing(annotations, containerAnnotationKey) { + securityContext := container.SecurityContext + if isContainerSeccompProfileMissing(securityContext) { return false } - containerSeccompProfile := annotations[containerAnnotationKey] - if !isSeccompEnabled(containerSeccompProfile) { + containerSeccompProfileType := securityContext.SeccompProfile.Type + if !isSeccompEnabled(containerSeccompProfileType) { return false } } @@ -208,22 +201,37 @@ func isSeccompEnabledForContainers(annotations map[string]string, resource k8s.R return true } -func isSeccompProfileDeprecated(seccompProfile string) bool { - return seccompProfile == DeprecatedProfileRuntimeDefault +func isSeccompProfileMissingForAllContainers(resource k8s.Resource) bool { + for _, container := range k8s.GetContainers(resource) { + securityContext := container.SecurityContext + if !isContainerSeccompProfileMissing(securityContext) { + return false + } + } + + return true } -func isSeccompProfileCustom(seccompProfile string) bool { - return strings.HasPrefix(seccompProfile, ProfileNamePrefix) +func isSeccompEnabled(seccompProfileType apiv1.SeccompProfileType) bool { + return isSeccompProfileDefault(seccompProfileType) || isSeccompProfileLocalhost(seccompProfileType) } -func isSeccompEnabled(seccompProfile string) bool { - return isSeccompProfileDefault(seccompProfile) || isSeccompProfileCustom(seccompProfile) +func isSeccompProfileDefault(seccompProfileType apiv1.SeccompProfileType) bool { + return seccompProfileType == apiv1.SeccompProfileTypeRuntimeDefault } -func isSeccompProfileDefault(seccompProfile string) bool { - return seccompProfile == ProfileRuntimeDefault +func isSeccompProfileLocalhost(seccompProfileType apiv1.SeccompProfileType) bool { + return seccompProfileType == apiv1.SeccompProfileTypeLocalhost } -func getContainerAnnotationKey(container *k8s.ContainerV1) string { - return ContainerAnnotationKeyPrefix + container.Name +func findSeccompAnnotations(resource k8s.Resource) []string { + annotations := k8s.GetAnnotations(resource) + seccompAnnotations := []string{} + for annotation := range annotations { + if annotation == PodAnnotationKey || strings.HasPrefix(annotation, ContainerAnnotationKeyPrefix) { + seccompAnnotations = append(seccompAnnotations, annotation) + } + } + + return seccompAnnotations } diff --git a/auditors/seccomp/seccomp_test.go b/auditors/seccomp/seccomp_test.go index 9ec9aba0..6fa2b338 100644 --- a/auditors/seccomp/seccomp_test.go +++ b/auditors/seccomp/seccomp_test.go @@ -1,28 +1,24 @@ package seccomp import ( - "fmt" "strings" "testing" "github.com/Shopify/kubeaudit/internal/test" - "github.com/Shopify/kubeaudit/pkg/k8s" - "github.com/stretchr/testify/assert" ) -const fixtureDir = "fixtures" - func TestAuditSeccomp(t *testing.T) { cases := []struct { file string expectedErrors []string testLocalMode bool }{ - {"seccomp-annotation-missing.yml", []string{SeccompAnnotationMissing}, true}, - {"seccomp-deprecated-pod.yml", []string{SeccompDeprecatedPod}, true}, - {"seccomp-deprecated.yml", []string{SeccompDeprecatedContainer, SeccompAnnotationMissing}, true}, + {"seccomp-profile-missing.yml", []string{SeccompProfileMissing}, true}, + {"seccomp-profile-missing-disabled-container.yml", []string{SeccompProfileMissing, SeccompDisabledContainer}, true}, + {"seccomp-profile-missing-annotations.yml", []string{SeccompProfileMissing, SeccompDeprecatedAnnotations}, false}, {"seccomp-disabled-pod.yml", []string{SeccompDisabledPod}, true}, - {"seccomp-disabled.yml", []string{SeccompDisabledContainer}, false}, + {"seccomp-disabled.yml", []string{SeccompDisabledContainer}, true}, + {"seccomp-disabled-localhost.yml", []string{SeccompDisabledContainer}, true}, {"seccomp-enabled-pod.yml", nil, true}, {"seccomp-enabled.yml", nil, true}, } @@ -39,150 +35,3 @@ func TestAuditSeccomp(t *testing.T) { }) } } - -func TestFixSeccomp(t *testing.T) { - cases := []struct { - testName string - containerNames []string - annotations map[string]string - expectedAnnotations map[string]string - }{ - { - testName: "Annotation missing", - containerNames: []string{"container1"}, - annotations: map[string]string{}, - expectedAnnotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - }, - { - testName: "Annotation missing container", - containerNames: []string{"container1"}, - annotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - expectedAnnotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - }, - { - testName: "Seccomp enabled pod", - containerNames: []string{"container1"}, - annotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - expectedAnnotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - }, - { - testName: "Seccomp enabled container", - containerNames: []string{"container1"}, - annotations: map[string]string{ - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - }, - expectedAnnotations: map[string]string{ - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - }, - }, - { - testName: "Seccomp enabled pod and container", - containerNames: []string{"container1", "container2", "container3"}, - annotations: map[string]string{ - PodAnnotationKey: ProfileNamePrefix + "myprofile", - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - ContainerAnnotationKeyPrefix + "container2": ProfileNamePrefix + "containerprofile", - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileNamePrefix + "myprofile", - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - ContainerAnnotationKeyPrefix + "container2": ProfileNamePrefix + "containerprofile", - }, - }, - { - testName: "Seccomp disabled pod", - containerNames: []string{"container1"}, - annotations: map[string]string{PodAnnotationKey: "badprofile"}, - expectedAnnotations: map[string]string{PodAnnotationKey: ProfileRuntimeDefault}, - }, - { - testName: "Seccomp disabled container", - containerNames: []string{"container1", "container2"}, - annotations: map[string]string{ - ContainerAnnotationKeyPrefix + "container1": "badprofile", - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - }, - }, - { - testName: "Seccomp disabled pod", - containerNames: []string{"container1", "container2"}, - annotations: map[string]string{ - PodAnnotationKey: "badprofile", - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - }, - }, - { - testName: "Seccomp disabled container enabled pod", - containerNames: []string{"container1"}, - annotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - ContainerAnnotationKeyPrefix + "container1": "badprofile", - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - }, - }, - { - testName: "Seccomp disabled container and pod", - containerNames: []string{"container1", "container2"}, - annotations: map[string]string{ - PodAnnotationKey: "badprofile", - ContainerAnnotationKeyPrefix + "container1": "badprofile", - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - }, - }, - { - testName: "Seccomp enabled container disabled pod", - containerNames: []string{"container1", "container2"}, - annotations: map[string]string{ - PodAnnotationKey: "badprofile", - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - }, - expectedAnnotations: map[string]string{ - PodAnnotationKey: ProfileRuntimeDefault, - ContainerAnnotationKeyPrefix + "container1": ProfileRuntimeDefault, - }, - }, - } - - auditor := New() - for _, tc := range cases { - t.Run(tc.testName, func(t *testing.T) { - resource := newPod(tc.containerNames, tc.annotations) - auditResults, err := auditor.Audit(resource, nil) - if !assert.Nil(t, err) { - return - } - - for _, auditResult := range auditResults { - auditResult.Fix(resource) - ok, plan := auditResult.FixPlan() - if ok { - fmt.Println(plan) - } - } - - fixedAnnotations := k8s.GetAnnotations(resource) - assert.Equal(t, tc.expectedAnnotations, fixedAnnotations) - }) - } -} - -func newPod(containerNames []string, annotations map[string]string) k8s.Resource { - pod := k8s.NewPod() - containers := make([]k8s.ContainerV1, 0, len(containerNames)) - for _, containerName := range containerNames { - containers = append(containers, k8s.ContainerV1{ - Name: containerName, - }) - } - k8s.GetObjectMeta(pod).SetAnnotations(annotations) - k8s.GetPodSpec(pod).Containers = containers - return pod -} diff --git a/docs/all.md b/docs/all.md index d46883cf..ba084e1b 100644 --- a/docs/all.md +++ b/docs/all.md @@ -115,10 +115,8 @@ $ kubeaudit all -f "internal/test/fixtures/all_resources/deployment-apps-v1.yml" Metadata: Container: container --- [error] SeccompAnnotationMissing - Message: Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. - Metadata: - MissingAnnotation: seccomp.security.alpha.kubernetes.io/pod +-- [error] SeccompProfileMissing + Message: Pod Seccomp profile is missing. Seccomp profile should be added to the pod SecurityContext. ``` ### Example with Kubeaudit Config diff --git a/docs/auditors/seccomp.md b/docs/auditors/seccomp.md index 0f29f3f2..5c86a355 100644 --- a/docs/auditors/seccomp.md +++ b/docs/auditors/seccomp.md @@ -13,7 +13,7 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit seccomp -f "auditors/seccomp/fixtures/seccomp-annotation-missing.yml" +$ kubeaudit seccomp -f "auditors/seccomp/fixtures/seccomp-profile-missing.yml" ---------------- Results for --------------- @@ -21,35 +21,42 @@ $ kubeaudit seccomp -f "auditors/seccomp/fixtures/seccomp-annotation-missing.yml kind: Pod metadata: name: pod - namespace: seccomp-annotation-missing + namespace: seccomp-profile-missing -------------------------------------------- --- [error] SeccompAnnotationMissing - Message: Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. - Metadata: - MissingAnnotation: seccomp.security.alpha.kubernetes.io/pod +-- [error] SeccompProfileMissing + Message: Pod Seccomp profile is missing. Seccomp profile should be added to the pod SecurityContext. ``` ## Explanation Seccomp (Secure computing mode) is a Linux kernel feature. -Seccomp is enabled by adding a pod-level annotation. The annotation can be either a pod annotation, which enables seccomp for all containers within that pod, or a container annotation, which enables seccomp only for that container. +Seccomp is enabled by adding a seccomp profile to the security context. The seccomp profile can be either added to a pod security context, which enables seccomp for all containers within that pod, or a security context, which enables seccomp only for that container. -The pod annotation has the following format: +The seccomp profile added to a pod security context has the following format: ``` -seccomp.security.alpha.kubernetes.io/pod: [seccomp profile] +spec: + securityContext: + seccompProfile: + type: [seccomp profile] ``` -The container annotation has the following format: +The seccomp profile added to a container security context has the following format: ``` -container.seccomp.security.alpha.kubernetes.io/[container name]: [seccomp profile] +spec: + containers: + - name: [container name] + image: [container image] + securityContext: + seccompProfile: + type: [seccomp profile] ``` -Ideally the pod annotation should be used. +Ideally, the pod security context should be used. -The value of the annotation (the `seccomp profile`) can be set to either the default profile (`runtime/default`) or a custom profile (`localhost/[profile name]`). +The value of the seccomp profile type can be set to either the default profile (`RuntimeDefault`) or a custom profile (`Localhost`). For `Localhost` type `localhostProfile: [profile file]` should be added. Example of a resource which passes the `seccomp` audit: ```yaml @@ -57,10 +64,10 @@ apiVersion: apps/v1 kind: Deployment spec: template: - metadata: - annotations: - seccomp.security.alpha.kubernetes.io/pod: runtime/default spec: + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: myContainer ``` diff --git a/docs/autofix.md b/docs/autofix.md index c3d15ed2..54e66a74 100644 --- a/docs/autofix.md +++ b/docs/autofix.md @@ -45,21 +45,23 @@ spec: template: spec: containers: - - name: myContainer - resources: {} - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - privileged: false - readOnlyRootFilesystem: true - runAsNonRoot: true + - name: myContainer + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault metadata: annotations: - container.apparmor.security.beta.kubernetes.io/fakeContainerSC: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default + container.apparmor.security.beta.kubernetes.io/myContainer: runtime/default selector: null strategy: {} metadata: @@ -95,34 +97,23 @@ spec: template: spec: containers: - - name: myContainer - resources: {} - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - AUDIT_WRITE - - CHOWN - - DAC_OVERRIDE - - FOWNER - - FSETID - - KILL - - MKNOD - - NET_BIND_SERVICE - - NET_RAW - - SETFCAP - - SETGID - - SETPCAP - - SETUID - - SYS_CHROOT - privileged: false - readOnlyRootFilesystem: true - runAsNonRoot: true + - name: myContainer + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault metadata: annotations: container.apparmor.security.beta.kubernetes.io/myContainer: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default selector: null strategy: {} metadata: @@ -133,35 +124,24 @@ apiVersion: v1 kind: Pod spec: containers: - - name: myContainer2 - image: polinux/stress - resources: {} - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - AUDIT_WRITE - - CHOWN - - DAC_OVERRIDE - - FOWNER - - FSETID - - KILL - - MKNOD - - NET_BIND_SERVICE - - NET_RAW - - SETFCAP - - SETGID - - SETPCAP - - SETUID - - SYS_CHROOT - privileged: false - readOnlyRootFilesystem: true - runAsNonRoot: true + - name: myContainer2 + image: polinux/stress + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault metadata: annotations: container.apparmor.security.beta.kubernetes.io/myContainer2: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default ``` ### Example with Comments @@ -211,38 +191,26 @@ spec: # ContainerSpec spec: containers: - - name: myContainer # this is a sample container - resources: {} - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - AUDIT_WRITE - - CHOWN - - DAC_OVERRIDE - - FOWNER - - FSETID - - KILL - - MKNOD - - NET_BIND_SERVICE - - NET_RAW - - SETFCAP - - SETGID - - SETPCAP - - SETUID - - SYS_CHROOT - privileged: false - readOnlyRootFilesystem: true - runAsNonRoot: true + - name: myContainer # this is a sample container + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsNonRoot: true automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault metadata: annotations: container.apparmor.security.beta.kubernetes.io/myContainer: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default selector: null strategy: {} metadata: - ``` ### Example with Custom Output File @@ -260,4 +228,4 @@ To fix a manifest based on custom rules specified on a kubeaudit config file (e. kubeaudit autofix -k "/path/to/kubeaudit-config.yml" -f "/path/to/manifest.yml" -o "/path/to/fixed" ``` -Also see [Configuration File](/README.md#configuration-file) \ No newline at end of file +Also see [Configuration File](/README.md#configuration-file) diff --git a/docs/cluster.md b/docs/cluster.md index efbb33d9..d24809f5 100644 --- a/docs/cluster.md +++ b/docs/cluster.md @@ -25,10 +25,12 @@ spec: metadata: annotations: container.apparmor.security.beta.kubernetes.io/kubeaudit: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default spec: serviceAccountName: kubeaudit restartPolicy: OnFailure + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: kubeaudit image: shopify/kubeaudit:v0.11 @@ -110,10 +112,12 @@ spec: metadata: annotations: container.apparmor.security.beta.kubernetes.io/kubeaudit: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default spec: serviceAccountName: kubeaudit restartPolicy: OnFailure + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: kubeaudit image: shopify/kubeaudit:v0.11 @@ -247,10 +251,12 @@ spec: metadata: annotations: container.apparmor.security.beta.kubernetes.io/kubeaudit: runtime/default - seccomp.security.alpha.kubernetes.io/pod: runtime/default spec: serviceAccountName: kubeaudit restartPolicy: OnFailure + securityContext: + seccompProfile: + type: RuntimeDefault containers: - name: kubeaudit image: shopify/kubeaudit:v0.11 diff --git a/pkg/fix/annotations.go b/pkg/fix/annotations.go index 83dccb23..075ed7c8 100644 --- a/pkg/fix/annotations.go +++ b/pkg/fix/annotations.go @@ -54,24 +54,26 @@ func (pending *ByAddingPodAnnotation) Plan() string { return fmt.Sprintf("Add pod-level annotation '%v: %v'", pending.Key, pending.Value) } -type ByRemovingPodAnnotation struct { - Key string +type ByRemovingPodAnnotations struct { + Keys []string } // Apply removes the pod annotation -func (pending *ByRemovingPodAnnotation) Apply(resource k8s.Resource) []k8s.Resource { +func (pending *ByRemovingPodAnnotations) Apply(resource k8s.Resource) []k8s.Resource { objectMeta := k8s.GetPodObjectMeta(resource) if objectMeta.GetAnnotations() == nil { return nil } - delete(objectMeta.GetAnnotations(), pending.Key) + for _, key := range pending.Keys { + delete(objectMeta.GetAnnotations(), key) + } return nil } // Plan is a description of what apply will do -func (pending *ByRemovingPodAnnotation) Plan() string { - return fmt.Sprintf("Remove pod-level annotation '%v'", pending.Key) +func (pending *ByRemovingPodAnnotations) Plan() string { + return fmt.Sprintf("Remove pod-level annotations '%v'", pending.Keys) } diff --git a/pkg/fix/annotations_test.go b/pkg/fix/annotations_test.go index 2e4bc69d..63b1495d 100644 --- a/pkg/fix/annotations_test.go +++ b/pkg/fix/annotations_test.go @@ -41,15 +41,17 @@ func TestFix(t *testing.T) { }, }, { - testName: "ByRemovingPodAnnotation", - pendingFix: &ByRemovingPodAnnotation{Key: "mykey"}, + testName: "ByRemovingPodAnnotations", + pendingFix: &ByRemovingPodAnnotations{Keys: []string{"mykey", "mykey2"}}, preFix: func(resource k8s.Resource) { - k8s.GetPodObjectMeta(resource).SetAnnotations(map[string]string{"mykey": "myvalue"}) + k8s.GetPodObjectMeta(resource).SetAnnotations(map[string]string{"mykey": "myvalue", "mykey2": "myvalue2"}) }, assertFixed: func(t *testing.T, resource k8s.Resource) { annotations := k8s.GetAnnotations(resource) _, ok := annotations["mykey"] assert.False(t, ok) + _, ok2 := annotations["mykey2"] + assert.False(t, ok2) }, }, }