Remove PodSecurityPolicy admission plugin
This commit is contained in:
parent
889e60ab33
commit
410ac59c0d
@ -33,8 +33,15 @@ import (
|
||||
core "k8s.io/kubernetes/pkg/apis/core"
|
||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
"k8s.io/kubernetes/pkg/apis/policy"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllowAny is the wildcard used to allow any profile.
|
||||
seccompAllowAny = "*"
|
||||
// DefaultProfileAnnotationKey specifies the default seccomp profile.
|
||||
seccompDefaultProfileAnnotationKey = "seccomp.security.alpha.kubernetes.io/defaultProfileName"
|
||||
// AllowedProfilesAnnotationKey specifies the allowed seccomp profiles.
|
||||
seccompAllowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
|
||||
)
|
||||
|
||||
// ValidatePodDisruptionBudget validates a PodDisruptionBudget and returns an ErrorList
|
||||
@ -149,15 +156,15 @@ func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string,
|
||||
}
|
||||
}
|
||||
|
||||
if p := annotations[seccomp.DefaultProfileAnnotationKey]; p != "" {
|
||||
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.DefaultProfileAnnotationKey))...)
|
||||
if p := annotations[seccompDefaultProfileAnnotationKey]; p != "" {
|
||||
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccompDefaultProfileAnnotationKey))...)
|
||||
}
|
||||
if allowed := annotations[seccomp.AllowedProfilesAnnotationKey]; allowed != "" {
|
||||
if allowed := annotations[seccompAllowedProfilesAnnotationKey]; allowed != "" {
|
||||
for _, p := range strings.Split(allowed, ",") {
|
||||
if p == seccomp.AllowAny {
|
||||
if p == seccompAllowAny {
|
||||
continue
|
||||
}
|
||||
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.AllowedProfilesAnnotationKey))...)
|
||||
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccompAllowedProfilesAnnotationKey))...)
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
@ -321,7 +328,7 @@ func validatePSPSupplementalGroup(fldPath *field.Path, groupOptions *policy.Supp
|
||||
// validatePodSecurityPolicyVolumes validates the volume fields of PodSecurityPolicy.
|
||||
func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []policy.FSType) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
allowed := psputil.GetAllFSTypesAsSet()
|
||||
allowed := getAllFSTypesAsSet()
|
||||
// add in the * value since that is a pseudo type that is not included by default
|
||||
allowed.Insert(string(policy.All))
|
||||
for _, v := range volumes {
|
||||
@ -332,6 +339,44 @@ func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []policy.FSTy
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// getAllFSTypesAsSet returns all actual volume types, regardless
|
||||
// of feature gates. The special policy.All pseudo type is not included.
|
||||
func getAllFSTypesAsSet() sets.String {
|
||||
fstypes := sets.NewString()
|
||||
fstypes.Insert(
|
||||
string(policy.HostPath),
|
||||
string(policy.AzureFile),
|
||||
string(policy.Flocker),
|
||||
string(policy.FlexVolume),
|
||||
string(policy.EmptyDir),
|
||||
string(policy.GCEPersistentDisk),
|
||||
string(policy.AWSElasticBlockStore),
|
||||
string(policy.GitRepo),
|
||||
string(policy.Secret),
|
||||
string(policy.NFS),
|
||||
string(policy.ISCSI),
|
||||
string(policy.Glusterfs),
|
||||
string(policy.PersistentVolumeClaim),
|
||||
string(policy.RBD),
|
||||
string(policy.Cinder),
|
||||
string(policy.CephFS),
|
||||
string(policy.DownwardAPI),
|
||||
string(policy.FC),
|
||||
string(policy.ConfigMap),
|
||||
string(policy.VsphereVolume),
|
||||
string(policy.Quobyte),
|
||||
string(policy.AzureDisk),
|
||||
string(policy.PhotonPersistentDisk),
|
||||
string(policy.StorageOS),
|
||||
string(policy.Projected),
|
||||
string(policy.PortworxVolume),
|
||||
string(policy.ScaleIO),
|
||||
string(policy.CSI),
|
||||
string(policy.Ephemeral),
|
||||
)
|
||||
return fstypes
|
||||
}
|
||||
|
||||
// validatePSPDefaultAllowPrivilegeEscalation validates the DefaultAllowPrivilegeEscalation field against the AllowPrivilegeEscalation field of a PodSecurityPolicy.
|
||||
func validatePSPDefaultAllowPrivilegeEscalation(fldPath *field.Path, defaultAllowPrivilegeEscalation *bool, allowPrivilegeEscalation bool) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
@ -30,8 +30,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/policy"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
@ -373,15 +371,15 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
|
||||
invalidSeccompDefault := validPSP()
|
||||
invalidSeccompDefault.Annotations = map[string]string{
|
||||
seccomp.DefaultProfileAnnotationKey: "not-good",
|
||||
seccompDefaultProfileAnnotationKey: "not-good",
|
||||
}
|
||||
invalidSeccompAllowAnyDefault := validPSP()
|
||||
invalidSeccompAllowAnyDefault.Annotations = map[string]string{
|
||||
seccomp.DefaultProfileAnnotationKey: "*",
|
||||
seccompDefaultProfileAnnotationKey: "*",
|
||||
}
|
||||
invalidSeccompAllowed := validPSP()
|
||||
invalidSeccompAllowed.Annotations = map[string]string{
|
||||
seccomp.AllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",not-good",
|
||||
seccompAllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",not-good",
|
||||
}
|
||||
|
||||
invalidAllowedHostPathMissingPath := validPSP()
|
||||
@ -660,8 +658,8 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
|
||||
validSeccomp := validPSP()
|
||||
validSeccomp.Annotations = map[string]string{
|
||||
seccomp.DefaultProfileAnnotationKey: api.SeccompProfileRuntimeDefault,
|
||||
seccomp.AllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",unconfined,localhost/foo,*",
|
||||
seccompDefaultProfileAnnotationKey: api.SeccompProfileRuntimeDefault,
|
||||
seccompAllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",unconfined,localhost/foo,*",
|
||||
}
|
||||
|
||||
validDefaultAllowPrivilegeEscalation := validPSP()
|
||||
@ -779,7 +777,7 @@ func TestValidatePSPVolumes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
volumes := psputil.GetAllFSTypesAsSet()
|
||||
volumes := getAllFSTypesAsSet()
|
||||
// add in the * value since that is a pseudo type that is not included by default
|
||||
volumes.Insert(string(policy.All))
|
||||
|
||||
|
@ -45,7 +45,6 @@ import (
|
||||
podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label"
|
||||
@ -75,8 +74,7 @@ var AllOrderedPlugins = []string{
|
||||
nodetaint.PluginName, // TaintNodesByCondition
|
||||
alwayspullimages.PluginName, // AlwaysPullImages
|
||||
imagepolicy.PluginName, // ImagePolicyWebhook
|
||||
podsecurity.PluginName, // PodSecurity - before PodSecurityPolicy so audit/warn get exercised even if PodSecurityPolicy denies
|
||||
podsecuritypolicy.PluginName, // PodSecurityPolicy
|
||||
podsecurity.PluginName, // PodSecurity
|
||||
podnodeselector.PluginName, // PodNodeSelector
|
||||
podpriority.PluginName, // Priority
|
||||
defaulttolerationseconds.PluginName, // DefaultTolerationSeconds
|
||||
@ -129,7 +127,6 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
runtimeclass.Register(plugins)
|
||||
resourcequota.Register(plugins)
|
||||
podsecurity.Register(plugins)
|
||||
podsecuritypolicy.Register(plugins)
|
||||
podpriority.Register(plugins)
|
||||
scdeny.Register(plugins)
|
||||
serviceaccount.Register(plugins)
|
||||
|
@ -108,7 +108,6 @@ import (
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/sliceutils"
|
||||
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
sysctlallowlist "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
"k8s.io/kubernetes/pkg/util/oom"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
"k8s.io/kubernetes/pkg/volume/csi"
|
||||
@ -776,7 +775,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
||||
|
||||
// Safe, allowed sysctls can always be used as unsafe sysctls in the spec.
|
||||
// Hence, we concatenate those two lists.
|
||||
safeAndUnsafeSysctls := append(sysctlallowlist.SafeSysctlAllowlist(), allowedUnsafeSysctls...)
|
||||
safeAndUnsafeSysctls := append(sysctl.SafeSysctlAllowlist(), allowedUnsafeSysctls...)
|
||||
sysctlsAllowlist, err := sysctl.NewAllowlist(safeAndUnsafeSysctls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -18,8 +18,6 @@ package sysctl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
)
|
||||
|
||||
func TestNewAllowlist(t *testing.T) {
|
||||
@ -37,7 +35,7 @@ func TestNewAllowlist(t *testing.T) {
|
||||
{sysctls: []string{"net.*/foo"}, err: true},
|
||||
{sysctls: []string{"foo"}, err: true},
|
||||
} {
|
||||
_, err := NewAllowlist(append(sysctl.SafeSysctlAllowlist(), test.sysctls...))
|
||||
_, err := NewAllowlist(append(SafeSysctlAllowlist(), test.sysctls...))
|
||||
if test.err && err == nil {
|
||||
t.Errorf("expected an error creating a allowlist for %v", test.sysctls)
|
||||
} else if !test.err && err != nil {
|
||||
@ -69,7 +67,7 @@ func TestAllowlist(t *testing.T) {
|
||||
{sysctl: "kernel.sem", hostIPC: true},
|
||||
}
|
||||
|
||||
w, err := NewAllowlist(append(sysctl.SafeSysctlAllowlist(), "kernel.msg*", "kernel.sem"))
|
||||
w, err := NewAllowlist(append(SafeSysctlAllowlist(), "kernel.msg*", "kernel.sem"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create allowlist: %v", err)
|
||||
}
|
||||
|
@ -16,13 +16,17 @@ limitations under the License.
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// SysctlsStrategy defines the interface for all sysctl strategies.
|
||||
type SysctlsStrategy interface {
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(pod *api.Pod) field.ErrorList
|
||||
// SafeSysctlAllowlist returns the allowlist of safe sysctls and safe sysctl patterns (ending in *).
|
||||
//
|
||||
// A sysctl is called safe iff
|
||||
// - it is namespaced in the container or the pod
|
||||
// - it is isolated, i.e. has no influence on any other pod on the same node.
|
||||
func SafeSysctlAllowlist() []string {
|
||||
return []string{
|
||||
"kernel.shm_rmid_forced",
|
||||
"net.ipv4.ip_local_port_range",
|
||||
"net.ipv4.tcp_syncookies",
|
||||
"net.ipv4.ping_group_range",
|
||||
"net.ipv4.ip_unprivileged_port_start",
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-auth-policy-approvers
|
||||
reviewers:
|
||||
- sig-auth-policy-reviewers
|
||||
labels:
|
||||
- sig/auth
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
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/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
"k8s.io/kubernetes/pkg/util/maps"
|
||||
)
|
||||
|
||||
// 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[v1.AppArmorBetaAllowedProfilesAnnotationKey]; ok {
|
||||
profiles := strings.Split(allowed, ",")
|
||||
allowedProfiles = make(map[string]bool, len(profiles))
|
||||
for _, p := range profiles {
|
||||
allowedProfiles[p] = true
|
||||
}
|
||||
}
|
||||
return &strategy{
|
||||
defaultProfile: pspAnnotations[v1.AppArmorBetaDefaultProfileAnnotationKey],
|
||||
allowedProfiles: allowedProfiles,
|
||||
allowedProfilesString: pspAnnotations[v1.AppArmorBetaAllowedProfilesAnnotationKey],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *strategy) Generate(annotations map[string]string, container *api.Container) (map[string]string, error) {
|
||||
copy := maps.CopySS(annotations)
|
||||
|
||||
if annotations[v1.AppArmorBetaContainerAnnotationKeyPrefix+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[v1.AppArmorBetaContainerAnnotationKeyPrefix+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(v1.AppArmorBetaContainerAnnotationKeyPrefix + container.Name)
|
||||
|
||||
profile := apparmor.GetProfileNameFromPodAnnotations(pod.Annotations, 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
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/*
|
||||
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/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/util/maps"
|
||||
)
|
||||
|
||||
const (
|
||||
containerName = "test-c"
|
||||
)
|
||||
|
||||
var (
|
||||
withoutAppArmor = map[string]string{"foo": "bar"}
|
||||
withDefault = map[string]string{
|
||||
"foo": "bar",
|
||||
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileRuntimeDefault,
|
||||
}
|
||||
withLocal = map[string]string{
|
||||
"foo": "bar",
|
||||
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileNamePrefix + "foo",
|
||||
}
|
||||
withDisallowed = map[string]string{
|
||||
"foo": "bar",
|
||||
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileNamePrefix + "bad",
|
||||
}
|
||||
|
||||
noAppArmor = map[string]string{"foo": "bar"}
|
||||
unconstrainedWithDefault = map[string]string{
|
||||
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
|
||||
}
|
||||
constrained = map[string]string{
|
||||
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault + "," +
|
||||
v1.AppArmorBetaProfileNamePrefix + "foo",
|
||||
}
|
||||
constrainedWithDefault = map[string]string{
|
||||
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
|
||||
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault + "," +
|
||||
v1.AppArmorBetaProfileNamePrefix + "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: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Annotations: maps.CopySS(annotations),
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{container},
|
||||
},
|
||||
}, &container
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
/*
|
||||
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 capabilities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// defaultCapabilities implements the Strategy interface
|
||||
type defaultCapabilities struct {
|
||||
defaultAddCapabilities []api.Capability
|
||||
requiredDropCapabilities []api.Capability
|
||||
allowedCaps []api.Capability
|
||||
}
|
||||
|
||||
var _ Strategy = &defaultCapabilities{}
|
||||
|
||||
// NewDefaultCapabilities creates a new defaultCapabilities strategy that will provide defaults and validation
|
||||
// based on the configured initial caps and allowed caps.
|
||||
func NewDefaultCapabilities(defaultAddCapabilities, requiredDropCapabilities, allowedCaps []corev1.Capability) (Strategy, error) {
|
||||
internalDefaultAddCaps := make([]api.Capability, len(defaultAddCapabilities))
|
||||
for i, capability := range defaultAddCapabilities {
|
||||
internalDefaultAddCaps[i] = api.Capability(capability)
|
||||
}
|
||||
internalRequiredDropCaps := make([]api.Capability, len(requiredDropCapabilities))
|
||||
for i, capability := range requiredDropCapabilities {
|
||||
internalRequiredDropCaps[i] = api.Capability(capability)
|
||||
}
|
||||
internalAllowedCaps := make([]api.Capability, len(allowedCaps))
|
||||
for i, capability := range allowedCaps {
|
||||
internalAllowedCaps[i] = api.Capability(capability)
|
||||
}
|
||||
return &defaultCapabilities{
|
||||
defaultAddCapabilities: internalDefaultAddCaps,
|
||||
requiredDropCapabilities: internalRequiredDropCaps,
|
||||
allowedCaps: internalAllowedCaps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates the capabilities based on policy rules. Generate will produce the following:
|
||||
// 1. a capabilities.Add set containing all the required adds (unless the
|
||||
// container specifically is dropping the cap) and container requested adds
|
||||
// 2. a capabilities.Drop set containing all the required drops and container requested drops
|
||||
//
|
||||
// Returns the original container capabilities if no changes are required.
|
||||
func (s *defaultCapabilities) Generate(pod *api.Pod, container *api.Container) (*api.Capabilities, error) {
|
||||
defaultAdd := makeCapSet(s.defaultAddCapabilities)
|
||||
requiredDrop := makeCapSet(s.requiredDropCapabilities)
|
||||
containerAdd := sets.NewString()
|
||||
containerDrop := sets.NewString()
|
||||
|
||||
var containerCapabilities *api.Capabilities
|
||||
if container.SecurityContext != nil && container.SecurityContext.Capabilities != nil {
|
||||
containerCapabilities = container.SecurityContext.Capabilities
|
||||
containerAdd = makeCapSet(container.SecurityContext.Capabilities.Add)
|
||||
containerDrop = makeCapSet(container.SecurityContext.Capabilities.Drop)
|
||||
}
|
||||
|
||||
// remove any default adds that the container is specifically dropping
|
||||
defaultAdd = defaultAdd.Difference(containerDrop)
|
||||
|
||||
combinedAdd := defaultAdd.Union(containerAdd)
|
||||
combinedDrop := requiredDrop.Union(containerDrop)
|
||||
|
||||
// no changes? return the original capabilities
|
||||
if (len(combinedAdd) == len(containerAdd)) && (len(combinedDrop) == len(containerDrop)) {
|
||||
return containerCapabilities, nil
|
||||
}
|
||||
|
||||
return &api.Capabilities{
|
||||
Add: capabilityFromStringSlice(combinedAdd.List()),
|
||||
Drop: capabilityFromStringSlice(combinedDrop.List()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *defaultCapabilities) Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, capabilities *api.Capabilities) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if capabilities == nil {
|
||||
// if container.SC.Caps is nil then nothing was defaulted by the strategy or requested by the pod author
|
||||
// if there are no required caps on the strategy and nothing is requested on the pod
|
||||
// then we can safely return here without further validation.
|
||||
if len(s.defaultAddCapabilities) == 0 && len(s.requiredDropCapabilities) == 0 {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// container has no requested caps but we have required caps. We should have something in
|
||||
// at least the drops on the container.
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, capabilities,
|
||||
"required capabilities are not set on the securityContext"))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
allowedAdd := makeCapSet(s.allowedCaps)
|
||||
allowAllCaps := allowedAdd.Has(string(policy.AllowAllCapabilities))
|
||||
if allowAllCaps {
|
||||
// skip validation against allowed/defaultAdd/requiredDrop because all capabilities are allowed by a wildcard
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validate that anything being added is in the default or allowed sets
|
||||
defaultAdd := makeCapSet(s.defaultAddCapabilities)
|
||||
|
||||
for _, cap := range capabilities.Add {
|
||||
sCap := string(cap)
|
||||
if !defaultAdd.Has(sCap) && !allowedAdd.Has(sCap) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("add"), sCap, "capability may not be added"))
|
||||
}
|
||||
}
|
||||
|
||||
// validate that anything that is required to be dropped is in the drop set
|
||||
containerDrops := makeCapSet(capabilities.Drop)
|
||||
|
||||
for _, requiredDrop := range s.requiredDropCapabilities {
|
||||
sDrop := string(requiredDrop)
|
||||
if !containerDrops.Has(sDrop) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("drop"), capabilities.Drop,
|
||||
fmt.Sprintf("%s is required to be dropped but was not found", sDrop)))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// capabilityFromStringSlice creates a capability slice from a string slice.
|
||||
func capabilityFromStringSlice(slice []string) []api.Capability {
|
||||
if len(slice) == 0 {
|
||||
return nil
|
||||
}
|
||||
caps := []api.Capability{}
|
||||
for _, c := range slice {
|
||||
caps = append(caps, api.Capability(c))
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// makeCapSet makes a string set from capabilities.
|
||||
func makeCapSet(caps []api.Capability) sets.String {
|
||||
s := sets.NewString()
|
||||
for _, c := range caps {
|
||||
s.Insert(string(c))
|
||||
}
|
||||
return s
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
/*
|
||||
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 capabilities
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func TestGenerateAdds(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
defaultAddCaps []corev1.Capability
|
||||
containerCaps *api.Capabilities
|
||||
expectedCaps *api.Capabilities
|
||||
}{
|
||||
"no required, no container requests": {},
|
||||
"no required, no container requests, non-nil": {
|
||||
containerCaps: &api.Capabilities{},
|
||||
expectedCaps: &api.Capabilities{},
|
||||
},
|
||||
"required, no container requests": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"required, container requests add required": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"multiple required, container requests add required": {
|
||||
defaultAddCaps: []corev1.Capability{"foo", "bar", "baz"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar", "baz", "foo"},
|
||||
},
|
||||
},
|
||||
"required, container requests add non-required": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar", "foo"},
|
||||
},
|
||||
},
|
||||
"generation does not mutate unnecessarily": {
|
||||
defaultAddCaps: []corev1.Capability{"foo", "bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo", "foo", "bar", "baz"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo", "foo", "bar", "baz"},
|
||||
},
|
||||
},
|
||||
"generation dedupes": {
|
||||
defaultAddCaps: []corev1.Capability{"foo", "bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo", "baz"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar", "baz", "foo"},
|
||||
},
|
||||
},
|
||||
"generation is case sensitive - will not dedupe": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"FOO"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"FOO", "foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
container := &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
Capabilities: v.containerCaps,
|
||||
},
|
||||
}
|
||||
|
||||
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", k, err)
|
||||
continue
|
||||
}
|
||||
generatedCaps, err := strategy.Generate(nil, container)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed generating: %v", k, err)
|
||||
continue
|
||||
}
|
||||
if v.expectedCaps == nil && generatedCaps != nil {
|
||||
t.Errorf("%s expected nil caps to be generated but got %v", k, generatedCaps)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(v.expectedCaps, generatedCaps) {
|
||||
t.Errorf("%s did not generate correctly. Expected: %#v, Actual: %#v", k, v.expectedCaps, generatedCaps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDrops(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
defaultAddCaps []corev1.Capability
|
||||
requiredDropCaps []corev1.Capability
|
||||
containerCaps *api.Capabilities
|
||||
expectedCaps *api.Capabilities
|
||||
}{
|
||||
"no required, no container requests": {
|
||||
expectedCaps: nil,
|
||||
},
|
||||
"no required, no container requests, non-nil": {
|
||||
containerCaps: &api.Capabilities{},
|
||||
expectedCaps: &api.Capabilities{},
|
||||
},
|
||||
"required drops are defaulted": {
|
||||
requiredDropCaps: []corev1.Capability{"foo"},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"required drops are defaulted when making container requests": {
|
||||
requiredDropCaps: []corev1.Capability{"baz"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo", "bar"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"bar", "baz", "foo"},
|
||||
},
|
||||
},
|
||||
"required drops do not mutate unnecessarily": {
|
||||
requiredDropCaps: []corev1.Capability{"baz"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo", "bar", "baz"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo", "bar", "baz"},
|
||||
},
|
||||
},
|
||||
"can drop a required add": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"can drop non-required add": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"bar"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
Drop: []api.Capability{"bar"},
|
||||
},
|
||||
},
|
||||
"defaulting adds and drops, dropping a required add": {
|
||||
defaultAddCaps: []corev1.Capability{"foo", "bar", "baz"},
|
||||
requiredDropCaps: []corev1.Capability{"abc"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar", "baz"},
|
||||
Drop: []api.Capability{"abc", "foo"},
|
||||
},
|
||||
},
|
||||
"generation dedupes": {
|
||||
requiredDropCaps: []corev1.Capability{"baz", "foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"bar", "foo"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"bar", "baz", "foo"},
|
||||
},
|
||||
},
|
||||
"generation is case sensitive - will not dedupe": {
|
||||
requiredDropCaps: []corev1.Capability{"bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"BAR"},
|
||||
},
|
||||
expectedCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"BAR", "bar"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range tests {
|
||||
container := &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
Capabilities: v.containerCaps,
|
||||
},
|
||||
}
|
||||
|
||||
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, v.requiredDropCaps, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", k, err)
|
||||
continue
|
||||
}
|
||||
generatedCaps, err := strategy.Generate(nil, container)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed generating: %v", k, err)
|
||||
continue
|
||||
}
|
||||
if v.expectedCaps == nil && generatedCaps != nil {
|
||||
t.Errorf("%s expected nil caps to be generated but got %#v", k, generatedCaps)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(v.expectedCaps, generatedCaps) {
|
||||
t.Errorf("%s did not generate correctly. Expected: %#v, Actual: %#v", k, v.expectedCaps, generatedCaps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAdds(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
defaultAddCaps []corev1.Capability
|
||||
allowedCaps []corev1.Capability
|
||||
containerCaps *api.Capabilities
|
||||
expectedError string
|
||||
}{
|
||||
// no container requests
|
||||
"no required, no allowed, no container requests": {},
|
||||
"no required, allowed, no container requests": {
|
||||
allowedCaps: []corev1.Capability{"foo"},
|
||||
},
|
||||
"required, no allowed, no container requests": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
expectedError: `capabilities: Invalid value: "null": required capabilities are not set on the securityContext`,
|
||||
},
|
||||
|
||||
// container requests match required
|
||||
"required, no allowed, container requests valid": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"required, no allowed, container requests invalid": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar"},
|
||||
},
|
||||
expectedError: `capabilities.add: Invalid value: "bar": capability may not be added`,
|
||||
},
|
||||
|
||||
// container requests match allowed
|
||||
"no required, allowed, container requests valid": {
|
||||
allowedCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"no required, all allowed, container requests valid": {
|
||||
allowedCaps: []corev1.Capability{policy.AllowAllCapabilities},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"no required, allowed, container requests invalid": {
|
||||
allowedCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar"},
|
||||
},
|
||||
expectedError: `capabilities.add: Invalid value: "bar": capability may not be added`,
|
||||
},
|
||||
|
||||
// required and allowed
|
||||
"required, allowed, container requests valid required": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
allowedCaps: []corev1.Capability{"bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"required, allowed, container requests valid allowed": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
allowedCaps: []corev1.Capability{"bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"bar"},
|
||||
},
|
||||
},
|
||||
"required, allowed, container requests invalid": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
allowedCaps: []corev1.Capability{"bar"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"baz"},
|
||||
},
|
||||
expectedError: `capabilities.add: Invalid value: "baz": capability may not be added`,
|
||||
},
|
||||
"validation is case sensitive": {
|
||||
defaultAddCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Add: []api.Capability{"FOO"},
|
||||
},
|
||||
expectedError: `capabilities.add: Invalid value: "FOO": capability may not be added`,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, nil, v.allowedCaps)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", k, err)
|
||||
continue
|
||||
}
|
||||
errs := strategy.Validate(field.NewPath("capabilities"), nil, nil, v.containerCaps)
|
||||
if v.expectedError == "" && len(errs) > 0 {
|
||||
t.Errorf("%s should have passed but had errors %v", k, errs)
|
||||
continue
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 0 {
|
||||
t.Errorf("%s should have failed but received no errors", k)
|
||||
continue
|
||||
}
|
||||
if len(errs) == 1 && errs[0].Error() != v.expectedError {
|
||||
t.Errorf("%s should have failed with %v but received %v", k, v.expectedError, errs[0])
|
||||
continue
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Errorf("%s should have failed with at most one error, but received %v: %v", k, len(errs), errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDrops(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
requiredDropCaps []corev1.Capability
|
||||
containerCaps *api.Capabilities
|
||||
expectedError string
|
||||
}{
|
||||
// no container requests
|
||||
"no required, no container requests": {},
|
||||
"required, no container requests": {
|
||||
requiredDropCaps: []corev1.Capability{"foo"},
|
||||
expectedError: `capabilities: Invalid value: "null": required capabilities are not set on the securityContext`,
|
||||
},
|
||||
|
||||
// container requests match required
|
||||
"required, container requests valid": {
|
||||
requiredDropCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"foo"},
|
||||
},
|
||||
},
|
||||
"required, container requests invalid": {
|
||||
requiredDropCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"bar"},
|
||||
},
|
||||
expectedError: `capabilities.drop: Invalid value: []core.Capability{"bar"}: foo is required to be dropped but was not found`,
|
||||
},
|
||||
"validation is case sensitive": {
|
||||
requiredDropCaps: []corev1.Capability{"foo"},
|
||||
containerCaps: &api.Capabilities{
|
||||
Drop: []api.Capability{"FOO"},
|
||||
},
|
||||
expectedError: `capabilities.drop: Invalid value: []core.Capability{"FOO"}: foo is required to be dropped but was not found`,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
strategy, err := NewDefaultCapabilities(nil, v.requiredDropCaps, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", k, err)
|
||||
continue
|
||||
}
|
||||
errs := strategy.Validate(field.NewPath("capabilities"), nil, nil, v.containerCaps)
|
||||
if v.expectedError == "" && len(errs) > 0 {
|
||||
t.Errorf("%s should have passed but had errors %v", k, errs)
|
||||
continue
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 0 {
|
||||
t.Errorf("%s should have failed but received no errors", k)
|
||||
continue
|
||||
}
|
||||
if len(errs) == 1 && errs[0].Error() != v.expectedError {
|
||||
t.Errorf("%s should have failed with %v but received %v", k, v.expectedError, errs[0])
|
||||
continue
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Errorf("%s should have failed with at most one error, but received %v: %v", k, len(errs), errs)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 capabilities contains code for validating and defaulting a pod's
|
||||
// kernel capabilities according to a security policy.
|
||||
package capabilities
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
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 capabilities
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// Strategy defines the interface for all cap constraint strategies.
|
||||
type Strategy interface {
|
||||
// Generate creates the capabilities based on policy rules.
|
||||
Generate(pod *api.Pod, container *api.Container) (*api.Capabilities, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, capabilities *api.Capabilities) field.ErrorList
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/*
|
||||
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 podsecuritypolicy contains code for validating and defaulting the
|
||||
// security context of a pod and its containers according to a security
|
||||
// policy.
|
||||
package podsecuritypolicy
|
@ -1,196 +0,0 @@
|
||||
/*
|
||||
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 podsecuritypolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
"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/seccomp"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
|
||||
)
|
||||
|
||||
type simpleStrategyFactory struct{}
|
||||
|
||||
var _ StrategyFactory = &simpleStrategyFactory{}
|
||||
|
||||
func NewSimpleStrategyFactory() StrategyFactory {
|
||||
return &simpleStrategyFactory{}
|
||||
}
|
||||
|
||||
func (f *simpleStrategyFactory) CreateStrategies(psp *policy.PodSecurityPolicy, namespace string) (*ProviderStrategies, error) {
|
||||
errs := []error{}
|
||||
|
||||
userStrat, err := createUserStrategy(&psp.Spec.RunAsUser)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
var groupStrat group.GroupStrategy
|
||||
groupStrat, err = createRunAsGroupStrategy(psp.Spec.RunAsGroup)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
seLinuxStrat, err := createSELinuxStrategy(&psp.Spec.SELinux)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
appArmorStrat, err := createAppArmorStrategy(psp)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
seccompStrat, err := createSeccompStrategy(psp)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
fsGroupStrat, err := createFSGroupStrategy(&psp.Spec.FSGroup)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
supGroupStrat, err := createSupplementalGroupStrategy(&psp.Spec.SupplementalGroups)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
capStrat, err := createCapabilitiesStrategy(psp.Spec.DefaultAddCapabilities, psp.Spec.RequiredDropCapabilities, psp.Spec.AllowedCapabilities)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
sysctlsStrat := createSysctlsStrategy(sysctl.SafeSysctlAllowlist(), psp.Spec.AllowedUnsafeSysctls, psp.Spec.ForbiddenSysctls)
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
strategies := &ProviderStrategies{
|
||||
RunAsUserStrategy: userStrat,
|
||||
RunAsGroupStrategy: groupStrat,
|
||||
SELinuxStrategy: seLinuxStrat,
|
||||
AppArmorStrategy: appArmorStrat,
|
||||
FSGroupStrategy: fsGroupStrat,
|
||||
SupplementalGroupStrategy: supGroupStrat,
|
||||
CapabilitiesStrategy: capStrat,
|
||||
SeccompStrategy: seccompStrat,
|
||||
SysctlsStrategy: sysctlsStrat,
|
||||
}
|
||||
|
||||
return strategies, nil
|
||||
}
|
||||
|
||||
// createUserStrategy creates a new user strategy.
|
||||
func createUserStrategy(opts *policy.RunAsUserStrategyOptions) (user.RunAsUserStrategy, error) {
|
||||
switch opts.Rule {
|
||||
case policy.RunAsUserStrategyMustRunAs:
|
||||
return user.NewMustRunAs(opts)
|
||||
case policy.RunAsUserStrategyMustRunAsNonRoot:
|
||||
return user.NewRunAsNonRoot(opts)
|
||||
case policy.RunAsUserStrategyRunAsAny:
|
||||
return user.NewRunAsAny(opts)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognized RunAsUser strategy type %s", opts.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
// createRunAsGroupStrategy creates a new group strategy.
|
||||
func createRunAsGroupStrategy(opts *policy.RunAsGroupStrategyOptions) (group.GroupStrategy, error) {
|
||||
if opts == nil {
|
||||
return group.NewRunAsAny()
|
||||
}
|
||||
switch opts.Rule {
|
||||
case policy.RunAsGroupStrategyMustRunAs:
|
||||
return group.NewMustRunAs(opts.Ranges)
|
||||
case policy.RunAsGroupStrategyRunAsAny:
|
||||
return group.NewRunAsAny()
|
||||
case policy.RunAsGroupStrategyMayRunAs:
|
||||
return group.NewMayRunAs(opts.Ranges)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognized RunAsGroup strategy type %s", opts.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
// createSELinuxStrategy creates a new selinux strategy.
|
||||
func createSELinuxStrategy(opts *policy.SELinuxStrategyOptions) (selinux.SELinuxStrategy, error) {
|
||||
switch opts.Rule {
|
||||
case policy.SELinuxStrategyMustRunAs:
|
||||
return selinux.NewMustRunAs(opts)
|
||||
case policy.SELinuxStrategyRunAsAny:
|
||||
return selinux.NewRunAsAny(opts)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognized SELinuxContext strategy type %s", opts.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
// createAppArmorStrategy creates a new AppArmor strategy.
|
||||
func createAppArmorStrategy(psp *policy.PodSecurityPolicy) (apparmor.Strategy, error) {
|
||||
return apparmor.NewStrategy(psp.Annotations), nil
|
||||
}
|
||||
|
||||
// createSeccompStrategy creates a new seccomp strategy.
|
||||
func createSeccompStrategy(psp *policy.PodSecurityPolicy) (seccomp.Strategy, error) {
|
||||
return seccomp.NewStrategy(psp.Annotations), nil
|
||||
}
|
||||
|
||||
// createFSGroupStrategy creates a new fsgroup strategy
|
||||
func createFSGroupStrategy(opts *policy.FSGroupStrategyOptions) (group.GroupStrategy, error) {
|
||||
switch opts.Rule {
|
||||
case policy.FSGroupStrategyRunAsAny:
|
||||
return group.NewRunAsAny()
|
||||
case policy.FSGroupStrategyMayRunAs:
|
||||
return group.NewMayRunAs(opts.Ranges)
|
||||
case policy.FSGroupStrategyMustRunAs:
|
||||
return group.NewMustRunAs(opts.Ranges)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognized FSGroup strategy type %s", opts.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
// createSupplementalGroupStrategy creates a new supplemental group strategy
|
||||
func createSupplementalGroupStrategy(opts *policy.SupplementalGroupsStrategyOptions) (group.GroupStrategy, error) {
|
||||
switch opts.Rule {
|
||||
case policy.SupplementalGroupsStrategyRunAsAny:
|
||||
return group.NewRunAsAny()
|
||||
case policy.SupplementalGroupsStrategyMayRunAs:
|
||||
return group.NewMayRunAs(opts.Ranges)
|
||||
case policy.SupplementalGroupsStrategyMustRunAs:
|
||||
return group.NewMustRunAs(opts.Ranges)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognized SupplementalGroups strategy type %s", opts.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
// createCapabilitiesStrategy creates a new capabilities strategy.
|
||||
func createCapabilitiesStrategy(defaultAddCaps, requiredDropCaps, allowedCaps []corev1.Capability) (capabilities.Strategy, error) {
|
||||
return capabilities.NewDefaultCapabilities(defaultAddCaps, requiredDropCaps, allowedCaps)
|
||||
}
|
||||
|
||||
// createSysctlsStrategy creates a new sysctls strategy.
|
||||
func createSysctlsStrategy(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls []string) sysctl.SysctlsStrategy {
|
||||
return sysctl.NewMustMatchPatterns(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 group contains code for validating and defaulting the FSGroup and
|
||||
// supplemental groups of a pod according to a security policy.
|
||||
package group
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
)
|
||||
|
||||
func ValidateGroupsInRanges(fldPath *field.Path, ranges []policy.IDRange, groups []int64) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
for _, group := range groups {
|
||||
if !isGroupInRanges(group, ranges) {
|
||||
detail := fmt.Sprintf("group %d must be in the ranges: %v", group, ranges)
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, groups, detail))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func isGroupInRanges(group int64, ranges []policy.IDRange) bool {
|
||||
for _, rng := range ranges {
|
||||
if psputil.GroupFallsInRange(group, rng) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// mayRunAs implements the GroupStrategy interface.
|
||||
type mayRunAs struct {
|
||||
ranges []policy.IDRange
|
||||
}
|
||||
|
||||
var _ GroupStrategy = &mayRunAs{}
|
||||
|
||||
// NewMayRunAs provides a new MayRunAs strategy.
|
||||
func NewMayRunAs(ranges []policy.IDRange) (GroupStrategy, error) {
|
||||
if len(ranges) == 0 {
|
||||
return nil, fmt.Errorf("ranges must be supplied for MayRunAs")
|
||||
}
|
||||
return &mayRunAs{
|
||||
ranges: ranges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates the group based on policy rules. This strategy returns an empty slice.
|
||||
func (s *mayRunAs) Generate(_ *api.Pod) ([]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Generate a single value to be applied. This is used for FSGroup. This strategy returns nil.
|
||||
func (s *mayRunAs) GenerateSingle(_ *api.Pod) (*int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
// Groups are passed in here to allow this strategy to support multiple group fields (fsgroup and
|
||||
// supplemental groups).
|
||||
func (s *mayRunAs) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
|
||||
return ValidateGroupsInRanges(fldPath, s.ranges, groups)
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestMayRunAsOptions(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
ranges []policy.IDRange
|
||||
pass bool
|
||||
}{
|
||||
"empty": {
|
||||
ranges: []policy.IDRange{},
|
||||
},
|
||||
"ranges": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
},
|
||||
pass: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
_, err := NewMayRunAs(v.ranges)
|
||||
if v.pass && err != nil {
|
||||
t.Errorf("error creating strategy for %s: %v", k, err)
|
||||
}
|
||||
if !v.pass && err == nil {
|
||||
t.Errorf("expected error for %s but got none", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMayRunAsValidate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
ranges []policy.IDRange
|
||||
groups []int64
|
||||
expectedErrors []string
|
||||
}{
|
||||
"empty groups": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"not in range": {
|
||||
groups: []int64{5},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
{Min: 4, Max: 4},
|
||||
},
|
||||
expectedErrors: []string{"group 5 must be in the ranges: [{1 3} {4 4}]"},
|
||||
},
|
||||
"not in ranges - multiple groups": {
|
||||
groups: []int64{5, 10, 2020},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
{Min: 15, Max: 70},
|
||||
},
|
||||
expectedErrors: []string{
|
||||
"group 5 must be in the ranges: [{1 3} {15 70}]",
|
||||
"group 10 must be in the ranges: [{1 3} {15 70}]",
|
||||
"group 2020 must be in the ranges: [{1 3} {15 70}]",
|
||||
},
|
||||
},
|
||||
"not in ranges - one of multiple groups does not match": {
|
||||
groups: []int64{5, 10, 2020},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 5},
|
||||
{Min: 8, Max: 12},
|
||||
{Min: 15, Max: 70},
|
||||
},
|
||||
expectedErrors: []string{
|
||||
"group 2020 must be in the ranges: [{1 5} {8 12} {15 70}]",
|
||||
},
|
||||
},
|
||||
"in range 1": {
|
||||
groups: []int64{2},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"in range boundary min": {
|
||||
groups: []int64{1},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"in range boundary max": {
|
||||
groups: []int64{3},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"singular range": {
|
||||
groups: []int64{4},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 4, Max: 4},
|
||||
},
|
||||
},
|
||||
"in one of multiple ranges": {
|
||||
groups: []int64{4},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 4},
|
||||
{Min: 10, Max: 15},
|
||||
},
|
||||
},
|
||||
"multiple groups matches one range": {
|
||||
groups: []int64{4, 8, 12},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 20},
|
||||
},
|
||||
},
|
||||
"multiple groups match multiple ranges": {
|
||||
groups: []int64{4, 8, 12},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 4},
|
||||
{Min: 200, Max: 2000},
|
||||
{Min: 7, Max: 11},
|
||||
{Min: 5, Max: 7},
|
||||
{Min: 17, Max: 53},
|
||||
{Min: 12, Max: 71},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
s, err := NewMayRunAs(v.ranges)
|
||||
if err != nil {
|
||||
t.Errorf("error creating strategy for %s: %v", k, err)
|
||||
}
|
||||
errs := s.Validate(field.NewPath(""), nil, v.groups)
|
||||
if len(v.expectedErrors) != len(errs) {
|
||||
// number of expected errors is different from actual, includes cases when we expected errors and they appeared or vice versa
|
||||
t.Errorf("number of expected errors for '%s' does not match with errors received:\n"+
|
||||
"expected:\n%s\nbut got:\n%s",
|
||||
k, concatenateStrings(v.expectedErrors), concatenateErrors(errs))
|
||||
} else if len(v.expectedErrors) > 0 {
|
||||
// check that the errors received match the expectations
|
||||
for i, s := range v.expectedErrors {
|
||||
if !strings.Contains(errs[i].Error(), s) {
|
||||
t.Errorf("expected errors in particular order for '%s':\n%s\nbut got:\n%s",
|
||||
k, concatenateStrings(v.expectedErrors), concatenateErrors(errs))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func concatenateErrors(errs field.ErrorList) string {
|
||||
var errStrings []string
|
||||
for _, e := range errs {
|
||||
errStrings = append(errStrings, e.Error())
|
||||
}
|
||||
return concatenateStrings(errStrings)
|
||||
}
|
||||
|
||||
func concatenateStrings(ss []string) string {
|
||||
var ret string
|
||||
for i, v := range ss {
|
||||
ret += fmt.Sprintf("%d: %s\n", i+1, v)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
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 group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// mustRunAs implements the GroupStrategy interface
|
||||
type mustRunAs struct {
|
||||
ranges []policy.IDRange
|
||||
}
|
||||
|
||||
var _ GroupStrategy = &mustRunAs{}
|
||||
|
||||
// NewMustRunAs provides a new MustRunAs strategy based on ranges.
|
||||
func NewMustRunAs(ranges []policy.IDRange) (GroupStrategy, error) {
|
||||
if len(ranges) == 0 {
|
||||
return nil, fmt.Errorf("ranges must be supplied for MustRunAs")
|
||||
}
|
||||
return &mustRunAs{
|
||||
ranges: ranges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates the group based on policy rules. By default this returns the first group of the
|
||||
// first range (min val).
|
||||
func (s *mustRunAs) Generate(_ *api.Pod) ([]int64, error) {
|
||||
return []int64{s.ranges[0].Min}, nil
|
||||
}
|
||||
|
||||
// Generate a single value to be applied. This is used for FSGroup. This strategy will return
|
||||
// the first group of the first range (min val).
|
||||
func (s *mustRunAs) GenerateSingle(_ *api.Pod) (*int64, error) {
|
||||
single := new(int64)
|
||||
*single = s.ranges[0].Min
|
||||
return single, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
// Groups are passed in here to allow this strategy to support multiple group fields (fsgroup and
|
||||
// supplemental groups).
|
||||
func (s *mustRunAs) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if len(groups) == 0 && len(s.ranges) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, groups, "unable to validate empty groups against required ranges"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, ValidateGroupsInRanges(fldPath, s.ranges, groups)...)
|
||||
|
||||
return allErrs
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 group
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestMustRunAsOptions(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
ranges []policy.IDRange
|
||||
pass bool
|
||||
}{
|
||||
"empty": {
|
||||
ranges: []policy.IDRange{},
|
||||
},
|
||||
"ranges": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
},
|
||||
pass: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
_, err := NewMustRunAs(v.ranges)
|
||||
if v.pass && err != nil {
|
||||
t.Errorf("error creating strategy for %s: %v", k, err)
|
||||
}
|
||||
if !v.pass && err == nil {
|
||||
t.Errorf("expected error for %s but got none", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
ranges []policy.IDRange
|
||||
expected []int64
|
||||
}{
|
||||
"multi value": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 2},
|
||||
},
|
||||
expected: []int64{1},
|
||||
},
|
||||
"single value": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
},
|
||||
expected: []int64{1},
|
||||
},
|
||||
"multi range": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
{Min: 2, Max: 500},
|
||||
},
|
||||
expected: []int64{1},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
s, err := NewMustRunAs(v.ranges)
|
||||
if err != nil {
|
||||
t.Errorf("error creating strategy for %s: %v", k, err)
|
||||
}
|
||||
actual, err := s.Generate(nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for %s: %v", k, err)
|
||||
}
|
||||
if len(actual) != len(v.expected) {
|
||||
t.Errorf("unexpected generated values. Expected %v, got %v", v.expected, actual)
|
||||
continue
|
||||
}
|
||||
if len(actual) > 0 && len(v.expected) > 0 {
|
||||
if actual[0] != v.expected[0] {
|
||||
t.Errorf("unexpected generated values. Expected %v, got %v", v.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
single, err := s.GenerateSingle(nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for %s: %v", k, err)
|
||||
}
|
||||
if single == nil {
|
||||
t.Errorf("unexpected nil generated value for %s: %v", k, single)
|
||||
}
|
||||
if *single != v.expected[0] {
|
||||
t.Errorf("unexpected generated single value. Expected %v, got %v", v.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
ranges []policy.IDRange
|
||||
groups []int64
|
||||
expectedError string
|
||||
}{
|
||||
"nil security context": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
expectedError: "unable to validate empty groups against required ranges",
|
||||
},
|
||||
"empty groups": {
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
expectedError: "unable to validate empty groups against required ranges",
|
||||
},
|
||||
"not in range": {
|
||||
groups: []int64{5},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
{Min: 4, Max: 4},
|
||||
},
|
||||
expectedError: "group 5 must be in the ranges: [{1 3} {4 4}]",
|
||||
},
|
||||
"in range 1": {
|
||||
groups: []int64{2},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"in range boundary min": {
|
||||
groups: []int64{1},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"in range boundary max": {
|
||||
groups: []int64{3},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 3},
|
||||
},
|
||||
},
|
||||
"singular range": {
|
||||
groups: []int64{4},
|
||||
ranges: []policy.IDRange{
|
||||
{Min: 4, Max: 4},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
s, err := NewMustRunAs(v.ranges)
|
||||
if err != nil {
|
||||
t.Errorf("error creating strategy for %s: %v", k, err)
|
||||
}
|
||||
errs := s.Validate(field.NewPath(""), nil, v.groups)
|
||||
if v.expectedError == "" && len(errs) > 0 {
|
||||
t.Errorf("unexpected errors for %s: %v", k, errs)
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 0 {
|
||||
t.Errorf("expected errors for %s but got: %v", k, errs)
|
||||
}
|
||||
if v.expectedError != "" && len(errs) > 0 && !strings.Contains(errs[0].Error(), v.expectedError) {
|
||||
t.Errorf("expected error for %s: %v, but got: %v", k, v.expectedError, errs[0])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
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 group
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// runAsAny implements the GroupStrategy interface.
|
||||
type runAsAny struct {
|
||||
}
|
||||
|
||||
var _ GroupStrategy = &runAsAny{}
|
||||
|
||||
// NewRunAsAny provides a new RunAsAny strategy.
|
||||
func NewRunAsAny() (GroupStrategy, error) {
|
||||
return &runAsAny{}, nil
|
||||
}
|
||||
|
||||
// Generate creates the group based on policy rules. This strategy returns an empty slice.
|
||||
func (s *runAsAny) Generate(_ *api.Pod) ([]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Generate a single value to be applied. This is used for FSGroup. This strategy returns nil.
|
||||
func (s *runAsAny) GenerateSingle(_ *api.Pod) (*int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *runAsAny) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
|
||||
return field.ErrorList{}
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
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 group
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestRunAsAnyGenerate(t *testing.T) {
|
||||
s, err := NewRunAsAny()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
groups, err := s.Generate(nil)
|
||||
if len(groups) > 0 {
|
||||
t.Errorf("expected empty but got %v", groups)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error generating groups: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyGenerateSingle(t *testing.T) {
|
||||
s, err := NewRunAsAny()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
group, err := s.GenerateSingle(nil)
|
||||
if group != nil {
|
||||
t.Errorf("expected empty but got %v", group)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error generating groups: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyValidate(t *testing.T) {
|
||||
s, err := NewRunAsAny()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
errs := s.Validate(field.NewPath(""), nil, nil)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("unexpected errors: %v", errs)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
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 group
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// GroupStrategy defines the interface for all group constraint strategies.
|
||||
type GroupStrategy interface {
|
||||
// Generate creates the group based on policy rules. The underlying implementation can
|
||||
// decide whether it will return a full range of values or a subset of values from the
|
||||
// configured ranges.
|
||||
Generate(pod *api.Pod) ([]int64, error)
|
||||
// Generate a single value to be applied. The underlying implementation decides which
|
||||
// value to return if configured with multiple ranges. This is used for FSGroup.
|
||||
GenerateSingle(pod *api.Pod) (*int64, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(fldPath *field.Path, pod *api.Pod, groups []int64) field.ErrorList
|
||||
}
|
@ -1,459 +0,0 @@
|
||||
/*
|
||||
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 podsecuritypolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
podutil "k8s.io/kubernetes/pkg/api/pod"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/pods"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/pkg/securitycontext"
|
||||
)
|
||||
|
||||
// simpleProvider is the default implementation of Provider.
|
||||
type simpleProvider struct {
|
||||
psp *policy.PodSecurityPolicy
|
||||
strategies *ProviderStrategies
|
||||
}
|
||||
|
||||
// ensure we implement the interface correctly.
|
||||
var _ Provider = &simpleProvider{}
|
||||
|
||||
// NewSimpleProvider creates a new Provider instance.
|
||||
func NewSimpleProvider(psp *policy.PodSecurityPolicy, namespace string, strategyFactory StrategyFactory) (Provider, error) {
|
||||
if psp == nil {
|
||||
return nil, fmt.Errorf("NewSimpleProvider requires a PodSecurityPolicy")
|
||||
}
|
||||
if strategyFactory == nil {
|
||||
return nil, fmt.Errorf("NewSimpleProvider requires a StrategyFactory")
|
||||
}
|
||||
|
||||
strategies, err := strategyFactory.CreateStrategies(psp, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &simpleProvider{
|
||||
psp: psp,
|
||||
strategies: strategies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MutatePod sets the default values of the required but not filled fields.
|
||||
// Validation should be used after the context is defaulted to ensure it
|
||||
// complies with the required restrictions.
|
||||
func (s *simpleProvider) MutatePod(pod *api.Pod) error {
|
||||
sc := securitycontext.NewPodSecurityContextMutator(pod.Spec.SecurityContext)
|
||||
|
||||
if sc.SupplementalGroups() == nil {
|
||||
supGroups, err := s.strategies.SupplementalGroupStrategy.Generate(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetSupplementalGroups(supGroups)
|
||||
}
|
||||
|
||||
if sc.FSGroup() == nil {
|
||||
fsGroup, err := s.strategies.FSGroupStrategy.GenerateSingle(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetFSGroup(fsGroup)
|
||||
}
|
||||
|
||||
if sc.SELinuxOptions() == nil {
|
||||
seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetSELinuxOptions(seLinux)
|
||||
}
|
||||
|
||||
// This is only generated on the pod level. Containers inherit the pod's profile. If the
|
||||
// container has a specific profile set then it will be caught in the validation step.
|
||||
seccompProfile, err := s.strategies.SeccompStrategy.Generate(pod.Annotations, pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if seccompProfile != "" {
|
||||
if pod.Annotations == nil {
|
||||
pod.Annotations = map[string]string{}
|
||||
}
|
||||
pod.Annotations[api.SeccompPodAnnotationKey] = seccompProfile
|
||||
}
|
||||
|
||||
pod.Spec.SecurityContext = sc.PodSecurityContext()
|
||||
|
||||
if s.psp.Spec.RuntimeClass != nil && pod.Spec.RuntimeClassName == nil {
|
||||
pod.Spec.RuntimeClassName = s.psp.Spec.RuntimeClass.DefaultRuntimeClassName
|
||||
}
|
||||
|
||||
var retErr error
|
||||
podutil.VisitContainers(&pod.Spec, podutil.AllContainers, func(c *api.Container, containerType podutil.ContainerType) bool {
|
||||
retErr = s.mutateContainer(pod, c)
|
||||
if retErr != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// mutateContainer sets the default values of the required but not filled fields.
|
||||
// It modifies the SecurityContext of the container and annotations of the pod. Validation should
|
||||
// be used after the context is defaulted to ensure it complies with the required restrictions.
|
||||
func (s *simpleProvider) mutateContainer(pod *api.Pod, container *api.Container) error {
|
||||
sc := securitycontext.NewEffectiveContainerSecurityContextMutator(
|
||||
securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext),
|
||||
securitycontext.NewContainerSecurityContextMutator(container.SecurityContext),
|
||||
)
|
||||
|
||||
if sc.RunAsUser() == nil {
|
||||
uid, err := s.strategies.RunAsUserStrategy.Generate(pod, container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetRunAsUser(uid)
|
||||
}
|
||||
|
||||
if sc.RunAsGroup() == nil {
|
||||
gid, err := s.strategies.RunAsGroupStrategy.GenerateSingle(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetRunAsGroup(gid)
|
||||
}
|
||||
|
||||
if sc.SELinuxOptions() == nil {
|
||||
seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetSELinuxOptions(seLinux)
|
||||
}
|
||||
|
||||
annotations, err := s.strategies.AppArmorStrategy.Generate(pod.Annotations, container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we're using the non-root strategy set the marker that this container should not be
|
||||
// run as root which will signal to the kubelet to do a final check either on the runAsUser
|
||||
// or, if runAsUser is not set, the image UID will be checked.
|
||||
if sc.RunAsNonRoot() == nil && sc.RunAsUser() == nil && s.psp.Spec.RunAsUser.Rule == policy.RunAsUserStrategyMustRunAsNonRoot {
|
||||
nonRoot := true
|
||||
sc.SetRunAsNonRoot(&nonRoot)
|
||||
}
|
||||
|
||||
caps, err := s.strategies.CapabilitiesStrategy.Generate(pod, container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.SetCapabilities(caps)
|
||||
|
||||
// if the PSP requires a read only root filesystem and the container has not made a specific
|
||||
// request then default ReadOnlyRootFilesystem to true.
|
||||
if s.psp.Spec.ReadOnlyRootFilesystem && sc.ReadOnlyRootFilesystem() == nil {
|
||||
readOnlyRootFS := true
|
||||
sc.SetReadOnlyRootFilesystem(&readOnlyRootFS)
|
||||
}
|
||||
|
||||
// if the PSP sets DefaultAllowPrivilegeEscalation and the container security context
|
||||
// allowPrivilegeEscalation is not set, then default to that set by the PSP.
|
||||
if s.psp.Spec.DefaultAllowPrivilegeEscalation != nil && sc.AllowPrivilegeEscalation() == nil {
|
||||
sc.SetAllowPrivilegeEscalation(s.psp.Spec.DefaultAllowPrivilegeEscalation)
|
||||
}
|
||||
|
||||
// if the PSP sets psp.AllowPrivilegeEscalation to false, set that as the default
|
||||
if !*s.psp.Spec.AllowPrivilegeEscalation && sc.AllowPrivilegeEscalation() == nil {
|
||||
sc.SetAllowPrivilegeEscalation(s.psp.Spec.AllowPrivilegeEscalation)
|
||||
}
|
||||
|
||||
pod.Annotations = annotations
|
||||
container.SecurityContext = sc.ContainerSecurityContext()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePod ensure a pod is in compliance with the given constraints.
|
||||
func (s *simpleProvider) ValidatePod(pod *api.Pod) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
sc := securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext)
|
||||
scPath := field.NewPath("spec", "securityContext")
|
||||
|
||||
var fsGroups []int64
|
||||
if fsGroup := sc.FSGroup(); fsGroup != nil {
|
||||
fsGroups = []int64{*fsGroup}
|
||||
}
|
||||
allErrs = append(allErrs, s.strategies.FSGroupStrategy.Validate(scPath.Child("fsGroup"), pod, fsGroups)...)
|
||||
allErrs = append(allErrs, s.strategies.SupplementalGroupStrategy.Validate(scPath.Child("supplementalGroups"), pod, sc.SupplementalGroups())...)
|
||||
allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidatePod(pod)...)
|
||||
|
||||
allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(scPath.Child("seLinuxOptions"), pod, nil, sc.SELinuxOptions())...)
|
||||
|
||||
if !s.psp.Spec.HostNetwork && sc.HostNetwork() {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("hostNetwork"), sc.HostNetwork(), "Host network is not allowed to be used"))
|
||||
}
|
||||
|
||||
if !s.psp.Spec.HostPID && sc.HostPID() {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("hostPID"), sc.HostPID(), "Host PID is not allowed to be used"))
|
||||
}
|
||||
|
||||
if !s.psp.Spec.HostIPC && sc.HostIPC() {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("hostIPC"), sc.HostIPC(), "Host IPC is not allowed to be used"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, s.strategies.SysctlsStrategy.Validate(pod)...)
|
||||
|
||||
allErrs = append(allErrs, s.validatePodVolumes(pod)...)
|
||||
|
||||
if s.psp.Spec.RuntimeClass != nil {
|
||||
allErrs = append(allErrs, validateRuntimeClassName(pod.Spec.RuntimeClassName, s.psp.Spec.RuntimeClass.AllowedRuntimeClassNames)...)
|
||||
}
|
||||
|
||||
pods.VisitContainersWithPath(&pod.Spec, field.NewPath("spec"), func(c *api.Container, p *field.Path) bool {
|
||||
allErrs = append(allErrs, s.validateContainer(pod, c, p)...)
|
||||
return true
|
||||
})
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func (s *simpleProvider) validatePodVolumes(pod *api.Pod) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if len(pod.Spec.Volumes) > 0 {
|
||||
allowsAllVolumeTypes := psputil.PSPAllowsAllVolumes(s.psp)
|
||||
allowedVolumes := psputil.FSTypeToStringSet(s.psp.Spec.Volumes)
|
||||
for i, v := range pod.Spec.Volumes {
|
||||
fsType, err := psputil.GetVolumeFSType(v)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "volumes").Index(i), string(fsType), err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
if !allowsAllVolumeTypes && !allowsVolumeType(allowedVolumes, fsType, v) {
|
||||
allErrs = append(allErrs, field.Invalid(
|
||||
field.NewPath("spec", "volumes").Index(i), string(fsType),
|
||||
fmt.Sprintf("%s volumes are not allowed to be used", string(fsType))))
|
||||
continue
|
||||
}
|
||||
|
||||
switch fsType {
|
||||
case policy.HostPath:
|
||||
allows, mustBeReadOnly := psputil.AllowsHostVolumePath(s.psp, v.HostPath.Path)
|
||||
if !allows {
|
||||
allErrs = append(allErrs, field.Invalid(
|
||||
field.NewPath("spec", "volumes").Index(i).Child("hostPath", "pathPrefix"), v.HostPath.Path,
|
||||
fmt.Sprintf("is not allowed to be used")))
|
||||
} else if mustBeReadOnly {
|
||||
// Ensure all the VolumeMounts that use this volume are read-only
|
||||
pods.VisitContainersWithPath(&pod.Spec, field.NewPath("spec"), func(c *api.Container, p *field.Path) bool {
|
||||
for i, cv := range c.VolumeMounts {
|
||||
if cv.Name == v.Name && !cv.ReadOnly {
|
||||
allErrs = append(allErrs, field.Invalid(p.Child("volumeMounts").Index(i).Child("readOnly"), cv.ReadOnly, "must be read-only"))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
case policy.FlexVolume:
|
||||
if len(s.psp.Spec.AllowedFlexVolumes) > 0 {
|
||||
found := false
|
||||
driver := v.FlexVolume.Driver
|
||||
for _, allowedFlexVolume := range s.psp.Spec.AllowedFlexVolumes {
|
||||
if driver == allowedFlexVolume.Driver {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
allErrs = append(allErrs,
|
||||
field.Invalid(field.NewPath("spec", "volumes").Index(i).Child("driver"), driver,
|
||||
"Flexvolume driver is not allowed to be used"))
|
||||
}
|
||||
}
|
||||
|
||||
case policy.CSI:
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
|
||||
if len(s.psp.Spec.AllowedCSIDrivers) > 0 {
|
||||
found := false
|
||||
driver := v.CSI.Driver
|
||||
for _, allowedCSIDriver := range s.psp.Spec.AllowedCSIDrivers {
|
||||
if driver == allowedCSIDriver.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
allErrs = append(allErrs,
|
||||
field.Invalid(field.NewPath("spec", "volumes").Index(i).Child("csi", "driver"), driver,
|
||||
"Inline CSI driver is not allowed to be used"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// Ensure a container's SecurityContext is in compliance with the given constraints
|
||||
func (s *simpleProvider) validateContainer(pod *api.Pod, container *api.Container, containerPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
podSC := securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext)
|
||||
sc := securitycontext.NewEffectiveContainerSecurityContextAccessor(podSC, securitycontext.NewContainerSecurityContextMutator(container.SecurityContext))
|
||||
|
||||
scPath := containerPath.Child("securityContext")
|
||||
allErrs = append(allErrs, s.strategies.RunAsUserStrategy.Validate(scPath, pod, container, sc.RunAsNonRoot(), sc.RunAsUser())...)
|
||||
var runAsGroups []int64
|
||||
if sc.RunAsGroup() != nil {
|
||||
runAsGroups = []int64{*sc.RunAsGroup()}
|
||||
}
|
||||
allErrs = append(allErrs, s.strategies.RunAsGroupStrategy.Validate(scPath, pod, runAsGroups)...)
|
||||
|
||||
allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(scPath.Child("seLinuxOptions"), pod, container, sc.SELinuxOptions())...)
|
||||
allErrs = append(allErrs, s.strategies.AppArmorStrategy.Validate(pod, container)...)
|
||||
allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidateContainer(pod, container)...)
|
||||
|
||||
privileged := sc.Privileged()
|
||||
if !s.psp.Spec.Privileged && privileged != nil && *privileged {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("privileged"), *privileged, "Privileged containers are not allowed"))
|
||||
}
|
||||
|
||||
procMount := sc.ProcMount()
|
||||
allowedProcMounts := s.psp.Spec.AllowedProcMountTypes
|
||||
if len(allowedProcMounts) == 0 {
|
||||
allowedProcMounts = []corev1.ProcMountType{corev1.DefaultProcMount}
|
||||
}
|
||||
foundProcMountType := false
|
||||
for _, pm := range allowedProcMounts {
|
||||
if string(pm) == string(procMount) {
|
||||
foundProcMountType = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundProcMountType {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("procMount"), procMount, "ProcMountType is not allowed"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, s.strategies.CapabilitiesStrategy.Validate(scPath.Child("capabilities"), pod, container, sc.Capabilities())...)
|
||||
|
||||
allErrs = append(allErrs, s.hasInvalidHostPort(container, containerPath)...)
|
||||
|
||||
if s.psp.Spec.ReadOnlyRootFilesystem {
|
||||
readOnly := sc.ReadOnlyRootFilesystem()
|
||||
if readOnly == nil {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("readOnlyRootFilesystem"), readOnly, "ReadOnlyRootFilesystem may not be nil and must be set to true"))
|
||||
} else if !*readOnly {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("readOnlyRootFilesystem"), *readOnly, "ReadOnlyRootFilesystem must be set to true"))
|
||||
}
|
||||
}
|
||||
|
||||
allowEscalation := sc.AllowPrivilegeEscalation()
|
||||
if !*s.psp.Spec.AllowPrivilegeEscalation && (allowEscalation == nil || *allowEscalation) {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("allowPrivilegeEscalation"), allowEscalation, "Allowing privilege escalation for containers is not allowed"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// hasInvalidHostPort checks whether the port definitions on the container fall outside of the ranges allowed by the PSP.
|
||||
func (s *simpleProvider) hasInvalidHostPort(container *api.Container, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for _, cp := range container.Ports {
|
||||
if cp.HostPort > 0 && !s.isValidHostPort(cp.HostPort) {
|
||||
detail := fmt.Sprintf("Host port %d is not allowed to be used. Allowed ports: [%s]", cp.HostPort, hostPortRangesToString(s.psp.Spec.HostPorts))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPort"), cp.HostPort, detail))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// isValidHostPort returns true if the port falls in any range allowed by the PSP.
|
||||
func (s *simpleProvider) isValidHostPort(port int32) bool {
|
||||
for _, hostPortRange := range s.psp.Spec.HostPorts {
|
||||
if port >= hostPortRange.Min && port <= hostPortRange.Max {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the name of the PSP that this provider was initialized with.
|
||||
func (s *simpleProvider) GetPSPName() string {
|
||||
return s.psp.Name
|
||||
}
|
||||
|
||||
func hostPortRangesToString(ranges []policy.HostPortRange) string {
|
||||
formattedString := ""
|
||||
if ranges != nil {
|
||||
strRanges := []string{}
|
||||
for _, r := range ranges {
|
||||
if r.Min == r.Max {
|
||||
strRanges = append(strRanges, fmt.Sprintf("%d", r.Min))
|
||||
} else {
|
||||
strRanges = append(strRanges, fmt.Sprintf("%d-%d", r.Min, r.Max))
|
||||
}
|
||||
}
|
||||
formattedString = strings.Join(strRanges, ",")
|
||||
}
|
||||
return formattedString
|
||||
}
|
||||
|
||||
// validates that the actual RuntimeClassName is contained in the list of valid names.
|
||||
func validateRuntimeClassName(actual *string, validNames []string) field.ErrorList {
|
||||
if actual == nil {
|
||||
return nil // An unset RuntimeClassName is always allowed.
|
||||
}
|
||||
|
||||
for _, valid := range validNames {
|
||||
if valid == policy.AllowAllRuntimeClassNames {
|
||||
return nil
|
||||
}
|
||||
if *actual == valid {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return field.ErrorList{field.Invalid(field.NewPath("spec", "runtimeClassName"), *actual, "")}
|
||||
}
|
||||
|
||||
func allowsVolumeType(allowedVolumes sets.String, fsType policy.FSType, volume api.Volume) bool {
|
||||
if allowedVolumes.Has(string(fsType)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if secret volume is allowed, all the projected volume sources that projected service account token volumes expose are allowed, regardless of psp.
|
||||
if allowedVolumes.Has(string(policy.Secret)) && fsType == policy.Projected && psputil.IsOnlyServiceAccountTokenSources(volume.VolumeSource.Projected) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,178 +0,0 @@
|
||||
/*
|
||||
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 seccomp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
podutil "k8s.io/kubernetes/pkg/api/pod"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllowAny is the wildcard used to allow any profile.
|
||||
AllowAny = "*"
|
||||
// DefaultProfileAnnotationKey specifies the default seccomp profile.
|
||||
DefaultProfileAnnotationKey = "seccomp.security.alpha.kubernetes.io/defaultProfileName"
|
||||
// AllowedProfilesAnnotationKey specifies the allowed seccomp profiles.
|
||||
AllowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
|
||||
)
|
||||
|
||||
// Strategy defines the interface for all seccomp constraint strategies.
|
||||
type Strategy interface {
|
||||
// Generate returns a profile based on constraint rules.
|
||||
Generate(annotations map[string]string, pod *api.Pod) (string, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
ValidatePod(pod *api.Pod) field.ErrorList
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
ValidateContainer(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
|
||||
// does the strategy allow any profile (wildcard)
|
||||
allowAnyProfile bool
|
||||
}
|
||||
|
||||
var _ Strategy = &strategy{}
|
||||
|
||||
// NewStrategy creates a new strategy that enforces seccomp profile constraints.
|
||||
func NewStrategy(pspAnnotations map[string]string) Strategy {
|
||||
var allowedProfiles map[string]bool
|
||||
allowAnyProfile := false
|
||||
if allowed, ok := pspAnnotations[AllowedProfilesAnnotationKey]; ok {
|
||||
profiles := strings.Split(allowed, ",")
|
||||
allowedProfiles = make(map[string]bool, len(profiles))
|
||||
for _, p := range profiles {
|
||||
if p == AllowAny {
|
||||
allowAnyProfile = true
|
||||
continue
|
||||
}
|
||||
// With the graduation of seccomp to GA we automatically convert
|
||||
// the deprecated seccomp profile annotation `docker/default` to
|
||||
// `runtime/default`. This means that we now have to automatically
|
||||
// allow `runtime/default` if a user specifies `docker/default` and
|
||||
// vice versa in a PSP.
|
||||
if p == v1.DeprecatedSeccompProfileDockerDefault || p == v1.SeccompProfileRuntimeDefault {
|
||||
allowedProfiles[v1.SeccompProfileRuntimeDefault] = true
|
||||
allowedProfiles[v1.DeprecatedSeccompProfileDockerDefault] = true
|
||||
}
|
||||
allowedProfiles[p] = true
|
||||
}
|
||||
}
|
||||
return &strategy{
|
||||
defaultProfile: pspAnnotations[DefaultProfileAnnotationKey],
|
||||
allowedProfiles: allowedProfiles,
|
||||
allowedProfilesString: pspAnnotations[AllowedProfilesAnnotationKey],
|
||||
allowAnyProfile: allowAnyProfile,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate returns a profile based on constraint rules.
|
||||
func (s *strategy) Generate(annotations map[string]string, pod *api.Pod) (string, error) {
|
||||
if annotations[api.SeccompPodAnnotationKey] != "" {
|
||||
// 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
|
||||
}
|
||||
|
||||
// ValidatePod ensures that the specified values on the pod fall within the range
|
||||
// of the strategy.
|
||||
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"))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
if !s.profileAllowed(podProfile) {
|
||||
msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", podProfile, s.allowedProfilesString)
|
||||
allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, msg))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateContainer ensures that the specified values on the container fall within
|
||||
// the range of the strategy.
|
||||
func (s *strategy) ValidateContainer(pod *api.Pod, container *api.Container) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
fieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompContainerAnnotationKeyPrefix + container.Name)
|
||||
containerProfile := profileForContainer(pod, container)
|
||||
|
||||
if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && containerProfile != "" {
|
||||
allErrs = append(allErrs, field.Forbidden(fieldPath, "seccomp may not be set"))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
if !s.profileAllowed(containerProfile) {
|
||||
msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", containerProfile, s.allowedProfilesString)
|
||||
allErrs = append(allErrs, field.Forbidden(fieldPath, msg))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// profileAllowed checks if profile is in allowedProfiles or if allowedProfiles
|
||||
// contains the wildcard.
|
||||
func (s *strategy) profileAllowed(profile string) bool {
|
||||
// for backwards compatibility and PSPs without a defined list of allowed profiles.
|
||||
// If a PSP does not have allowedProfiles set then we should allow an empty profile.
|
||||
// This will mean that the runtime default is used.
|
||||
if len(s.allowedProfiles) == 0 && profile == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return s.allowAnyProfile || s.allowedProfiles[profile]
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
@ -1,419 +0,0 @@
|
||||
/*
|
||||
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 seccomp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
var (
|
||||
withoutSeccomp = map[string]string{"foo": "bar"}
|
||||
allowAnyNoDefault = map[string]string{
|
||||
AllowedProfilesAnnotationKey: "*",
|
||||
}
|
||||
allowAnyDefault = map[string]string{
|
||||
AllowedProfilesAnnotationKey: "*",
|
||||
DefaultProfileAnnotationKey: "foo",
|
||||
}
|
||||
allowAnyAndSpecificDefault = map[string]string{
|
||||
AllowedProfilesAnnotationKey: "*,bar",
|
||||
DefaultProfileAnnotationKey: "foo",
|
||||
}
|
||||
allowSpecific = map[string]string{
|
||||
AllowedProfilesAnnotationKey: "foo",
|
||||
}
|
||||
allowSpecificLocalhost = map[string]string{
|
||||
AllowedProfilesAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + "foo",
|
||||
}
|
||||
allowSpecificDockerDefault = map[string]string{
|
||||
AllowedProfilesAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault,
|
||||
}
|
||||
allowSpecificRuntimeDefault = map[string]string{
|
||||
AllowedProfilesAnnotationKey: v1.SeccompProfileRuntimeDefault,
|
||||
}
|
||||
)
|
||||
|
||||
func TestNewStrategy(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
annotations map[string]string
|
||||
expectedAllowedProfilesString string
|
||||
expectedAllowAny bool
|
||||
expectedAllowedProfiles map[string]bool
|
||||
expectedDefaultProfile string
|
||||
}{
|
||||
"no seccomp": {
|
||||
annotations: withoutSeccomp,
|
||||
expectedAllowAny: false,
|
||||
expectedAllowedProfilesString: "",
|
||||
expectedAllowedProfiles: nil,
|
||||
expectedDefaultProfile: "",
|
||||
},
|
||||
"allow any, no default": {
|
||||
annotations: allowAnyNoDefault,
|
||||
expectedAllowAny: true,
|
||||
expectedAllowedProfilesString: "*",
|
||||
expectedAllowedProfiles: map[string]bool{},
|
||||
expectedDefaultProfile: "",
|
||||
},
|
||||
"allow any, default": {
|
||||
annotations: allowAnyDefault,
|
||||
expectedAllowAny: true,
|
||||
expectedAllowedProfilesString: "*",
|
||||
expectedAllowedProfiles: map[string]bool{},
|
||||
expectedDefaultProfile: "foo",
|
||||
},
|
||||
"allow any and specific, default": {
|
||||
annotations: allowAnyAndSpecificDefault,
|
||||
expectedAllowAny: true,
|
||||
expectedAllowedProfilesString: "*,bar",
|
||||
expectedAllowedProfiles: map[string]bool{
|
||||
"bar": true,
|
||||
},
|
||||
expectedDefaultProfile: "foo",
|
||||
},
|
||||
}
|
||||
for k, v := range tests {
|
||||
s := NewStrategy(v.annotations)
|
||||
internalStrat, _ := s.(*strategy)
|
||||
|
||||
if internalStrat.allowAnyProfile != v.expectedAllowAny {
|
||||
t.Errorf("%s expected allowAnyProfile to be %t but found %t", k, v.expectedAllowAny, internalStrat.allowAnyProfile)
|
||||
}
|
||||
if internalStrat.allowedProfilesString != v.expectedAllowedProfilesString {
|
||||
t.Errorf("%s expected allowedProfilesString to be %s but found %s", k, v.expectedAllowedProfilesString, internalStrat.allowedProfilesString)
|
||||
}
|
||||
if internalStrat.defaultProfile != v.expectedDefaultProfile {
|
||||
t.Errorf("%s expected defaultProfile to be %s but found %s", k, v.expectedDefaultProfile, internalStrat.defaultProfile)
|
||||
}
|
||||
if !reflect.DeepEqual(v.expectedAllowedProfiles, internalStrat.allowedProfiles) {
|
||||
t.Errorf("%s expected expectedAllowedProfiles to be %#v but found %#v", k, v.expectedAllowedProfiles, internalStrat.allowedProfiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: nil,
|
||||
expectedProfile: "",
|
||||
},
|
||||
"no seccomp, pod annotations": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedProfile: "foo",
|
||||
},
|
||||
"seccomp with no default, no pod annotations": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
podAnnotations: nil,
|
||||
expectedProfile: "",
|
||||
},
|
||||
"seccomp with no default, pod annotations": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedProfile: "foo",
|
||||
},
|
||||
"seccomp with default, no pod annotations": {
|
||||
pspAnnotations: allowAnyDefault,
|
||||
podAnnotations: nil,
|
||||
expectedProfile: "foo",
|
||||
},
|
||||
"seccomp with default, pod annotations": {
|
||||
pspAnnotations: allowAnyDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "bar",
|
||||
},
|
||||
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, &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
|
||||
}
|
||||
if actual != v.expectedProfile {
|
||||
t.Errorf("%s expected profile %s but received %s", k, v.expectedProfile, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: nil,
|
||||
expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo",
|
||||
},
|
||||
"no pod annotations, no required profiles": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: nil,
|
||||
expectedError: "",
|
||||
},
|
||||
"valid pod annotations, required profiles": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"invalid pod annotations, required profiles": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "bar",
|
||||
},
|
||||
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
|
||||
},
|
||||
"pod annotations, no required profiles": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedError: "Forbidden: seccomp may not be set",
|
||||
},
|
||||
"pod annotations, allow any": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"no pod annotations, allow any": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
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: "",
|
||||
},
|
||||
"docker/default PSP annotation automatically allows runtime/default pods": {
|
||||
pspAnnotations: allowSpecificDockerDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault,
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"runtime/default PSP annotation automatically allows docker/default pods": {
|
||||
pspAnnotations: allowSpecificRuntimeDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault,
|
||||
},
|
||||
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)
|
||||
if v.expectedError == "" && len(errs) != 0 {
|
||||
t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error())
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 0 {
|
||||
t.Errorf("%s expected error %s but received none", k, v.expectedError)
|
||||
}
|
||||
if v.expectedError != "" && len(errs) > 1 {
|
||||
t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error())
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) {
|
||||
t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: nil,
|
||||
expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo",
|
||||
},
|
||||
"no pod annotations, no required profiles": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: nil,
|
||||
expectedError: "",
|
||||
},
|
||||
"valid pod annotations, required profiles": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"invalid pod annotations, required profiles": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompContainerAnnotationKeyPrefix + "container": "bar",
|
||||
},
|
||||
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
|
||||
},
|
||||
"pod annotations, no required profiles": {
|
||||
pspAnnotations: withoutSeccomp,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
|
||||
},
|
||||
expectedError: "Forbidden: seccomp may not be set",
|
||||
},
|
||||
"pod annotations, allow any": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"no pod annotations, allow any": {
|
||||
pspAnnotations: allowAnyNoDefault,
|
||||
podAnnotations: nil,
|
||||
expectedError: "",
|
||||
},
|
||||
"container inherits valid pod annotation": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "foo",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
"container inherits invalid pod annotation": {
|
||||
pspAnnotations: allowSpecific,
|
||||
podAnnotations: map[string]string{
|
||||
api.SeccompPodAnnotationKey: "bar",
|
||||
},
|
||||
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{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: v.podAnnotations,
|
||||
},
|
||||
}
|
||||
container := &api.Container{
|
||||
Name: "container",
|
||||
SecurityContext: &api.SecurityContext{
|
||||
SeccompProfile: v.seccompProfile,
|
||||
},
|
||||
}
|
||||
|
||||
s := NewStrategy(v.pspAnnotations)
|
||||
errs := s.ValidateContainer(pod, container)
|
||||
if v.expectedError == "" && len(errs) != 0 {
|
||||
t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error())
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 0 {
|
||||
t.Errorf("%s expected error %s but received none", k, v.expectedError)
|
||||
}
|
||||
if v.expectedError != "" && len(errs) > 1 {
|
||||
t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error())
|
||||
}
|
||||
if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) {
|
||||
t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 selinux contains code for validating and defaulting the SELinux
|
||||
// context of a pod according to a security policy.
|
||||
package selinux
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
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 selinux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/v1"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
)
|
||||
|
||||
type mustRunAs struct {
|
||||
opts *api.SELinuxOptions
|
||||
}
|
||||
|
||||
var _ SELinuxStrategy = &mustRunAs{}
|
||||
|
||||
func NewMustRunAs(options *policy.SELinuxStrategyOptions) (SELinuxStrategy, error) {
|
||||
if options == nil {
|
||||
return nil, fmt.Errorf("MustRunAs requires SELinuxContextStrategyOptions")
|
||||
}
|
||||
if options.SELinuxOptions == nil {
|
||||
return nil, fmt.Errorf("MustRunAs requires SELinuxOptions")
|
||||
}
|
||||
|
||||
internalSELinuxOptions := &api.SELinuxOptions{}
|
||||
if err := v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(options.SELinuxOptions, internalSELinuxOptions, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mustRunAs{
|
||||
opts: internalSELinuxOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates the SELinuxOptions based on constraint rules.
|
||||
func (s *mustRunAs) Generate(_ *api.Pod, _ *api.Container) (*api.SELinuxOptions, error) {
|
||||
return s.opts, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *mustRunAs) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, seLinux *api.SELinuxOptions) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if seLinux == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath, ""))
|
||||
return allErrs
|
||||
}
|
||||
if !equalLevels(s.opts.Level, seLinux.Level) {
|
||||
detail := fmt.Sprintf("must be %s", s.opts.Level)
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("level"), seLinux.Level, detail))
|
||||
}
|
||||
if seLinux.Role != s.opts.Role {
|
||||
detail := fmt.Sprintf("must be %s", s.opts.Role)
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("role"), seLinux.Role, detail))
|
||||
}
|
||||
if seLinux.Type != s.opts.Type {
|
||||
detail := fmt.Sprintf("must be %s", s.opts.Type)
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), seLinux.Type, detail))
|
||||
}
|
||||
if seLinux.User != s.opts.User {
|
||||
detail := fmt.Sprintf("must be %s", s.opts.User)
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("user"), seLinux.User, detail))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// equalLevels compares SELinux levels for equality.
|
||||
func equalLevels(expected, actual string) bool {
|
||||
if expected == actual {
|
||||
return true
|
||||
}
|
||||
// "s0:c6,c0" => [ "s0", "c6,c0" ]
|
||||
expectedParts := strings.SplitN(expected, ":", 2)
|
||||
actualParts := strings.SplitN(actual, ":", 2)
|
||||
|
||||
// both SELinux levels must be in a format "sX:cY"
|
||||
if len(expectedParts) != 2 || len(actualParts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !equalSensitivity(expectedParts[0], actualParts[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !equalCategories(expectedParts[1], actualParts[1]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// equalSensitivity compares sensitivities of the SELinux levels for equality.
|
||||
func equalSensitivity(expected, actual string) bool {
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
// equalCategories compares categories of the SELinux levels for equality.
|
||||
func equalCategories(expected, actual string) bool {
|
||||
expectedCategories := strings.Split(expected, ",")
|
||||
actualCategories := strings.Split(actual, ",")
|
||||
|
||||
sort.Strings(expectedCategories)
|
||||
sort.Strings(actualCategories)
|
||||
|
||||
return util.EqualStringSlices(expectedCategories, actualCategories)
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
/*
|
||||
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 selinux
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/v1"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMustRunAsOptions(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
opts *policy.SELinuxStrategyOptions
|
||||
pass bool
|
||||
}{
|
||||
"nil opts": {
|
||||
opts: nil,
|
||||
pass: false,
|
||||
},
|
||||
"invalid opts": {
|
||||
opts: &policy.SELinuxStrategyOptions{},
|
||||
pass: false,
|
||||
},
|
||||
"valid opts": {
|
||||
opts: &policy.SELinuxStrategyOptions{SELinuxOptions: &corev1.SELinuxOptions{}},
|
||||
pass: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
_, err := NewMustRunAs(tc.opts)
|
||||
if err != nil && tc.pass {
|
||||
t.Errorf("%s expected to pass but received error %#v", name, err)
|
||||
}
|
||||
if err == nil && !tc.pass {
|
||||
t.Errorf("%s expected to fail but did not receive an error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustRunAsGenerate(t *testing.T) {
|
||||
opts := &policy.SELinuxStrategyOptions{
|
||||
SELinuxOptions: &corev1.SELinuxOptions{
|
||||
User: "user",
|
||||
Role: "role",
|
||||
Type: "type",
|
||||
Level: "level",
|
||||
},
|
||||
}
|
||||
mustRunAs, err := NewMustRunAs(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
|
||||
}
|
||||
generated, err := mustRunAs.Generate(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating selinux %v", err)
|
||||
}
|
||||
internalSELinuxOptions := &api.SELinuxOptions{}
|
||||
v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(opts.SELinuxOptions, internalSELinuxOptions, nil)
|
||||
if !reflect.DeepEqual(generated, internalSELinuxOptions) {
|
||||
t.Errorf("generated selinux does not equal configured selinux")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustRunAsValidate(t *testing.T) {
|
||||
newValidOpts := func() *corev1.SELinuxOptions {
|
||||
return &corev1.SELinuxOptions{
|
||||
User: "user",
|
||||
Role: "role",
|
||||
Level: "s0:c0,c6",
|
||||
Type: "type",
|
||||
}
|
||||
}
|
||||
|
||||
newValidOptsWithLevel := func(level string) *corev1.SELinuxOptions {
|
||||
opts := newValidOpts()
|
||||
opts.Level = level
|
||||
return opts
|
||||
}
|
||||
|
||||
role := newValidOpts()
|
||||
role.Role = "invalid"
|
||||
|
||||
user := newValidOpts()
|
||||
user.User = "invalid"
|
||||
|
||||
seType := newValidOpts()
|
||||
seType.Type = "invalid"
|
||||
|
||||
validOpts := newValidOpts()
|
||||
|
||||
tests := map[string]struct {
|
||||
podSeLinux *corev1.SELinuxOptions
|
||||
pspSeLinux *corev1.SELinuxOptions
|
||||
expectedMsg string
|
||||
}{
|
||||
"invalid role": {
|
||||
podSeLinux: role,
|
||||
pspSeLinux: validOpts,
|
||||
expectedMsg: "role: Invalid value",
|
||||
},
|
||||
"invalid user": {
|
||||
podSeLinux: user,
|
||||
pspSeLinux: validOpts,
|
||||
expectedMsg: "user: Invalid value",
|
||||
},
|
||||
"levels are not equal": {
|
||||
podSeLinux: newValidOptsWithLevel("s0"),
|
||||
pspSeLinux: newValidOptsWithLevel("s0:c1,c2"),
|
||||
expectedMsg: "level: Invalid value",
|
||||
},
|
||||
"levels differ by sensitivity": {
|
||||
podSeLinux: newValidOptsWithLevel("s0:c6"),
|
||||
pspSeLinux: newValidOptsWithLevel("s1:c6"),
|
||||
expectedMsg: "level: Invalid value",
|
||||
},
|
||||
"levels differ by categories": {
|
||||
podSeLinux: newValidOptsWithLevel("s0:c0,c8"),
|
||||
pspSeLinux: newValidOptsWithLevel("s0:c1,c7"),
|
||||
expectedMsg: "level: Invalid value",
|
||||
},
|
||||
"valid": {
|
||||
podSeLinux: validOpts,
|
||||
pspSeLinux: validOpts,
|
||||
expectedMsg: "",
|
||||
},
|
||||
"valid with different order of categories": {
|
||||
podSeLinux: newValidOptsWithLevel("s0:c6,c0"),
|
||||
pspSeLinux: validOpts,
|
||||
expectedMsg: "",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
opts := &policy.SELinuxStrategyOptions{
|
||||
SELinuxOptions: tc.pspSeLinux,
|
||||
}
|
||||
mustRunAs, err := NewMustRunAs(opts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
internalSELinuxOptions := api.SELinuxOptions{}
|
||||
v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(tc.podSeLinux, &internalSELinuxOptions, nil)
|
||||
errs := mustRunAs.Validate(nil, nil, nil, &internalSELinuxOptions)
|
||||
//should've passed but didn't
|
||||
if len(tc.expectedMsg) == 0 && len(errs) > 0 {
|
||||
t.Errorf("%s expected no errors but received %v", name, errs)
|
||||
}
|
||||
//should've failed but didn't
|
||||
if len(tc.expectedMsg) != 0 && len(errs) == 0 {
|
||||
t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg)
|
||||
}
|
||||
//failed with additional messages
|
||||
if len(tc.expectedMsg) != 0 && len(errs) > 1 {
|
||||
t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
|
||||
}
|
||||
//check that we got the right message
|
||||
if len(tc.expectedMsg) != 0 && len(errs) == 1 {
|
||||
if !strings.Contains(errs[0].Error(), tc.expectedMsg) {
|
||||
t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
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 selinux
|
||||
|
||||
import (
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// runAsAny implements the SELinuxStrategy interface.
|
||||
type runAsAny struct{}
|
||||
|
||||
var _ SELinuxStrategy = &runAsAny{}
|
||||
|
||||
// NewRunAsAny provides a strategy that will return the configured se linux context or nil.
|
||||
func NewRunAsAny(options *policy.SELinuxStrategyOptions) (SELinuxStrategy, error) {
|
||||
return &runAsAny{}, nil
|
||||
}
|
||||
|
||||
// Generate creates the SELinuxOptions based on constraint rules.
|
||||
func (s *runAsAny) Generate(pod *api.Pod, container *api.Container) (*api.SELinuxOptions, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *runAsAny) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, options *api.SELinuxOptions) field.ErrorList {
|
||||
return field.ErrorList{}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
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 selinux
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunAsAnyOptions(t *testing.T) {
|
||||
_, err := NewRunAsAny(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
_, err = NewRunAsAny(&policy.SELinuxStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyGenerate(t *testing.T) {
|
||||
s, err := NewRunAsAny(&policy.SELinuxStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
uid, err := s.Generate(nil, nil)
|
||||
if uid != nil {
|
||||
t.Errorf("expected nil uid but got %v", *uid)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error generating uid %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyValidate(t *testing.T) {
|
||||
s, err := NewRunAsAny(&policy.SELinuxStrategyOptions{
|
||||
SELinuxOptions: &corev1.SELinuxOptions{
|
||||
Level: "foo",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
errs := s.Validate(nil, nil, nil, nil)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("unexpected errors validating with ")
|
||||
}
|
||||
s, err = NewRunAsAny(&policy.SELinuxStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
errs = s.Validate(nil, nil, nil, nil)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("unexpected errors validating %v", errs)
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
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 selinux
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// SELinuxStrategy defines the interface for all SELinux constraint strategies.
|
||||
type SELinuxStrategy interface {
|
||||
// Generate creates the SELinuxOptions based on constraint rules.
|
||||
Generate(pod *api.Pod, container *api.Container) (*api.SELinuxOptions, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, options *api.SELinuxOptions) field.ErrorList
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
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 sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// SafeSysctlAllowlist returns the allowlist of safe sysctls and safe sysctl patterns (ending in *).
|
||||
//
|
||||
// A sysctl is called safe iff
|
||||
// - it is namespaced in the container or the pod
|
||||
// - it is isolated, i.e. has no influence on any other pod on the same node.
|
||||
func SafeSysctlAllowlist() []string {
|
||||
return []string{
|
||||
"kernel.shm_rmid_forced",
|
||||
"net.ipv4.ip_local_port_range",
|
||||
"net.ipv4.tcp_syncookies",
|
||||
"net.ipv4.ping_group_range",
|
||||
"net.ipv4.ip_unprivileged_port_start",
|
||||
}
|
||||
}
|
||||
|
||||
// mustMatchPatterns implements the SysctlsStrategy interface
|
||||
type mustMatchPatterns struct {
|
||||
safeAllowlist []string
|
||||
allowedUnsafeSysctls []string
|
||||
forbiddenSysctls []string
|
||||
}
|
||||
|
||||
var (
|
||||
_ SysctlsStrategy = &mustMatchPatterns{}
|
||||
)
|
||||
|
||||
// NewMustMatchPatterns creates a new mustMatchPatterns strategy that will provide validation.
|
||||
// Passing nil means the default pattern, passing an empty list means to disallow all sysctls.
|
||||
func NewMustMatchPatterns(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls []string) SysctlsStrategy {
|
||||
return &mustMatchPatterns{
|
||||
safeAllowlist: safeAllowlist,
|
||||
allowedUnsafeSysctls: allowedUnsafeSysctls,
|
||||
forbiddenSysctls: forbiddenSysctls,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mustMatchPatterns) isForbidden(sysctlName string) bool {
|
||||
// Is the sysctl forbidden?
|
||||
for _, s := range s.forbiddenSysctls {
|
||||
if strings.HasSuffix(s, "*") {
|
||||
prefix := strings.TrimSuffix(s, "*")
|
||||
if strings.HasPrefix(sysctlName, prefix) {
|
||||
return true
|
||||
}
|
||||
} else if sysctlName == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *mustMatchPatterns) isSafe(sysctlName string) bool {
|
||||
for _, ws := range s.safeAllowlist {
|
||||
if sysctlName == ws {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *mustMatchPatterns) isAllowedUnsafe(sysctlName string) bool {
|
||||
for _, s := range s.allowedUnsafeSysctls {
|
||||
if strings.HasSuffix(s, "*") {
|
||||
prefix := strings.TrimSuffix(s, "*")
|
||||
if strings.HasPrefix(sysctlName, prefix) {
|
||||
return true
|
||||
}
|
||||
} else if sysctlName == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *mustMatchPatterns) Validate(pod *api.Pod) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
var sysctls []api.Sysctl
|
||||
if pod.Spec.SecurityContext != nil {
|
||||
sysctls = pod.Spec.SecurityContext.Sysctls
|
||||
}
|
||||
|
||||
fieldPath := field.NewPath("pod", "spec", "securityContext").Child("sysctls")
|
||||
|
||||
for i, sysctl := range sysctls {
|
||||
switch {
|
||||
case s.isForbidden(sysctl.Name):
|
||||
allErrs = append(allErrs, field.ErrorList{field.Forbidden(fieldPath.Index(i), fmt.Sprintf("sysctl %q is not allowed", sysctl.Name))}...)
|
||||
case s.isSafe(sysctl.Name):
|
||||
continue
|
||||
case s.isAllowedUnsafe(sysctl.Name):
|
||||
continue
|
||||
default:
|
||||
allErrs = append(allErrs, field.ErrorList{field.Forbidden(fieldPath.Index(i), fmt.Sprintf("unsafe sysctl %q is not allowed", sysctl.Name))}...)
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/*
|
||||
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 sysctl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
allowlist []string
|
||||
forbiddenSafe []string
|
||||
allowedUnsafe []string
|
||||
allowed []string
|
||||
disallowed []string
|
||||
}{
|
||||
// no container requests
|
||||
"with allow all": {
|
||||
allowlist: []string{"foo"},
|
||||
allowed: []string{"foo"},
|
||||
},
|
||||
"empty": {
|
||||
allowlist: []string{"foo"},
|
||||
forbiddenSafe: []string{"*"},
|
||||
disallowed: []string{"foo"},
|
||||
},
|
||||
"without wildcard": {
|
||||
allowlist: []string{"a", "a.b"},
|
||||
allowed: []string{"a", "a.b"},
|
||||
disallowed: []string{"b"},
|
||||
},
|
||||
"with catch-all wildcard and non-wildcard": {
|
||||
allowedUnsafe: []string{"a.b.c", "*"},
|
||||
allowed: []string{"a", "a.b", "a.b.c", "b"},
|
||||
},
|
||||
"without catch-all wildcard": {
|
||||
allowedUnsafe: []string{"a.*", "b.*", "c.d.e", "d.e.f.*"},
|
||||
allowed: []string{"a.b", "b.c", "c.d.e", "d.e.f.g.h"},
|
||||
disallowed: []string{"a", "b", "c", "c.d", "d.e", "d.e.f"},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
strategy := NewMustMatchPatterns(v.allowlist, v.allowedUnsafe, v.forbiddenSafe)
|
||||
|
||||
pod := &api.Pod{}
|
||||
errs := strategy.Validate(pod)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("%s: unexpected validaton errors for empty sysctls: %v", k, errs)
|
||||
}
|
||||
|
||||
testAllowed := func() {
|
||||
sysctls := []api.Sysctl{}
|
||||
for _, s := range v.allowed {
|
||||
sysctls = append(sysctls, api.Sysctl{
|
||||
Name: s,
|
||||
Value: "dummy",
|
||||
})
|
||||
}
|
||||
pod.Spec.SecurityContext = &api.PodSecurityContext{
|
||||
Sysctls: sysctls,
|
||||
}
|
||||
errs = strategy.Validate(pod)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("%s: unexpected validaton errors for sysctls: %v", k, errs)
|
||||
}
|
||||
}
|
||||
testDisallowed := func() {
|
||||
for _, s := range v.disallowed {
|
||||
pod.Spec.SecurityContext = &api.PodSecurityContext{
|
||||
Sysctls: []api.Sysctl{
|
||||
{
|
||||
Name: s,
|
||||
Value: "dummy",
|
||||
},
|
||||
},
|
||||
}
|
||||
errs = strategy.Validate(pod)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("%s: expected error for sysctl %q", k, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testAllowed()
|
||||
testDisallowed()
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
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 podsecuritypolicy
|
||||
|
||||
import (
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"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/seccomp"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
|
||||
)
|
||||
|
||||
// Provider provides the implementation to generate a new security
|
||||
// context based on constraints or validate an existing security context against constraints.
|
||||
type Provider interface {
|
||||
// MutatePod sets the default values of the required but not filled fields of the pod and all
|
||||
// containers in the pod.
|
||||
MutatePod(pod *api.Pod) error
|
||||
// ValidatePod ensures a pod and all its containers are in compliance with the given constraints.
|
||||
// ValidatePod MUST NOT mutate the pod.
|
||||
ValidatePod(pod *api.Pod) field.ErrorList
|
||||
// Get the name of the PSP that this provider was initialized with.
|
||||
GetPSPName() string
|
||||
}
|
||||
|
||||
// StrategyFactory abstracts how the strategies are created from the provider so that you may
|
||||
// implement your own custom strategies that may pull information from other resources as necessary.
|
||||
// For example, if you would like to populate the strategies with values from namespace annotations
|
||||
// you may create a factory with a client that can pull the namespace and populate the appropriate
|
||||
// values.
|
||||
type StrategyFactory interface {
|
||||
// CreateStrategies creates the strategies that a provider will use. The namespace argument
|
||||
// should be the namespace of the object being checked (the pod's namespace).
|
||||
CreateStrategies(psp *policy.PodSecurityPolicy, namespace string) (*ProviderStrategies, error)
|
||||
}
|
||||
|
||||
// ProviderStrategies is a holder for all strategies that the provider requires to be populated.
|
||||
type ProviderStrategies struct {
|
||||
RunAsUserStrategy user.RunAsUserStrategy
|
||||
RunAsGroupStrategy group.GroupStrategy
|
||||
SELinuxStrategy selinux.SELinuxStrategy
|
||||
AppArmorStrategy apparmor.Strategy
|
||||
FSGroupStrategy group.GroupStrategy
|
||||
SupplementalGroupStrategy group.GroupStrategy
|
||||
CapabilitiesStrategy capabilities.Strategy
|
||||
SysctlsStrategy sysctl.SysctlsStrategy
|
||||
SeccompStrategy seccomp.Strategy
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 user contains code for validating and defaulting the UID of a pod
|
||||
// or container according to a security policy.
|
||||
package user
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
)
|
||||
|
||||
// mustRunAs implements the RunAsUserStrategy interface
|
||||
type mustRunAs struct {
|
||||
opts *policy.RunAsUserStrategyOptions
|
||||
}
|
||||
|
||||
// NewMustRunAs provides a strategy that requires the container to run as a specific UID in a range.
|
||||
func NewMustRunAs(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
|
||||
if options == nil {
|
||||
return nil, fmt.Errorf("MustRunAs requires run as user options")
|
||||
}
|
||||
if len(options.Ranges) == 0 {
|
||||
return nil, fmt.Errorf("MustRunAs requires at least one range")
|
||||
}
|
||||
return &mustRunAs{
|
||||
opts: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate creates the uid based on policy rules. MustRunAs returns the first range's Min.
|
||||
func (s *mustRunAs) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
|
||||
return &s.opts.Ranges[0].Min, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *mustRunAs) Validate(scPath *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if runAsUser == nil {
|
||||
allErrs = append(allErrs, field.Required(scPath.Child("runAsUser"), ""))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
if !s.isValidUID(*runAsUser) {
|
||||
detail := fmt.Sprintf("must be in the ranges: %v", s.opts.Ranges)
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsUser"), *runAsUser, detail))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func (s *mustRunAs) isValidUID(id int64) bool {
|
||||
for _, rng := range s.opts.Ranges {
|
||||
if psputil.UserFallsInRange(id, rng) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewMustRunAs(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
opts *policy.RunAsUserStrategyOptions
|
||||
pass bool
|
||||
}{
|
||||
"nil opts": {
|
||||
opts: nil,
|
||||
pass: false,
|
||||
},
|
||||
"invalid opts": {
|
||||
opts: &policy.RunAsUserStrategyOptions{},
|
||||
pass: false,
|
||||
},
|
||||
"valid opts": {
|
||||
opts: &policy.RunAsUserStrategyOptions{
|
||||
Ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
},
|
||||
},
|
||||
pass: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
_, err := NewMustRunAs(tc.opts)
|
||||
if err != nil && tc.pass {
|
||||
t.Errorf("%s expected to pass but received error %#v", name, err)
|
||||
}
|
||||
if err == nil && !tc.pass {
|
||||
t.Errorf("%s expected to fail but did not receive an error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
opts := &policy.RunAsUserStrategyOptions{
|
||||
Ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
},
|
||||
}
|
||||
mustRunAs, err := NewMustRunAs(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
|
||||
}
|
||||
generated, err := mustRunAs.Generate(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating runAsUser %v", err)
|
||||
}
|
||||
if *generated != opts.Ranges[0].Min {
|
||||
t.Errorf("generated runAsUser does not equal configured runAsUser")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
opts := &policy.RunAsUserStrategyOptions{
|
||||
Ranges: []policy.IDRange{
|
||||
{Min: 1, Max: 1},
|
||||
{Min: 10, Max: 20},
|
||||
},
|
||||
}
|
||||
|
||||
validID := int64(15)
|
||||
invalidID := int64(21)
|
||||
|
||||
tests := map[string]struct {
|
||||
container *api.Container
|
||||
expectedMsg string
|
||||
}{
|
||||
"good container": {
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsUser: &validID,
|
||||
},
|
||||
},
|
||||
},
|
||||
"nil run as user": {
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsUser: nil,
|
||||
},
|
||||
},
|
||||
expectedMsg: "runAsUser: Required",
|
||||
},
|
||||
"invalid id": {
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsUser: &invalidID,
|
||||
},
|
||||
},
|
||||
expectedMsg: "runAsUser: Invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
mustRunAs, err := NewMustRunAs(opts)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err)
|
||||
continue
|
||||
}
|
||||
errs := mustRunAs.Validate(nil, nil, nil, tc.container.SecurityContext.RunAsNonRoot, tc.container.SecurityContext.RunAsUser)
|
||||
//should've passed but didn't
|
||||
if len(tc.expectedMsg) == 0 && len(errs) > 0 {
|
||||
t.Errorf("%s expected no errors but received %v", name, errs)
|
||||
}
|
||||
//should've failed but didn't
|
||||
if len(tc.expectedMsg) != 0 && len(errs) == 0 {
|
||||
t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg)
|
||||
}
|
||||
//failed with additional messages
|
||||
if len(tc.expectedMsg) != 0 && len(errs) > 1 {
|
||||
t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
|
||||
}
|
||||
//check that we got the right message
|
||||
if len(tc.expectedMsg) != 0 && len(errs) == 1 {
|
||||
if !strings.Contains(errs[0].Error(), tc.expectedMsg) {
|
||||
t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
type nonRoot struct{}
|
||||
|
||||
var _ RunAsUserStrategy = &nonRoot{}
|
||||
|
||||
func NewRunAsNonRoot(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
|
||||
return &nonRoot{}, nil
|
||||
}
|
||||
|
||||
// Generate creates the uid based on policy rules. This strategy does return a UID. It assumes
|
||||
// that the user will specify a UID or the container image specifies a UID.
|
||||
func (s *nonRoot) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy. Validation
|
||||
// of this will pass if either the UID is not set, assuming that the image will provided the UID
|
||||
// or if the UID is set it is not root. Validation will fail if RunAsNonRoot is set to false.
|
||||
// In order to work properly this assumes that the kubelet performs a final check on runAsUser
|
||||
// or the image UID when runAsUser is nil.
|
||||
func (s *nonRoot) Validate(scPath *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if runAsNonRoot == nil && runAsUser == nil {
|
||||
allErrs = append(allErrs, field.Required(scPath.Child("runAsNonRoot"), "must be true"))
|
||||
return allErrs
|
||||
}
|
||||
if runAsNonRoot != nil && *runAsNonRoot == false {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsNonRoot"), *runAsNonRoot, "must be true"))
|
||||
return allErrs
|
||||
}
|
||||
if runAsUser != nil && *runAsUser == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsUser"), *runAsUser, "running with the root UID is forbidden"))
|
||||
return allErrs
|
||||
}
|
||||
return allErrs
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
api "k8s.io/api/core/v1"
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNonRootOptions(t *testing.T) {
|
||||
_, err := NewRunAsNonRoot(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsNonRoot %v", err)
|
||||
}
|
||||
_, err = NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing NewRunAsNonRoot %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonRootGenerate(t *testing.T) {
|
||||
s, err := NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsNonRoot %v", err)
|
||||
}
|
||||
uid, err := s.Generate(nil, nil)
|
||||
if uid != nil {
|
||||
t.Errorf("expected nil uid but got %d", *uid)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error generating uid %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonRootValidate(t *testing.T) {
|
||||
goodUID := int64(1)
|
||||
badUID := int64(0)
|
||||
untrue := false
|
||||
unfalse := true
|
||||
s, err := NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
container *api.Container
|
||||
expectedErr bool
|
||||
msg string
|
||||
}{
|
||||
{
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsUser: &badUID,
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
msg: "in test case %d, expected errors from root uid but got none: %v",
|
||||
},
|
||||
{
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsUser: &goodUID,
|
||||
},
|
||||
},
|
||||
expectedErr: false,
|
||||
msg: "in test case %d, expected no errors from non-root uid but got %v",
|
||||
},
|
||||
{
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsNonRoot: &untrue,
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
msg: "in test case %d, expected errors from RunAsNonRoot but got none: %v",
|
||||
},
|
||||
{
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsNonRoot: &unfalse,
|
||||
RunAsUser: &goodUID,
|
||||
},
|
||||
},
|
||||
expectedErr: false,
|
||||
msg: "in test case %d, expected no errors from non-root uid but got %v",
|
||||
},
|
||||
{
|
||||
container: &api.Container{
|
||||
SecurityContext: &api.SecurityContext{
|
||||
RunAsNonRoot: nil,
|
||||
RunAsUser: nil,
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
msg: "in test case %d, expected errors from nil runAsNonRoot and nil runAsUser but got %v",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
errs := s.Validate(nil, nil, nil, tc.container.SecurityContext.RunAsNonRoot, tc.container.SecurityContext.RunAsUser)
|
||||
if (len(errs) == 0) == tc.expectedErr {
|
||||
t.Errorf(tc.msg, i, errs)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// runAsAny implements the interface RunAsUserStrategy.
|
||||
type runAsAny struct{}
|
||||
|
||||
var _ RunAsUserStrategy = &runAsAny{}
|
||||
|
||||
// NewRunAsAny provides a strategy that will return nil.
|
||||
func NewRunAsAny(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
|
||||
return &runAsAny{}, nil
|
||||
}
|
||||
|
||||
// Generate creates the uid based on policy rules.
|
||||
func (s *runAsAny) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *runAsAny) Validate(_ *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
|
||||
return field.ErrorList{}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
)
|
||||
|
||||
func TestRunAsAnyOptions(t *testing.T) {
|
||||
_, err := NewRunAsAny(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
_, err = NewRunAsAny(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyGenerate(t *testing.T) {
|
||||
s, err := NewRunAsAny(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
uid, err := s.Generate(nil, nil)
|
||||
if uid != nil {
|
||||
t.Errorf("expected nil uid but got %d", *uid)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error generating uid %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAsAnyValidate(t *testing.T) {
|
||||
s, err := NewRunAsAny(&policy.RunAsUserStrategyOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
|
||||
}
|
||||
errs := s.Validate(nil, nil, nil, nil, nil)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("unexpected errors validating with ")
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
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 user
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// RunAsUserStrategy defines the interface for all uid constraint strategies.
|
||||
type RunAsUserStrategy interface {
|
||||
// Generate creates the uid based on policy rules.
|
||||
Generate(pod *api.Pod, container *api.Container) (*int64, error)
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
// scPath is the field path to the container's security context
|
||||
Validate(scPath *field.Path, pod *api.Pod, container *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 util contains utility code shared amongst different parts of the
|
||||
// pod security policy apparatus.
|
||||
package util
|
@ -1,276 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
const (
|
||||
ValidatedPSPAnnotation = "kubernetes.io/psp"
|
||||
)
|
||||
|
||||
// GetAllFSTypesExcept returns the result of GetAllFSTypesAsSet minus
|
||||
// the given exceptions.
|
||||
func GetAllFSTypesExcept(exceptions ...string) sets.String {
|
||||
fstypes := GetAllFSTypesAsSet()
|
||||
for _, e := range exceptions {
|
||||
fstypes.Delete(e)
|
||||
}
|
||||
return fstypes
|
||||
}
|
||||
|
||||
// GetAllFSTypesAsSet returns all actual volume types, regardless
|
||||
// of feature gates. The special policy.All pseudo type is not included.
|
||||
func GetAllFSTypesAsSet() sets.String {
|
||||
fstypes := sets.NewString()
|
||||
fstypes.Insert(
|
||||
string(policy.HostPath),
|
||||
string(policy.AzureFile),
|
||||
string(policy.Flocker),
|
||||
string(policy.FlexVolume),
|
||||
string(policy.EmptyDir),
|
||||
string(policy.GCEPersistentDisk),
|
||||
string(policy.AWSElasticBlockStore),
|
||||
string(policy.GitRepo),
|
||||
string(policy.Secret),
|
||||
string(policy.NFS),
|
||||
string(policy.ISCSI),
|
||||
string(policy.Glusterfs),
|
||||
string(policy.PersistentVolumeClaim),
|
||||
string(policy.RBD),
|
||||
string(policy.Cinder),
|
||||
string(policy.CephFS),
|
||||
string(policy.DownwardAPI),
|
||||
string(policy.FC),
|
||||
string(policy.ConfigMap),
|
||||
string(policy.VsphereVolume),
|
||||
string(policy.Quobyte),
|
||||
string(policy.AzureDisk),
|
||||
string(policy.PhotonPersistentDisk),
|
||||
string(policy.StorageOS),
|
||||
string(policy.Projected),
|
||||
string(policy.PortworxVolume),
|
||||
string(policy.ScaleIO),
|
||||
string(policy.CSI),
|
||||
string(policy.Ephemeral),
|
||||
)
|
||||
return fstypes
|
||||
}
|
||||
|
||||
// getVolumeFSType gets the FSType for a volume.
|
||||
func GetVolumeFSType(v api.Volume) (policy.FSType, error) {
|
||||
switch {
|
||||
case v.HostPath != nil:
|
||||
return policy.HostPath, nil
|
||||
case v.EmptyDir != nil:
|
||||
return policy.EmptyDir, nil
|
||||
case v.GCEPersistentDisk != nil:
|
||||
return policy.GCEPersistentDisk, nil
|
||||
case v.AWSElasticBlockStore != nil:
|
||||
return policy.AWSElasticBlockStore, nil
|
||||
case v.GitRepo != nil:
|
||||
return policy.GitRepo, nil
|
||||
case v.Secret != nil:
|
||||
return policy.Secret, nil
|
||||
case v.NFS != nil:
|
||||
return policy.NFS, nil
|
||||
case v.ISCSI != nil:
|
||||
return policy.ISCSI, nil
|
||||
case v.Glusterfs != nil:
|
||||
return policy.Glusterfs, nil
|
||||
case v.PersistentVolumeClaim != nil:
|
||||
return policy.PersistentVolumeClaim, nil
|
||||
case v.RBD != nil:
|
||||
return policy.RBD, nil
|
||||
case v.FlexVolume != nil:
|
||||
return policy.FlexVolume, nil
|
||||
case v.Cinder != nil:
|
||||
return policy.Cinder, nil
|
||||
case v.CephFS != nil:
|
||||
return policy.CephFS, nil
|
||||
case v.Flocker != nil:
|
||||
return policy.Flocker, nil
|
||||
case v.DownwardAPI != nil:
|
||||
return policy.DownwardAPI, nil
|
||||
case v.FC != nil:
|
||||
return policy.FC, nil
|
||||
case v.AzureFile != nil:
|
||||
return policy.AzureFile, nil
|
||||
case v.ConfigMap != nil:
|
||||
return policy.ConfigMap, nil
|
||||
case v.VsphereVolume != nil:
|
||||
return policy.VsphereVolume, nil
|
||||
case v.Quobyte != nil:
|
||||
return policy.Quobyte, nil
|
||||
case v.AzureDisk != nil:
|
||||
return policy.AzureDisk, nil
|
||||
case v.PhotonPersistentDisk != nil:
|
||||
return policy.PhotonPersistentDisk, nil
|
||||
case v.StorageOS != nil:
|
||||
return policy.StorageOS, nil
|
||||
case v.Projected != nil:
|
||||
return policy.Projected, nil
|
||||
case v.PortworxVolume != nil:
|
||||
return policy.PortworxVolume, nil
|
||||
case v.ScaleIO != nil:
|
||||
return policy.ScaleIO, nil
|
||||
case v.CSI != nil:
|
||||
return policy.CSI, nil
|
||||
case v.Ephemeral != nil:
|
||||
return policy.Ephemeral, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unknown volume type for volume: %#v", v)
|
||||
}
|
||||
|
||||
// FSTypeToStringSet converts an FSType slice to a string set.
|
||||
func FSTypeToStringSet(fsTypes []policy.FSType) sets.String {
|
||||
set := sets.NewString()
|
||||
for _, v := range fsTypes {
|
||||
set.Insert(string(v))
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// PSPAllowsAllVolumes checks for FSTypeAll in the psp's allowed volumes.
|
||||
func PSPAllowsAllVolumes(psp *policy.PodSecurityPolicy) bool {
|
||||
return PSPAllowsFSType(psp, policy.All)
|
||||
}
|
||||
|
||||
// PSPAllowsFSType is a utility for checking if a PSP allows a particular FSType.
|
||||
// If all volumes are allowed then this will return true for any FSType passed.
|
||||
func PSPAllowsFSType(psp *policy.PodSecurityPolicy, fsType policy.FSType) bool {
|
||||
if psp == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range psp.Spec.Volumes {
|
||||
if v == fsType || v == policy.All {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserFallsInRange is a utility to determine it the id falls in the valid range.
|
||||
func UserFallsInRange(id int64, rng policy.IDRange) bool {
|
||||
return id >= rng.Min && id <= rng.Max
|
||||
}
|
||||
|
||||
// GroupFallsInRange is a utility to determine it the id falls in the valid range.
|
||||
func GroupFallsInRange(id int64, rng policy.IDRange) bool {
|
||||
return id >= rng.Min && id <= rng.Max
|
||||
}
|
||||
|
||||
// AllowsHostVolumePath is a utility for checking if a PSP allows the host volume path.
|
||||
// This only checks the path. You should still check to make sure the host volume fs type is allowed.
|
||||
func AllowsHostVolumePath(psp *policy.PodSecurityPolicy, hostPath string) (pathIsAllowed, mustBeReadOnly bool) {
|
||||
if psp == nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// If no allowed paths are specified then allow any path
|
||||
if len(psp.Spec.AllowedHostPaths) == 0 {
|
||||
return true, false
|
||||
}
|
||||
|
||||
for _, allowedPath := range psp.Spec.AllowedHostPaths {
|
||||
if hasPathPrefix(hostPath, allowedPath.PathPrefix) {
|
||||
if !allowedPath.ReadOnly {
|
||||
return true, allowedPath.ReadOnly
|
||||
}
|
||||
pathIsAllowed = true
|
||||
mustBeReadOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
return pathIsAllowed, mustBeReadOnly
|
||||
}
|
||||
|
||||
// hasPathPrefix returns true if the string matches pathPrefix exactly, or if is prefixed with pathPrefix at a path segment boundary
|
||||
// the string and pathPrefix are both normalized to remove trailing slashes prior to checking.
|
||||
func hasPathPrefix(s, pathPrefix string) bool {
|
||||
|
||||
s = strings.TrimSuffix(s, "/")
|
||||
pathPrefix = strings.TrimSuffix(pathPrefix, "/")
|
||||
|
||||
// Short circuit if s doesn't contain the prefix at all
|
||||
if !strings.HasPrefix(s, pathPrefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
pathPrefixLength := len(pathPrefix)
|
||||
|
||||
if len(s) == pathPrefixLength {
|
||||
// Exact match
|
||||
return true
|
||||
}
|
||||
|
||||
if s[pathPrefixLength:pathPrefixLength+1] == "/" {
|
||||
// The next character in s is a path segment boundary
|
||||
// Check this instead of normalizing pathPrefix to avoid allocating on every call
|
||||
// Example where this check applies: s=/foo/bar and pathPrefix=/foo
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// EqualStringSlices compares string slices for equality. Slices are equal when
|
||||
// their sizes and elements on similar positions are equal.
|
||||
func EqualStringSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsOnlyServiceAccountTokenSources(v *api.ProjectedVolumeSource) bool {
|
||||
for _, s := range v.Sources {
|
||||
// reject any projected source that does not match any of our expected source types
|
||||
if s.ServiceAccountToken == nil && s.ConfigMap == nil && s.DownwardAPI == nil {
|
||||
return false
|
||||
}
|
||||
if t := s.ServiceAccountToken; t != nil && (t.Path != "token" || t.Audience != "") {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.ConfigMap != nil && s.ConfigMap.LocalObjectReference.Name != "kube-root-ca.crt" {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.DownwardAPI != nil {
|
||||
for _, d := range s.DownwardAPI.Items {
|
||||
if d.Path != "namespace" || d.FieldRef == nil || d.FieldRef.APIVersion != "v1" || d.FieldRef.FieldPath != "metadata.namespace" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
@ -1,471 +0,0 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
policy "k8s.io/api/policy/v1beta1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
)
|
||||
|
||||
// TestVolumeSourceFSTypeDrift ensures that for every known type of volume source (by the fields on
|
||||
// a VolumeSource object that GetVolumeFSType is returning a good value. This ensures both that we're
|
||||
// returning an FSType for the VolumeSource field (protect the GetVolumeFSType method) and that we
|
||||
// haven't drifted (ensure new fields in VolumeSource are covered).
|
||||
func TestVolumeSourceFSTypeDrift(t *testing.T) {
|
||||
allFSTypes := GetAllFSTypesAsSet()
|
||||
val := reflect.ValueOf(api.VolumeSource{})
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
fieldVal := val.Type().Field(i)
|
||||
|
||||
volumeSource := api.VolumeSource{}
|
||||
volumeSourceVolume := reflect.New(fieldVal.Type.Elem())
|
||||
|
||||
reflect.ValueOf(&volumeSource).Elem().FieldByName(fieldVal.Name).Set(volumeSourceVolume)
|
||||
|
||||
fsType, err := GetVolumeFSType(api.Volume{VolumeSource: volumeSource})
|
||||
if err != nil {
|
||||
t.Errorf("error getting fstype for field %s. This likely means that drift has occurred between FSType and VolumeSource. Please update the api and getVolumeFSType", fieldVal.Name)
|
||||
}
|
||||
|
||||
if !allFSTypes.Has(string(fsType)) {
|
||||
t.Errorf("%s was missing from GetFSTypesAsSet", fsType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPSPAllowsFSType(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
psp *policy.PodSecurityPolicy
|
||||
fsType policy.FSType
|
||||
allows bool
|
||||
}{
|
||||
"nil psp": {
|
||||
psp: nil,
|
||||
fsType: policy.HostPath,
|
||||
allows: false,
|
||||
},
|
||||
"empty volumes": {
|
||||
psp: &policy.PodSecurityPolicy{},
|
||||
fsType: policy.HostPath,
|
||||
allows: false,
|
||||
},
|
||||
"non-matching": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
Volumes: []policy.FSType{policy.AWSElasticBlockStore},
|
||||
},
|
||||
},
|
||||
fsType: policy.HostPath,
|
||||
allows: false,
|
||||
},
|
||||
"match on FSTypeAll": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
Volumes: []policy.FSType{policy.All},
|
||||
},
|
||||
},
|
||||
fsType: policy.HostPath,
|
||||
allows: true,
|
||||
},
|
||||
"match on direct match": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
Volumes: []policy.FSType{policy.HostPath},
|
||||
},
|
||||
},
|
||||
fsType: policy.HostPath,
|
||||
allows: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
allows := PSPAllowsFSType(v.psp, v.fsType)
|
||||
if v.allows != allows {
|
||||
t.Errorf("%s expected PSPAllowsFSType to return %t but got %t", k, v.allows, allows)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowsHostVolumePath(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
psp *policy.PodSecurityPolicy
|
||||
path string
|
||||
allows bool
|
||||
mustBeReadOnly bool
|
||||
}{
|
||||
"nil psp": {
|
||||
psp: nil,
|
||||
path: "/test",
|
||||
allows: false,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
"empty allowed paths": {
|
||||
psp: &policy.PodSecurityPolicy{},
|
||||
path: "/test",
|
||||
allows: true,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
"non-matching": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{
|
||||
PathPrefix: "/foo",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foobar",
|
||||
allows: false,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
"match on direct match": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{
|
||||
PathPrefix: "/foo",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foo",
|
||||
allows: true,
|
||||
mustBeReadOnly: true,
|
||||
},
|
||||
"match with trailing slash on host path": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{PathPrefix: "/foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foo/",
|
||||
allows: true,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
"match with trailing slash on allowed path": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{PathPrefix: "/foo/"},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foo",
|
||||
allows: true,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
"match child directory": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{
|
||||
PathPrefix: "/foo/",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foo/bar",
|
||||
allows: true,
|
||||
mustBeReadOnly: true,
|
||||
},
|
||||
"non-matching parent directory": {
|
||||
psp: &policy.PodSecurityPolicy{
|
||||
Spec: policy.PodSecurityPolicySpec{
|
||||
AllowedHostPaths: []policy.AllowedHostPath{
|
||||
{PathPrefix: "/foo/bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
path: "/foo",
|
||||
allows: false,
|
||||
mustBeReadOnly: false,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
allows, mustBeReadOnly := AllowsHostVolumePath(v.psp, v.path)
|
||||
if v.allows != allows {
|
||||
t.Errorf("allows: %s expected %t but got %t", k, v.allows, allows)
|
||||
}
|
||||
if v.mustBeReadOnly != mustBeReadOnly {
|
||||
t.Errorf("mustBeReadOnly: %s expected %t but got %t", k, v.mustBeReadOnly, mustBeReadOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualStringSlices(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
arg1 []string
|
||||
arg2 []string
|
||||
expectedResult bool
|
||||
}{
|
||||
"nil equals to nil": {
|
||||
arg1: nil,
|
||||
arg2: nil,
|
||||
expectedResult: true,
|
||||
},
|
||||
"equal by size": {
|
||||
arg1: []string{"1", "1"},
|
||||
arg2: []string{"1", "1"},
|
||||
expectedResult: true,
|
||||
},
|
||||
"not equal by size": {
|
||||
arg1: []string{"1"},
|
||||
arg2: []string{"1", "1"},
|
||||
expectedResult: false,
|
||||
},
|
||||
"not equal by elements": {
|
||||
arg1: []string{"1", "1"},
|
||||
arg2: []string{"1", "2"},
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
if result := EqualStringSlices(v.arg1, v.arg2); result != v.expectedResult {
|
||||
t.Errorf("%s expected to return %t but got %t", k, v.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOnlyServiceAccountTokenSources(t *testing.T) {
|
||||
serviceAccountToken := api.VolumeProjection{
|
||||
ServiceAccountToken: &api.ServiceAccountTokenProjection{
|
||||
Path: "token",
|
||||
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
|
||||
}}
|
||||
configMap := api.VolumeProjection{
|
||||
ConfigMap: &api.ConfigMapProjection{
|
||||
LocalObjectReference: api.LocalObjectReference{
|
||||
Name: "kube-root-ca.crt",
|
||||
},
|
||||
Items: []api.KeyToPath{
|
||||
{
|
||||
Key: "ca.crt",
|
||||
Path: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
downwardAPI := api.VolumeProjection{
|
||||
DownwardAPI: &api.DownwardAPIProjection{
|
||||
Items: []api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "namespace",
|
||||
FieldRef: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
volume *api.ProjectedVolumeSource
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "deny if ServiceAccountToken has wrong path",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
{ServiceAccountToken: &api.ServiceAccountTokenProjection{
|
||||
Path: "notatoken",
|
||||
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
|
||||
}},
|
||||
configMap,
|
||||
downwardAPI,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if ServiceAccountToken has wrong audience",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
{ServiceAccountToken: &api.ServiceAccountTokenProjection{
|
||||
Path: "token",
|
||||
Audience: "not api server",
|
||||
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
|
||||
}},
|
||||
configMap,
|
||||
downwardAPI,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if CondigMap has wrong LocalObjectReference.Name",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
serviceAccountToken,
|
||||
{
|
||||
ConfigMap: &api.ConfigMapProjection{
|
||||
LocalObjectReference: api.LocalObjectReference{
|
||||
Name: "foo-ca.crt",
|
||||
},
|
||||
Items: []api.KeyToPath{
|
||||
{
|
||||
Key: "ca.crt",
|
||||
Path: "ca.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
downwardAPI,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if DownwardAPI has wrong path",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
serviceAccountToken,
|
||||
configMap,
|
||||
{
|
||||
DownwardAPI: &api.DownwardAPIProjection{
|
||||
Items: []api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "foo",
|
||||
FieldRef: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if DownwardAPI has nil field ref",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
serviceAccountToken,
|
||||
configMap,
|
||||
{
|
||||
DownwardAPI: &api.DownwardAPIProjection{
|
||||
Items: []api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if DownwardAPI has wrong api version",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
serviceAccountToken,
|
||||
configMap,
|
||||
{
|
||||
DownwardAPI: &api.DownwardAPIProjection{
|
||||
Items: []api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "namespace",
|
||||
FieldRef: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1beta1",
|
||||
FieldPath: "metadata.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if DownwardAPI has wrong field path",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
serviceAccountToken,
|
||||
configMap,
|
||||
{
|
||||
DownwardAPI: &api.DownwardAPIProjection{
|
||||
Items: []api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "namespace",
|
||||
FieldRef: &api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if Secret exists",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
{
|
||||
Secret: &api.SecretProjection{},
|
||||
},
|
||||
configMap,
|
||||
downwardAPI,
|
||||
serviceAccountToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deny if none of ServiceAccountToken, ConfigMap and DownwardAPI exist",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "allow if any of ServiceAccountToken, ConfigMap and DownwardAPI matches",
|
||||
volume: &api.ProjectedVolumeSource{
|
||||
Sources: []api.VolumeProjection{
|
||||
configMap,
|
||||
downwardAPI,
|
||||
serviceAccountToken,
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := IsOnlyServiceAccountTokenSources(test.volume); got != test.want {
|
||||
t.Errorf("IsOnlyServiceAccountTokenSources(%+v) = %v, want %v", test.volume, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-auth-policy-approvers
|
||||
reviewers:
|
||||
- sig-auth-policy-reviewers
|
||||
labels:
|
||||
- sig/auth
|
@ -1,380 +0,0 @@
|
||||
/*
|
||||
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 podsecuritypolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/informers"
|
||||
policylisters "k8s.io/client-go/listers/policy/v1beta1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/apis/policy"
|
||||
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
|
||||
psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
)
|
||||
|
||||
// PluginName is a string with the name of the plugin
|
||||
const PluginName = "PodSecurityPolicy"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
plugin := newPlugin(psp.NewSimpleStrategyFactory(), true)
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin holds state for and implements the admission plugin.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
strategyFactory psp.StrategyFactory
|
||||
failOnNoPolicies bool
|
||||
authz authorizer.Authorizer
|
||||
lister policylisters.PodSecurityPolicyLister
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
p.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (p *Plugin) ValidateInitialization() error {
|
||||
if p.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
if p.lister == nil {
|
||||
return fmt.Errorf("%s requires a lister", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.MutationInterface = &Plugin{}
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
|
||||
var _ genericadmissioninit.WantsExternalKubeInformerFactory = &Plugin{}
|
||||
var auditKeyPrefix = strings.ToLower(PluginName) + "." + policy.GroupName + ".k8s.io"
|
||||
|
||||
// newPlugin creates a new PSP admission plugin.
|
||||
func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
strategyFactory: strategyFactory,
|
||||
failOnNoPolicies: failOnNoPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory registers an informer
|
||||
func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
podSecurityPolicyInformer := f.Policy().V1beta1().PodSecurityPolicies()
|
||||
p.lister = podSecurityPolicyInformer.Lister()
|
||||
p.SetReadyFunc(podSecurityPolicyInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
// Admit determines if the pod should be admitted based on the requested security context
|
||||
// and the available PSPs.
|
||||
//
|
||||
// 1. Find available PSPs.
|
||||
// 2. Create the providers, includes setting pre-allocated values if necessary.
|
||||
// 3. Try to generate and validate a PSP with providers. If we find one then admit the pod
|
||||
// with the validated PSP. If we don't find any reject the pod and give all errors from the
|
||||
// failed attempts.
|
||||
func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
if ignore, err := shouldIgnore(a); err != nil {
|
||||
return err
|
||||
} else if ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
// only mutate if this is a CREATE request. On updates we only validate.
|
||||
if a.GetOperation() != admission.Create {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
|
||||
// compute the context. Mutation is allowed. ValidatedPSPAnnotation is not taken into account.
|
||||
allowedPod, pspName, validationErrs, err := p.computeSecurityContext(ctx, a, pod, true, "")
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: %w", err))
|
||||
}
|
||||
if allowedPod != nil {
|
||||
*pod = *allowedPod
|
||||
// annotate and accept the pod
|
||||
klog.V(4).Infof("pod %s (generate: %s) in namespace %s validated against provider %s", pod.Name, pod.GenerateName, a.GetNamespace(), pspName)
|
||||
if pod.ObjectMeta.Annotations == nil {
|
||||
pod.ObjectMeta.Annotations = map[string]string{}
|
||||
}
|
||||
pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = pspName
|
||||
key := auditKeyPrefix + "/" + "admit-policy"
|
||||
if err := a.AddAnnotation(key, pspName); err != nil {
|
||||
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
||||
klog.V(4).Infof("unable to admit pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
||||
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: unable to admit pod: %v", validationErrs))
|
||||
}
|
||||
|
||||
// Validate verifies attributes against the PodSecurityPolicy
|
||||
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
if ignore, err := shouldIgnore(a); err != nil {
|
||||
return err
|
||||
} else if ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
|
||||
// compute the context. Mutation is not allowed. ValidatedPSPAnnotation is used as a hint to gain same speed-up.
|
||||
allowedPod, pspName, validationErrs, err := p.computeSecurityContext(ctx, a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: %w", err))
|
||||
}
|
||||
if apiequality.Semantic.DeepEqual(pod, allowedPod) {
|
||||
key := auditKeyPrefix + "/" + "validate-policy"
|
||||
if err := a.AddAnnotation(key, pspName); err != nil {
|
||||
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
||||
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
||||
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: unable to validate pod: %v", validationErrs))
|
||||
}
|
||||
|
||||
func shouldIgnore(a admission.Attributes) (bool, error) {
|
||||
if a.GetResource().GroupResource() != api.Resource("pods") {
|
||||
return true, nil
|
||||
}
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if we can't convert then fail closed since we've already checked that this is supposed to be a pod object.
|
||||
// this shouldn't normally happen during admission but could happen if an integrator passes a versioned
|
||||
// pod object rather than an internal object.
|
||||
if _, ok := a.GetObject().(*api.Pod); !ok {
|
||||
return false, admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
||||
}
|
||||
|
||||
// if this is an update, see if we are only updating the ownerRef/finalizers. Garbage collection does this
|
||||
// and we should allow it in general, since you had the power to update and the power to delete.
|
||||
// The worst that happens is that you delete something, but you aren't controlling the privileged object itself
|
||||
if a.GetOperation() == admission.Update && rbacregistry.IsOnlyMutatingGCFields(a.GetObject(), a.GetOldObject(), apiequality.Semantic) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// computeSecurityContext derives a valid security context while trying to avoid any changes to the given pod. I.e.
|
||||
// if there is a matching policy with the same security context as given, it will be reused. If there is no
|
||||
// matching policy the returned pod will be nil and the pspName empty. validatedPSPHint is the validated psp name
|
||||
// saved in kubernetes.io/psp annotation. This psp is usually the one we are looking for.
|
||||
func (p *Plugin) computeSecurityContext(ctx context.Context, a admission.Attributes, pod *api.Pod, specMutationAllowed bool, validatedPSPHint string) (*api.Pod, string, field.ErrorList, error) {
|
||||
// get all constraints that are usable by the user
|
||||
klog.V(4).Infof("getting pod security policies for pod %s (generate: %s)", pod.Name, pod.GenerateName)
|
||||
var saInfo user.Info
|
||||
if len(pod.Spec.ServiceAccountName) > 0 {
|
||||
saInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "")
|
||||
}
|
||||
|
||||
policies, err := p.lister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
// if we have no policies and want to succeed then return. Otherwise we'll end up with no
|
||||
// providers and fail with "unable to validate against any pod security policy" below.
|
||||
if len(policies) == 0 && !p.failOnNoPolicies {
|
||||
return pod, "", nil, nil
|
||||
}
|
||||
|
||||
// sort policies by name to make order deterministic
|
||||
// If mutation is not allowed and validatedPSPHint is provided, check the validated policy first.
|
||||
sort.SliceStable(policies, func(i, j int) bool {
|
||||
if !specMutationAllowed {
|
||||
if policies[i].Name == validatedPSPHint {
|
||||
return true
|
||||
}
|
||||
if policies[j].Name == validatedPSPHint {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return strings.Compare(policies[i].Name, policies[j].Name) < 0
|
||||
})
|
||||
|
||||
providers, errs := p.createProvidersFromPolicies(policies, pod.Namespace)
|
||||
for _, err := range errs {
|
||||
klog.V(4).Infof("provider creation error: %v", err)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, "", nil, fmt.Errorf("no providers available to validate pod request")
|
||||
}
|
||||
|
||||
var (
|
||||
allowedMutatedPod *api.Pod
|
||||
allowingMutatingPSP string
|
||||
// Map of PSP name to associated validation errors.
|
||||
validationErrs = map[string]field.ErrorList{}
|
||||
)
|
||||
|
||||
for _, provider := range providers {
|
||||
podCopy := pod.DeepCopy()
|
||||
|
||||
if errs := assignSecurityContext(provider, podCopy); len(errs) > 0 {
|
||||
validationErrs[provider.GetPSPName()] = errs
|
||||
continue
|
||||
}
|
||||
|
||||
// the entire pod validated
|
||||
mutated := !apiequality.Semantic.DeepEqual(pod, podCopy)
|
||||
if mutated && !specMutationAllowed {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isAuthorizedForPolicy(ctx, a.GetUserInfo(), saInfo, a.GetNamespace(), provider.GetPSPName(), p.authz) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case !mutated:
|
||||
// if it validated without mutating anything, use this result
|
||||
return podCopy, provider.GetPSPName(), nil, nil
|
||||
|
||||
case specMutationAllowed && allowedMutatedPod == nil:
|
||||
// if mutation is allowed and this is the first PSP to allow the pod, remember it,
|
||||
// but continue to see if another PSP allows without mutating
|
||||
allowedMutatedPod = podCopy
|
||||
allowingMutatingPSP = provider.GetPSPName()
|
||||
}
|
||||
}
|
||||
|
||||
if allowedMutatedPod != nil {
|
||||
return allowedMutatedPod, allowingMutatingPSP, nil, nil
|
||||
}
|
||||
|
||||
// Pod is rejected. Filter the validation errors to only include errors from authorized PSPs.
|
||||
aggregate := field.ErrorList{}
|
||||
for psp, errs := range validationErrs {
|
||||
if isAuthorizedForPolicy(ctx, a.GetUserInfo(), saInfo, a.GetNamespace(), psp, p.authz) {
|
||||
aggregate = append(aggregate, errs...)
|
||||
}
|
||||
}
|
||||
return nil, "", aggregate, nil
|
||||
}
|
||||
|
||||
// assignSecurityContext creates a security context for each container in the pod
|
||||
// and validates that the sc falls within the psp constraints. All containers must validate against
|
||||
// the same psp or is not considered valid.
|
||||
func assignSecurityContext(provider psp.Provider, pod *api.Pod) field.ErrorList {
|
||||
errs := field.ErrorList{}
|
||||
|
||||
if err := provider.MutatePod(pod); err != nil {
|
||||
// TODO(tallclair): MutatePod should return a field.ErrorList
|
||||
errs = append(errs, field.Invalid(field.NewPath(""), pod, err.Error()))
|
||||
}
|
||||
|
||||
errs = append(errs, provider.ValidatePod(pod)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// createProvidersFromPolicies creates providers from the constraints supplied.
|
||||
func (p *Plugin) createProvidersFromPolicies(psps []*policyv1beta1.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
|
||||
var (
|
||||
// collected providers
|
||||
providers []psp.Provider
|
||||
// collected errors to return
|
||||
errs []error
|
||||
)
|
||||
|
||||
for _, constraint := range psps {
|
||||
provider, err := psp.NewSimpleProvider(constraint, namespace, p.strategyFactory)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("error creating provider for PSP %s: %v", constraint.Name, err))
|
||||
continue
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
return providers, errs
|
||||
}
|
||||
|
||||
func isAuthorizedForPolicy(ctx context.Context, user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
|
||||
// Check the service account first, as that is the more common use case.
|
||||
return authorizedForPolicy(ctx, sa, namespace, policyName, authz) ||
|
||||
authorizedForPolicy(ctx, user, namespace, policyName, authz)
|
||||
}
|
||||
|
||||
// authorizedForPolicy returns true if info is authorized to perform the "use" verb on the policy resource.
|
||||
// TODO: check against only the policy group when PSP will be completely moved out of the extensions
|
||||
func authorizedForPolicy(ctx context.Context, info user.Info, namespace string, policyName string, authz authorizer.Authorizer) bool {
|
||||
// Check against extensions API group for backward compatibility
|
||||
return authorizedForPolicyInAPIGroup(ctx, info, namespace, policyName, policy.GroupName, authz) ||
|
||||
authorizedForPolicyInAPIGroup(ctx, info, namespace, policyName, extensions.GroupName, authz)
|
||||
}
|
||||
|
||||
// authorizedForPolicyInAPIGroup returns true if info is authorized to perform the "use" verb on the policy resource in the specified API group.
|
||||
func authorizedForPolicyInAPIGroup(ctx context.Context, info user.Info, namespace, policyName, apiGroupName string, authz authorizer.Authorizer) bool {
|
||||
if info == nil {
|
||||
return false
|
||||
}
|
||||
attr := buildAttributes(info, namespace, policyName, apiGroupName)
|
||||
decision, reason, err := authz.Authorize(ctx, attr)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("cannot authorize for policy: %v,%v", reason, err)
|
||||
}
|
||||
return (decision == authorizer.DecisionAllow)
|
||||
}
|
||||
|
||||
// buildAttributes builds an attributes record for a SAR based on the user info and policy.
|
||||
func buildAttributes(info user.Info, namespace, policyName, apiGroupName string) authorizer.Attributes {
|
||||
// check against the namespace that the pod is being created in to allow per-namespace PSP grants.
|
||||
attr := authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: "use",
|
||||
Namespace: namespace,
|
||||
Name: policyName,
|
||||
APIGroup: apiGroupName,
|
||||
APIVersion: "*",
|
||||
Resource: "podsecuritypolicies",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
return attr
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,367 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
|
||||
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
||||
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
|
||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||
admissionapi "k8s.io/pod-security-admission/api"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
|
||||
"github.com/onsi/ginkgo"
|
||||
)
|
||||
|
||||
const nobodyUser = int64(65534)
|
||||
|
||||
var _ = SIGDescribe("PodSecurityPolicy [Feature:PodSecurityPolicy]", func() {
|
||||
f := framework.NewDefaultFramework("podsecuritypolicy")
|
||||
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged
|
||||
f.SkipPrivilegedPSPBinding = true
|
||||
|
||||
// Client that will impersonate the default service account, in order to run
|
||||
// with reduced privileges.
|
||||
var c clientset.Interface
|
||||
var ns string // Test namespace, for convenience
|
||||
ginkgo.BeforeEach(func() {
|
||||
if !framework.IsPodSecurityPolicyEnabled(f.ClientSet) {
|
||||
framework.Failf("PodSecurityPolicy not enabled")
|
||||
return
|
||||
}
|
||||
if !e2eauth.IsRBACEnabled(f.ClientSet.RbacV1()) {
|
||||
e2eskipper.Skipf("RBAC not enabled")
|
||||
}
|
||||
ns = f.Namespace.Name
|
||||
|
||||
ginkgo.By("Creating a kubernetes client that impersonates the default service account")
|
||||
config, err := framework.LoadConfig()
|
||||
framework.ExpectNoError(err)
|
||||
config.Impersonate = restclient.ImpersonationConfig{
|
||||
UserName: serviceaccount.MakeUsername(ns, "default"),
|
||||
Groups: serviceaccount.MakeGroupNames(ns),
|
||||
}
|
||||
c, err = clientset.NewForConfig(config)
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
ginkgo.By("Binding the edit role to the default SA")
|
||||
err = e2eauth.BindClusterRole(f.ClientSet.RbacV1(), "edit", ns,
|
||||
rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Namespace: ns, Name: "default"})
|
||||
framework.ExpectNoError(err)
|
||||
})
|
||||
|
||||
ginkgo.It("should forbid pod creation when no PSP is available", func() {
|
||||
ginkgo.By("Running a restricted pod")
|
||||
_, err := c.CoreV1().Pods(ns).Create(context.TODO(), restrictedPod("restricted"), metav1.CreateOptions{})
|
||||
expectForbidden(err)
|
||||
})
|
||||
|
||||
ginkgo.It("should enforce the restricted policy.PodSecurityPolicy", func() {
|
||||
ginkgo.By("Creating & Binding a restricted policy for the test service account")
|
||||
_, cleanup := createAndBindPSP(f, restrictedPSP("restrictive"))
|
||||
defer cleanup()
|
||||
|
||||
ginkgo.By("Running a restricted pod")
|
||||
pod, err := c.CoreV1().Pods(ns).Create(context.TODO(), restrictedPod("allowed"), metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(c, pod.Name, pod.Namespace))
|
||||
|
||||
testPrivilegedPods(func(pod *v1.Pod) {
|
||||
_, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
|
||||
expectForbidden(err)
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.It("should allow pods under the privileged policy.PodSecurityPolicy", func() {
|
||||
ginkgo.By("Creating & Binding a privileged policy for the test service account")
|
||||
// Ensure that the permissive policy is used even in the presence of the restricted policy.
|
||||
_, cleanup := createAndBindPSP(f, restrictedPSP("restrictive"))
|
||||
defer cleanup()
|
||||
expectedPSP, cleanup := createAndBindPSP(f, privilegedPSP("permissive"))
|
||||
defer cleanup()
|
||||
|
||||
testPrivilegedPods(func(pod *v1.Pod) {
|
||||
p, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(c, p.Name, p.Namespace))
|
||||
|
||||
// Verify expected PSP was used.
|
||||
p, err = c.CoreV1().Pods(ns).Get(context.TODO(), p.Name, metav1.GetOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
validated, found := p.Annotations[psputil.ValidatedPSPAnnotation]
|
||||
framework.ExpectEqual(found, true, "PSP annotation not found")
|
||||
framework.ExpectEqual(validated, expectedPSP.Name, "Unexpected validated PSP")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func expectForbidden(err error) {
|
||||
framework.ExpectError(err, "should be forbidden")
|
||||
framework.ExpectEqual(apierrors.IsForbidden(err), true, "should be forbidden error")
|
||||
}
|
||||
|
||||
func testPrivilegedPods(tester func(pod *v1.Pod)) {
|
||||
ginkgo.By("Running a privileged pod", func() {
|
||||
privileged := restrictedPod("privileged")
|
||||
privileged.Spec.Containers[0].SecurityContext.Privileged = boolPtr(true)
|
||||
privileged.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation = nil
|
||||
tester(privileged)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a HostPath pod", func() {
|
||||
hostpath := restrictedPod("hostpath")
|
||||
hostpath.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{{
|
||||
Name: "hp",
|
||||
MountPath: "/hp",
|
||||
}}
|
||||
hostpath.Spec.Volumes = []v1.Volume{{
|
||||
Name: "hp",
|
||||
VolumeSource: v1.VolumeSource{
|
||||
HostPath: &v1.HostPathVolumeSource{Path: "/tmp"},
|
||||
},
|
||||
}}
|
||||
tester(hostpath)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a HostNetwork pod", func() {
|
||||
hostnet := restrictedPod("hostnet")
|
||||
hostnet.Spec.HostNetwork = true
|
||||
tester(hostnet)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a HostPID pod", func() {
|
||||
hostpid := restrictedPod("hostpid")
|
||||
hostpid.Spec.HostPID = true
|
||||
tester(hostpid)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a HostIPC pod", func() {
|
||||
hostipc := restrictedPod("hostipc")
|
||||
hostipc.Spec.HostIPC = true
|
||||
tester(hostipc)
|
||||
})
|
||||
|
||||
ginkgo.By("Running an unconfined Seccomp pod", func() {
|
||||
unconfined := restrictedPod("seccomp")
|
||||
unconfined.Annotations[v1.SeccompPodAnnotationKey] = "unconfined"
|
||||
tester(unconfined)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a SYS_ADMIN pod", func() {
|
||||
sysadmin := restrictedPod("sysadmin")
|
||||
sysadmin.Spec.Containers[0].SecurityContext.Capabilities = &v1.Capabilities{
|
||||
Add: []v1.Capability{"SYS_ADMIN"},
|
||||
}
|
||||
sysadmin.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation = nil
|
||||
tester(sysadmin)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a RunAsGroup pod", func() {
|
||||
sysadmin := restrictedPod("runasgroup")
|
||||
gid := int64(0)
|
||||
sysadmin.Spec.Containers[0].SecurityContext.RunAsGroup = &gid
|
||||
tester(sysadmin)
|
||||
})
|
||||
|
||||
ginkgo.By("Running a RunAsUser pod", func() {
|
||||
sysadmin := restrictedPod("runasuser")
|
||||
uid := int64(0)
|
||||
sysadmin.Spec.Containers[0].SecurityContext.RunAsUser = &uid
|
||||
tester(sysadmin)
|
||||
})
|
||||
}
|
||||
|
||||
// createAndBindPSP creates a PSP in the policy API group.
|
||||
func createAndBindPSP(f *framework.Framework, pspTemplate *policyv1beta1.PodSecurityPolicy) (psp *policyv1beta1.PodSecurityPolicy, cleanup func()) {
|
||||
// Create the PodSecurityPolicy object.
|
||||
psp = pspTemplate.DeepCopy()
|
||||
// Add the namespace to the name to ensure uniqueness and tie it to the namespace.
|
||||
ns := f.Namespace.Name
|
||||
name := fmt.Sprintf("%s-%s", ns, psp.Name)
|
||||
psp.Name = name
|
||||
psp, err := f.ClientSet.PolicyV1beta1().PodSecurityPolicies().Create(context.TODO(), psp, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err, "Failed to create PSP")
|
||||
|
||||
// Create the Role to bind it to the namespace.
|
||||
_, err = f.ClientSet.RbacV1().Roles(ns).Create(context.TODO(), &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{{
|
||||
APIGroups: []string{"policy"},
|
||||
Resources: []string{"podsecuritypolicies"},
|
||||
ResourceNames: []string{name},
|
||||
Verbs: []string{"use"},
|
||||
}},
|
||||
}, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err, "Failed to create PSP role")
|
||||
|
||||
// Bind the role to the namespace.
|
||||
err = e2eauth.BindRoleInNamespace(f.ClientSet.RbacV1(), name, ns, rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Namespace: ns,
|
||||
Name: "default",
|
||||
})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
framework.ExpectNoError(e2eauth.WaitForNamedAuthorizationUpdate(f.ClientSet.AuthorizationV1(),
|
||||
serviceaccount.MakeUsername(ns, "default"), ns, "use", name,
|
||||
schema.GroupResource{Group: "policy", Resource: "podsecuritypolicies"}, true))
|
||||
|
||||
return psp, func() {
|
||||
// Cleanup non-namespaced PSP object.
|
||||
f.ClientSet.PolicyV1beta1().PodSecurityPolicies().Delete(context.TODO(), name, metav1.DeleteOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
func restrictedPod(name string) *v1.Pod {
|
||||
return &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{
|
||||
v1.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault,
|
||||
v1.AppArmorBetaContainerAnnotationKeyPrefix + "pause": v1.AppArmorBetaProfileRuntimeDefault,
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{{
|
||||
Name: "pause",
|
||||
Image: imageutils.GetPauseImageName(),
|
||||
SecurityContext: &v1.SecurityContext{
|
||||
AllowPrivilegeEscalation: boolPtr(false),
|
||||
RunAsUser: utilpointer.Int64Ptr(nobodyUser),
|
||||
RunAsGroup: utilpointer.Int64Ptr(nobodyUser),
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// privilegedPSPInPolicy creates a PodSecurityPolicy (in the "policy" API Group) that allows everything.
|
||||
func privilegedPSP(name string) *policyv1beta1.PodSecurityPolicy {
|
||||
return &policyv1beta1.PodSecurityPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny},
|
||||
},
|
||||
Spec: policyv1beta1.PodSecurityPolicySpec{
|
||||
Privileged: true,
|
||||
AllowPrivilegeEscalation: utilpointer.BoolPtr(true),
|
||||
AllowedCapabilities: []v1.Capability{"*"},
|
||||
Volumes: []policyv1beta1.FSType{policyv1beta1.All},
|
||||
HostNetwork: true,
|
||||
HostPorts: []policyv1beta1.HostPortRange{{Min: 0, Max: 65535}},
|
||||
HostIPC: true,
|
||||
HostPID: true,
|
||||
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
|
||||
Rule: policyv1beta1.RunAsUserStrategyRunAsAny,
|
||||
},
|
||||
RunAsGroup: &policyv1beta1.RunAsGroupStrategyOptions{
|
||||
Rule: policyv1beta1.RunAsGroupStrategyRunAsAny,
|
||||
},
|
||||
SELinux: policyv1beta1.SELinuxStrategyOptions{
|
||||
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
|
||||
},
|
||||
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
|
||||
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
|
||||
},
|
||||
FSGroup: policyv1beta1.FSGroupStrategyOptions{
|
||||
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
|
||||
},
|
||||
ReadOnlyRootFilesystem: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// restrictedPSPInPolicy creates a PodSecurityPolicy (in the "policy" API Group) that is most strict.
|
||||
func restrictedPSP(name string) *policyv1beta1.PodSecurityPolicy {
|
||||
return &policyv1beta1.PodSecurityPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{
|
||||
seccomp.AllowedProfilesAnnotationKey: v1.SeccompProfileRuntimeDefault,
|
||||
seccomp.DefaultProfileAnnotationKey: v1.SeccompProfileRuntimeDefault,
|
||||
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
|
||||
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
|
||||
},
|
||||
},
|
||||
Spec: policyv1beta1.PodSecurityPolicySpec{
|
||||
Privileged: false,
|
||||
AllowPrivilegeEscalation: utilpointer.BoolPtr(false),
|
||||
RequiredDropCapabilities: []v1.Capability{
|
||||
"AUDIT_WRITE",
|
||||
"CHOWN",
|
||||
"DAC_OVERRIDE",
|
||||
"FOWNER",
|
||||
"FSETID",
|
||||
"KILL",
|
||||
"MKNOD",
|
||||
"NET_RAW",
|
||||
"SETGID",
|
||||
"SETUID",
|
||||
"SYS_CHROOT",
|
||||
},
|
||||
Volumes: []policyv1beta1.FSType{
|
||||
policyv1beta1.ConfigMap,
|
||||
policyv1beta1.EmptyDir,
|
||||
policyv1beta1.PersistentVolumeClaim,
|
||||
"projected",
|
||||
policyv1beta1.Secret,
|
||||
},
|
||||
HostNetwork: false,
|
||||
HostIPC: false,
|
||||
HostPID: false,
|
||||
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
|
||||
Rule: policyv1beta1.RunAsUserStrategyMustRunAsNonRoot,
|
||||
},
|
||||
RunAsGroup: &policyv1beta1.RunAsGroupStrategyOptions{
|
||||
Rule: policyv1beta1.RunAsGroupStrategyMustRunAs,
|
||||
Ranges: []policyv1beta1.IDRange{
|
||||
{Min: nobodyUser, Max: nobodyUser}},
|
||||
},
|
||||
SELinux: policyv1beta1.SELinuxStrategyOptions{
|
||||
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
|
||||
},
|
||||
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
|
||||
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
|
||||
},
|
||||
FSGroup: policyv1beta1.FSGroupStrategyOptions{
|
||||
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
|
||||
},
|
||||
ReadOnlyRootFilesystem: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
@ -83,7 +83,6 @@ type Framework struct {
|
||||
Namespace *v1.Namespace // Every test has at least one namespace unless creation is skipped
|
||||
namespacesToDelete []*v1.Namespace // Some tests have more than one.
|
||||
NamespaceDeletionTimeout time.Duration
|
||||
SkipPrivilegedPSPBinding bool // Whether to skip creating a binding to the privileged PSP in the test namespace
|
||||
NamespacePodSecurityEnforceLevel admissionapi.Level // The pod security enforcement level for namespaces to be applied.
|
||||
|
||||
gatherer *ContainerResourceGatherer
|
||||
@ -545,10 +544,6 @@ func (f *Framework) CreateNamespace(baseName string, labels map[string]string) (
|
||||
// fail to create serviceAccount in it.
|
||||
f.AddNamespacesToDelete(ns)
|
||||
|
||||
if err == nil && !f.SkipPrivilegedPSPBinding {
|
||||
CreatePrivilegedPSPBinding(f.ClientSet, ns.Name)
|
||||
}
|
||||
|
||||
return ns, err
|
||||
}
|
||||
|
||||
|
@ -1,192 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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 framework
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||
|
||||
"github.com/onsi/ginkgo"
|
||||
|
||||
// TODO: Remove the following imports (ref: https://github.com/kubernetes/kubernetes/issues/81245)
|
||||
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
podSecurityPolicyPrivileged = "e2e-test-privileged-psp"
|
||||
|
||||
// allowAny is the wildcard used to allow any profile.
|
||||
allowAny = "*"
|
||||
|
||||
// allowedProfilesAnnotationKey specifies the allowed seccomp profiles.
|
||||
allowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
|
||||
)
|
||||
|
||||
var (
|
||||
isPSPEnabledOnce sync.Once
|
||||
isPSPEnabled bool
|
||||
)
|
||||
|
||||
// privilegedPSP creates a PodSecurityPolicy that allows everything.
|
||||
func privilegedPSP(name string) *policyv1beta1.PodSecurityPolicy {
|
||||
allowPrivilegeEscalation := true
|
||||
return &policyv1beta1.PodSecurityPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{allowedProfilesAnnotationKey: allowAny},
|
||||
},
|
||||
Spec: policyv1beta1.PodSecurityPolicySpec{
|
||||
Privileged: true,
|
||||
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
|
||||
AllowedCapabilities: []v1.Capability{"*"},
|
||||
Volumes: []policyv1beta1.FSType{policyv1beta1.All},
|
||||
HostNetwork: true,
|
||||
HostPorts: []policyv1beta1.HostPortRange{{Min: 0, Max: 65535}},
|
||||
HostIPC: true,
|
||||
HostPID: true,
|
||||
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
|
||||
Rule: policyv1beta1.RunAsUserStrategyRunAsAny,
|
||||
},
|
||||
SELinux: policyv1beta1.SELinuxStrategyOptions{
|
||||
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
|
||||
},
|
||||
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
|
||||
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
|
||||
},
|
||||
FSGroup: policyv1beta1.FSGroupStrategyOptions{
|
||||
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
|
||||
},
|
||||
ReadOnlyRootFilesystem: false,
|
||||
AllowedUnsafeSysctls: []string{"*"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsPodSecurityPolicyEnabled returns true if PodSecurityPolicy is enabled. Otherwise false.
|
||||
func IsPodSecurityPolicyEnabled(kubeClient clientset.Interface) bool {
|
||||
isPSPEnabledOnce.Do(func() {
|
||||
psps, err := kubeClient.PolicyV1beta1().PodSecurityPolicies().List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
Logf("Error listing PodSecurityPolicies; assuming PodSecurityPolicy is disabled: %v", err)
|
||||
return
|
||||
}
|
||||
if psps == nil || len(psps.Items) == 0 {
|
||||
Logf("No PodSecurityPolicies found; assuming PodSecurityPolicy is disabled.")
|
||||
return
|
||||
}
|
||||
Logf("Found PodSecurityPolicies; testing pod creation to see if PodSecurityPolicy is enabled")
|
||||
testPod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{GenerateName: "psp-test-pod-"},
|
||||
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "test", Image: imageutils.GetPauseImageName()}}},
|
||||
}
|
||||
dryRunPod, err := kubeClient.CoreV1().Pods("kube-system").Create(context.TODO(), testPod, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "PodSecurityPolicy") {
|
||||
Logf("PodSecurityPolicy error creating dryrun pod; assuming PodSecurityPolicy is enabled: %v", err)
|
||||
isPSPEnabled = true
|
||||
} else {
|
||||
Logf("Error creating dryrun pod; assuming PodSecurityPolicy is disabled: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
pspAnnotation, pspAnnotationExists := dryRunPod.Annotations["kubernetes.io/psp"]
|
||||
if !pspAnnotationExists {
|
||||
Logf("No PSP annotation exists on dry run pod; assuming PodSecurityPolicy is disabled")
|
||||
return
|
||||
}
|
||||
Logf("PSP annotation exists on dry run pod: %q; assuming PodSecurityPolicy is enabled", pspAnnotation)
|
||||
isPSPEnabled = true
|
||||
})
|
||||
return isPSPEnabled
|
||||
}
|
||||
|
||||
var (
|
||||
privilegedPSPOnce sync.Once
|
||||
)
|
||||
|
||||
// CreatePrivilegedPSPBinding creates the privileged PSP & role
|
||||
func CreatePrivilegedPSPBinding(kubeClient clientset.Interface, namespace string) {
|
||||
if !IsPodSecurityPolicyEnabled(kubeClient) {
|
||||
return
|
||||
}
|
||||
// Create the privileged PSP & role
|
||||
privilegedPSPOnce.Do(func() {
|
||||
_, err := kubeClient.PolicyV1beta1().PodSecurityPolicies().Get(context.TODO(), podSecurityPolicyPrivileged, metav1.GetOptions{})
|
||||
if !apierrors.IsNotFound(err) {
|
||||
// Privileged PSP was already created.
|
||||
ExpectNoError(err, "Failed to get PodSecurityPolicy %s", podSecurityPolicyPrivileged)
|
||||
return
|
||||
}
|
||||
|
||||
psp := privilegedPSP(podSecurityPolicyPrivileged)
|
||||
_, err = kubeClient.PolicyV1beta1().PodSecurityPolicies().Create(context.TODO(), psp, metav1.CreateOptions{})
|
||||
if !apierrors.IsAlreadyExists(err) {
|
||||
ExpectNoError(err, "Failed to create PSP %s", podSecurityPolicyPrivileged)
|
||||
}
|
||||
|
||||
if e2eauth.IsRBACEnabled(kubeClient.RbacV1()) {
|
||||
// Create the Role to bind it to the namespace.
|
||||
_, err = kubeClient.RbacV1().ClusterRoles().Create(context.TODO(), &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: podSecurityPolicyPrivileged},
|
||||
Rules: []rbacv1.PolicyRule{{
|
||||
APIGroups: []string{"extensions"},
|
||||
Resources: []string{"podsecuritypolicies"},
|
||||
ResourceNames: []string{podSecurityPolicyPrivileged},
|
||||
Verbs: []string{"use"},
|
||||
}},
|
||||
}, metav1.CreateOptions{})
|
||||
if !apierrors.IsAlreadyExists(err) {
|
||||
ExpectNoError(err, "Failed to create PSP role")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if e2eauth.IsRBACEnabled(kubeClient.RbacV1()) {
|
||||
ginkgo.By(fmt.Sprintf("Binding the %s PodSecurityPolicy to the default service account in %s",
|
||||
podSecurityPolicyPrivileged, namespace))
|
||||
err := e2eauth.BindClusterRoleInNamespace(kubeClient.RbacV1(),
|
||||
podSecurityPolicyPrivileged,
|
||||
namespace,
|
||||
rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Namespace: namespace,
|
||||
Name: "default",
|
||||
},
|
||||
rbacv1.Subject{
|
||||
Kind: rbacv1.GroupKind,
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Name: "system:serviceaccounts:" + namespace,
|
||||
},
|
||||
)
|
||||
ExpectNoError(err)
|
||||
ExpectNoError(e2eauth.WaitForNamedAuthorizationUpdate(kubeClient.AuthorizationV1(),
|
||||
serviceaccount.MakeUsername(namespace, "default"), namespace, "use", podSecurityPolicyPrivileged,
|
||||
schema.GroupResource{Group: "extensions", Resource: "podsecuritypolicies"}, true))
|
||||
}
|
||||
}
|
@ -32,14 +32,12 @@ import (
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
@ -65,11 +63,6 @@ const (
|
||||
maxValidSize string = "10Ei"
|
||||
)
|
||||
|
||||
const (
|
||||
// ClusterRole name for e2e test Priveledged Pod Security Policy User
|
||||
podSecurityPolicyPrivilegedClusterRoleName = "e2e-test-privileged-psp"
|
||||
)
|
||||
|
||||
// VerifyFSGroupInPod verifies that the passed in filePath contains the expectedFSGroup
|
||||
func VerifyFSGroupInPod(f *framework.Framework, filePath, expectedFSGroup string, pod *v1.Pod) {
|
||||
cmd := fmt.Sprintf("ls -l %s", filePath)
|
||||
@ -417,54 +410,6 @@ func StartExternalProvisioner(c clientset.Interface, ns string, externalPluginNa
|
||||
return pod
|
||||
}
|
||||
|
||||
// PrivilegedTestPSPClusterRoleBinding test Pod Security Policy Role bindings
|
||||
func PrivilegedTestPSPClusterRoleBinding(client clientset.Interface,
|
||||
namespace string,
|
||||
teardown bool,
|
||||
saNames []string) {
|
||||
bindingString := "Binding"
|
||||
if teardown {
|
||||
bindingString = "Unbinding"
|
||||
}
|
||||
roleBindingClient := client.RbacV1().RoleBindings(namespace)
|
||||
for _, saName := range saNames {
|
||||
ginkgo.By(fmt.Sprintf("%v priviledged Pod Security Policy to the service account %s", bindingString, saName))
|
||||
binding := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "psp-" + saName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: saName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
Kind: "ClusterRole",
|
||||
Name: podSecurityPolicyPrivilegedClusterRoleName,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
},
|
||||
}
|
||||
|
||||
roleBindingClient.Delete(context.TODO(), binding.GetName(), metav1.DeleteOptions{})
|
||||
err := wait.Poll(2*time.Second, 2*time.Minute, func() (bool, error) {
|
||||
_, err := roleBindingClient.Get(context.TODO(), binding.GetName(), metav1.GetOptions{})
|
||||
return apierrors.IsNotFound(err), nil
|
||||
})
|
||||
framework.ExpectNoError(err, "Timed out waiting for RBAC binding %s deletion: %v", binding.GetName(), err)
|
||||
|
||||
if teardown {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = roleBindingClient.Create(context.TODO(), binding, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err, "Failed to create %s role binding: %v", binding.GetName(), err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func isSudoPresent(nodeIP string, provider string) bool {
|
||||
framework.Logf("Checking if sudo command is present")
|
||||
sshResult, err := e2essh.SSH("sudo --version", nodeIP, provider)
|
||||
|
Loading…
Reference in New Issue
Block a user