Skip to content

Commit

Permalink
Implement PodSecurityPolicy enforcement for seccomp GA
Browse files Browse the repository at this point in the history
This implements the necessary pieced for the PodSecurityPolicy
enforcement like described in the appropriate KEP section:

https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/20190717-seccomp-ga.md#podsecuritypolicy-enforcement

Signed-off-by: Sascha Grunert <sgrunert@suse.com>
  • Loading branch information
saschagrunert committed Jul 10, 2020
1 parent 26f0227 commit 96fb83c
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 60 deletions.
55 changes: 55 additions & 0 deletions pkg/api/pod/util.go
Expand Up @@ -734,3 +734,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
}
63 changes: 4 additions & 59 deletions pkg/registry/core/pod/strategy.go
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
2 changes: 2 additions & 0 deletions pkg/security/podsecuritypolicy/seccomp/BUILD
Expand Up @@ -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",
],
Expand All @@ -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",
],
)
Expand Down
19 changes: 19 additions & 0 deletions pkg/security/podsecuritypolicy/seccomp/strategy.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}

Expand All @@ -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"))
Expand Down Expand Up @@ -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]
}
79 changes: 78 additions & 1 deletion pkg/security/podsecuritypolicy/seccomp/strategy_test.go
Expand Up @@ -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"
)
Expand All @@ -41,6 +42,9 @@ var (
allowSpecific = map[string]string{
AllowedProfilesAnnotationKey: "foo",
}
allowSpecificLocalhost = map[string]string{
AllowedProfilesAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + "foo",
}
)

func TestNewStrategy(t *testing.T) {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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
Expand All @@ -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": {
Expand Down Expand Up @@ -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)
Expand All @@ -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": {
Expand Down Expand Up @@ -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{
Expand All @@ -302,6 +376,9 @@ func TestValidateContainer(t *testing.T) {
}
container := &api.Container{
Name: "container",
SecurityContext: &api.SecurityContext{
SeccompProfile: v.seccompProfile,
},
}

s := NewStrategy(v.pspAnnotations)
Expand Down
1 change: 1 addition & 0 deletions test/e2e/framework/.import-restrictions
Expand Up @@ -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
Expand Down

0 comments on commit 96fb83c

Please sign in to comment.