mirror of https://github.com/k3s-io/k3s
Merge pull request #30183 from timstclair/aa-psp
Automatic merge from submit-queue AppArmor PodSecurityPolicy support Implements the AppArmor PodSecurityPolicy support based on the alpha API proposed [here](https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/apparmor.md#pod-security-policy) This implementation deviates from the original proposal in one way: it adds a separate option for specifying a default profile: ``` apparmor.security.alpha.kubernetes.io/defaultProfileName ``` This has several advantages over the original proposal: - The default is explicit, rather than implicit on the ordering - The default can be specified without constraining the allowed profiles - The allowed profiles can be restricted without specifying a default (requires every pod to explicitly set a profile) The E2E cluster does not currently enable the PodSecurityPolicy, so I will submit E2E tests in a separate PR. /cc @dchen1107 @pweil- @sttts @jfrazelle @Amey-Dpull/6/head
commit
0b5547f462
|
@ -206,4 +206,5 @@ pkg/util/maps
|
|||
pkg/volume/quobyte
|
||||
test/integration/discoverysummarizer
|
||||
test/integration/examples
|
||||
test/integration/federation
|
||||
test/integration/federation
|
||||
pkg/security/podsecuritypolicy/apparmor
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
apivalidation "k8s.io/kubernetes/pkg/api/validation"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/pkg/util/intstr"
|
||||
"k8s.io/kubernetes/pkg/util/sets"
|
||||
|
@ -552,6 +553,7 @@ var ValidatePodSecurityPolicyName = apivalidation.NameIsDNSSubdomain
|
|||
func ValidatePodSecurityPolicy(psp *extensions.PodSecurityPolicy) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&psp.ObjectMeta, false, ValidatePodSecurityPolicyName, field.NewPath("metadata"))...)
|
||||
allErrs = append(allErrs, ValidatePodSecurityPolicySpecificAnnotations(psp.Annotations, field.NewPath("metadata").Child("annotations"))...)
|
||||
allErrs = append(allErrs, ValidatePodSecurityPolicySpec(&psp.Spec, field.NewPath("spec"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
@ -570,6 +572,23 @@ func ValidatePodSecurityPolicySpec(spec *extensions.PodSecurityPolicySpec, fldPa
|
|||
return allErrs
|
||||
}
|
||||
|
||||
func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if p := annotations[apparmor.DefaultProfileAnnotationKey]; p != "" {
|
||||
if err := apparmor.ValidateProfileFormat(p); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(apparmor.DefaultProfileAnnotationKey), p, err.Error()))
|
||||
}
|
||||
}
|
||||
if allowed := annotations[apparmor.AllowedProfilesAnnotationKey]; allowed != "" {
|
||||
for _, p := range strings.Split(allowed, ",") {
|
||||
if err := apparmor.ValidateProfileFormat(p); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(apparmor.AllowedProfilesAnnotationKey), allowed, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validatePSPSELinux validates the SELinux fields of PodSecurityPolicy.
|
||||
func validatePSPSELinux(fldPath *field.Path, seLinux *extensions.SELinuxStrategyOptions) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/pkg/util/intstr"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
|
@ -1510,7 +1511,9 @@ func TestValidateReplicaSet(t *testing.T) {
|
|||
func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
validPSP := func() *extensions.PodSecurityPolicy {
|
||||
return &extensions.PodSecurityPolicy{
|
||||
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: extensions.PodSecurityPolicySpec{
|
||||
SELinux: extensions.SELinuxStrategyOptions{
|
||||
Rule: extensions.SELinuxStrategyRunAsAny,
|
||||
|
@ -1584,6 +1587,15 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
|||
allowedCapListedInRequiredDrop.Spec.RequiredDropCapabilities = []api.Capability{"foo"}
|
||||
allowedCapListedInRequiredDrop.Spec.AllowedCapabilities = []api.Capability{"foo"}
|
||||
|
||||
invalidAppArmorDefault := validPSP()
|
||||
invalidAppArmorDefault.Annotations = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: "not-good",
|
||||
}
|
||||
invalidAppArmorAllowed := validPSP()
|
||||
invalidAppArmorAllowed.Annotations = map[string]string{
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + ",not-good",
|
||||
}
|
||||
|
||||
errorCases := map[string]struct {
|
||||
psp *extensions.PodSecurityPolicy
|
||||
errorType field.ErrorType
|
||||
|
@ -1664,6 +1676,16 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
|||
errorType: field.ErrorTypeInvalid,
|
||||
errorDetail: "capability is listed in allowedCapabilities and requiredDropCapabilities",
|
||||
},
|
||||
"invalid AppArmor default profile": {
|
||||
psp: invalidAppArmorDefault,
|
||||
errorType: field.ErrorTypeInvalid,
|
||||
errorDetail: "invalid AppArmor profile name: \"not-good\"",
|
||||
},
|
||||
"invalid AppArmor allowed profile": {
|
||||
psp: invalidAppArmorAllowed,
|
||||
errorType: field.ErrorTypeInvalid,
|
||||
errorDetail: "invalid AppArmor profile name: \"not-good\"",
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range errorCases {
|
||||
|
@ -1700,6 +1722,12 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
|||
caseInsensitiveAllowedDrop.Spec.RequiredDropCapabilities = []api.Capability{"FOO"}
|
||||
caseInsensitiveAllowedDrop.Spec.AllowedCapabilities = []api.Capability{"foo"}
|
||||
|
||||
validAppArmor := validPSP()
|
||||
validAppArmor.Annotations = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
|
||||
successCases := map[string]struct {
|
||||
psp *extensions.PodSecurityPolicy
|
||||
}{
|
||||
|
@ -1718,6 +1746,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
|||
"comparison for allowed -> drop is case sensitive": {
|
||||
psp: caseInsensitiveAllowedDrop,
|
||||
},
|
||||
"valid AppArmor annotations": {
|
||||
psp: validAppArmor,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range successCases {
|
||||
|
|
|
@ -26,6 +26,10 @@ import (
|
|||
const (
|
||||
// The prefix to an annotation key specifying a container profile.
|
||||
ContainerAnnotationKeyPrefix = "container.apparmor.security.alpha.kubernetes.io/"
|
||||
// The annotation key specifying the default AppArmor profile.
|
||||
DefaultProfileAnnotationKey = "apparmor.security.alpha.kubernetes.io/defaultProfileName"
|
||||
// The annotation key specifying the allowed AppArmor profiles.
|
||||
AllowedProfilesAnnotationKey = "apparmor.security.alpha.kubernetes.io/allowedProfileNames"
|
||||
|
||||
// The profile specifying the runtime default.
|
||||
ProfileRuntimeDefault = "runtime/default"
|
||||
|
@ -47,3 +51,12 @@ func isRequired(pod *api.Pod) bool {
|
|||
func GetProfileName(pod *api.Pod, containerName string) string {
|
||||
return pod.Annotations[ContainerAnnotationKeyPrefix+containerName]
|
||||
}
|
||||
|
||||
// Sets the name of the profile to use with the container.
|
||||
func SetProfileName(pod *api.Pod, containerName, profileName string) error {
|
||||
if pod.Annotations == nil {
|
||||
pod.Annotations = map[string]string{}
|
||||
}
|
||||
pod.Annotations[ContainerAnnotationKeyPrefix+containerName] = profileName
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2016 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 apparmor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
"k8s.io/kubernetes/pkg/util/maps"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Strategy defines the interface for all AppArmor constraint strategies.
|
||||
type Strategy interface {
|
||||
// Generate updates the annotations based on constraint rules. The updates are applied to a copy
|
||||
// of the annotations, and returned.
|
||||
Generate(annotations map[string]string, container *api.Container) (map[string]string, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(pod *api.Pod, container *api.Container) field.ErrorList
|
||||
}
|
||||
|
||||
type strategy struct {
|
||||
defaultProfile string
|
||||
allowedProfiles map[string]bool
|
||||
// For printing error messages (preserves order).
|
||||
allowedProfilesString string
|
||||
}
|
||||
|
||||
var _ Strategy = &strategy{}
|
||||
|
||||
// NewStrategy creates a new strategy that enforces AppArmor profile constraints.
|
||||
func NewStrategy(pspAnnotations map[string]string) Strategy {
|
||||
var allowedProfiles map[string]bool
|
||||
if allowed, ok := pspAnnotations[apparmor.AllowedProfilesAnnotationKey]; ok {
|
||||
profiles := strings.Split(allowed, ",")
|
||||
allowedProfiles = make(map[string]bool, len(profiles))
|
||||
for _, p := range profiles {
|
||||
allowedProfiles[p] = true
|
||||
}
|
||||
}
|
||||
return &strategy{
|
||||
defaultProfile: pspAnnotations[apparmor.DefaultProfileAnnotationKey],
|
||||
allowedProfiles: allowedProfiles,
|
||||
allowedProfilesString: pspAnnotations[apparmor.AllowedProfilesAnnotationKey],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *strategy) Generate(annotations map[string]string, container *api.Container) (map[string]string, error) {
|
||||
copy := maps.CopySS(annotations)
|
||||
|
||||
if annotations[apparmor.ContainerAnnotationKeyPrefix+container.Name] != "" {
|
||||
// Profile already set, nothing to do.
|
||||
return copy, nil
|
||||
}
|
||||
|
||||
if s.defaultProfile == "" {
|
||||
// No default set.
|
||||
return copy, nil
|
||||
}
|
||||
|
||||
if copy == nil {
|
||||
copy = map[string]string{}
|
||||
}
|
||||
// Add the default profile.
|
||||
copy[apparmor.ContainerAnnotationKeyPrefix+container.Name] = s.defaultProfile
|
||||
|
||||
return copy, nil
|
||||
}
|
||||
|
||||
func (s *strategy) Validate(pod *api.Pod, container *api.Container) field.ErrorList {
|
||||
if s.allowedProfiles == nil {
|
||||
// Unrestricted: allow all.
|
||||
return nil
|
||||
}
|
||||
|
||||
allErrs := field.ErrorList{}
|
||||
fieldPath := field.NewPath("pod", "metadata", "annotations").Key(apparmor.ContainerAnnotationKeyPrefix + container.Name)
|
||||
|
||||
profile := apparmor.GetProfileName(pod, container.Name)
|
||||
if profile == "" {
|
||||
if len(s.allowedProfiles) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fieldPath, "AppArmor profile must be set"))
|
||||
return allErrs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !s.allowedProfiles[profile] {
|
||||
msg := fmt.Sprintf("%s is not an allowed profile. Allowed values: %q", profile, s.allowedProfilesString)
|
||||
allErrs = append(allErrs, field.Forbidden(fieldPath, msg))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2016 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 apparmor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
"k8s.io/kubernetes/pkg/util/maps"
|
||||
)
|
||||
|
||||
const (
|
||||
containerName = "test-c"
|
||||
)
|
||||
|
||||
var (
|
||||
withoutAppArmor = map[string]string{"foo": "bar"}
|
||||
withDefault = map[string]string{
|
||||
"foo": "bar",
|
||||
apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
withLocal = map[string]string{
|
||||
"foo": "bar",
|
||||
apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
withDisallowed = map[string]string{
|
||||
"foo": "bar",
|
||||
apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileNamePrefix + "bad",
|
||||
}
|
||||
|
||||
noAppArmor = map[string]string{"foo": "bar"}
|
||||
unconstrainedWithDefault = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
constrained = map[string]string{
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," +
|
||||
apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
constrainedWithDefault = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," +
|
||||
apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
|
||||
container = api.Container{
|
||||
Name: containerName,
|
||||
Image: "busybox",
|
||||
}
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
type testcase struct {
|
||||
pspAnnotations map[string]string
|
||||
podAnnotations map[string]string
|
||||
expected map[string]string
|
||||
}
|
||||
tests := []testcase{{
|
||||
pspAnnotations: noAppArmor,
|
||||
podAnnotations: withoutAppArmor,
|
||||
expected: withoutAppArmor,
|
||||
}, {
|
||||
pspAnnotations: unconstrainedWithDefault,
|
||||
podAnnotations: withoutAppArmor,
|
||||
expected: withDefault,
|
||||
}, {
|
||||
pspAnnotations: constrained,
|
||||
podAnnotations: withoutAppArmor,
|
||||
expected: withoutAppArmor,
|
||||
}, {
|
||||
pspAnnotations: constrainedWithDefault,
|
||||
podAnnotations: withoutAppArmor,
|
||||
expected: withDefault,
|
||||
}}
|
||||
|
||||
// Add unchanging permutations.
|
||||
for _, podAnnotations := range []map[string]string{withDefault, withLocal} {
|
||||
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} {
|
||||
tests = append(tests, testcase{
|
||||
pspAnnotations: pspAnnotations,
|
||||
podAnnotations: podAnnotations,
|
||||
expected: podAnnotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
s := NewStrategy(test.pspAnnotations)
|
||||
msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)}
|
||||
actual, err := s.Generate(test.podAnnotations, &container)
|
||||
assert.NoError(t, err, msgAndArgs...)
|
||||
assert.Equal(t, test.expected, actual, msgAndArgs...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
type testcase struct {
|
||||
pspAnnotations map[string]string
|
||||
podAnnotations map[string]string
|
||||
expectErr bool
|
||||
}
|
||||
tests := []testcase{}
|
||||
// Valid combinations
|
||||
for _, podAnnotations := range []map[string]string{withDefault, withLocal} {
|
||||
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} {
|
||||
tests = append(tests, testcase{
|
||||
pspAnnotations: pspAnnotations,
|
||||
podAnnotations: podAnnotations,
|
||||
expectErr: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} {
|
||||
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault} {
|
||||
tests = append(tests, testcase{
|
||||
pspAnnotations: pspAnnotations,
|
||||
podAnnotations: podAnnotations,
|
||||
expectErr: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Invalid combinations
|
||||
for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} {
|
||||
for _, pspAnnotations := range []map[string]string{constrained, constrainedWithDefault} {
|
||||
tests = append(tests, testcase{
|
||||
pspAnnotations: pspAnnotations,
|
||||
podAnnotations: podAnnotations,
|
||||
expectErr: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
s := NewStrategy(test.pspAnnotations)
|
||||
pod, container := makeTestPod(test.podAnnotations)
|
||||
msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)}
|
||||
errs := s.Validate(pod, container)
|
||||
if test.expectErr {
|
||||
assert.Len(t, errs, 1, msgAndArgs...)
|
||||
} else {
|
||||
assert.Len(t, errs, 0, msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestPod(annotations map[string]string) (*api.Pod, *api.Container) {
|
||||
return &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Annotations: maps.CopySS(annotations),
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{container},
|
||||
},
|
||||
}, &container
|
||||
}
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
|
@ -49,6 +50,11 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli
|
|||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
appArmorStrat, err := createAppArmorStrategy(psp)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
fsGroupStrat, err := createFSGroupStrategy(&psp.Spec.FSGroup)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
|
@ -71,6 +77,7 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli
|
|||
strategies := &ProviderStrategies{
|
||||
RunAsUserStrategy: userStrat,
|
||||
SELinuxStrategy: seLinuxStrat,
|
||||
AppArmorStrategy: appArmorStrat,
|
||||
FSGroupStrategy: fsGroupStrat,
|
||||
SupplementalGroupStrategy: supGroupStrat,
|
||||
CapabilitiesStrategy: capStrat,
|
||||
|
@ -105,6 +112,11 @@ func createSELinuxStrategy(opts *extensions.SELinuxStrategyOptions) (selinux.SEL
|
|||
}
|
||||
}
|
||||
|
||||
// createAppArmorStrategy creates a new AppArmor strategy.
|
||||
func createAppArmorStrategy(psp *extensions.PodSecurityPolicy) (apparmor.Strategy, error) {
|
||||
return apparmor.NewStrategy(psp.Annotations), nil
|
||||
}
|
||||
|
||||
// createFSGroupStrategy creates a new fsgroup strategy
|
||||
func createFSGroupStrategy(opts *extensions.FSGroupStrategyOptions) (group.GroupStrategy, error) {
|
||||
switch opts.Rule {
|
||||
|
|
|
@ -139,6 +139,11 @@ func (s *simpleProvider) CreateContainerSecurityContext(pod *api.Pod, container
|
|||
sc.SELinuxOptions = seLinux
|
||||
}
|
||||
|
||||
annotations, err := s.strategies.AppArmorStrategy.Generate(annotations, container)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if sc.Privileged == nil {
|
||||
priv := false
|
||||
sc.Privileged = &priv
|
||||
|
@ -220,6 +225,7 @@ func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, containe
|
|||
sc := container.SecurityContext
|
||||
allErrs = append(allErrs, s.strategies.RunAsUserStrategy.Validate(pod, container)...)
|
||||
allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(pod, container)...)
|
||||
allErrs = append(allErrs, s.strategies.AppArmorStrategy.Validate(pod, container)...)
|
||||
|
||||
if !s.psp.Spec.Privileged && *sc.Privileged {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("privileged"), *sc.Privileged, "Privileged containers are not allowed"))
|
||||
|
|
|
@ -22,13 +22,18 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/pkg/util/diff"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
const defaultContainerName = "test-c"
|
||||
|
||||
func TestCreatePodSecurityContextNonmutating(t *testing.T) {
|
||||
// Create a pod with a security context that needs filling in
|
||||
createPod := func() *api.Pod {
|
||||
|
@ -303,6 +308,14 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) {
|
|||
Level: "bar",
|
||||
}
|
||||
|
||||
failNilAppArmorPod := defaultPod()
|
||||
failInvalidAppArmorPod := defaultPod()
|
||||
apparmor.SetProfileName(failInvalidAppArmorPod, defaultContainerName, apparmor.ProfileNamePrefix+"foo")
|
||||
failAppArmorPSP := defaultPSP()
|
||||
failAppArmorPSP.Annotations = map[string]string{
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
|
||||
failPrivPod := defaultPod()
|
||||
var priv bool = true
|
||||
failPrivPod.Spec.Containers[0].SecurityContext.Privileged = &priv
|
||||
|
@ -347,6 +360,16 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) {
|
|||
psp: failSELinuxPSP,
|
||||
expectedError: "does not match required level",
|
||||
},
|
||||
"failNilAppArmor": {
|
||||
pod: failNilAppArmorPod,
|
||||
psp: failAppArmorPSP,
|
||||
expectedError: "AppArmor profile must be set",
|
||||
},
|
||||
"failInvalidAppArmor": {
|
||||
pod: failInvalidAppArmorPod,
|
||||
psp: failAppArmorPSP,
|
||||
expectedError: "localhost/foo is not an allowed profile. Allowed values: \"runtime/default\"",
|
||||
},
|
||||
"failPrivPSP": {
|
||||
pod: failPrivPod,
|
||||
psp: defaultPSP(),
|
||||
|
@ -499,6 +522,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
SecurityContext: &api.PodSecurityContext{},
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: defaultContainerName,
|
||||
SecurityContext: &api.SecurityContext{
|
||||
// expected to be set by defaulting mechanisms
|
||||
Privileged: ¬Priv,
|
||||
|
@ -510,7 +534,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// fail user strat
|
||||
// success user strat
|
||||
userPSP := defaultPSP()
|
||||
var uid int64 = 999
|
||||
userPSP.Spec.RunAsUser = extensions.RunAsUserStrategyOptions{
|
||||
|
@ -520,7 +544,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
userPod := defaultPod()
|
||||
userPod.Spec.Containers[0].SecurityContext.RunAsUser = &uid
|
||||
|
||||
// fail selinux strat
|
||||
// success selinux strat
|
||||
seLinuxPSP := defaultPSP()
|
||||
seLinuxPSP.Spec.SELinux = extensions.SELinuxStrategyOptions{
|
||||
Rule: extensions.SELinuxStrategyMustRunAs,
|
||||
|
@ -533,6 +557,13 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
Level: "foo",
|
||||
}
|
||||
|
||||
appArmorPSP := defaultPSP()
|
||||
appArmorPSP.Annotations = map[string]string{
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
appArmorPod := defaultPod()
|
||||
apparmor.SetProfileName(appArmorPod, defaultContainerName, apparmor.ProfileRuntimeDefault)
|
||||
|
||||
privPSP := defaultPSP()
|
||||
privPSP.Spec.Privileged = true
|
||||
privPod := defaultPod()
|
||||
|
@ -591,6 +622,10 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
pod: seLinuxPod,
|
||||
psp: seLinuxPSP,
|
||||
},
|
||||
"pass AppArmor allowed profiles": {
|
||||
pod: appArmorPod,
|
||||
psp: appArmorPSP,
|
||||
},
|
||||
"pass priv validating PSP": {
|
||||
pod: privPod,
|
||||
psp: privPSP,
|
||||
|
@ -632,7 +667,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
|
|||
}
|
||||
errs := provider.ValidateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0], field.NewPath(""))
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("%s expected validation pass but received errors %v", k, errs)
|
||||
t.Errorf("%s expected validation pass but received errors %v\n%s", k, errs, spew.Sdump(v.pod.ObjectMeta))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -748,6 +783,7 @@ func defaultPod() *api.Pod {
|
|||
},
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: defaultContainerName,
|
||||
SecurityContext: &api.SecurityContext{
|
||||
// expected to be set by defaulting mechanisms
|
||||
Privileged: ¬Priv,
|
||||
|
|
|
@ -19,6 +19,7 @@ package podsecuritypolicy
|
|||
import (
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
|
@ -58,6 +59,7 @@ type StrategyFactory interface {
|
|||
type ProviderStrategies struct {
|
||||
RunAsUserStrategy user.RunAsUserStrategy
|
||||
SELinuxStrategy selinux.SELinuxStrategy
|
||||
AppArmorStrategy apparmor.Strategy
|
||||
FSGroupStrategy group.GroupStrategy
|
||||
SupplementalGroupStrategy group.GroupStrategy
|
||||
CapabilitiesStrategy capabilities.Strategy
|
||||
|
|
|
@ -22,6 +22,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
kadmission "k8s.io/kubernetes/pkg/admission"
|
||||
kapi "k8s.io/kubernetes/pkg/api"
|
||||
extensions "k8s.io/kubernetes/pkg/apis/extensions"
|
||||
|
@ -29,11 +31,14 @@ import (
|
|||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
clientsetfake "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
diff "k8s.io/kubernetes/pkg/util/diff"
|
||||
)
|
||||
|
||||
const defaultContainerName = "test-c"
|
||||
|
||||
func NewTestAdmission(store cache.Store, kclient clientset.Interface) kadmission.Interface {
|
||||
return &podSecurityPolicyPlugin{
|
||||
Handler: kadmission.NewHandler(kadmission.Create),
|
||||
|
@ -610,6 +615,85 @@ func TestAdmitSELinux(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAdmitAppArmor(t *testing.T) {
|
||||
createPodWithAppArmor := func(profile string) *kapi.Pod {
|
||||
pod := goodPod()
|
||||
apparmor.SetProfileName(pod, defaultContainerName, profile)
|
||||
return pod
|
||||
}
|
||||
|
||||
unconstrainedPSP := restrictivePSP()
|
||||
defaultedPSP := restrictivePSP()
|
||||
defaultedPSP.Annotations = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
appArmorPSP := restrictivePSP()
|
||||
appArmorPSP.Annotations = map[string]string{
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
}
|
||||
appArmorDefaultPSP := restrictivePSP()
|
||||
appArmorDefaultPSP.Annotations = map[string]string{
|
||||
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
pod *kapi.Pod
|
||||
psp *extensions.PodSecurityPolicy
|
||||
shouldPass bool
|
||||
expectedProfile string
|
||||
}{
|
||||
"unconstrained with no profile": {
|
||||
pod: goodPod(),
|
||||
psp: unconstrainedPSP,
|
||||
shouldPass: true,
|
||||
expectedProfile: "",
|
||||
},
|
||||
"unconstrained with profile": {
|
||||
pod: createPodWithAppArmor(apparmor.ProfileRuntimeDefault),
|
||||
psp: unconstrainedPSP,
|
||||
shouldPass: true,
|
||||
expectedProfile: apparmor.ProfileRuntimeDefault,
|
||||
},
|
||||
"unconstrained with default profile": {
|
||||
pod: goodPod(),
|
||||
psp: defaultedPSP,
|
||||
shouldPass: true,
|
||||
expectedProfile: apparmor.ProfileRuntimeDefault,
|
||||
},
|
||||
"AppArmor enforced with no profile": {
|
||||
pod: goodPod(),
|
||||
psp: appArmorPSP,
|
||||
shouldPass: false,
|
||||
},
|
||||
"AppArmor enforced with default profile": {
|
||||
pod: goodPod(),
|
||||
psp: appArmorDefaultPSP,
|
||||
shouldPass: true,
|
||||
expectedProfile: apparmor.ProfileRuntimeDefault,
|
||||
},
|
||||
"AppArmor enforced with good profile": {
|
||||
pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "foo"),
|
||||
psp: appArmorDefaultPSP,
|
||||
shouldPass: true,
|
||||
expectedProfile: apparmor.ProfileNamePrefix + "foo",
|
||||
},
|
||||
"AppArmor enforced with local profile": {
|
||||
pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "bar"),
|
||||
psp: appArmorPSP,
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
testPSPAdmit(k, []*extensions.PodSecurityPolicy{v.psp}, v.pod, v.shouldPass, v.psp.Name, t)
|
||||
|
||||
if v.shouldPass {
|
||||
assert.Equal(t, v.expectedProfile, apparmor.GetProfileName(v.pod, defaultContainerName), k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmitRunAsUser(t *testing.T) {
|
||||
createPodWithRunAsUser := func(user int64) *kapi.Pod {
|
||||
pod := goodPod()
|
||||
|
@ -1212,6 +1296,7 @@ func goodPod() *kapi.Pod {
|
|||
SecurityContext: &kapi.PodSecurityContext{},
|
||||
Containers: []kapi.Container{
|
||||
{
|
||||
Name: defaultContainerName,
|
||||
SecurityContext: &kapi.SecurityContext{},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue