Add seccomp enforcement and validation based on new GA fields

Adds seccomp validation.

This ensures that field and annotation values must match when present.

Co-authored-by: Sascha Grunert <sgrunert@suse.com>
This commit is contained in:
Paulo Gomes
2020-06-24 21:37:49 +01:00
parent 865cbf0bdf
commit 8976e3620f
93 changed files with 17247 additions and 15078 deletions

View File

@@ -24,6 +24,8 @@ import (
"strings"
"testing"
asserttestify "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -7287,6 +7289,107 @@ func TestValidatePod(t *testing.T) {
},
Spec: validPodSpec(nil),
},
{ // runtime default seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
},
},
{ // runtime default seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
}},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
{ // unconfined seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeUnconfined,
},
},
},
},
{ // unconfined seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeUnconfined,
},
},
}},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
{ // localhost seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: utilpointer.StringPtr("filename.json"),
},
},
},
},
{ // localhost seccomp 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{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: utilpointer.StringPtr("filename.json"),
},
},
}},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
{ // default AppArmor profile for a container
ObjectMeta: metav1.ObjectMeta{
Name: "123",
@@ -7983,6 +8086,50 @@ func TestValidatePod(t *testing.T) {
Spec: validPodSpec(nil),
},
},
"must match seccomp profile type and pod annotation": {
expectedError: "seccomp type in annotation and field must match",
spec: core.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
core.SeccompPodAnnotationKey: "unconfined",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSClusterFirst,
},
},
},
"must match seccomp profile type and container annotation": {
expectedError: "seccomp type in annotation and field must match",
spec: core.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
core.SeccompContainerAnnotationKeyPrefix + "ctr": "unconfined",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File",
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
}}},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSClusterFirst,
},
},
},
"must be a relative path in a node-local seccomp profile annotation": {
expectedError: "must be a relative path",
spec: core.Pod{
@@ -15200,3 +15347,456 @@ func TestValidateNodeCIDRs(t *testing.T) {
}
}
}
func TestValidateSeccompAnnotationAndField(t *testing.T) {
const containerName = "container"
testProfile := "test"
for _, test := range []struct {
description string
pod *core.Pod
validation func(*testing.T, string, field.ErrorList, *v1.Pod)
}{
{
description: "Field type unconfined and annotation does not match",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: "not-matching",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeUnconfined,
},
},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type default and annotation does not match",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: "not-matching",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type localhost and annotation does not match",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: "not-matching",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: &testProfile,
},
},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type localhost and localhost/ prefixed annotation does not match",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: "localhost/not-matching",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: &testProfile,
},
},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type unconfined and annotation does not match (container)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{
Name: containerName,
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeUnconfined,
},
},
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type default and annotation does not match (container)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{
Name: containerName,
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type localhost and annotation does not match (container)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompContainerAnnotationKeyPrefix + containerName: "not-matching",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{
Name: containerName,
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: &testProfile,
},
},
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Field type localhost and localhost/ prefixed annotation does not match (container)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompContainerAnnotationKeyPrefix + containerName: "localhost/not-matching",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{
Name: containerName,
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeLocalhost,
LocalhostProfile: &testProfile,
},
},
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.NotNil(t, allErrs, desc)
},
},
{
description: "Nil errors must not be appended (pod)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: "localhost/anyprofile",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: "Abc",
},
},
Containers: []core.Container{{
Name: containerName,
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.Empty(t, allErrs, desc)
},
},
{
description: "Nil errors must not be appended (container)",
pod: &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1.SeccompContainerAnnotationKeyPrefix + containerName: "localhost/not-matching",
},
},
Spec: core.PodSpec{
Containers: []core.Container{{
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: "Abc",
},
},
Name: containerName,
}},
},
},
validation: func(t *testing.T, desc string, allErrs field.ErrorList, pod *v1.Pod) {
require.Empty(t, allErrs, desc)
},
},
} {
output := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}},
}
for i, ctr := range test.pod.Spec.Containers {
output.Spec.Containers = append(output.Spec.Containers, v1.Container{})
if ctr.SecurityContext != nil && ctr.SecurityContext.SeccompProfile != nil {
output.Spec.Containers[i].SecurityContext = &v1.SecurityContext{
SeccompProfile: &v1.SeccompProfile{
Type: v1.SeccompProfileType(ctr.SecurityContext.SeccompProfile.Type),
LocalhostProfile: ctr.SecurityContext.SeccompProfile.LocalhostProfile,
},
}
}
}
errList := validateSeccompAnnotationsAndFields(test.pod.ObjectMeta, &test.pod.Spec, field.NewPath(""))
test.validation(t, test.description, errList, output)
}
}
func TestValidateSeccompAnnotationsAndFieldsMatch(t *testing.T) {
rootFld := field.NewPath("")
tests := []struct {
description string
annotationValue string
seccompField *core.SeccompProfile
fldPath *field.Path
expectedErr *field.Error
}{
{
description: "seccompField nil should return empty",
expectedErr: nil,
},
{
description: "unconfined annotation and SeccompProfileTypeUnconfined should return empty",
annotationValue: "unconfined",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeUnconfined},
expectedErr: nil,
},
{
description: "runtime/default annotation and SeccompProfileTypeRuntimeDefault should return empty",
annotationValue: "runtime/default",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault},
expectedErr: nil,
},
{
description: "docker/default annotation and SeccompProfileTypeRuntimeDefault should return empty",
annotationValue: "docker/default",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault},
expectedErr: nil,
},
{
description: "localhost/test.json annotation and SeccompProfileTypeLocalhost with correct profile should return empty",
annotationValue: "localhost/test.json",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost, LocalhostProfile: utilpointer.StringPtr("test.json")},
expectedErr: nil,
},
{
description: "localhost/test.json annotation and SeccompProfileTypeLocalhost without profile should error",
annotationValue: "localhost/test.json",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost},
fldPath: rootFld,
expectedErr: field.Forbidden(rootFld.Child("localhostProfile"), "seccomp profile in annotation and field must match"),
},
{
description: "localhost/test.json annotation and SeccompProfileTypeLocalhost with different profile should error",
annotationValue: "localhost/test.json",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeLocalhost, LocalhostProfile: utilpointer.StringPtr("different.json")},
fldPath: rootFld,
expectedErr: field.Forbidden(rootFld.Child("localhostProfile"), "seccomp profile in annotation and field must match"),
},
{
description: "localhost/test.json annotation and SeccompProfileTypeUnconfined with different profile should error",
annotationValue: "localhost/test.json",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeUnconfined},
fldPath: rootFld,
expectedErr: field.Forbidden(rootFld.Child("type"), "seccomp type in annotation and field must match"),
},
{
description: "localhost/test.json annotation and SeccompProfileTypeRuntimeDefault with different profile should error",
annotationValue: "localhost/test.json",
seccompField: &core.SeccompProfile{Type: core.SeccompProfileTypeRuntimeDefault},
fldPath: rootFld,
expectedErr: field.Forbidden(rootFld.Child("type"), "seccomp type in annotation and field must match"),
},
}
for i, test := range tests {
err := validateSeccompAnnotationsAndFieldsMatch(test.annotationValue, test.seccompField, test.fldPath)
asserttestify.Equal(t, test.expectedErr, err, "TestCase[%d]: %s", i, test.description)
}
}
func TestValidatePodTemplateSpecSeccomp(t *testing.T) {
rootFld := field.NewPath("template")
tests := []struct {
description string
spec *core.PodTemplateSpec
fldPath *field.Path
expectedErr field.ErrorList
}{
{
description: "seccomp field and container annotation must match",
fldPath: rootFld,
expectedErr: field.ErrorList{
field.Forbidden(
rootFld.Child("spec").Child("containers").Index(1).Child("securityContext").Child("seccompProfile").Child("type"),
"seccomp type in annotation and field must match"),
},
spec: &core.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"container.seccomp.security.alpha.kubernetes.io/test2": "unconfined",
},
},
Spec: core.PodSpec{
Containers: []core.Container{
{
Name: "test1",
Image: "alpine",
ImagePullPolicy: core.PullAlways,
TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError,
},
{
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
Name: "test2",
Image: "alpine",
ImagePullPolicy: core.PullAlways,
TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError,
},
},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
},
{
description: "seccomp field and pod annotation must match",
fldPath: rootFld,
expectedErr: field.ErrorList{
field.Forbidden(
rootFld.Child("spec").Child("securityContext").Child("seccompProfile").Child("type"),
"seccomp type in annotation and field must match"),
},
spec: &core.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"seccomp.security.alpha.kubernetes.io/pod": "runtime/default",
},
},
Spec: core.PodSpec{
SecurityContext: &core.PodSecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeUnconfined,
},
},
Containers: []core.Container{
{
Name: "test",
Image: "alpine",
ImagePullPolicy: core.PullAlways,
TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError,
},
},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
},
{
description: "init seccomp field and container annotation must match",
fldPath: rootFld,
expectedErr: field.ErrorList{
field.Forbidden(
rootFld.Child("spec").Child("initContainers").Index(0).Child("securityContext").Child("seccompProfile").Child("type"),
"seccomp type in annotation and field must match"),
},
spec: &core.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"container.seccomp.security.alpha.kubernetes.io/init-test": "unconfined",
},
},
Spec: core.PodSpec{
Containers: []core.Container{
{
Name: "test",
Image: "alpine",
ImagePullPolicy: core.PullAlways,
TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError,
},
},
InitContainers: []core.Container{
{
Name: "init-test",
SecurityContext: &core.SecurityContext{
SeccompProfile: &core.SeccompProfile{
Type: core.SeccompProfileTypeRuntimeDefault,
},
},
Image: "alpine",
ImagePullPolicy: core.PullAlways,
TerminationMessagePolicy: core.TerminationMessageFallbackToLogsOnError,
},
},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSDefault,
},
},
},
}
for i, test := range tests {
err := ValidatePodTemplateSpec(test.spec, rootFld)
asserttestify.Equal(t, test.expectedErr, err, "TestCase[%d]: %s", i, test.description)
}
}