diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 475abd5827c..7a307d2c7e8 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -624,6 +624,8 @@ type ReplicaSetStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } +// +genclient=true,nonNamespaced=true + // PodSecurityPolicy governs the ability to make requests that affect the SecurityContext // that will be applied to a pod and container. type PodSecurityPolicy struct { @@ -638,8 +640,17 @@ type PodSecurityPolicy struct { type PodSecurityPolicySpec struct { // Privileged determines if a pod can request to be run as privileged. Privileged bool `json:"privileged,omitempty"` - // Capabilities is a list of capabilities that can be added. - Capabilities []api.Capability `json:"capabilities,omitempty"` + // DefaultAddCapabilities is the default set of capabilities that will be added to the container + // unless the pod spec specifically drops the capability. You may not list a capabiility in both + // DefaultAddCapabilities and RequiredDropCapabilities. + DefaultAddCapabilities []api.Capability `json:"defaultAddCapabilities,omitempty"` + // RequiredDropCapabilities are the capabilities that will be dropped from the container. These + // are required to be dropped and cannot be added. + RequiredDropCapabilities []api.Capability `json:"requiredDropCapabilities,omitempty"` + // AllowedCapabilities is a list of capabilities that can be requested to add to the container. + // Capabilities in this field may be added at the pod author's discretion. + // You must not list a capability in both AllowedCapabilities and RequiredDropCapabilities. + AllowedCapabilities []api.Capability `json:"allowedCapabilities,omitempty"` // Volumes is a white list of allowed volume plugins. Empty indicates that all plugins // may be used. Volumes []FSType `json:"volumes,omitempty"` @@ -652,9 +663,19 @@ type PodSecurityPolicySpec struct { // HostIPC determines if the policy allows the use of HostIPC in the pod spec. HostIPC bool `json:"hostIPC,omitempty"` // SELinux is the strategy that will dictate the allowable labels that may be set. - SELinux SELinuxStrategyOptions `json:"seLinux,omitempty"` + SELinux SELinuxStrategyOptions `json:"seLinux"` // RunAsUser is the strategy that will dictate the allowable RunAsUser values that may be set. - RunAsUser RunAsUserStrategyOptions `json:"runAsUser,omitempty"` + RunAsUser RunAsUserStrategyOptions `json:"runAsUser"` + // SupplementalGroups is the strategy that will dictate what supplemental groups are used by the SecurityContext. + SupplementalGroups SupplementalGroupsStrategyOptions `json:"supplementalGroups"` + // FSGroup is the strategy that will dictate what fs group is used by the SecurityContext. + FSGroup FSGroupStrategyOptions `json:"fsGroup"` + // ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file + // system. If the container specifically requests to run with a non-read only root file system + // the PSP should deny the pod. + // If set to false the container may run with a read only root file system if it wishes but it + // will not be forced to. + ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem,omitempty"` } // HostPortRange defines a range of host ports that will be enabled by a policy @@ -670,6 +691,9 @@ type HostPortRange struct { type FSType string var ( + AzureFile FSType = "azureFile" + Flocker FSType = "flocker" + FlexVolume FSType = "flexVolume" HostPath FSType = "hostPath" EmptyDir FSType = "emptyDir" GCEPersistentDisk FSType = "gcePersistentDisk" @@ -685,6 +709,8 @@ var ( CephFS FSType = "cephFS" DownwardAPI FSType = "downwardAPI" FC FSType = "fc" + ConfigMap FSType = "configMap" + All FSType = "*" ) // SELinuxStrategyOptions defines the strategy type and any options used to create the strategy. @@ -736,6 +762,46 @@ const ( RunAsUserStrategyRunAsAny RunAsUserStrategy = "RunAsAny" ) +// FSGroupStrategyOptions defines the strategy type and options used to create the strategy. +type FSGroupStrategyOptions struct { + // Rule is the strategy that will dictate what FSGroup is used in the SecurityContext. + Rule FSGroupStrategyType `json:"rule,omitempty"` + // Ranges are the allowed ranges of fs groups. If you would like to force a single + // fs group then supply a single range with the same start and end. + Ranges []IDRange `json:"ranges,omitempty"` +} + +// FSGroupStrategyType denotes strategy types for generating FSGroup values for a +// SecurityContext +type FSGroupStrategyType string + +const ( + // container must have FSGroup of X applied. + FSGroupStrategyMustRunAs FSGroupStrategyType = "MustRunAs" + // container may make requests for any FSGroup labels. + FSGroupStrategyRunAsAny FSGroupStrategyType = "RunAsAny" +) + +// SupplementalGroupsStrategyOptions defines the strategy type and options used to create the strategy. +type SupplementalGroupsStrategyOptions struct { + // Rule is the strategy that will dictate what supplemental groups is used in the SecurityContext. + Rule SupplementalGroupsStrategyType `json:"rule,omitempty"` + // Ranges are the allowed ranges of supplemental groups. If you would like to force a single + // supplemental group then supply a single range with the same start and end. + Ranges []IDRange `json:"ranges,omitempty"` +} + +// SupplementalGroupsStrategyType denotes strategy types for determining valid supplemental +// groups for a SecurityContext. +type SupplementalGroupsStrategyType string + +const ( + // container must run as a particular gid. + SupplementalGroupsStrategyMustRunAs SupplementalGroupsStrategyType = "MustRunAs" + // container may make requests for any gid. + SupplementalGroupsStrategyRunAsAny SupplementalGroupsStrategyType = "RunAsAny" +) + // PodSecurityPolicyList is a list of PodSecurityPolicy objects. type PodSecurityPolicyList struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index af3b394d093..b7a255d6bf0 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -912,6 +912,8 @@ type ReplicaSetStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` } +// +genclient=true,nonNamespaced=true + // Pod Security Policy governs the ability to make requests that affect the Security Context // that will be applied to a pod and container. type PodSecurityPolicy struct { @@ -928,29 +930,51 @@ type PodSecurityPolicy struct { type PodSecurityPolicySpec struct { // privileged determines if a pod can request to be run as privileged. Privileged bool `json:"privileged,omitempty" protobuf:"varint,1,opt,name=privileged"` - // capabilities is a list of capabilities that can be added. - Capabilities []v1.Capability `json:"capabilities,omitempty" protobuf:"bytes,2,rep,name=capabilities,casttype=k8s.io/kubernetes/pkg/api/v1.Capability"` + // DefaultAddCapabilities is the default set of capabilities that will be added to the container + // unless the pod spec specifically drops the capability. You may not list a capabiility in both + // DefaultAddCapabilities and RequiredDropCapabilities. + DefaultAddCapabilities []v1.Capability `json:"defaultAddCapabilities,omitempty" protobuf:"bytes,2,rep,name=defaultAddCapabilities,casttype=k8s.io/kubernetes/pkg/api/v1.Capability"` + // RequiredDropCapabilities are the capabilities that will be dropped from the container. These + // are required to be dropped and cannot be added. + RequiredDropCapabilities []v1.Capability `json:"requiredDropCapabilities,omitempty" protobuf:"bytes,3,rep,name=requiredDropCapabilities,casttype=k8s.io/kubernetes/pkg/api/v1.Capability"` + // AllowedCapabilities is a list of capabilities that can be requested to add to the container. + // Capabilities in this field may be added at the pod author's discretion. + // You must not list a capability in both AllowedCapabilities and RequiredDropCapabilities. + AllowedCapabilities []v1.Capability `json:"allowedCapabilities,omitempty" protobuf:"bytes,4,rep,name=allowedCapabilities,casttype=k8s.io/kubernetes/pkg/api/v1.Capability"` // volumes is a white list of allowed volume plugins. Empty indicates that all plugins // may be used. - Volumes []FSType `json:"volumes,omitempty" protobuf:"bytes,3,rep,name=volumes,casttype=FSType"` + Volumes []FSType `json:"volumes,omitempty" protobuf:"bytes,5,rep,name=volumes,casttype=FSType"` // hostNetwork determines if the policy allows the use of HostNetwork in the pod spec. - HostNetwork bool `json:"hostNetwork,omitempty" protobuf:"varint,4,opt,name=hostNetwork"` + HostNetwork bool `json:"hostNetwork,omitempty" protobuf:"varint,6,opt,name=hostNetwork"` // hostPorts determines which host port ranges are allowed to be exposed. - HostPorts []HostPortRange `json:"hostPorts,omitempty" protobuf:"bytes,5,rep,name=hostPorts"` + HostPorts []HostPortRange `json:"hostPorts,omitempty" protobuf:"bytes,7,rep,name=hostPorts"` // hostPID determines if the policy allows the use of HostPID in the pod spec. - HostPID bool `json:"hostPID,omitempty" protobuf:"varint,6,opt,name=hostPID"` + HostPID bool `json:"hostPID,omitempty" protobuf:"varint,8,opt,name=hostPID"` // hostIPC determines if the policy allows the use of HostIPC in the pod spec. - HostIPC bool `json:"hostIPC,omitempty" protobuf:"varint,7,opt,name=hostIPC"` + HostIPC bool `json:"hostIPC,omitempty" protobuf:"varint,9,opt,name=hostIPC"` // seLinux is the strategy that will dictate the allowable labels that may be set. - SELinux SELinuxStrategyOptions `json:"seLinux,omitempty" protobuf:"bytes,8,opt,name=seLinux"` + SELinux SELinuxStrategyOptions `json:"seLinux" protobuf:"bytes,10,opt,name=seLinux"` // runAsUser is the strategy that will dictate the allowable RunAsUser values that may be set. - RunAsUser RunAsUserStrategyOptions `json:"runAsUser,omitempty" protobuf:"bytes,9,opt,name=runAsUser"` + RunAsUser RunAsUserStrategyOptions `json:"runAsUser" protobuf:"bytes,11,opt,name=runAsUser"` + // SupplementalGroups is the strategy that will dictate what supplemental groups are used by the SecurityContext. + SupplementalGroups SupplementalGroupsStrategyOptions `json:"supplementalGroups" protobuf:"bytes,12,opt,name=supplementalGroups"` + // FSGroup is the strategy that will dictate what fs group is used by the SecurityContext. + FSGroup FSGroupStrategyOptions `json:"fsGroup" protobuf:"bytes,13,opt,name=fsGroup"` + // ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file + // system. If the container specifically requests to run with a non-read only root file system + // the PSP should deny the pod. + // If set to false the container may run with a read only root file system if it wishes but it + // will not be forced to. + ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem,omitempty" protobuf:"varint,14,opt,name=readOnlyRootFilesystem"` } // FS Type gives strong typing to different file systems that are used by volumes. type FSType string var ( + AzureFile FSType = "azureFile" + Flocker FSType = "flocker" + FlexVolume FSType = "flexVolume" HostPath FSType = "hostPath" EmptyDir FSType = "emptyDir" GCEPersistentDisk FSType = "gcePersistentDisk" @@ -966,6 +990,8 @@ var ( CephFS FSType = "cephFS" DownwardAPI FSType = "downwardAPI" FC FSType = "fc" + ConfigMap FSType = "configMap" + All FSType = "*" ) // Host Port Range defines a range of host ports that will be enabled by a policy @@ -1026,6 +1052,46 @@ const ( RunAsUserStrategyRunAsAny RunAsUserStrategy = "RunAsAny" ) +// FSGroupStrategyOptions defines the strategy type and options used to create the strategy. +type FSGroupStrategyOptions struct { + // Rule is the strategy that will dictate what FSGroup is used in the SecurityContext. + Rule FSGroupStrategyType `json:"rule,omitempty" protobuf:"bytes,1,opt,name=rule,casttype=FSGroupStrategyType"` + // Ranges are the allowed ranges of fs groups. If you would like to force a single + // fs group then supply a single range with the same start and end. + Ranges []IDRange `json:"ranges,omitempty" protobuf:"bytes,2,rep,name=ranges"` +} + +// FSGroupStrategyType denotes strategy types for generating FSGroup values for a +// SecurityContext +type FSGroupStrategyType string + +const ( + // container must have FSGroup of X applied. + FSGroupStrategyMustRunAs FSGroupStrategyType = "MustRunAs" + // container may make requests for any FSGroup labels. + FSGroupStrategyRunAsAny FSGroupStrategyType = "RunAsAny" +) + +// SupplementalGroupsStrategyOptions defines the strategy type and options used to create the strategy. +type SupplementalGroupsStrategyOptions struct { + // Rule is the strategy that will dictate what supplemental groups is used in the SecurityContext. + Rule SupplementalGroupsStrategyType `json:"rule,omitempty" protobuf:"bytes,1,opt,name=rule,casttype=SupplementalGroupsStrategyType"` + // Ranges are the allowed ranges of supplemental groups. If you would like to force a single + // supplemental group then supply a single range with the same start and end. + Ranges []IDRange `json:"ranges,omitempty" protobuf:"bytes,2,rep,name=ranges"` +} + +// SupplementalGroupsStrategyType denotes strategy types for determining valid supplemental +// groups for a SecurityContext. +type SupplementalGroupsStrategyType string + +const ( + // container must run as a particular gid. + SupplementalGroupsStrategyMustRunAs SupplementalGroupsStrategyType = "MustRunAs" + // container may make requests for any gid. + SupplementalGroupsStrategyRunAsAny SupplementalGroupsStrategyType = "RunAsAny" +) + // Pod Security Policy List is a list of PodSecurityPolicy objects. type PodSecurityPolicyList struct { unversioned.TypeMeta `json:",inline"` diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index ddb24505b92..4ef1e7ff009 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "fmt" "net" "regexp" "strconv" @@ -28,6 +29,7 @@ import ( apivalidation "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/labels" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/validation" @@ -529,7 +531,11 @@ func ValidatePodSecurityPolicySpec(spec *extensions.PodSecurityPolicySpec, fldPa allErrs = append(allErrs, validatePSPRunAsUser(fldPath.Child("runAsUser"), &spec.RunAsUser)...) allErrs = append(allErrs, validatePSPSELinux(fldPath.Child("seLinux"), &spec.SELinux)...) + allErrs = append(allErrs, validatePSPSupplementalGroup(fldPath.Child("supplementalGroups"), &spec.SupplementalGroups)...) + allErrs = append(allErrs, validatePSPFSGroup(fldPath.Child("fsGroup"), &spec.FSGroup)...) allErrs = append(allErrs, validatePodSecurityPolicyVolumes(fldPath, spec.Volumes)...) + allErrs = append(allErrs, validatePSPCapsAgainstDrops(spec.RequiredDropCapabilities, spec.DefaultAddCapabilities, field.NewPath("defaultAddCapabilities"))...) + allErrs = append(allErrs, validatePSPCapsAgainstDrops(spec.RequiredDropCapabilities, spec.AllowedCapabilities, field.NewPath("allowedCapabilities"))...) return allErrs } @@ -568,24 +574,48 @@ func validatePSPRunAsUser(fldPath *field.Path, runAsUser *extensions.RunAsUserSt return allErrs } +// validatePSPFSGroup validates the FSGroupStrategyOptions fields of the PodSecurityPolicy. +func validatePSPFSGroup(fldPath *field.Path, groupOptions *extensions.FSGroupStrategyOptions) field.ErrorList { + allErrs := field.ErrorList{} + + supportedRules := sets.NewString( + string(extensions.FSGroupStrategyMustRunAs), + string(extensions.FSGroupStrategyRunAsAny), + ) + if !supportedRules.Has(string(groupOptions.Rule)) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("rule"), groupOptions.Rule, supportedRules.List())) + } + + for idx, rng := range groupOptions.Ranges { + allErrs = append(allErrs, validateIDRanges(fldPath.Child("ranges").Index(idx), rng)...) + } + return allErrs +} + +// validatePSPSupplementalGroup validates the SupplementalGroupsStrategyOptions fields of the PodSecurityPolicy. +func validatePSPSupplementalGroup(fldPath *field.Path, groupOptions *extensions.SupplementalGroupsStrategyOptions) field.ErrorList { + allErrs := field.ErrorList{} + + supportedRules := sets.NewString( + string(extensions.SupplementalGroupsStrategyRunAsAny), + string(extensions.SupplementalGroupsStrategyMustRunAs), + ) + if !supportedRules.Has(string(groupOptions.Rule)) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("rule"), groupOptions.Rule, supportedRules.List())) + } + + for idx, rng := range groupOptions.Ranges { + allErrs = append(allErrs, validateIDRanges(fldPath.Child("ranges").Index(idx), rng)...) + } + return allErrs +} + // validatePodSecurityPolicyVolumes validates the volume fields of PodSecurityPolicy. func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []extensions.FSType) field.ErrorList { allErrs := field.ErrorList{} - allowed := sets.NewString(string(extensions.HostPath), - string(extensions.EmptyDir), - string(extensions.GCEPersistentDisk), - string(extensions.AWSElasticBlockStore), - string(extensions.GitRepo), - string(extensions.Secret), - string(extensions.NFS), - string(extensions.ISCSI), - string(extensions.Glusterfs), - string(extensions.PersistentVolumeClaim), - string(extensions.RBD), - string(extensions.Cinder), - string(extensions.CephFS), - string(extensions.DownwardAPI), - string(extensions.FC)) + allowed := psputil.GetAllFSTypesAsSet() + // add in the * value since that is a pseudo type that is not included by default + allowed.Insert(string(extensions.All)) for _, v := range volumes { if !allowed.Has(string(v)) { allErrs = append(allErrs, field.NotSupported(fldPath.Child("volumes"), v, allowed.List())) @@ -614,6 +644,31 @@ func validateIDRanges(fldPath *field.Path, rng extensions.IDRange) field.ErrorLi return allErrs } +// validatePSPCapsAgainstDrops ensures an allowed cap is not listed in the required drops. +func validatePSPCapsAgainstDrops(requiredDrops []api.Capability, capsToCheck []api.Capability, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if requiredDrops == nil { + return allErrs + } + for _, cap := range capsToCheck { + if hasCap(cap, requiredDrops) { + allErrs = append(allErrs, field.Invalid(fldPath, cap, + fmt.Sprintf("capability is listed in %s and requiredDropCapabilities", fldPath.String()))) + } + } + return allErrs +} + +// hasCap checks for needle in haystack. +func hasCap(needle api.Capability, haystack []api.Capability) bool { + for _, c := range haystack { + if needle == c { + return true + } + } + return false +} + // ValidatePodSecurityPolicyUpdate validates a PSP for updates. func ValidatePodSecurityPolicyUpdate(old *extensions.PodSecurityPolicy, new *extensions.PodSecurityPolicy) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index c1632fa6d81..c019f3ed1ac 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -24,7 +24,9 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" + "k8s.io/kubernetes/pkg/util/validation/field" ) func TestValidateDaemonSetStatusUpdate(t *testing.T) { @@ -1418,7 +1420,7 @@ func TestValidateReplicaSet(t *testing.T) { } func TestValidatePodSecurityPolicy(t *testing.T) { - validSCC := func() *extensions.PodSecurityPolicy { + validPSP := func() *extensions.PodSecurityPolicy { return &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: extensions.PodSecurityPolicySpec{ @@ -1428,118 +1430,250 @@ func TestValidatePodSecurityPolicy(t *testing.T) { RunAsUser: extensions.RunAsUserStrategyOptions{ Rule: extensions.RunAsUserStrategyRunAsAny, }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, }, } } - noUserOptions := validSCC() + noUserOptions := validPSP() noUserOptions.Spec.RunAsUser.Rule = "" - noSELinuxOptions := validSCC() + noSELinuxOptions := validPSP() noSELinuxOptions.Spec.SELinux.Rule = "" - invalidUserStratRule := validSCC() - invalidUserStratRule.Spec.RunAsUser.Rule = "invalid" + invalidUserStratType := validPSP() + invalidUserStratType.Spec.RunAsUser.Rule = "invalid" - invalidSELinuxStratRule := validSCC() - invalidSELinuxStratRule.Spec.SELinux.Rule = "invalid" + invalidSELinuxStratType := validPSP() + invalidSELinuxStratType.Spec.SELinux.Rule = "invalid" - missingObjectMetaName := validSCC() + invalidUIDPSP := validPSP() + invalidUIDPSP.Spec.RunAsUser.Rule = extensions.RunAsUserStrategyMustRunAs + invalidUIDPSP.Spec.RunAsUser.Ranges = []extensions.IDRange{ + {Min: -1, Max: 1}, + } + + missingObjectMetaName := validPSP() missingObjectMetaName.ObjectMeta.Name = "" - invalidRangeMinGreaterThanMax := validSCC() - invalidRangeMinGreaterThanMax.Spec.RunAsUser.Ranges = []extensions.IDRange{ + noFSGroupOptions := validPSP() + noFSGroupOptions.Spec.FSGroup.Rule = "" + + invalidFSGroupStratType := validPSP() + invalidFSGroupStratType.Spec.FSGroup.Rule = "invalid" + + noSupplementalGroupsOptions := validPSP() + noSupplementalGroupsOptions.Spec.SupplementalGroups.Rule = "" + + invalidSupGroupStratType := validPSP() + invalidSupGroupStratType.Spec.SupplementalGroups.Rule = "invalid" + + invalidRangeMinGreaterThanMax := validPSP() + invalidRangeMinGreaterThanMax.Spec.FSGroup.Ranges = []extensions.IDRange{ {Min: 2, Max: 1}, } - invalidRangeNegativeMin := validSCC() - invalidRangeNegativeMin.Spec.RunAsUser.Ranges = []extensions.IDRange{ + invalidRangeNegativeMin := validPSP() + invalidRangeNegativeMin.Spec.FSGroup.Ranges = []extensions.IDRange{ {Min: -1, Max: 10}, } - invalidRangeNegativeMax := validSCC() - invalidRangeNegativeMax.Spec.RunAsUser.Ranges = []extensions.IDRange{ + invalidRangeNegativeMax := validPSP() + invalidRangeNegativeMax.Spec.FSGroup.Ranges = []extensions.IDRange{ {Min: 1, Max: -10}, } + requiredCapAddAndDrop := validPSP() + requiredCapAddAndDrop.Spec.DefaultAddCapabilities = []api.Capability{"foo"} + requiredCapAddAndDrop.Spec.RequiredDropCapabilities = []api.Capability{"foo"} + + allowedCapListedInRequiredDrop := validPSP() + allowedCapListedInRequiredDrop.Spec.RequiredDropCapabilities = []api.Capability{"foo"} + allowedCapListedInRequiredDrop.Spec.AllowedCapabilities = []api.Capability{"foo"} + errorCases := map[string]struct { - scc *extensions.PodSecurityPolicy + psp *extensions.PodSecurityPolicy + errorType field.ErrorType errorDetail string }{ "no user options": { - scc: noUserOptions, + psp: noUserOptions, + errorType: field.ErrorTypeNotSupported, errorDetail: "supported values: MustRunAs, MustRunAsNonRoot, RunAsAny", }, "no selinux options": { - scc: noSELinuxOptions, + psp: noSELinuxOptions, + errorType: field.ErrorTypeNotSupported, errorDetail: "supported values: MustRunAs, RunAsAny", }, - "invalid user strategy rule": { - scc: invalidUserStratRule, + "no fsgroup options": { + psp: noFSGroupOptions, + errorType: field.ErrorTypeNotSupported, + errorDetail: "supported values: MustRunAs, RunAsAny", + }, + "no sup group options": { + psp: noSupplementalGroupsOptions, + errorType: field.ErrorTypeNotSupported, + errorDetail: "supported values: MustRunAs, RunAsAny", + }, + "invalid user strategy type": { + psp: invalidUserStratType, + errorType: field.ErrorTypeNotSupported, errorDetail: "supported values: MustRunAs, MustRunAsNonRoot, RunAsAny", }, - "invalid selinux strategy rule": { - scc: invalidSELinuxStratRule, + "invalid selinux strategy type": { + psp: invalidSELinuxStratType, + errorType: field.ErrorTypeNotSupported, errorDetail: "supported values: MustRunAs, RunAsAny", }, + "invalid sup group strategy type": { + psp: invalidSupGroupStratType, + errorType: field.ErrorTypeNotSupported, + errorDetail: "supported values: MustRunAs, RunAsAny", + }, + "invalid fs group strategy type": { + psp: invalidFSGroupStratType, + errorType: field.ErrorTypeNotSupported, + errorDetail: "supported values: MustRunAs, RunAsAny", + }, + "invalid uid": { + psp: invalidUIDPSP, + errorType: field.ErrorTypeInvalid, + errorDetail: "min cannot be negative", + }, "missing object meta name": { - scc: missingObjectMetaName, + psp: missingObjectMetaName, + errorType: field.ErrorTypeRequired, errorDetail: "name or generateName is required", }, "invalid range min greater than max": { - scc: invalidRangeMinGreaterThanMax, + psp: invalidRangeMinGreaterThanMax, + errorType: field.ErrorTypeInvalid, errorDetail: "min cannot be greater than max", }, "invalid range negative min": { - scc: invalidRangeNegativeMin, + psp: invalidRangeNegativeMin, + errorType: field.ErrorTypeInvalid, errorDetail: "min cannot be negative", }, "invalid range negative max": { - scc: invalidRangeNegativeMax, + psp: invalidRangeNegativeMax, + errorType: field.ErrorTypeInvalid, errorDetail: "max cannot be negative", }, + "invalid required caps": { + psp: requiredCapAddAndDrop, + errorType: field.ErrorTypeInvalid, + errorDetail: "capability is listed in defaultAddCapabilities and requiredDropCapabilities", + }, + "allowed cap listed in required drops": { + psp: allowedCapListedInRequiredDrop, + errorType: field.ErrorTypeInvalid, + errorDetail: "capability is listed in allowedCapabilities and requiredDropCapabilities", + }, } for k, v := range errorCases { - if errs := ValidatePodSecurityPolicy(v.scc); len(errs) == 0 || errs[0].Detail != v.errorDetail { - t.Errorf("Expected error with detail %s for %s, got %v", v.errorDetail, k, errs[0].Detail) + errs := ValidatePodSecurityPolicy(v.psp) + if len(errs) == 0 { + t.Errorf("%s expected errors but got none", k) + continue + } + if errs[0].Type != v.errorType { + t.Errorf("%s received an unexpected error type. Expected: %v got: %v", k, v.errorType, errs[0].Type) + } + if errs[0].Detail != v.errorDetail { + t.Errorf("%s received an unexpected error detail. Expected %v got: %v", k, v.errorDetail, errs[0].Detail) } } - mustRunAs := validSCC() + mustRunAs := validPSP() + mustRunAs.Spec.FSGroup.Rule = extensions.FSGroupStrategyMustRunAs + mustRunAs.Spec.SupplementalGroups.Rule = extensions.SupplementalGroupsStrategyMustRunAs mustRunAs.Spec.RunAsUser.Rule = extensions.RunAsUserStrategyMustRunAs mustRunAs.Spec.RunAsUser.Ranges = []extensions.IDRange{ - { - Min: 1, - Max: 1, - }, + {Min: 1, Max: 1}, } mustRunAs.Spec.SELinux.Rule = extensions.SELinuxStrategyMustRunAs - runAsNonRoot := validSCC() + runAsNonRoot := validPSP() runAsNonRoot.Spec.RunAsUser.Rule = extensions.RunAsUserStrategyMustRunAsNonRoot + caseInsensitiveAddDrop := validPSP() + caseInsensitiveAddDrop.Spec.DefaultAddCapabilities = []api.Capability{"foo"} + caseInsensitiveAddDrop.Spec.RequiredDropCapabilities = []api.Capability{"FOO"} + + caseInsensitiveAllowedDrop := validPSP() + caseInsensitiveAllowedDrop.Spec.RequiredDropCapabilities = []api.Capability{"FOO"} + caseInsensitiveAllowedDrop.Spec.AllowedCapabilities = []api.Capability{"foo"} + successCases := map[string]struct { - scc *extensions.PodSecurityPolicy + psp *extensions.PodSecurityPolicy }{ "must run as": { - scc: mustRunAs, + psp: mustRunAs, }, "run as any": { - scc: validSCC(), + psp: validPSP(), }, "run as non-root (user only)": { - scc: runAsNonRoot, + psp: runAsNonRoot, + }, + "comparison for add -> drop is case sensitive": { + psp: caseInsensitiveAddDrop, + }, + "comparison for allowed -> drop is case sensitive": { + psp: caseInsensitiveAllowedDrop, }, } for k, v := range successCases { - if errs := ValidatePodSecurityPolicy(v.scc); len(errs) != 0 { + if errs := ValidatePodSecurityPolicy(v.psp); len(errs) != 0 { t.Errorf("Expected success for %s, got %v", k, errs) } } } +func TestValidatePSPVolumes(t *testing.T) { + validPSP := func() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: extensions.PodSecurityPolicySpec{ + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyRunAsAny, + }, + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyRunAsAny, + }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, + }, + } + } + + volumes := psputil.GetAllFSTypesAsSet() + // add in the * value since that is a pseudo type that is not included by default + volumes.Insert(string(extensions.All)) + + for _, strVolume := range volumes.List() { + psp := validPSP() + psp.Spec.Volumes = []extensions.FSType{extensions.FSType(strVolume)} + errs := ValidatePodSecurityPolicy(psp) + if len(errs) != 0 { + t.Errorf("%s validation expected no errors but received %v", strVolume, errs) + } + } +} + func newBool(val bool) *bool { p := new(bool) *p = val diff --git a/pkg/client/unversioned/podsecuritypolicy.go b/pkg/client/unversioned/podsecuritypolicy.go index 64a34b4b23b..356d913dbea 100644 --- a/pkg/client/unversioned/podsecuritypolicy.go +++ b/pkg/client/unversioned/podsecuritypolicy.go @@ -28,7 +28,7 @@ type PodSecurityPoliciesInterface interface { type PodSecurityPolicyInterface interface { Get(name string) (result *extensions.PodSecurityPolicy, err error) - Create(scc *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) + Create(psp *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) List(opts api.ListOptions) (*extensions.PodSecurityPolicyList, error) Delete(name string) error Update(*extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) @@ -45,11 +45,11 @@ func newPodSecurityPolicy(c *ExtensionsClient) *podSecurityPolicy { return &podSecurityPolicy{c} } -func (s *podSecurityPolicy) Create(scc *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { +func (s *podSecurityPolicy) Create(psp *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { result := &extensions.PodSecurityPolicy{} err := s.client.Post(). Resource("podsecuritypolicies"). - Body(scc). + Body(psp). Do(). Into(result) diff --git a/pkg/client/unversioned/podsecuritypolicy_test.go b/pkg/client/unversioned/podsecuritypolicy_test.go index d51e2c5a04a..06fae477e3a 100644 --- a/pkg/client/unversioned/podsecuritypolicy_test.go +++ b/pkg/client/unversioned/podsecuritypolicy_test.go @@ -29,7 +29,7 @@ import ( func TestPodSecurityPolicyCreate(t *testing.T) { ns := api.NamespaceNone - scc := &extensions.PodSecurityPolicy{ + psp := &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ Name: "abc", }, @@ -40,18 +40,18 @@ func TestPodSecurityPolicyCreate(t *testing.T) { Method: "POST", Path: testapi.Extensions.ResourcePath(getPSPResourcename(), ns, ""), Query: simple.BuildQueryValues(nil), - Body: scc, + Body: psp, }, - Response: simple.Response{StatusCode: 200, Body: scc}, + Response: simple.Response{StatusCode: 200, Body: psp}, } - response, err := c.Setup(t).PodSecurityPolicies().Create(scc) + response, err := c.Setup(t).PodSecurityPolicies().Create(psp) c.Validate(t, response, err) } func TestPodSecurityPolicyGet(t *testing.T) { ns := api.NamespaceNone - scc := &extensions.PodSecurityPolicy{ + psp := &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ Name: "abc", }, @@ -63,7 +63,7 @@ func TestPodSecurityPolicyGet(t *testing.T) { Query: simple.BuildQueryValues(nil), Body: nil, }, - Response: simple.Response{StatusCode: 200, Body: scc}, + Response: simple.Response{StatusCode: 200, Body: psp}, } response, err := c.Setup(t).PodSecurityPolicies().Get("abc") @@ -72,7 +72,7 @@ func TestPodSecurityPolicyGet(t *testing.T) { func TestPodSecurityPolicyList(t *testing.T) { ns := api.NamespaceNone - sccList := &extensions.PodSecurityPolicyList{ + pspList := &extensions.PodSecurityPolicyList{ Items: []extensions.PodSecurityPolicy{ { ObjectMeta: api.ObjectMeta{ @@ -88,7 +88,7 @@ func TestPodSecurityPolicyList(t *testing.T) { Query: simple.BuildQueryValues(nil), Body: nil, }, - Response: simple.Response{StatusCode: 200, Body: sccList}, + Response: simple.Response{StatusCode: 200, Body: pspList}, } response, err := c.Setup(t).PodSecurityPolicies().List(api.ListOptions{}) c.Validate(t, response, err) @@ -96,7 +96,7 @@ func TestPodSecurityPolicyList(t *testing.T) { func TestPodSecurityPolicyUpdate(t *testing.T) { ns := api.NamespaceNone - scc := &extensions.PodSecurityPolicy{ + psp := &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ Name: "abc", ResourceVersion: "1", @@ -104,9 +104,9 @@ func TestPodSecurityPolicyUpdate(t *testing.T) { } c := &simple.Client{ Request: simple.Request{Method: "PUT", Path: testapi.Extensions.ResourcePath(getPSPResourcename(), ns, "abc"), Query: simple.BuildQueryValues(nil)}, - Response: simple.Response{StatusCode: 200, Body: scc}, + Response: simple.Response{StatusCode: 200, Body: psp}, } - response, err := c.Setup(t).PodSecurityPolicies().Update(scc) + response, err := c.Setup(t).PodSecurityPolicies().Update(psp) c.Validate(t, response, err) } diff --git a/pkg/client/unversioned/testclient/fake_podsecuritypolicy.go b/pkg/client/unversioned/testclient/fake_podsecuritypolicy.go index bb611d322c5..06bd10991fb 100644 --- a/pkg/client/unversioned/testclient/fake_podsecuritypolicy.go +++ b/pkg/client/unversioned/testclient/fake_podsecuritypolicy.go @@ -47,16 +47,16 @@ func (c *FakePodSecurityPolicy) Get(name string) (*extensions.PodSecurityPolicy, return obj.(*extensions.PodSecurityPolicy), err } -func (c *FakePodSecurityPolicy) Create(scc *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { - obj, err := c.Fake.Invokes(NewCreateAction("podsecuritypolicies", c.Namespace, scc), &extensions.PodSecurityPolicy{}) +func (c *FakePodSecurityPolicy) Create(psp *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { + obj, err := c.Fake.Invokes(NewCreateAction("podsecuritypolicies", c.Namespace, psp), &extensions.PodSecurityPolicy{}) if obj == nil { return nil, err } return obj.(*extensions.PodSecurityPolicy), err } -func (c *FakePodSecurityPolicy) Update(scc *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { - obj, err := c.Fake.Invokes(NewUpdateAction("podsecuritypolicies", c.Namespace, scc), &extensions.PodSecurityPolicy{}) +func (c *FakePodSecurityPolicy) Update(psp *extensions.PodSecurityPolicy) (*extensions.PodSecurityPolicy, error) { + obj, err := c.Fake.Invokes(NewUpdateAction("podsecuritypolicies", c.Namespace, psp), &extensions.PodSecurityPolicy{}) if obj == nil { return nil, err } diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 5c605877936..a649ac861a6 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -1689,9 +1689,9 @@ func printConfigMapList(list *api.ConfigMapList, w io.Writer, options PrintOptio } func printPodSecurityPolicy(item *extensions.PodSecurityPolicy, w io.Writer, options PrintOptions) error { - _, err := fmt.Fprintf(w, "%s\t%t\t%v\t%v\t%s\t%s\n", item.Name, item.Spec.Privileged, - item.Spec.Capabilities, item.Spec.Volumes, item.Spec.SELinux.Rule, - item.Spec.RunAsUser.Rule) + _, err := fmt.Fprintf(w, "%s\t%t\t%v\t%s\t%s\t%s\t%s\t%t\t%v\n", item.Name, item.Spec.Privileged, + item.Spec.AllowedCapabilities, item.Spec.SELinux.Rule, + item.Spec.RunAsUser.Rule, item.Spec.FSGroup.Rule, item.Spec.SupplementalGroups.Rule, item.Spec.ReadOnlyRootFilesystem, item.Spec.Volumes) return err } diff --git a/pkg/registry/podsecuritypolicy/etcd/etcd_test.go b/pkg/registry/podsecuritypolicy/etcd/etcd_test.go index 525bb9e19d2..b0c4371025b 100644 --- a/pkg/registry/podsecuritypolicy/etcd/etcd_test.go +++ b/pkg/registry/podsecuritypolicy/etcd/etcd_test.go @@ -49,6 +49,12 @@ func validNewPodSecurityPolicy() *extensions.PodSecurityPolicy { RunAsUser: extensions.RunAsUserStrategyOptions{ Rule: extensions.RunAsUserStrategyRunAsAny, }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, }, } } @@ -57,11 +63,11 @@ func TestCreate(t *testing.T) { storage, server := newStorage(t) defer server.Terminate(t) test := registrytest.New(t, storage.Store).ClusterScope() - scc := validNewPodSecurityPolicy() - scc.ObjectMeta = api.ObjectMeta{GenerateName: "foo-"} + psp := validNewPodSecurityPolicy() + psp.ObjectMeta = api.ObjectMeta{GenerateName: "foo-"} test.TestCreate( // valid - scc, + psp, // invalid &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{Name: "name with spaces"}, diff --git a/pkg/security/doc.go b/pkg/security/doc.go new file mode 100644 index 00000000000..7638b29591b --- /dev/null +++ b/pkg/security/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 security contains security apis and implementations. +package security diff --git a/pkg/security/podsecuritypolicy/capabilities/mustrunas.go b/pkg/security/podsecuritypolicy/capabilities/mustrunas.go new file mode 100644 index 00000000000..1ffc55fdba7 --- /dev/null +++ b/pkg/security/podsecuritypolicy/capabilities/mustrunas.go @@ -0,0 +1,149 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/sets" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// defaultCapabilities implements the CapabilitiesStrategy interface +type defaultCapabilities struct { + defaultAddCapabilities []api.Capability + requiredDropCapabilities []api.Capability + allowedCaps []api.Capability +} + +var _ CapabilitiesStrategy = &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 []api.Capability) (CapabilitiesStrategy, error) { + return &defaultCapabilities{ + defaultAddCapabilities: defaultAddCapabilities, + requiredDropCapabilities: requiredDropCapabilities, + allowedCaps: allowedCaps, + }, 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 +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() + + if container.SecurityContext != nil && container.SecurityContext.Capabilities != nil { + 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).List() + combinedDrop := requiredDrop.Union(containerDrop).List() + + // nothing generated? return nil + if len(combinedAdd) == 0 && len(combinedDrop) == 0 { + return nil, nil + } + + return &api.Capabilities{ + Add: capabilityFromStringSlice(combinedAdd), + Drop: capabilityFromStringSlice(combinedDrop), + }, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *defaultCapabilities) Validate(pod *api.Pod, container *api.Container) field.ErrorList { + allErrs := field.ErrorList{} + + // if the security context isn't set then we haven't generated correctly. Shouldn't get here + // if using the provider correctly + if container.SecurityContext == nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("securityContext"), container.SecurityContext, "no security context is set")) + return allErrs + } + + if container.SecurityContext.Capabilities == nil { + // if container.SC.Caps is nil then nothing was defaulted by the strat 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(field.NewPath("capabilities"), container.SecurityContext.Capabilities, + "required capabilities are not set on the securityContext")) + return allErrs + } + + // validate that anything being added is in the default or allowed sets + defaultAdd := makeCapSet(s.defaultAddCapabilities) + allowedAdd := makeCapSet(s.allowedCaps) + + for _, cap := range container.SecurityContext.Capabilities.Add { + sCap := string(cap) + if !defaultAdd.Has(sCap) && !allowedAdd.Has(sCap) { + allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities", "add"), sCap, "capability may not be added")) + } + } + + // validate that anything that is required to be dropped is in the drop set + containerDrops := makeCapSet(container.SecurityContext.Capabilities.Drop) + + for _, requiredDrop := range s.requiredDropCapabilities { + sDrop := string(requiredDrop) + if !containerDrops.Has(sDrop) { + allErrs = append(allErrs, field.Invalid(field.NewPath("capabilities", "drop"), container.SecurityContext.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 and normalizes them to be all lower case to help +// with comparisons. +func makeCapSet(caps []api.Capability) sets.String { + s := sets.NewString() + for _, c := range caps { + s.Insert(string(c)) + } + return s +} diff --git a/pkg/security/podsecuritypolicy/capabilities/mustrunas_test.go b/pkg/security/podsecuritypolicy/capabilities/mustrunas_test.go new file mode 100644 index 00000000000..46cd2a2a4ec --- /dev/null +++ b/pkg/security/podsecuritypolicy/capabilities/mustrunas_test.go @@ -0,0 +1,387 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "reflect" + "testing" +) + +func TestGenerateAdds(t *testing.T) { + tests := map[string]struct { + defaultAddCaps []api.Capability + requiredDropCaps []api.Capability + containerCaps *api.Capabilities + expectedCaps *api.Capabilities + }{ + "no required, no container requests": { + expectedCaps: nil, + }, + "required, no container requests": { + defaultAddCaps: []api.Capability{"foo"}, + expectedCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + }, + "required, container requests add required": { + defaultAddCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + expectedCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + }, + "multiple required, container requests add required": { + defaultAddCaps: []api.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: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"bar"}, + }, + expectedCaps: &api.Capabilities{ + Add: []api.Capability{"bar", "foo"}, + }, + }, + "generation dedupes": { + defaultAddCaps: []api.Capability{"foo", "foo", "foo", "foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"foo", "foo", "foo"}, + }, + expectedCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + }, + "generation is case sensitive - will not dedupe": { + defaultAddCaps: []api.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, 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 TestGenerateDrops(t *testing.T) { + tests := map[string]struct { + defaultAddCaps []api.Capability + requiredDropCaps []api.Capability + containerCaps *api.Capabilities + expectedCaps *api.Capabilities + }{ + "no required, no container requests": { + expectedCaps: nil, + }, + "required drops are defaulted": { + requiredDropCaps: []api.Capability{"foo"}, + expectedCaps: &api.Capabilities{ + Drop: []api.Capability{"foo"}, + }, + }, + "required drops are defaulted when making container requests": { + requiredDropCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"foo", "bar"}, + }, + expectedCaps: &api.Capabilities{ + Drop: []api.Capability{"bar", "foo"}, + }, + }, + "can drop a required add": { + defaultAddCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"foo"}, + }, + expectedCaps: &api.Capabilities{ + Drop: []api.Capability{"foo"}, + }, + }, + "can drop non-required add": { + defaultAddCaps: []api.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: []api.Capability{"foo", "bar", "baz"}, + requiredDropCaps: []api.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: []api.Capability{"bar", "bar", "bar", "bar"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"bar", "bar", "bar"}, + }, + expectedCaps: &api.Capabilities{ + Drop: []api.Capability{"bar"}, + }, + }, + "generation is case sensitive - will not dedupe": { + requiredDropCaps: []api.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 []api.Capability + requiredDropCaps []api.Capability + allowedCaps []api.Capability + containerCaps *api.Capabilities + shouldPass bool + }{ + // no container requests + "no required, no allowed, no container requests": { + shouldPass: true, + }, + "no required, allowed, no container requests": { + allowedCaps: []api.Capability{"foo"}, + shouldPass: true, + }, + "required, no allowed, no container requests": { + defaultAddCaps: []api.Capability{"foo"}, + shouldPass: false, + }, + + // container requests match required + "required, no allowed, container requests valid": { + defaultAddCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + shouldPass: true, + }, + "required, no allowed, container requests invalid": { + defaultAddCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"bar"}, + }, + shouldPass: false, + }, + + // container requests match allowed + "no required, allowed, container requests valid": { + allowedCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + shouldPass: true, + }, + "no required, allowed, container requests invalid": { + allowedCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"bar"}, + }, + shouldPass: false, + }, + + // required and allowed + "required, allowed, container requests valid required": { + defaultAddCaps: []api.Capability{"foo"}, + allowedCaps: []api.Capability{"bar"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"foo"}, + }, + shouldPass: true, + }, + "required, allowed, container requests valid allowed": { + defaultAddCaps: []api.Capability{"foo"}, + allowedCaps: []api.Capability{"bar"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"bar"}, + }, + shouldPass: true, + }, + "required, allowed, container requests invalid": { + defaultAddCaps: []api.Capability{"foo"}, + allowedCaps: []api.Capability{"bar"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"baz"}, + }, + shouldPass: false, + }, + "validation is case sensitive": { + defaultAddCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Add: []api.Capability{"FOO"}, + }, + shouldPass: false, + }, + } + + for k, v := range tests { + container := &api.Container{ + SecurityContext: &api.SecurityContext{ + Capabilities: v.containerCaps, + }, + } + + strategy, err := NewDefaultCapabilities(v.defaultAddCaps, v.requiredDropCaps, v.allowedCaps) + if err != nil { + t.Errorf("%s failed: %v", k, err) + continue + } + errs := strategy.Validate(nil, container) + if v.shouldPass && len(errs) > 0 { + t.Errorf("%s should have passed but had errors %v", k, errs) + continue + } + if !v.shouldPass && len(errs) == 0 { + t.Errorf("%s should have failed but recieved no errors", k) + } + } +} + +func TestValidateDrops(t *testing.T) { + tests := map[string]struct { + defaultAddCaps []api.Capability + requiredDropCaps []api.Capability + containerCaps *api.Capabilities + shouldPass bool + }{ + // no container requests + "no required, no container requests": { + shouldPass: true, + }, + "required, no container requests": { + requiredDropCaps: []api.Capability{"foo"}, + shouldPass: false, + }, + + // container requests match required + "required, container requests valid": { + requiredDropCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"foo"}, + }, + shouldPass: true, + }, + "required, container requests invalid": { + requiredDropCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"bar"}, + }, + shouldPass: false, + }, + "validation is case sensitive": { + requiredDropCaps: []api.Capability{"foo"}, + containerCaps: &api.Capabilities{ + Drop: []api.Capability{"FOO"}, + }, + shouldPass: false, + }, + } + + 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 + } + errs := strategy.Validate(nil, container) + if v.shouldPass && len(errs) > 0 { + t.Errorf("%s should have passed but had errors %v", k, errs) + continue + } + if !v.shouldPass && len(errs) == 0 { + t.Errorf("%s should have failed but recieved no errors", k) + } + } +} diff --git a/pkg/security/podsecuritypolicy/capabilities/types.go b/pkg/security/podsecuritypolicy/capabilities/types.go new file mode 100644 index 00000000000..428f2455842 --- /dev/null +++ b/pkg/security/podsecuritypolicy/capabilities/types.go @@ -0,0 +1,30 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// CapabilitiesStrategy defines the interface for all cap constraint strategies. +type CapabilitiesStrategy 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(pod *api.Pod, container *api.Container) field.ErrorList +} diff --git a/pkg/security/podsecuritypolicy/factory.go b/pkg/security/podsecuritypolicy/factory.go new file mode 100644 index 00000000000..477845f1476 --- /dev/null +++ b/pkg/security/podsecuritypolicy/factory.go @@ -0,0 +1,135 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" + "k8s.io/kubernetes/pkg/util/errors" +) + +type simpleStrategyFactory struct{} + +var _ StrategyFactory = &simpleStrategyFactory{} + +func NewSimpleStrategyFactory() StrategyFactory { + return &simpleStrategyFactory{} +} + +func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPolicy, namespace string) (*ProviderStrategies, error) { + errs := []error{} + + userStrat, err := createUserStrategy(&psp.Spec.RunAsUser) + if err != nil { + errs = append(errs, err) + } + + seLinuxStrat, err := createSELinuxStrategy(&psp.Spec.SELinux) + 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) + } + + if len(errs) > 0 { + return nil, errors.NewAggregate(errs) + } + + strategies := &ProviderStrategies{ + RunAsUserStrategy: userStrat, + SELinuxStrategy: seLinuxStrat, + FSGroupStrategy: fsGroupStrat, + SupplementalGroupStrategy: supGroupStrat, + CapabilitiesStrategy: capStrat, + } + + return strategies, nil +} + +// createUserStrategy creates a new user strategy. +func createUserStrategy(opts *extensions.RunAsUserStrategyOptions) (user.RunAsUserStrategy, error) { + switch opts.Rule { + case extensions.RunAsUserStrategyMustRunAs: + return user.NewMustRunAs(opts) + case extensions.RunAsUserStrategyMustRunAsNonRoot: + return user.NewRunAsNonRoot(opts) + case extensions.RunAsUserStrategyRunAsAny: + return user.NewRunAsAny(opts) + default: + return nil, fmt.Errorf("Unrecognized RunAsUser strategy type %s", opts.Rule) + } +} + +// createSELinuxStrategy creates a new selinux strategy. +func createSELinuxStrategy(opts *extensions.SELinuxStrategyOptions) (selinux.SELinuxStrategy, error) { + switch opts.Rule { + case extensions.SELinuxStrategyMustRunAs: + return selinux.NewMustRunAs(opts) + case extensions.SELinuxStrategyRunAsAny: + return selinux.NewRunAsAny(opts) + default: + return nil, fmt.Errorf("Unrecognized SELinuxContext strategy type %s", opts.Rule) + } +} + +// createFSGroupStrategy creates a new fsgroup strategy +func createFSGroupStrategy(opts *extensions.FSGroupStrategyOptions) (group.GroupStrategy, error) { + switch opts.Rule { + case extensions.FSGroupStrategyRunAsAny: + return group.NewRunAsAny() + case extensions.FSGroupStrategyMustRunAs: + return group.NewMustRunAs(opts.Ranges, fsGroupField) + default: + return nil, fmt.Errorf("Unrecognized FSGroup strategy type %s", opts.Rule) + } +} + +// createSupplementalGroupStrategy creates a new supplemental group strategy +func createSupplementalGroupStrategy(opts *extensions.SupplementalGroupsStrategyOptions) (group.GroupStrategy, error) { + switch opts.Rule { + case extensions.SupplementalGroupsStrategyRunAsAny: + return group.NewRunAsAny() + case extensions.SupplementalGroupsStrategyMustRunAs: + return group.NewMustRunAs(opts.Ranges, supplementalGroupsField) + default: + return nil, fmt.Errorf("Unrecognized SupplementalGroups strategy type %s", opts.Rule) + } +} + +// createCapabilitiesStrategy creates a new capabilities strategy. +func createCapabilitiesStrategy(defaultAddCaps, requiredDropCaps, allowedCaps []api.Capability) (capabilities.CapabilitiesStrategy, error) { + return capabilities.NewDefaultCapabilities(defaultAddCaps, requiredDropCaps, allowedCaps) +} diff --git a/pkg/security/podsecuritypolicy/group/mustrunas.go b/pkg/security/podsecuritypolicy/group/mustrunas.go new file mode 100644 index 00000000000..bcb2edade9f --- /dev/null +++ b/pkg/security/podsecuritypolicy/group/mustrunas.go @@ -0,0 +1,93 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// mustRunAs implements the GroupStrategy interface +type mustRunAs struct { + ranges []extensions.IDRange + field string +} + +var _ GroupStrategy = &mustRunAs{} + +// NewMustRunAs provides a new MustRunAs strategy based on ranges. +func NewMustRunAs(ranges []extensions.IDRange, field string) (GroupStrategy, error) { + if len(ranges) == 0 { + return nil, fmt.Errorf("ranges must be supplied for MustRunAs") + } + return &mustRunAs{ + ranges: ranges, + field: field, + }, 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(pod *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(pod *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(pod *api.Pod, groups []int64) field.ErrorList { + allErrs := field.ErrorList{} + + if pod.Spec.SecurityContext == nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("securityContext"), pod.Spec.SecurityContext, "unable to validate nil security context")) + return allErrs + } + + if len(groups) == 0 && len(s.ranges) > 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath(s.field), groups, "unable to validate empty groups against required ranges")) + } + + for _, group := range groups { + if !s.isGroupValid(group) { + detail := fmt.Sprintf("%d is not an allowed group", group) + allErrs = append(allErrs, field.Invalid(field.NewPath(s.field), groups, detail)) + } + } + + return allErrs +} + +func (s *mustRunAs) isGroupValid(group int64) bool { + for _, rng := range s.ranges { + if psputil.FallsInRange(group, rng) { + return true + } + } + return false +} diff --git a/pkg/security/podsecuritypolicy/group/mustrunas_test.go b/pkg/security/podsecuritypolicy/group/mustrunas_test.go new file mode 100644 index 00000000000..31c2c21098b --- /dev/null +++ b/pkg/security/podsecuritypolicy/group/mustrunas_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestMustRunAsOptions(t *testing.T) { + tests := map[string]struct { + ranges []extensions.IDRange + pass bool + }{ + "empty": { + ranges: []extensions.IDRange{}, + }, + "ranges": { + ranges: []extensions.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 []extensions.IDRange + expected []int64 + }{ + "multi value": { + ranges: []extensions.IDRange{ + {Min: 1, Max: 2}, + }, + expected: []int64{1}, + }, + "single value": { + ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + }, + expected: []int64{1}, + }, + "multi range": { + ranges: []extensions.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) { + validPod := func() *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{}, + }, + } + } + + tests := map[string]struct { + ranges []extensions.IDRange + pod *api.Pod + groups []int64 + pass bool + }{ + "nil security context": { + pod: &api.Pod{}, + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + }, + }, + "empty groups": { + pod: validPod(), + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + }, + }, + "not in range": { + pod: validPod(), + groups: []int64{5}, + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + {Min: 4, Max: 4}, + }, + }, + "in range 1": { + pod: validPod(), + groups: []int64{2}, + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + }, + pass: true, + }, + "in range boundry min": { + pod: validPod(), + groups: []int64{1}, + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + }, + pass: true, + }, + "in range boundry max": { + pod: validPod(), + groups: []int64{3}, + ranges: []extensions.IDRange{ + {Min: 1, Max: 3}, + }, + pass: true, + }, + "singular range": { + pod: validPod(), + groups: []int64{4}, + ranges: []extensions.IDRange{ + {Min: 4, Max: 4}, + }, + pass: true, + }, + } + + 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(v.pod, v.groups) + if v.pass && len(errs) > 0 { + t.Errorf("unexpected errors for %s: %v", k, errs) + } + if !v.pass && len(errs) == 0 { + t.Errorf("expected no errors for %s but got: %v", k, errs) + } + } +} diff --git a/pkg/security/podsecuritypolicy/group/runasany.go b/pkg/security/podsecuritypolicy/group/runasany.go new file mode 100644 index 00000000000..2398a2ab538 --- /dev/null +++ b/pkg/security/podsecuritypolicy/group/runasany.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// mustRunAs 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(pod *api.Pod) ([]int64, error) { + return []int64{}, nil +} + +// Generate a single value to be applied. This is used for FSGroup. This strategy returns nil. +func (s *runAsAny) GenerateSingle(pod *api.Pod) (*int64, error) { + return nil, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *runAsAny) Validate(pod *api.Pod, groups []int64) field.ErrorList { + return field.ErrorList{} + +} diff --git a/pkg/security/podsecuritypolicy/group/runasany_test.go b/pkg/security/podsecuritypolicy/group/runasany_test.go new file mode 100644 index 00000000000..be8b239b578 --- /dev/null +++ b/pkg/security/podsecuritypolicy/group/runasany_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" +) + +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 TestRunAsAnyValidte(t *testing.T) { + s, err := NewRunAsAny() + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + errs := s.Validate(nil, nil) + if len(errs) != 0 { + t.Errorf("unexpected errors: %v", errs) + } +} diff --git a/pkg/security/podsecuritypolicy/group/types.go b/pkg/security/podsecuritypolicy/group/types.go new file mode 100644 index 00000000000..be19fe9b9dc --- /dev/null +++ b/pkg/security/podsecuritypolicy/group/types.go @@ -0,0 +1,35 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// 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(pod *api.Pod, groups []int64) field.ErrorList +} diff --git a/pkg/security/podsecuritypolicy/provider.go b/pkg/security/podsecuritypolicy/provider.go new file mode 100644 index 00000000000..bf606a3384b --- /dev/null +++ b/pkg/security/podsecuritypolicy/provider.go @@ -0,0 +1,297 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// used to pass in the field being validated for reusable group strategies so they +// can create informative error messages. +const ( + fsGroupField = "fsGroup" + supplementalGroupsField = "supplementalGroups" +) + +// simpleProvider is the default implementation of Provider. +type simpleProvider struct { + psp *extensions.PodSecurityPolicy + strategies *ProviderStrategies +} + +// ensure we implement the interface correctly. +var _ Provider = &simpleProvider{} + +// NewSimpleProvider creates a new Provider instance. +func NewSimpleProvider(psp *extensions.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 +} + +// Create a PodSecurityContext based on the given constraints. If a setting is already set +// on the PodSecurityContext it will not be changed. Validate should be used after the context +// is created to ensure it complies with the required restrictions. +// +// NOTE: this method works on a copy of the PodSecurityContext. It is up to the caller to +// apply the PSC if validation passes. +func (s *simpleProvider) CreatePodSecurityContext(pod *api.Pod) (*api.PodSecurityContext, error) { + var sc *api.PodSecurityContext = nil + if pod.Spec.SecurityContext != nil { + // work with a copy + copy := *pod.Spec.SecurityContext + sc = © + } else { + sc = &api.PodSecurityContext{} + } + + if len(sc.SupplementalGroups) == 0 { + supGroups, err := s.strategies.SupplementalGroupStrategy.Generate(pod) + if err != nil { + return nil, err + } + sc.SupplementalGroups = supGroups + } + + if sc.FSGroup == nil { + fsGroup, err := s.strategies.FSGroupStrategy.GenerateSingle(pod) + if err != nil { + return nil, err + } + sc.FSGroup = fsGroup + } + + if sc.SELinuxOptions == nil { + seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, nil) + if err != nil { + return nil, err + } + sc.SELinuxOptions = seLinux + } + + return sc, nil +} + +// Create a SecurityContext based on the given constraints. If a setting is already set on the +// container's security context then it will not be changed. Validation should be used after +// the context is created to ensure it complies with the required restrictions. +// +// NOTE: this method works on a copy of the SC of the container. It is up to the caller to apply +// the SC if validation passes. +func (s *simpleProvider) CreateContainerSecurityContext(pod *api.Pod, container *api.Container) (*api.SecurityContext, error) { + var sc *api.SecurityContext = nil + if container.SecurityContext != nil { + // work with a copy of the original + copy := *container.SecurityContext + sc = © + } else { + sc = &api.SecurityContext{} + } + if sc.RunAsUser == nil { + uid, err := s.strategies.RunAsUserStrategy.Generate(pod, container) + if err != nil { + return nil, err + } + sc.RunAsUser = uid + } + + if sc.SELinuxOptions == nil { + seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, container) + if err != nil { + return nil, err + } + sc.SELinuxOptions = seLinux + } + + if sc.Privileged == nil { + priv := false + sc.Privileged = &priv + } + + // 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 s.psp.Spec.RunAsUser.Rule == extensions.RunAsUserStrategyMustRunAsNonRoot { + nonRoot := true + sc.RunAsNonRoot = &nonRoot + } + + caps, err := s.strategies.CapabilitiesStrategy.Generate(pod, container) + if err != nil { + return nil, err + } + sc.Capabilities = 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.ReadOnlyRootFilesystem = &readOnlyRootFS + } + + return sc, nil +} + +// Ensure a pod's SecurityContext is in compliance with the given constraints. +func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if pod.Spec.SecurityContext == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("securityContext"), pod.Spec.SecurityContext, "No security context is set")) + return allErrs + } + + fsGroups := []int64{} + if pod.Spec.SecurityContext.FSGroup != nil { + fsGroups = append(fsGroups, *pod.Spec.SecurityContext.FSGroup) + } + allErrs = append(allErrs, s.strategies.FSGroupStrategy.Validate(pod, fsGroups)...) + allErrs = append(allErrs, s.strategies.SupplementalGroupStrategy.Validate(pod, pod.Spec.SecurityContext.SupplementalGroups)...) + + // make a dummy container context to reuse the selinux strategies + container := &api.Container{ + Name: pod.Name, + SecurityContext: &api.SecurityContext{ + SELinuxOptions: pod.Spec.SecurityContext.SELinuxOptions, + }, + } + allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(pod, container)...) + + if !s.psp.Spec.HostNetwork && pod.Spec.SecurityContext.HostNetwork { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostNetwork"), pod.Spec.SecurityContext.HostNetwork, "Host network is not allowed to be used")) + } + + if !s.psp.Spec.HostPID && pod.Spec.SecurityContext.HostPID { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPID"), pod.Spec.SecurityContext.HostPID, "Host PID is not allowed to be used")) + } + + if !s.psp.Spec.HostIPC && pod.Spec.SecurityContext.HostIPC { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostIPC"), pod.Spec.SecurityContext.HostIPC, "Host IPC is not allowed to be used")) + } + + return allErrs +} + +// Ensure a container's SecurityContext is in compliance with the given constraints +func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, container *api.Container, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if container.SecurityContext == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("securityContext"), container.SecurityContext, "No security context is set")) + return allErrs + } + + sc := container.SecurityContext + allErrs = append(allErrs, s.strategies.RunAsUserStrategy.Validate(pod, container)...) + allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(pod, container)...) + + if !s.psp.Spec.Privileged && *sc.Privileged { + allErrs = append(allErrs, field.Invalid(fldPath.Child("privileged"), *sc.Privileged, "Privileged containers are not allowed")) + } + + allErrs = append(allErrs, s.strategies.CapabilitiesStrategy.Validate(pod, container)...) + + if len(pod.Spec.Volumes) > 0 && !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(fldPath.Child("volumes").Index(i), string(fsType), err.Error())) + continue + } + + if !allowedVolumes.Has(string(fsType)) { + allErrs = append(allErrs, field.Invalid( + fldPath.Child("volumes").Index(i), string(fsType), + fmt.Sprintf("%s volumes are not allowed to be used", string(fsType)))) + } + } + } + + if !s.psp.Spec.HostNetwork && pod.Spec.SecurityContext.HostNetwork { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostNetwork"), pod.Spec.SecurityContext.HostNetwork, "Host network is not allowed to be used")) + } + + containersPath := fldPath.Child("containers") + for idx, c := range pod.Spec.Containers { + idxPath := containersPath.Index(idx) + allErrs = append(allErrs, s.hasInvalidHostPort(&c, idxPath)...) + } + + if !s.psp.Spec.HostPID && pod.Spec.SecurityContext.HostPID { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPID"), pod.Spec.SecurityContext.HostPID, "Host PID is not allowed to be used")) + } + + if !s.psp.Spec.HostIPC && pod.Spec.SecurityContext.HostIPC { + allErrs = append(allErrs, field.Invalid(fldPath.Child("hostIPC"), pod.Spec.SecurityContext.HostIPC, "Host IPC is not allowed to be used")) + } + + if s.psp.Spec.ReadOnlyRootFilesystem { + if sc.ReadOnlyRootFilesystem == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("readOnlyRootFilesystem"), sc.ReadOnlyRootFilesystem, "ReadOnlyRootFilesystem may not be nil and must be set to true")) + } else if !*sc.ReadOnlyRootFilesystem { + allErrs = append(allErrs, field.Invalid(fldPath.Child("readOnlyRootFilesystem"), *sc.ReadOnlyRootFilesystem, "ReadOnlyRootFilesystem must be set to true")) + } + } + + return allErrs +} + +// hasHostPort checks the port definitions on the container for HostPort > 0. +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(int(cp.HostPort)) { + detail := fmt.Sprintf("Host port %d is not allowed to be used. Allowed ports: %v", cp.HostPort, 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 int) 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 +} diff --git a/pkg/security/podsecuritypolicy/provider_test.go b/pkg/security/podsecuritypolicy/provider_test.go new file mode 100644 index 00000000000..70c89b7e54d --- /dev/null +++ b/pkg/security/podsecuritypolicy/provider_test.go @@ -0,0 +1,822 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + "k8s.io/kubernetes/pkg/util/diff" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +func TestCreatePodSecurityContextNonmutating(t *testing.T) { + // Create a pod with a security context that needs filling in + createPod := func() *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{}, + }, + } + } + + // Create a PSP with strategies that will populate a blank psc + createPSP := func() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: api.ObjectMeta{ + Name: "psp-sa", + }, + Spec: extensions.PodSecurityPolicySpec{ + DefaultAddCapabilities: []api.Capability{"foo"}, + RequiredDropCapabilities: []api.Capability{"bar"}, + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyRunAsAny, + }, + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyRunAsAny, + }, + // these are pod mutating strategies that are tested above + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + }, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + }, + }, + }, + } + } + + pod := createPod() + psp := createPSP() + + provider, err := NewSimpleProvider(psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + sc, err := provider.CreatePodSecurityContext(pod) + if err != nil { + t.Fatalf("unable to create psc %v", err) + } + + // The generated security context should have filled in missing options, so they should differ + if reflect.DeepEqual(sc, &pod.Spec.SecurityContext) { + t.Error("expected created security context to be different than container's, but they were identical") + } + + // Creating the provider or the security context should not have mutated the psp or pod + if !reflect.DeepEqual(createPod(), pod) { + diffs := diff.ObjectDiff(createPod(), pod) + t.Errorf("pod was mutated by CreatePodSecurityContext. diff:\n%s", diffs) + } + if !reflect.DeepEqual(createPSP(), psp) { + t.Error("psp was mutated by CreatePodSecurityContext") + } +} + +func TestCreateContainerSecurityContextNonmutating(t *testing.T) { + // Create a pod with a security context that needs filling in + createPod := func() *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + SecurityContext: &api.SecurityContext{}, + }}, + }, + } + } + + // Create a PSP with strategies that will populate a blank security context + createPSP := func() *extensions.PodSecurityPolicy { + var uid int64 = 1 + return &extensions.PodSecurityPolicy{ + ObjectMeta: api.ObjectMeta{ + Name: "psp-sa", + }, + Spec: extensions.PodSecurityPolicySpec{ + DefaultAddCapabilities: []api.Capability{"foo"}, + RequiredDropCapabilities: []api.Capability{"bar"}, + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyMustRunAs, + Ranges: []extensions.IDRange{{Min: uid, Max: uid}}, + }, + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyMustRunAs, + SELinuxOptions: &api.SELinuxOptions{User: "you"}, + }, + // these are pod mutating strategies that are tested above + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, + // mutates the container SC by defaulting to true if container sets nil + ReadOnlyRootFilesystem: true, + }, + } + } + + pod := createPod() + psp := createPSP() + + provider, err := NewSimpleProvider(psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + sc, err := provider.CreateContainerSecurityContext(pod, &pod.Spec.Containers[0]) + if err != nil { + t.Fatalf("unable to create container security context %v", err) + } + + // The generated security context should have filled in missing options, so they should differ + if reflect.DeepEqual(sc, &pod.Spec.Containers[0].SecurityContext) { + t.Error("expected created security context to be different than container's, but they were identical") + } + + // Creating the provider or the security context should not have mutated the psp or pod + if !reflect.DeepEqual(createPod(), pod) { + diffs := diff.ObjectDiff(createPod(), pod) + t.Errorf("pod was mutated by CreateContainerSecurityContext. diff:\n%s", diffs) + } + if !reflect.DeepEqual(createPSP(), psp) { + t.Error("psp was mutated by CreateContainerSecurityContext") + } +} + +func TestValidatePodSecurityContextFailures(t *testing.T) { + failHostNetworkPod := defaultPod() + failHostNetworkPod.Spec.SecurityContext.HostNetwork = true + + failHostPIDPod := defaultPod() + failHostPIDPod.Spec.SecurityContext.HostPID = true + + failHostIPCPod := defaultPod() + failHostIPCPod.Spec.SecurityContext.HostIPC = true + + failSupplementalGroupPod := defaultPod() + failSupplementalGroupPod.Spec.SecurityContext.SupplementalGroups = []int64{999} + failSupplementalGroupPSP := defaultPSP() + failSupplementalGroupPSP.Spec.SupplementalGroups = extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + }, + } + + failFSGroupPod := defaultPod() + fsGroup := int64(999) + failFSGroupPod.Spec.SecurityContext.FSGroup = &fsGroup + failFSGroupPSP := defaultPSP() + failFSGroupPSP.Spec.FSGroup = extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + }, + } + + failNilSELinuxPod := defaultPod() + failSELinuxPSP := defaultPSP() + failSELinuxPSP.Spec.SELinux.Rule = extensions.SELinuxStrategyMustRunAs + failSELinuxPSP.Spec.SELinux.SELinuxOptions = &api.SELinuxOptions{ + Level: "foo", + } + + failInvalidSELinuxPod := defaultPod() + failInvalidSELinuxPod.Spec.SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "bar", + } + + errorCases := map[string]struct { + pod *api.Pod + psp *extensions.PodSecurityPolicy + expectedError string + }{ + "failHostNetwork": { + pod: failHostNetworkPod, + psp: defaultPSP(), + expectedError: "Host network is not allowed to be used", + }, + "failHostPID": { + pod: failHostPIDPod, + psp: defaultPSP(), + expectedError: "Host PID is not allowed to be used", + }, + "failHostIPC": { + pod: failHostIPCPod, + psp: defaultPSP(), + expectedError: "Host IPC is not allowed to be used", + }, + "failSupplementalGroupOutOfRange": { + pod: failSupplementalGroupPod, + psp: failSupplementalGroupPSP, + expectedError: "999 is not an allowed group", + }, + "failSupplementalGroupEmpty": { + pod: defaultPod(), + psp: failSupplementalGroupPSP, + expectedError: "unable to validate empty groups against required ranges", + }, + "failFSGroupOutOfRange": { + pod: failFSGroupPod, + psp: failFSGroupPSP, + expectedError: "999 is not an allowed group", + }, + "failFSGroupEmpty": { + pod: defaultPod(), + psp: failFSGroupPSP, + expectedError: "unable to validate empty groups against required ranges", + }, + "failNilSELinux": { + pod: failNilSELinuxPod, + psp: failSELinuxPSP, + expectedError: "unable to validate nil seLinuxOptions", + }, + "failInvalidSELinux": { + pod: failInvalidSELinuxPod, + psp: failSELinuxPSP, + expectedError: "does not match required level. Found bar, wanted foo", + }, + } + for k, v := range errorCases { + provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + errs := provider.ValidatePodSecurityContext(v.pod, field.NewPath("")) + if len(errs) == 0 { + t.Errorf("%s expected validation failure but did not receive errors", k) + continue + } + if !strings.Contains(errs[0].Error(), v.expectedError) { + t.Errorf("%s received unexpected error %v", k, errs) + } + } +} + +func TestValidateContainerSecurityContextFailures(t *testing.T) { + // fail user strat + failUserPSP := defaultPSP() + var uid int64 = 999 + var badUID int64 = 1 + failUserPSP.Spec.RunAsUser = extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyMustRunAs, + Ranges: []extensions.IDRange{{Min: uid, Max: uid}}, + } + failUserPod := defaultPod() + failUserPod.Spec.Containers[0].SecurityContext.RunAsUser = &badUID + + // fail selinux strat + failSELinuxPSP := defaultPSP() + failSELinuxPSP.Spec.SELinux = extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyMustRunAs, + SELinuxOptions: &api.SELinuxOptions{ + Level: "foo", + }, + } + failSELinuxPod := defaultPod() + failSELinuxPod.Spec.Containers[0].SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "bar", + } + + failPrivPod := defaultPod() + var priv bool = true + failPrivPod.Spec.Containers[0].SecurityContext.Privileged = &priv + + failCapsPod := defaultPod() + failCapsPod.Spec.Containers[0].SecurityContext.Capabilities = &api.Capabilities{ + Add: []api.Capability{"foo"}, + } + + failHostDirPod := defaultPod() + failHostDirPod.Spec.Volumes = []api.Volume{ + { + Name: "bad volume", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{}, + }, + }, + } + + failHostPortPod := defaultPod() + failHostPortPod.Spec.Containers[0].Ports = []api.ContainerPort{{HostPort: 1}} + + readOnlyRootFSPSP := defaultPSP() + readOnlyRootFSPSP.Spec.ReadOnlyRootFilesystem = true + + readOnlyRootFSPodFalse := defaultPod() + readOnlyRootFS := false + readOnlyRootFSPodFalse.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFS + + errorCases := map[string]struct { + pod *api.Pod + psp *extensions.PodSecurityPolicy + expectedError string + }{ + "failUserPSP": { + pod: failUserPod, + psp: failUserPSP, + expectedError: "does not match required range", + }, + "failSELinuxPSP": { + pod: failSELinuxPod, + psp: failSELinuxPSP, + expectedError: "does not match required level", + }, + "failPrivPSP": { + pod: failPrivPod, + psp: defaultPSP(), + expectedError: "Privileged containers are not allowed", + }, + "failCapsPSP": { + pod: failCapsPod, + psp: defaultPSP(), + expectedError: "capability may not be added", + }, + "failHostDirPSP": { + pod: failHostDirPod, + psp: defaultPSP(), + expectedError: "hostPath volumes are not allowed to be used", + }, + "failHostPortPSP": { + pod: failHostPortPod, + psp: defaultPSP(), + expectedError: "Host port 1 is not allowed to be used. Allowed ports: []", + }, + "failReadOnlyRootFS - nil": { + pod: defaultPod(), + psp: readOnlyRootFSPSP, + expectedError: "ReadOnlyRootFilesystem may not be nil and must be set to true", + }, + "failReadOnlyRootFS - false": { + pod: readOnlyRootFSPodFalse, + psp: readOnlyRootFSPSP, + expectedError: "ReadOnlyRootFilesystem must be set to true", + }, + } + + for k, v := range errorCases { + provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + errs := provider.ValidateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0], field.NewPath("")) + if len(errs) == 0 { + t.Errorf("%s expected validation failure but did not receive errors", k) + continue + } + if !strings.Contains(errs[0].Error(), v.expectedError) { + t.Errorf("%s received unexpected error %v", k, errs) + } + } +} + +func TestValidatePodSecurityContextSuccess(t *testing.T) { + hostNetworkPSP := defaultPSP() + hostNetworkPSP.Spec.HostNetwork = true + hostNetworkPod := defaultPod() + hostNetworkPod.Spec.SecurityContext.HostNetwork = true + + hostPIDPSP := defaultPSP() + hostPIDPSP.Spec.HostPID = true + hostPIDPod := defaultPod() + hostPIDPod.Spec.SecurityContext.HostPID = true + + hostIPCPSP := defaultPSP() + hostIPCPSP.Spec.HostIPC = true + hostIPCPod := defaultPod() + hostIPCPod.Spec.SecurityContext.HostIPC = true + + supGroupPSP := defaultPSP() + supGroupPSP.Spec.SupplementalGroups = extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 5}, + }, + } + supGroupPod := defaultPod() + supGroupPod.Spec.SecurityContext.SupplementalGroups = []int64{3} + + fsGroupPSP := defaultPSP() + fsGroupPSP.Spec.FSGroup = extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyMustRunAs, + Ranges: []extensions.IDRange{ + {Min: 1, Max: 5}, + }, + } + fsGroupPod := defaultPod() + fsGroup := int64(3) + fsGroupPod.Spec.SecurityContext.FSGroup = &fsGroup + + seLinuxPod := defaultPod() + seLinuxPod.Spec.SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + User: "user", + Role: "role", + Type: "type", + Level: "level", + } + seLinuxPSP := defaultPSP() + seLinuxPSP.Spec.SELinux.Rule = extensions.SELinuxStrategyMustRunAs + seLinuxPSP.Spec.SELinux.SELinuxOptions = &api.SELinuxOptions{ + User: "user", + Role: "role", + Type: "type", + Level: "level", + } + + errorCases := map[string]struct { + pod *api.Pod + psp *extensions.PodSecurityPolicy + }{ + "pass hostNetwork validating PSP": { + pod: hostNetworkPod, + psp: hostNetworkPSP, + }, + "pass hostPID validating PSP": { + pod: hostPIDPod, + psp: hostPIDPSP, + }, + "pass hostIPC validating PSP": { + pod: hostIPCPod, + psp: hostIPCPSP, + }, + "pass supplemental group validating PSP": { + pod: supGroupPod, + psp: supGroupPSP, + }, + "pass fs group validating PSP": { + pod: fsGroupPod, + psp: fsGroupPSP, + }, + "pass selinux validating PSP": { + pod: seLinuxPod, + psp: seLinuxPSP, + }, + } + + for k, v := range errorCases { + provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + errs := provider.ValidatePodSecurityContext(v.pod, field.NewPath("")) + if len(errs) != 0 { + t.Errorf("%s expected validation pass but received errors %v", k, errs) + continue + } + } +} + +func TestValidateContainerSecurityContextSuccess(t *testing.T) { + var notPriv bool = false + defaultPod := func() *api.Pod { + return &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{}, + Containers: []api.Container{ + { + SecurityContext: &api.SecurityContext{ + // expected to be set by defaulting mechanisms + Privileged: ¬Priv, + // fill in the rest for test cases + }, + }, + }, + }, + } + } + + // fail user strat + userPSP := defaultPSP() + var uid int64 = 999 + userPSP.Spec.RunAsUser = extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyMustRunAs, + Ranges: []extensions.IDRange{{Min: uid, Max: uid}}, + } + userPod := defaultPod() + userPod.Spec.Containers[0].SecurityContext.RunAsUser = &uid + + // fail selinux strat + seLinuxPSP := defaultPSP() + seLinuxPSP.Spec.SELinux = extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyMustRunAs, + SELinuxOptions: &api.SELinuxOptions{ + Level: "foo", + }, + } + seLinuxPod := defaultPod() + seLinuxPod.Spec.Containers[0].SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "foo", + } + + privPSP := defaultPSP() + privPSP.Spec.Privileged = true + privPod := defaultPod() + var priv bool = true + privPod.Spec.Containers[0].SecurityContext.Privileged = &priv + + capsPSP := defaultPSP() + capsPSP.Spec.AllowedCapabilities = []api.Capability{"foo"} + capsPod := defaultPod() + capsPod.Spec.Containers[0].SecurityContext.Capabilities = &api.Capabilities{ + Add: []api.Capability{"foo"}, + } + + // pod should be able to request caps that are in the required set even if not specified in the allowed set + requiredCapsPSP := defaultPSP() + requiredCapsPSP.Spec.DefaultAddCapabilities = []api.Capability{"foo"} + requiredCapsPod := defaultPod() + requiredCapsPod.Spec.Containers[0].SecurityContext.Capabilities = &api.Capabilities{ + Add: []api.Capability{"foo"}, + } + + hostDirPSP := defaultPSP() + hostDirPSP.Spec.Volumes = []extensions.FSType{extensions.HostPath} + hostDirPod := defaultPod() + hostDirPod.Spec.Volumes = []api.Volume{ + { + Name: "bad volume", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{}, + }, + }, + } + + hostPortPSP := defaultPSP() + hostPortPSP.Spec.HostPorts = []extensions.HostPortRange{{Min: 1, Max: 1}} + hostPortPod := defaultPod() + hostPortPod.Spec.Containers[0].Ports = []api.ContainerPort{{HostPort: 1}} + + readOnlyRootFSPodFalse := defaultPod() + readOnlyRootFSFalse := false + readOnlyRootFSPodFalse.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFSFalse + + readOnlyRootFSPodTrue := defaultPod() + readOnlyRootFSTrue := true + readOnlyRootFSPodTrue.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFSTrue + + errorCases := map[string]struct { + pod *api.Pod + psp *extensions.PodSecurityPolicy + }{ + "pass user must run as PSP": { + pod: userPod, + psp: userPSP, + }, + "pass seLinux must run as PSP": { + pod: seLinuxPod, + psp: seLinuxPSP, + }, + "pass priv validating PSP": { + pod: privPod, + psp: privPSP, + }, + "pass allowed caps validating PSP": { + pod: capsPod, + psp: capsPSP, + }, + "pass required caps validating PSP": { + pod: requiredCapsPod, + psp: requiredCapsPSP, + }, + "pass hostDir validating PSP": { + pod: hostDirPod, + psp: hostDirPSP, + }, + "pass hostPort validating PSP": { + pod: hostPortPod, + psp: hostPortPSP, + }, + "pass read only root fs - nil": { + pod: defaultPod(), + psp: defaultPSP(), + }, + "pass read only root fs - false": { + pod: readOnlyRootFSPodFalse, + psp: defaultPSP(), + }, + "pass read only root fs - true": { + pod: readOnlyRootFSPodTrue, + psp: defaultPSP(), + }, + } + + for k, v := range errorCases { + provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Fatalf("unable to create provider %v", err) + } + errs := provider.ValidateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0], field.NewPath("")) + if len(errs) != 0 { + t.Errorf("%s expected validation pass but received errors %v", k, errs) + continue + } + } +} + +func TestGenerateContainerSecurityContextReadOnlyRootFS(t *testing.T) { + truePSP := defaultPSP() + truePSP.Spec.ReadOnlyRootFilesystem = true + + trueVal := true + expectTrue := &trueVal + falseVal := false + expectFalse := &falseVal + + falsePod := defaultPod() + falsePod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = expectFalse + + truePod := defaultPod() + truePod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = expectTrue + + tests := map[string]struct { + pod *api.Pod + psp *extensions.PodSecurityPolicy + expected *bool + }{ + "false psp, nil sc": { + psp: defaultPSP(), + pod: defaultPod(), + expected: nil, + }, + "false psp, false sc": { + psp: defaultPSP(), + pod: falsePod, + expected: expectFalse, + }, + "false psp, true sc": { + psp: defaultPSP(), + pod: truePod, + expected: expectTrue, + }, + "true psp, nil sc": { + psp: truePSP, + pod: defaultPod(), + expected: expectTrue, + }, + "true psp, false sc": { + psp: truePSP, + pod: falsePod, + // expect false even though it defaults to true to ensure it doesn't change set values + // validation catches the mismatch, not generation + expected: expectFalse, + }, + "true psp, true sc": { + psp: truePSP, + pod: truePod, + expected: expectTrue, + }, + } + + for k, v := range tests { + provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Errorf("%s unable to create provider %v", k, err) + continue + } + sc, err := provider.CreateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0]) + if err != nil { + t.Errorf("%s unable to create container security context %v", k, err) + continue + } + + if v.expected == nil && sc.ReadOnlyRootFilesystem != nil { + t.Errorf("%s expected a nil ReadOnlyRootFilesystem but got %t", k, *sc.ReadOnlyRootFilesystem) + } + if v.expected != nil && sc.ReadOnlyRootFilesystem == nil { + t.Errorf("%s expected a non nil ReadOnlyRootFilesystem but recieved nil", k) + } + if v.expected != nil && sc.ReadOnlyRootFilesystem != nil && (*v.expected != *sc.ReadOnlyRootFilesystem) { + t.Errorf("%s expected a non nil ReadOnlyRootFilesystem set to %t but got %t", k, *v.expected, *sc.ReadOnlyRootFilesystem) + } + + } +} + +func defaultPSP() *extensions.PodSecurityPolicy { + return &extensions.PodSecurityPolicy{ + ObjectMeta: api.ObjectMeta{ + Name: "psp-sa", + }, + Spec: extensions.PodSecurityPolicySpec{ + RunAsUser: extensions.RunAsUserStrategyOptions{ + Rule: extensions.RunAsUserStrategyRunAsAny, + }, + SELinux: extensions.SELinuxStrategyOptions{ + Rule: extensions.SELinuxStrategyRunAsAny, + }, + FSGroup: extensions.FSGroupStrategyOptions{ + Rule: extensions.FSGroupStrategyRunAsAny, + }, + SupplementalGroups: extensions.SupplementalGroupsStrategyOptions{ + Rule: extensions.SupplementalGroupsStrategyRunAsAny, + }, + }, + } +} + +func defaultPod() *api.Pod { + var notPriv bool = false + return &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + // fill in for test cases + }, + Containers: []api.Container{ + { + SecurityContext: &api.SecurityContext{ + // expected to be set by defaulting mechanisms + Privileged: ¬Priv, + // fill in the rest for test cases + }, + }, + }, + }, + } +} + +// TestValidateAllowedVolumes will test that for every field of VolumeSource we can create +// a pod with that type of volume and deny it, accept it explicitly, or accept it with +// the FSTypeAll wildcard. +func TestValidateAllowedVolumes(t *testing.T) { + val := reflect.ValueOf(api.VolumeSource{}) + + for i := 0; i < val.NumField(); i++ { + // reflectively create the volume source + fieldVal := val.Type().Field(i) + + volumeSource := api.VolumeSource{} + volumeSourceVolume := reflect.New(fieldVal.Type.Elem()) + + reflect.ValueOf(&volumeSource).Elem().FieldByName(fieldVal.Name).Set(volumeSourceVolume) + volume := api.Volume{VolumeSource: volumeSource} + + // sanity check before moving on + fsType, err := psputil.GetVolumeFSType(volume) + if err != nil { + t.Errorf("error getting FSType for %s: %s", fieldVal.Name, err.Error()) + continue + } + + // add the volume to the pod + pod := defaultPod() + pod.Spec.Volumes = []api.Volume{volume} + + // create a PSP that allows no volumes + psp := defaultPSP() + + provider, err := NewSimpleProvider(psp, "namespace", NewSimpleStrategyFactory()) + if err != nil { + t.Errorf("error creating provider for %s: %s", fieldVal.Name, err.Error()) + continue + } + + // expect a denial for this PSP and test the error message to ensure it's related to the volumesource + errs := provider.ValidateContainerSecurityContext(pod, &pod.Spec.Containers[0], field.NewPath("")) + if len(errs) != 1 { + t.Errorf("expected exactly 1 error for %s but got %v", fieldVal.Name, errs) + } else { + if !strings.Contains(errs.ToAggregate().Error(), fmt.Sprintf("%s volumes are not allowed to be used", fsType)) { + t.Errorf("did not find the expected error, received: %v", errs) + } + } + + // now add the fstype directly to the psp and it should validate + psp.Spec.Volumes = []extensions.FSType{fsType} + errs = provider.ValidateContainerSecurityContext(pod, &pod.Spec.Containers[0], field.NewPath("")) + if len(errs) != 0 { + t.Errorf("directly allowing volume expected no errors for %s but got %v", fieldVal.Name, errs) + } + + // now change the psp to allow any volumes and the pod should still validate + psp.Spec.Volumes = []extensions.FSType{extensions.All} + errs = provider.ValidateContainerSecurityContext(pod, &pod.Spec.Containers[0], field.NewPath("")) + if len(errs) != 0 { + t.Errorf("wildcard volume expected no errors for %s but got %v", fieldVal.Name, errs) + } + } +} diff --git a/pkg/security/podsecuritypolicy/selinux/mustrunas.go b/pkg/security/podsecuritypolicy/selinux/mustrunas.go new file mode 100644 index 00000000000..4b59211bb4d --- /dev/null +++ b/pkg/security/podsecuritypolicy/selinux/mustrunas.go @@ -0,0 +1,84 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +type mustRunAs struct { + opts *extensions.SELinuxStrategyOptions +} + +var _ SELinuxStrategy = &mustRunAs{} + +func NewMustRunAs(options *extensions.SELinuxStrategyOptions) (SELinuxStrategy, error) { + if options == nil { + return nil, fmt.Errorf("MustRunAs requires SELinuxContextStrategyOptions") + } + if options.SELinuxOptions == nil { + return nil, fmt.Errorf("MustRunAs requires SELinuxOptions") + } + return &mustRunAs{ + opts: options, + }, nil +} + +// Generate creates the SELinuxOptions based on constraint rules. +func (s *mustRunAs) Generate(pod *api.Pod, container *api.Container) (*api.SELinuxOptions, error) { + return s.opts.SELinuxOptions, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *mustRunAs) Validate(pod *api.Pod, container *api.Container) field.ErrorList { + allErrs := field.ErrorList{} + + if container.SecurityContext == nil { + detail := fmt.Sprintf("unable to validate nil security context for %s", container.Name) + allErrs = append(allErrs, field.Invalid(field.NewPath("securityContext"), container.SecurityContext, detail)) + return allErrs + } + if container.SecurityContext.SELinuxOptions == nil { + detail := fmt.Sprintf("unable to validate nil seLinuxOptions for %s", container.Name) + allErrs = append(allErrs, field.Invalid(field.NewPath("seLinuxOptions"), container.SecurityContext.SELinuxOptions, detail)) + return allErrs + } + seLinuxOptionsPath := field.NewPath("seLinuxOptions") + seLinux := container.SecurityContext.SELinuxOptions + if seLinux.Level != s.opts.SELinuxOptions.Level { + detail := fmt.Sprintf("seLinuxOptions.level on %s does not match required level. Found %s, wanted %s", container.Name, seLinux.Level, s.opts.SELinuxOptions.Level) + allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("level"), seLinux.Level, detail)) + } + if seLinux.Role != s.opts.SELinuxOptions.Role { + detail := fmt.Sprintf("seLinuxOptions.role on %s does not match required role. Found %s, wanted %s", container.Name, seLinux.Role, s.opts.SELinuxOptions.Role) + allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("role"), seLinux.Role, detail)) + } + if seLinux.Type != s.opts.SELinuxOptions.Type { + detail := fmt.Sprintf("seLinuxOptions.type on %s does not match required type. Found %s, wanted %s", container.Name, seLinux.Type, s.opts.SELinuxOptions.Type) + allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("type"), seLinux.Type, detail)) + } + if seLinux.User != s.opts.SELinuxOptions.User { + detail := fmt.Sprintf("seLinuxOptions.user on %s does not match required user. Found %s, wanted %s", container.Name, seLinux.User, s.opts.SELinuxOptions.User) + allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("user"), seLinux.User, detail)) + } + + return allErrs +} diff --git a/pkg/security/podsecuritypolicy/selinux/mustrunas_test.go b/pkg/security/podsecuritypolicy/selinux/mustrunas_test.go new file mode 100644 index 00000000000..153c3e5072c --- /dev/null +++ b/pkg/security/podsecuritypolicy/selinux/mustrunas_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 ( + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestMustRunAsOptions(t *testing.T) { + tests := map[string]struct { + opts *extensions.SELinuxStrategyOptions + pass bool + }{ + "invalid opts": { + opts: &extensions.SELinuxStrategyOptions{}, + pass: false, + }, + "valid opts": { + opts: &extensions.SELinuxStrategyOptions{SELinuxOptions: &api.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 := &extensions.SELinuxStrategyOptions{ + SELinuxOptions: &api.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) + } + if !reflect.DeepEqual(generated, opts.SELinuxOptions) { + t.Errorf("generated selinux does not equal configured selinux") + } +} + +func TestMustRunAsValidate(t *testing.T) { + newValidOpts := func() *api.SELinuxOptions { + return &api.SELinuxOptions{ + User: "user", + Role: "role", + Level: "level", + Type: "type", + } + } + + role := newValidOpts() + role.Role = "invalid" + + user := newValidOpts() + user.User = "invalid" + + level := newValidOpts() + level.Level = "invalid" + + seType := newValidOpts() + seType.Type = "invalid" + + tests := map[string]struct { + seLinux *api.SELinuxOptions + expectedMsg string + }{ + "invalid role": { + seLinux: role, + expectedMsg: "does not match required role", + }, + "invalid user": { + seLinux: user, + expectedMsg: "does not match required user", + }, + "invalid level": { + seLinux: level, + expectedMsg: "does not match required level", + }, + "invalid type": { + seLinux: seType, + expectedMsg: "does not match required type", + }, + "valid": { + seLinux: newValidOpts(), + expectedMsg: "", + }, + } + + opts := &extensions.SELinuxStrategyOptions{ + SELinuxOptions: newValidOpts(), + } + + 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 + } + container := &api.Container{ + SecurityContext: &api.SecurityContext{ + SELinuxOptions: tc.seLinux, + }, + } + + errs := mustRunAs.Validate(nil, container) + //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) + } + } + } +} diff --git a/pkg/security/podsecuritypolicy/selinux/runasany.go b/pkg/security/podsecuritypolicy/selinux/runasany.go new file mode 100644 index 00000000000..1418fc331c2 --- /dev/null +++ b/pkg/security/podsecuritypolicy/selinux/runasany.go @@ -0,0 +1,43 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// 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 *extensions.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(pod *api.Pod, container *api.Container) field.ErrorList { + return field.ErrorList{} +} diff --git a/pkg/security/podsecuritypolicy/selinux/runasany_test.go b/pkg/security/podsecuritypolicy/selinux/runasany_test.go new file mode 100644 index 00000000000..4f5db4e68fd --- /dev/null +++ b/pkg/security/podsecuritypolicy/selinux/runasany_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestRunAsAnyOptions(t *testing.T) { + _, err := NewRunAsAny(nil) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + _, err = NewRunAsAny(&extensions.SELinuxStrategyOptions{}) + if err != nil { + t.Errorf("unexpected error initializing NewRunAsAny %v", err) + } +} + +func TestRunAsAnyGenerate(t *testing.T) { + s, err := NewRunAsAny(&extensions.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(&extensions.SELinuxStrategyOptions{ + SELinuxOptions: &api.SELinuxOptions{ + Level: "foo", + }, + }, + ) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + errs := s.Validate(nil, nil) + if len(errs) != 0 { + t.Errorf("unexpected errors validating with ") + } + s, err = NewRunAsAny(&extensions.SELinuxStrategyOptions{}) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + errs = s.Validate(nil, nil) + if len(errs) != 0 { + t.Errorf("unexpected errors validating %v", errs) + } +} diff --git a/pkg/security/podsecuritypolicy/selinux/types.go b/pkg/security/podsecuritypolicy/selinux/types.go new file mode 100644 index 00000000000..25613d62a13 --- /dev/null +++ b/pkg/security/podsecuritypolicy/selinux/types.go @@ -0,0 +1,30 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// 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(pod *api.Pod, container *api.Container) field.ErrorList +} diff --git a/pkg/security/podsecuritypolicy/types.go b/pkg/security/podsecuritypolicy/types.go new file mode 100644 index 00000000000..64535ee4af7 --- /dev/null +++ b/pkg/security/podsecuritypolicy/types.go @@ -0,0 +1,62 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// Provider provides the implementation to generate a new security +// context based on constraints or validate an existing security context against constraints. +type Provider interface { + // Create a PodSecurityContext based on the given constraints. + CreatePodSecurityContext(pod *api.Pod) (*api.PodSecurityContext, error) + // Create a container SecurityContext based on the given constraints + CreateContainerSecurityContext(pod *api.Pod, container *api.Container) (*api.SecurityContext, error) + // Ensure a pod's SecurityContext is in compliance with the given constraints. + ValidatePodSecurityContext(pod *api.Pod, fldPath *field.Path) field.ErrorList + // Ensure a container's SecurityContext is in compliance with the given constraints + ValidateContainerSecurityContext(pod *api.Pod, container *api.Container, fldPath *field.Path) 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 *extensions.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 + SELinuxStrategy selinux.SELinuxStrategy + FSGroupStrategy group.GroupStrategy + SupplementalGroupStrategy group.GroupStrategy + CapabilitiesStrategy capabilities.CapabilitiesStrategy +} diff --git a/pkg/security/podsecuritypolicy/user/mustrunas.go b/pkg/security/podsecuritypolicy/user/mustrunas.go new file mode 100644 index 00000000000..f48c803c99e --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/mustrunas.go @@ -0,0 +1,84 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// mustRunAs implements the RunAsUserStrategy interface +type mustRunAs struct { + opts *extensions.RunAsUserStrategyOptions +} + +// NewMustRunAs provides a strategy that requires the container to run as a specific UID in a range. +func NewMustRunAs(options *extensions.RunAsUserStrategyOptions) (RunAsUserStrategy, error) { + if options == nil { + return nil, fmt.Errorf("MustRunAsRange requires run as user options") + } + if len(options.Ranges) == 0 { + return nil, fmt.Errorf("MustRunAsRange 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(pod *api.Pod, container *api.Container) field.ErrorList { + allErrs := field.ErrorList{} + + securityContextPath := field.NewPath("securityContext") + if container.SecurityContext == nil { + detail := fmt.Sprintf("unable to validate nil security context for container %s", container.Name) + allErrs = append(allErrs, field.Invalid(securityContextPath, container.SecurityContext, detail)) + return allErrs + } + if container.SecurityContext.RunAsUser == nil { + detail := fmt.Sprintf("unable to validate nil RunAsUser for container %s", container.Name) + allErrs = append(allErrs, field.Invalid(securityContextPath.Child("runAsUser"), container.SecurityContext.RunAsUser, detail)) + return allErrs + } + + if !s.isValidUID(*container.SecurityContext.RunAsUser) { + detail := fmt.Sprintf("UID on container %s does not match required range. Found %d, allowed: %v", + container.Name, + *container.SecurityContext.RunAsUser, + s.opts.Ranges) + allErrs = append(allErrs, field.Invalid(securityContextPath.Child("runAsUser"), *container.SecurityContext.RunAsUser, detail)) + } + return allErrs +} + +func (s *mustRunAs) isValidUID(id int64) bool { + for _, rng := range s.opts.Ranges { + if psputil.FallsInRange(id, rng) { + return true + } + } + return false +} diff --git a/pkg/security/podsecuritypolicy/user/mustrunas_test.go b/pkg/security/podsecuritypolicy/user/mustrunas_test.go new file mode 100644 index 00000000000..1a8c7eb0262 --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/mustrunas_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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 ( + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestNewMustRunAs(t *testing.T) { + tests := map[string]struct { + opts *extensions.RunAsUserStrategyOptions + pass bool + }{ + "nil opts": { + opts: nil, + pass: false, + }, + "invalid opts": { + opts: &extensions.RunAsUserStrategyOptions{}, + pass: false, + }, + "valid opts": { + opts: &extensions.RunAsUserStrategyOptions{ + Ranges: []extensions.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 := &extensions.RunAsUserStrategyOptions{ + Ranges: []extensions.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 := &extensions.RunAsUserStrategyOptions{ + Ranges: []extensions.IDRange{ + {Min: 1, Max: 1}, + {Min: 10, Max: 20}, + }, + } + + tests := map[string]struct { + container *api.Container + expectedMsg string + }{ + "good container": { + container: &api.Container{ + SecurityContext: &api.SecurityContext{ + RunAsUser: int64Ptr(15), + }, + }, + }, + "nil security context": { + container: &api.Container{ + SecurityContext: nil, + }, + expectedMsg: "unable to validate nil security context for container", + }, + "nil run as user": { + container: &api.Container{ + SecurityContext: &api.SecurityContext{ + RunAsUser: nil, + }, + }, + expectedMsg: "unable to validate nil RunAsUser for container", + }, + "invalid id": { + container: &api.Container{ + SecurityContext: &api.SecurityContext{ + RunAsUser: int64Ptr(21), + }, + }, + expectedMsg: "does not match required range", + }, + } + + 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, tc.container) + //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) + } + } + } +} + +func int64Ptr(i int64) *int64 { + return &i +} diff --git a/pkg/security/podsecuritypolicy/user/nonroot.go b/pkg/security/podsecuritypolicy/user/nonroot.go new file mode 100644 index 00000000000..fc7c356a0b3 --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/nonroot.go @@ -0,0 +1,59 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +type nonRoot struct{} + +var _ RunAsUserStrategy = &nonRoot{} + +func NewRunAsNonRoot(options *extensions.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. 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(pod *api.Pod, container *api.Container) field.ErrorList { + allErrs := field.ErrorList{} + securityContextPath := field.NewPath("securityContext") + if container.SecurityContext == nil { + detail := fmt.Sprintf("unable to validate nil security context for container %s", container.Name) + allErrs = append(allErrs, field.Invalid(securityContextPath, container.SecurityContext, detail)) + return allErrs + } + if container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser == 0 { + detail := fmt.Sprintf("running with the root UID is forbidden by the pod security policy %s", container.Name) + allErrs = append(allErrs, field.Invalid(securityContextPath.Child("runAsUser"), *container.SecurityContext.RunAsUser, detail)) + return allErrs + } + return allErrs +} diff --git a/pkg/security/podsecuritypolicy/user/nonroot_test.go b/pkg/security/podsecuritypolicy/user/nonroot_test.go new file mode 100644 index 00000000000..73e2b1abe7c --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/nonroot_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestNonRootOptions(t *testing.T) { + _, err := NewRunAsNonRoot(nil) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsNonRoot %v", err) + } + _, err = NewRunAsNonRoot(&extensions.RunAsUserStrategyOptions{}) + if err != nil { + t.Errorf("unexpected error initializing NewRunAsNonRoot %v", err) + } +} + +func TestNonRootGenerate(t *testing.T) { + s, err := NewRunAsNonRoot(&extensions.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) { + var uid int64 = 1 + var badUID int64 = 0 + s, err := NewRunAsNonRoot(&extensions.RunAsUserStrategyOptions{}) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAs %v", err) + } + container := &api.Container{ + SecurityContext: &api.SecurityContext{ + RunAsUser: &badUID, + }, + } + + errs := s.Validate(nil, container) + if len(errs) == 0 { + t.Errorf("expected errors from root uid but got none") + } + + container.SecurityContext.RunAsUser = &uid + errs = s.Validate(nil, container) + if len(errs) != 0 { + t.Errorf("expected no errors from non-root uid but got %v", errs) + } + + container.SecurityContext.RunAsUser = nil + errs = s.Validate(nil, container) + if len(errs) != 0 { + t.Errorf("expected no errors from nil uid but got %v", errs) + } +} diff --git a/pkg/security/podsecuritypolicy/user/runasany.go b/pkg/security/podsecuritypolicy/user/runasany.go new file mode 100644 index 00000000000..6fbf1e03215 --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/runasany.go @@ -0,0 +1,43 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// runAsAny implements the interface RunAsUserStrategy. +type runAsAny struct{} + +var _ RunAsUserStrategy = &runAsAny{} + +// NewRunAsAny provides a strategy that will return nil. +func NewRunAsAny(options *extensions.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(pod *api.Pod, container *api.Container) field.ErrorList { + return field.ErrorList{} +} diff --git a/pkg/security/podsecuritypolicy/user/runasany_test.go b/pkg/security/podsecuritypolicy/user/runasany_test.go new file mode 100644 index 00000000000..8da79fffc4e --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/runasany_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestRunAsAnyOptions(t *testing.T) { + _, err := NewRunAsAny(nil) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + _, err = NewRunAsAny(&extensions.RunAsUserStrategyOptions{}) + if err != nil { + t.Errorf("unexpected error initializing NewRunAsAny %v", err) + } +} + +func TestRunAsAnyGenerate(t *testing.T) { + s, err := NewRunAsAny(&extensions.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(&extensions.RunAsUserStrategyOptions{}) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + errs := s.Validate(nil, nil) + if len(errs) != 0 { + t.Errorf("unexpected errors validating with ") + } +} diff --git a/pkg/security/podsecuritypolicy/user/types.go b/pkg/security/podsecuritypolicy/user/types.go new file mode 100644 index 00000000000..ee691c7becf --- /dev/null +++ b/pkg/security/podsecuritypolicy/user/types.go @@ -0,0 +1,30 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// 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. + Validate(pod *api.Pod, container *api.Container) field.ErrorList +} diff --git a/pkg/security/podsecuritypolicy/util/util.go b/pkg/security/podsecuritypolicy/util/util.go new file mode 100644 index 00000000000..4c2935b4cba --- /dev/null +++ b/pkg/security/podsecuritypolicy/util/util.go @@ -0,0 +1,142 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/util/sets" +) + +const ( + ValidatedPSPAnnotation = "kubernetes.io/psp" +) + +func GetAllFSTypesExcept(exceptions ...string) sets.String { + fstypes := GetAllFSTypesAsSet() + for _, e := range exceptions { + fstypes.Delete(e) + } + return fstypes +} + +func GetAllFSTypesAsSet() sets.String { + fstypes := sets.NewString() + fstypes.Insert( + string(extensions.HostPath), + string(extensions.AzureFile), + string(extensions.Flocker), + string(extensions.FlexVolume), + string(extensions.EmptyDir), + string(extensions.GCEPersistentDisk), + string(extensions.AWSElasticBlockStore), + string(extensions.GitRepo), + string(extensions.Secret), + string(extensions.NFS), + string(extensions.ISCSI), + string(extensions.Glusterfs), + string(extensions.PersistentVolumeClaim), + string(extensions.RBD), + string(extensions.Cinder), + string(extensions.CephFS), + string(extensions.DownwardAPI), + string(extensions.FC), + string(extensions.ConfigMap)) + return fstypes +} + +// getVolumeFSType gets the FSType for a volume. +func GetVolumeFSType(v api.Volume) (extensions.FSType, error) { + switch { + case v.HostPath != nil: + return extensions.HostPath, nil + case v.EmptyDir != nil: + return extensions.EmptyDir, nil + case v.GCEPersistentDisk != nil: + return extensions.GCEPersistentDisk, nil + case v.AWSElasticBlockStore != nil: + return extensions.AWSElasticBlockStore, nil + case v.GitRepo != nil: + return extensions.GitRepo, nil + case v.Secret != nil: + return extensions.Secret, nil + case v.NFS != nil: + return extensions.NFS, nil + case v.ISCSI != nil: + return extensions.ISCSI, nil + case v.Glusterfs != nil: + return extensions.Glusterfs, nil + case v.PersistentVolumeClaim != nil: + return extensions.PersistentVolumeClaim, nil + case v.RBD != nil: + return extensions.RBD, nil + case v.FlexVolume != nil: + return extensions.FlexVolume, nil + case v.Cinder != nil: + return extensions.Cinder, nil + case v.CephFS != nil: + return extensions.CephFS, nil + case v.Flocker != nil: + return extensions.Flocker, nil + case v.DownwardAPI != nil: + return extensions.DownwardAPI, nil + case v.FC != nil: + return extensions.FC, nil + case v.AzureFile != nil: + return extensions.AzureFile, nil + case v.ConfigMap != nil: + return extensions.ConfigMap, nil + } + + return "", fmt.Errorf("unknown volume type for volume: %#v", v) +} + +// fsTypeToStringSet converts an FSType slice to a string set. +func FSTypeToStringSet(fsTypes []extensions.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 *extensions.PodSecurityPolicy) bool { + return PSPAllowsFSType(psp, extensions.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 *extensions.PodSecurityPolicy, fsType extensions.FSType) bool { + if psp == nil { + return false + } + + for _, v := range psp.Spec.Volumes { + if v == fsType || v == extensions.All { + return true + } + } + return false +} + +// FallsInRange is a utility to determine it the id falls in the valid range. +func FallsInRange(id int64, rng extensions.IDRange) bool { + return id >= rng.Min && id <= rng.Max +} diff --git a/pkg/security/podsecuritypolicy/util/util_test.go b/pkg/security/podsecuritypolicy/util/util_test.go new file mode 100644 index 00000000000..5c32b7487f6 --- /dev/null +++ b/pkg/security/podsecuritypolicy/util/util_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +// 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 occured 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 *extensions.PodSecurityPolicy + fsType extensions.FSType + allows bool + }{ + "nil psp": { + psp: nil, + fsType: extensions.HostPath, + allows: false, + }, + "empty volumes": { + psp: &extensions.PodSecurityPolicy{}, + fsType: extensions.HostPath, + allows: false, + }, + "non-matching": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + Volumes: []extensions.FSType{extensions.AWSElasticBlockStore}, + }, + }, + fsType: extensions.HostPath, + allows: false, + }, + "match on FSTypeAll": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + Volumes: []extensions.FSType{extensions.All}, + }, + }, + fsType: extensions.HostPath, + allows: true, + }, + "match on direct match": { + psp: &extensions.PodSecurityPolicy{ + Spec: extensions.PodSecurityPolicySpec{ + Volumes: []extensions.FSType{extensions.HostPath}, + }, + }, + fsType: extensions.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) + } + } +} diff --git a/plugin/pkg/admission/security/doc.go b/plugin/pkg/admission/security/doc.go new file mode 100644 index 00000000000..846a4d55cf4 --- /dev/null +++ b/plugin/pkg/admission/security/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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. +*/ + +// security contains admission plugins specific to cluster security. +package security