PodSecurity: seccompProfile_baseline: cleanup

Make messages consistent
Add unit tests for messages
Consolidate integration test fixtures
Rename to seccompProfile_baseline
This commit is contained in:
Jordan Liggitt 2021-07-08 00:20:26 -04:00
parent 2af08d1a5a
commit bebf612967
4 changed files with 344 additions and 156 deletions

View File

@ -0,0 +1,172 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/pod-security-admission/api"
)
/*
If seccomp profiles are specified, only runtime default and localhost profiles are allowed.
v1.0 - v1.18:
**Restricted Fields:**
metadata.annotations['seccomp.security.alpha.kubernetes.io/pod']
metadata.annotations['container.seccomp.security.alpha.kubernetes.io/*']
**Allowed Values:** 'runtime/default', 'docker/default', 'localhost/*', undefined
v1.19+:
**Restricted Fields:**
spec.securityContext.seccompProfile.type
spec.containers[*].securityContext.seccompProfile.type
spec.initContainers[*].securityContext.seccompProfile.type
**Allowed Values:** 'RuntimeDefault', 'Localhost', undefined
*/
const (
annotationKeyPod = "seccomp.security.alpha.kubernetes.io/pod"
annotationKeyContainerPrefix = "container.seccomp.security.alpha.kubernetes.io/"
)
func init() {
addCheck(CheckSeccompBaseline)
}
func CheckSeccompBaseline() Check {
return Check{
ID: "seccompProfile_baseline",
Level: api.LevelBaseline,
Versions: []VersionedCheck{
{
MinimumVersion: api.MajorMinorVersion(1, 0),
CheckPod: seccompProfileBaseline_1_0,
},
{
MinimumVersion: api.MajorMinorVersion(1, 19),
CheckPod: seccompProfileBaseline_1_19,
},
},
}
}
func validSeccomp(t corev1.SeccompProfileType) bool {
return t == corev1.SeccompProfileTypeLocalhost ||
t == corev1.SeccompProfileTypeRuntimeDefault
}
func validSeccompAnnotationValue(v string) bool {
return v == corev1.SeccompProfileRuntimeDefault ||
v == corev1.DeprecatedSeccompProfileDockerDefault ||
strings.HasPrefix(v, corev1.SeccompLocalhostProfileNamePrefix)
}
// seccompProfileBaseline_1_0 checks baseline policy on seccomp alpha annotation
func seccompProfileBaseline_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult {
forbidden := sets.NewString()
if val, ok := podMetadata.Annotations[annotationKeyPod]; ok {
if !validSeccompAnnotationValue(val) {
forbidden.Insert(fmt.Sprintf("%s=%q", annotationKeyPod, val))
}
}
visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) {
annotation := annotationKeyContainerPrefix + c.Name
if val, ok := podMetadata.Annotations[annotation]; ok {
if !validSeccompAnnotationValue(val) {
forbidden.Insert(fmt.Sprintf("%s=%q", annotation, val))
}
}
})
if len(forbidden) > 0 {
return CheckResult{
Allowed: false,
ForbiddenReason: "seccompProfile",
ForbiddenDetail: fmt.Sprintf(
"forbidden %s %s",
pluralize("annotation", "annotations", len(forbidden)),
strings.Join(forbidden.List(), ", "),
),
}
}
return CheckResult{Allowed: true}
}
// seccompProfileBaseline_1_19 checks baseline policy on securityContext.seccompProfile field
func seccompProfileBaseline_1_19(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult {
// things that explicitly set seccompProfile.type to a bad value
var badSetters []string
badValues := sets.NewString()
if podSpec.SecurityContext != nil && podSpec.SecurityContext.SeccompProfile != nil {
if !validSeccomp(podSpec.SecurityContext.SeccompProfile.Type) {
badSetters = append(badSetters, "pod")
badValues.Insert(string(podSpec.SecurityContext.SeccompProfile.Type))
}
}
// containers that explicitly set seccompProfile.type to a bad value
var explicitlyBadContainers []string
visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) {
if c.SecurityContext != nil && c.SecurityContext.SeccompProfile != nil {
// container explicitly set seccompProfile
if !validSeccomp(c.SecurityContext.SeccompProfile.Type) {
// container explicitly set seccompProfile to a bad value
explicitlyBadContainers = append(explicitlyBadContainers, c.Name)
badValues.Insert(string(c.SecurityContext.SeccompProfile.Type))
}
}
})
if len(explicitlyBadContainers) > 0 {
badSetters = append(
badSetters,
fmt.Sprintf(
"%s %s",
pluralize("container", "containers", len(explicitlyBadContainers)),
joinQuote(explicitlyBadContainers),
),
)
}
// pod or containers explicitly set bad seccompProfiles
if len(badSetters) > 0 {
return CheckResult{
Allowed: false,
ForbiddenReason: "seccompProfile",
ForbiddenDetail: fmt.Sprintf(
"%s must not set securityContext.seccompProfile.type to %s",
strings.Join(badSetters, " and "),
joinQuote(badValues.List()),
),
}
}
return CheckResult{Allowed: true}
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestSeccompProfileBaseline_1_0(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
expectReason string
expectDetail string
}{
{
name: "pod seccomp invalid",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"seccomp.security.alpha.kubernetes.io/pod": "unconfined",
},
},
},
expectReason: `seccompProfile`,
expectDetail: `forbidden annotation seccomp.security.alpha.kubernetes.io/pod="unconfined"`,
},
{
name: "containers seccomp invalid",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"container.seccomp.security.alpha.kubernetes.io/a": "unconfined",
"container.seccomp.security.alpha.kubernetes.io/b": "unknown",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
},
},
expectReason: `seccompProfile`,
expectDetail: `forbidden annotations container.seccomp.security.alpha.kubernetes.io/a="unconfined", container.seccomp.security.alpha.kubernetes.io/b="unknown"`,
},
{
name: "pod and containers seccomp invalid",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"seccomp.security.alpha.kubernetes.io/pod": "unconfined",
"container.seccomp.security.alpha.kubernetes.io/a": "unconfined",
"container.seccomp.security.alpha.kubernetes.io/b": "unknown",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
},
},
expectReason: `seccompProfile`,
expectDetail: `forbidden annotations container.seccomp.security.alpha.kubernetes.io/a="unconfined", container.seccomp.security.alpha.kubernetes.io/b="unknown", seccomp.security.alpha.kubernetes.io/pod="unconfined"`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := seccompProfileBaseline_1_0(&tc.pod.ObjectMeta, &tc.pod.Spec)
if result.Allowed {
t.Fatal("expected disallowed")
}
if e, a := tc.expectReason, result.ForbiddenReason; e != a {
t.Errorf("expected\n%s\ngot\n%s", e, a)
}
if e, a := tc.expectDetail, result.ForbiddenDetail; e != a {
t.Errorf("expected\n%s\ngot\n%s", e, a)
}
})
}
}
func TestSeccompProfileBaseline_1_19(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
expectReason string
expectDetail string
}{
{
name: "pod seccomp invalid",
pod: &corev1.Pod{Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}},
Containers: []corev1.Container{
{Name: "a", SecurityContext: nil},
},
}},
expectReason: `seccompProfile`,
expectDetail: `pod must not set securityContext.seccompProfile.type to "Unconfined"`,
},
{
name: "containers seccomp invalid",
pod: &corev1.Pod{Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}},
Containers: []corev1.Container{
{Name: "a", SecurityContext: nil},
{Name: "b", SecurityContext: &corev1.SecurityContext{}},
{Name: "c", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}},
{Name: "d", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}},
{Name: "e", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}},
{Name: "f", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}},
},
}},
expectReason: `seccompProfile`,
expectDetail: `containers "c", "d" must not set securityContext.seccompProfile.type to "Unconfined"`,
},
{
name: "pod and containers seccomp invalid",
pod: &corev1.Pod{Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}},
Containers: []corev1.Container{
{Name: "a", SecurityContext: nil},
{Name: "b", SecurityContext: &corev1.SecurityContext{}},
{Name: "c", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}},
{Name: "d", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}},
{Name: "e", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}},
{Name: "f", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}},
},
}},
expectReason: `seccompProfile`,
expectDetail: `pod and containers "c", "d" must not set securityContext.seccompProfile.type to "Unconfined"`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := seccompProfileBaseline_1_19(&tc.pod.ObjectMeta, &tc.pod.Spec)
if result.Allowed {
t.Fatal("expected disallowed")
}
if e, a := tc.expectReason, result.ForbiddenReason; e != a {
t.Errorf("expected\n%s\ngot\n%s", e, a)
}
if e, a := tc.expectDetail, result.ForbiddenDetail; e != a {
t.Errorf("expected\n%s\ngot\n%s", e, a)
}
})
}
}

View File

@ -1,140 +0,0 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/pod-security-admission/api"
)
const (
annotationKeyPod = "seccomp.security.alpha.kubernetes.io/pod"
annotationKeyContainerPrefix = "container.seccomp.security.alpha.kubernetes.io/"
missingRequiredValue = "<missing required value>"
)
func init() {
addCheck(CheckSeccompBaseline)
}
func fieldValue(f *field.Path, val string) string {
return fmt.Sprintf("%s=%s", f.String(), val)
}
func fieldValueRequired(f *field.Path) string {
return fmt.Sprintf("%s=%s", f.String(), missingRequiredValue)
}
func CheckSeccompBaseline() Check {
return Check{
ID: "seccomp_baseline",
Level: api.LevelBaseline,
Versions: []VersionedCheck{
{
MinimumVersion: api.MajorMinorVersion(1, 0),
CheckPod: seccomp_1_0_baseline,
},
{
MinimumVersion: api.MajorMinorVersion(1, 19),
CheckPod: seccomp_1_19_baseline,
},
},
}
}
func validSeccomp(t corev1.SeccompProfileType) bool {
return t == corev1.SeccompProfileTypeLocalhost ||
t == corev1.SeccompProfileTypeRuntimeDefault
}
// seccomp_1_0_baseline checks baseline policy on seccomp alpha annotation
func seccomp_1_0_baseline(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult {
forbidden := sets.NewString()
if val, ok := podMetadata.Annotations[annotationKeyPod]; ok {
if val == corev1.SeccompProfileNameUnconfined {
podAnnotationField := field.NewPath("metadata").Child("annotations", annotationKeyPod)
forbidden.Insert(fieldValue(podAnnotationField, val))
}
}
visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) {
annotation := annotationKeyContainerPrefix + c.Name
if val, ok := podMetadata.Annotations[annotation]; ok {
if val == corev1.SeccompProfileNameUnconfined {
containerAnnotationField := field.NewPath("metadata").
Child("annotations", annotation)
forbidden.Insert(fieldValue(containerAnnotationField, val))
}
}
})
if len(forbidden) > 0 {
return CheckResult{
Allowed: false,
ForbiddenReason: "seccomp profile",
ForbiddenDetail: strings.Join(forbidden.List(), ", "),
}
}
return CheckResult{Allowed: true}
}
// seccomp_1_19_baseline checks baseline policy on securityContext.seccompProfile field
func seccomp_1_19_baseline(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult {
forbidden := sets.NewString()
if podSpec.SecurityContext != nil {
if podSpec.SecurityContext.SeccompProfile != nil {
seccompType := podSpec.SecurityContext.SeccompProfile.Type
if !validSeccomp(seccompType) {
podSeccompField := field.NewPath("spec").Child("securityContext", "seccompProfile", "type")
forbidden.Insert(fieldValue(podSeccompField, string(seccompType)))
}
}
}
visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) {
if c.SecurityContext != nil {
if c.SecurityContext.SeccompProfile != nil {
if c.SecurityContext.SeccompProfile.Type != "" {
seccompType := c.SecurityContext.SeccompProfile.Type
if !validSeccomp(seccompType) {
containerSeccompField := path.Child("securityContext", "seccompProfile", "type")
forbidden.Insert(fieldValue(containerSeccompField, string(seccompType)))
}
}
}
}
})
if len(forbidden) > 0 {
return CheckResult{
Allowed: false,
ForbiddenReason: "seccomp profile",
ForbiddenDetail: strings.Join(forbidden.List(), ", "),
}
}
return CheckResult{Allowed: true}
}

View File

@ -30,7 +30,7 @@ The check implementation looks at the appropriate value based on version.
func init() {
fixtureData_baseline_1_0 := fixtureGenerator{
expectErrorSubstring: "seccomp profile",
expectErrorSubstring: "seccompProfile",
generatePass: func(p *corev1.Pod) []*corev1.Pod {
// don't generate fixtures if minimal valid pod already has seccomp config
if val, ok := p.Annotations[annotationKeyPod]; ok &&
@ -41,14 +41,11 @@ func init() {
p = ensureAnnotation(p)
return []*corev1.Pod{
tweak(p, func(p *corev1.Pod) {
// pod-level default
p.Annotations[annotationKeyPod] = corev1.SeccompProfileRuntimeDefault
p.Annotations[annotationKeyContainer(p.Spec.Containers[0])] = corev1.SeccompProfileRuntimeDefault
p.Annotations[annotationKeyContainer(p.Spec.InitContainers[0])] = corev1.SeccompProfileRuntimeDefault
}),
tweak(p, func(p *corev1.Pod) {
p.Annotations[annotationKeyPod] = corev1.SeccompLocalhostProfileNamePrefix + "testing"
// container-level localhost
p.Annotations[annotationKeyContainer(p.Spec.Containers[0])] = corev1.SeccompLocalhostProfileNamePrefix + "testing"
p.Annotations[annotationKeyContainer(p.Spec.InitContainers[0])] = corev1.SeccompLocalhostProfileNamePrefix + "testing"
// init-container unset
}),
}
},
@ -69,7 +66,7 @@ func init() {
}
fixtureData_baseline_1_19 := fixtureGenerator{
expectErrorSubstring: "seccomp profile",
expectErrorSubstring: "seccompProfile",
generatePass: func(p *corev1.Pod) []*corev1.Pod {
// don't generate fixtures if minimal valid pod already has seccomp config
if p.Spec.SecurityContext != nil &&
@ -81,14 +78,11 @@ func init() {
p = ensureSecurityContext(p)
return []*corev1.Pod{
tweak(p, func(p *corev1.Pod) {
// pod-level default
p.Spec.SecurityContext.SeccompProfile = seccompProfileRuntimeDefault
// container-level localhost
p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault
p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault
}),
tweak(p, func(p *corev1.Pod) {
p.Spec.SecurityContext.SeccompProfile = seccompProfileLocalhost("testing")
p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing")
p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing")
// init-container unset
}),
}
},
@ -109,12 +103,12 @@ func init() {
}
registerFixtureGenerator(
fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 0), check: "seccomp_baseline"},
fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 0), check: "seccompProfile_baseline"},
fixtureData_baseline_1_0,
)
registerFixtureGenerator(
fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 19), check: "seccomp_baseline"},
fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 19), check: "seccompProfile_baseline"},
fixtureData_baseline_1_19,
)
}