Merge pull request #92856 from saschagrunert/psp-seccomp-ga

Implement PodSecurityPolicy enforcement for seccomp GA
This commit is contained in:
Kubernetes Prow Robot 2020-07-11 15:35:22 -07:00 committed by GitHub
commit 70f68dbf74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 159 additions and 60 deletions

View File

@ -745,3 +745,58 @@ func setHostnameAsFQDNInUse(podSpec *api.PodSpec) bool {
}
return *podSpec.SetHostnameAsFQDN
}
// SeccompAnnotationForField takes a pod seccomp profile field and returns the
// converted annotation value
func SeccompAnnotationForField(field *api.SeccompProfile) string {
// If only seccomp fields are specified, add the corresponding annotations.
// This ensures that the fields are enforced even if the node version
// trails the API version
switch field.Type {
case api.SeccompProfileTypeUnconfined:
return v1.SeccompProfileNameUnconfined
case api.SeccompProfileTypeRuntimeDefault:
return v1.SeccompProfileRuntimeDefault
case api.SeccompProfileTypeLocalhost:
if field.LocalhostProfile != nil {
return v1.SeccompLocalhostProfileNamePrefix + *field.LocalhostProfile
}
}
// we can only reach this code path if the LocalhostProfile is nil but the
// provided field type is SeccompProfileTypeLocalhost or if an unrecognized
// type is specified
return ""
}
// SeccompFieldForAnnotation takes a pod annotation and returns the converted
// seccomp profile field.
func SeccompFieldForAnnotation(annotation string) *api.SeccompProfile {
// If only seccomp annotations are specified, copy the values into the
// corresponding fields. This ensures that existing applications continue
// to enforce seccomp, and prevents the kubelet from needing to resolve
// annotations & fields.
if annotation == v1.SeccompProfileNameUnconfined {
return &api.SeccompProfile{Type: api.SeccompProfileTypeUnconfined}
}
if annotation == api.SeccompProfileRuntimeDefault || annotation == api.DeprecatedSeccompProfileDockerDefault {
return &api.SeccompProfile{Type: api.SeccompProfileTypeRuntimeDefault}
}
if strings.HasPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) {
localhostProfile := strings.TrimPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix)
if localhostProfile != "" {
return &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &localhostProfile,
}
}
}
// we can only reach this code path if the localhostProfile name has a zero
// length or if the annotation has an unrecognized value
return nil
}

View File

@ -587,7 +587,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
// sync field and annotation
if hasField && !hasAnnotation {
newAnnotation := seccompAnnotationForField(field)
newAnnotation := podutil.SeccompAnnotationForField(field)
if newAnnotation != "" {
if pod.Annotations == nil {
@ -596,7 +596,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
pod.Annotations[v1.SeccompPodAnnotationKey] = newAnnotation
}
} else if hasAnnotation && !hasField {
newField := seccompFieldForAnnotation(annotation)
newField := podutil.SeccompFieldForAnnotation(annotation)
if newField != nil {
if pod.Spec.SecurityContext == nil {
@ -621,7 +621,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
// sync field and annotation
if hasField && !hasAnnotation {
newAnnotation := seccompAnnotationForField(field)
newAnnotation := podutil.SeccompAnnotationForField(field)
if newAnnotation != "" {
if pod.Annotations == nil {
@ -630,7 +630,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
pod.Annotations[key] = newAnnotation
}
} else if hasAnnotation && !hasField {
newField := seccompFieldForAnnotation(annotation)
newField := podutil.SeccompFieldForAnnotation(annotation)
if newField != nil {
if ctr.SecurityContext == nil {
@ -643,58 +643,3 @@ func applySeccompVersionSkew(pod *api.Pod) {
return true
})
}
// seccompFieldForAnnotation takes a pod seccomp profile field and returns the
// converted annotation value
func seccompAnnotationForField(field *api.SeccompProfile) string {
// If only seccomp fields are specified, add the corresponding annotations.
// This ensures that the fields are enforced even if the node version
// trails the API version
switch field.Type {
case api.SeccompProfileTypeUnconfined:
return v1.SeccompProfileNameUnconfined
case api.SeccompProfileTypeRuntimeDefault:
return v1.SeccompProfileRuntimeDefault
case api.SeccompProfileTypeLocalhost:
if field.LocalhostProfile != nil {
return v1.SeccompLocalhostProfileNamePrefix + *field.LocalhostProfile
}
}
// we can only reach this code path if the LocalhostProfile is nil but the
// provided field type is SeccompProfileTypeLocalhost or if an unrecognized
// type is specified
return ""
}
// seccompFieldForAnnotation takes a pod annotation and returns the converted
// seccomp profile field.
func seccompFieldForAnnotation(annotation string) *api.SeccompProfile {
// If only seccomp annotations are specified, copy the values into the
// corresponding fields. This ensures that existing applications continue
// to enforce seccomp, and prevents the kubelet from needing to resolve
// annotations & fields.
if annotation == v1.SeccompProfileNameUnconfined {
return &api.SeccompProfile{Type: api.SeccompProfileTypeUnconfined}
}
if annotation == api.SeccompProfileRuntimeDefault || annotation == api.DeprecatedSeccompProfileDockerDefault {
return &api.SeccompProfile{Type: api.SeccompProfileTypeRuntimeDefault}
}
if strings.HasPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) {
localhostProfile := strings.TrimPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix)
if localhostProfile != "" {
return &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &localhostProfile,
}
}
}
// we can only reach this code path if the localhostProfile name has a zero
// length or if the annotation has an unrecognized value
return nil
}

View File

@ -11,6 +11,7 @@ go_library(
srcs = ["strategy.go"],
importpath = "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp",
deps = [
"//pkg/api/pod:go_default_library",
"//pkg/apis/core:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
@ -22,6 +23,7 @@ go_test(
embed = [":go_default_library"],
deps = [
"//pkg/apis/core:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)

View File

@ -21,6 +21,7 @@ import (
"strings"
"k8s.io/apimachinery/pkg/util/validation/field"
podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core"
)
@ -83,6 +84,10 @@ func (s *strategy) Generate(annotations map[string]string, pod *api.Pod) (string
// Profile already set, nothing to do.
return annotations[api.SeccompPodAnnotationKey], nil
}
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
// Profile field already set, translate to annotation
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile), nil
}
return s.defaultProfile, nil
}
@ -92,6 +97,10 @@ func (s *strategy) ValidatePod(pod *api.Pod) field.ErrorList {
allErrs := field.ErrorList{}
podSpecFieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompPodAnnotationKey)
podProfile := pod.Annotations[api.SeccompPodAnnotationKey]
// if the annotation is not set, see if the field is set and derive the corresponding annotation value
if len(podProfile) == 0 && pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
podProfile = podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
}
if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && podProfile != "" {
allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, "seccomp may not be set"))
@ -141,9 +150,19 @@ func (s *strategy) profileAllowed(profile string) bool {
// profileForContainer returns the container profile if set, otherwise the pod profile.
func profileForContainer(pod *api.Pod, container *api.Container) string {
if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil {
// derive the annotation value from the container field
return podutil.SeccompAnnotationForField(container.SecurityContext.SeccompProfile)
}
containerProfile, ok := pod.Annotations[api.SeccompContainerAnnotationKeyPrefix+container.Name]
if ok {
// return the existing container annotation
return containerProfile
}
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
// derive the annotation value from the pod field
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
}
// return the existing pod annotation
return pod.Annotations[api.SeccompPodAnnotationKey]
}

View File

@ -21,6 +21,7 @@ import (
"strings"
"testing"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "k8s.io/kubernetes/pkg/apis/core"
)
@ -41,6 +42,9 @@ var (
allowSpecific = map[string]string{
AllowedProfilesAnnotationKey: "foo",
}
allowSpecificLocalhost = map[string]string{
AllowedProfilesAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + "foo",
}
)
func TestNewStrategy(t *testing.T) {
@ -102,9 +106,11 @@ func TestNewStrategy(t *testing.T) {
}
func TestGenerate(t *testing.T) {
bar := "bar"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedProfile string
}{
"no seccomp, no pod annotations": {
@ -143,10 +149,25 @@ func TestGenerate(t *testing.T) {
},
expectedProfile: "bar",
},
"seccomp with default, pod field": {
pspAnnotations: allowAnyDefault,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &bar,
},
expectedProfile: "localhost/bar",
},
}
for k, v := range tests {
s := NewStrategy(v.pspAnnotations)
actual, err := s.Generate(v.podAnnotations, nil)
actual, err := s.Generate(v.podAnnotations, &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
SeccompProfile: v.seccompProfile,
},
},
})
if err != nil {
t.Errorf("%s received error during generation %#v", k, err)
continue
@ -158,9 +179,11 @@ func TestGenerate(t *testing.T) {
}
func TestValidatePod(t *testing.T) {
foo := "foo"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedError string
}{
"no pod annotations, required profiles": {
@ -206,12 +229,44 @@ func TestValidatePod(t *testing.T) {
podAnnotations: nil,
expectedError: "",
},
"valid pod annotations and field, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
"valid pod field and no annotation, required profiles": {
pspAnnotations: allowSpecific,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "Forbidden: localhost/foo is not an allowed seccomp profile. Valid values are foo",
},
"valid pod field and no annotation, required profiles (localhost)": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
}
for k, v := range tests {
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: v.podAnnotations,
},
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
SeccompProfile: v.seccompProfile,
},
},
}
s := NewStrategy(v.pspAnnotations)
errs := s.ValidatePod(pod)
@ -231,9 +286,12 @@ func TestValidatePod(t *testing.T) {
}
func TestValidateContainer(t *testing.T) {
foo := "foo"
bar := "bar"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedError string
}{
"no pod annotations, required profiles": {
@ -293,6 +351,22 @@ func TestValidateContainer(t *testing.T) {
},
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
},
"valid container field and no annotation, required profiles": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
"invalid container field and no annotation, required profiles": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &bar,
},
expectedError: "Forbidden: localhost/bar is not an allowed seccomp profile. Valid values are localhost/foo",
},
}
for k, v := range tests {
pod := &api.Pod{
@ -302,6 +376,9 @@ func TestValidateContainer(t *testing.T) {
}
container := &api.Container{
Name: "container",
SecurityContext: &api.SecurityContext{
SeccompProfile: v.seccompProfile,
},
}
s := NewStrategy(v.pspAnnotations)

View File

@ -6,6 +6,7 @@ rules:
- k8s.io/kubernetes/pkg/api/v1/pod
- k8s.io/kubernetes/pkg/api/v1/resource
- k8s.io/kubernetes/pkg/api/v1/service
- k8s.io/kubernetes/pkg/api/pod
- k8s.io/kubernetes/pkg/apis/apps
- k8s.io/kubernetes/pkg/apis/apps/validation
- k8s.io/kubernetes/pkg/apis/autoscaling