Merge pull request #109798 from liggitt/psp

Remove PodSecurityPolicy admission plugin
This commit is contained in:
Kubernetes Prow Robot 2022-05-05 06:40:32 -07:00 committed by GitHub
commit 99de67958d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 140 additions and 10635 deletions

View File

@ -1,16 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gce:podsecuritypolicy:event-exporter
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gce:podsecuritypolicy:event-exporter
subjects:
- kind: ServiceAccount
name: event-exporter-sa
namespace: kube-system

View File

@ -1,17 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gce:podsecuritypolicy:event-exporter
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- policy
resourceNames:
- gce.event-exporter
resources:
- podsecuritypolicies
verbs:
- use

View File

@ -1,39 +0,0 @@
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: gce.event-exporter
annotations:
kubernetes.io/description: 'Policy used by the event-exporter addon.'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default,docker/default'
# 'runtime/default' is already the default, but must be filled in on the
# pod to pass admission.
apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default'
apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
labels:
kubernetes.io/cluster-service: 'true'
addonmanager.kubernetes.io/mode: Reconcile
spec:
privileged: false
allowPrivilegeEscalation: false
volumes:
- 'hostPath'
- 'secret'
- 'projected'
# TODO: This only needs a hostPath to read /etc/ssl/certs,
# but it should be able to just include these in the image.
allowedHostPaths:
- pathPrefix: /etc/ssl/certs
hostNetwork: false
hostIPC: false
hostPID: false
# TODO: This doesn't need to run as root.
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false

View File

@ -1,16 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gce:podsecuritypolicy:fluentd-gcp
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gce:podsecuritypolicy:fluentd-gcp
subjects:
- kind: ServiceAccount
name: fluentd-gcp
namespace: kube-system

View File

@ -1,17 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gce:podsecuritypolicy:fluentd-gcp
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- policy
resourceNames:
- gce.fluentd-gcp
resources:
- podsecuritypolicies
verbs:
- use

View File

@ -1,39 +0,0 @@
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: gce.fluentd-gcp
annotations:
kubernetes.io/description: 'Policy used by the fluentd-gcp addon.'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default,docker/default'
# 'runtime/default' is already the default, but must be filled in on the
# pod to pass admission.
apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default'
apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
labels:
kubernetes.io/cluster-service: 'true'
addonmanager.kubernetes.io/mode: Reconcile
spec:
privileged: false
allowPrivilegeEscalation: false
volumes:
- 'configMap'
- 'hostPath'
- 'secret'
- 'projected'
allowedHostPaths:
- pathPrefix: /var/log
- pathPrefix: /var/lib/docker/containers
- pathPrefix: /usr/lib64
hostNetwork: true
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false

View File

@ -1,16 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gce:podsecuritypolicy:kube-proxy
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gce:podsecuritypolicy:privileged
subjects:
- kind: ServiceAccount
name: kube-proxy
namespace: kube-system

View File

@ -1,17 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gce:podsecuritypolicy:unprivileged-addon
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gce:podsecuritypolicy:unprivileged-addon
subjects:
- kind: Group
# All service accounts in the kube-system namespace are allowed to use this.
name: system:serviceaccounts:kube-system
apiGroup: rbac.authorization.k8s.io

View File

@ -1,24 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gce:podsecuritypolicy:nodes
namespace: kube-system
annotations:
kubernetes.io/description: 'Allow nodes to create privileged pods. Should
be used in combination with the NodeRestriction admission plugin to limit
nodes to mirror pods bound to themselves.'
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: 'true'
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gce:podsecuritypolicy:privileged
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: system:nodes
- kind: User
apiGroup: rbac.authorization.k8s.io
# Legacy node ID
name: kubelet

View File

@ -1,18 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
# The persistent volume binder creates recycler pods in the default namespace,
# but the addon manager only creates namespaced objects in the kube-system
# namespace, so this is a ClusterRoleBinding.
kind: ClusterRoleBinding
metadata:
name: gce:podsecuritypolicy:persistent-volume-binder
labels:
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/cluster-service: "true"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gce:podsecuritypolicy:persistent-volume-binder
subjects:
- kind: ServiceAccount
name: persistent-volume-binder
namespace: kube-system

View File

@ -1,20 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
# The persistent volume binder creates recycler pods in the default namespace,
# but the addon manager only creates namespaced objects in the kube-system
# namespace, so this is a ClusterRole.
kind: ClusterRole
metadata:
name: gce:podsecuritypolicy:persistent-volume-binder
namespace: default
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- policy
resourceNames:
- gce.persistent-volume-binder
resources:
- podsecuritypolicies
verbs:
- use

View File

@ -1,30 +0,0 @@
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: gce.persistent-volume-binder
annotations:
kubernetes.io/description: 'Policy used by the persistent-volume-binder
(a.k.a. persistentvolume-controller) to run recycler pods.'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default,docker/default'
labels:
kubernetes.io/cluster-service: 'true'
addonmanager.kubernetes.io/mode: Reconcile
spec:
privileged: false
volumes:
- 'nfs'
- 'secret' # Required for service account credentials.
- 'projected'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false

View File

@ -1,16 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gce:podsecuritypolicy:privileged
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- policy
resourceNames:
- gce.privileged
resources:
- podsecuritypolicies
verbs:
- use

View File

@ -1,33 +0,0 @@
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: gce.privileged
annotations:
kubernetes.io/description: 'privileged allows full unrestricted access to
pod features, as if the PodSecurityPolicy controller was not enabled.'
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
privileged: true
allowPrivilegeEscalation: true
allowedCapabilities:
- '*'
volumes:
- '*'
hostNetwork: true
hostPorts:
- min: 0
max: 65535
hostIPC: true
hostPID: true
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false

View File

@ -1,17 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gce:podsecuritypolicy:unprivileged-addon
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- policy
resourceNames:
- gce.unprivileged-addon
resources:
- podsecuritypolicies
verbs:
- use

View File

@ -1,55 +0,0 @@
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: gce.unprivileged-addon
annotations:
kubernetes.io/description: 'This policy grants the minimum amount of
privilege necessary to run non-privileged kube-system pods. This policy is
not intended for use outside of kube-system, and may include further
restrictions in the future.'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default,docker/default'
# 'runtime/default' is already the default, but must be filled in on the
# pod to pass admission.
apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default'
apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
labels:
kubernetes.io/cluster-service: 'true'
addonmanager.kubernetes.io/mode: Reconcile
spec:
privileged: false
allowPrivilegeEscalation: false
# The docker default set of capabilities
allowedCapabilities:
- SETPCAP
- MKNOD
- AUDIT_WRITE
- CHOWN
- NET_RAW
- DAC_OVERRIDE
- FOWNER
- FSETID
- KILL
- SETGID
- SETUID
- NET_BIND_SERVICE
- SYS_CHROOT
- SETFCAP
volumes:
- 'emptyDir'
- 'configMap'
- 'secret'
- 'projected'
hostNetwork: false
hostIPC: false
hostPID: false
# TODO: The addons using this profile should not run as root.
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false

View File

@ -366,10 +366,6 @@ CUSTOM_INGRESS_YAML="${CUSTOM_INGRESS_YAML:-}"
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,PersistentVolumeClaimResize,DefaultTolerationSeconds,NodeRestriction,Priority,StorageObjectInUseProtection,RuntimeClass
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy"
fi
# MutatingAdmissionWebhook should be the last controller that modifies the
# request object, otherwise users will be confused if the mutating webhooks'
# modification is overwritten.

View File

@ -411,9 +411,6 @@ CUSTOM_INGRESS_YAML=${CUSTOM_INGRESS_YAML:-}
if [[ -z "${KUBE_ADMISSION_CONTROL:-}" ]]; then
ADMISSION_CONTROL='NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,Priority,StorageObjectInUseProtection,PersistentVolumeClaimResize,RuntimeClass'
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" = 'true' ]]; then
ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy"
fi
# ResourceQuota must come last, or a creation is recorded, but the pod may be forbidden.
ADMISSION_CONTROL="${ADMISSION_CONTROL},MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota"
else

View File

@ -2337,15 +2337,6 @@ function setup-addon-manifests {
local -r dst_dir="/etc/kubernetes/$1/$2"
copy-manifests "${src_dir}/$2" "${dst_dir}"
# If the PodSecurityPolicy admission controller is enabled,
# set up the corresponding addon policies.
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
local -r psp_dir="${src_dir}/${3:-$2}/podsecuritypolicies"
if [[ -d "${psp_dir}" ]]; then
copy-manifests "${psp_dir}" "${dst_dir}"
fi
fi
}
# A function that downloads extra addons from a URL and puts them in the GCI
@ -2695,10 +2686,6 @@ function start-kube-addons {
setup-addon-manifests "addons" "rbac/legacy-kubelet-user-disable"
fi
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
setup-addon-manifests "addons" "podsecuritypolicies"
fi
# Set up manifests of other addons.
if [[ "${KUBE_PROXY_DAEMONSET:-}" == "true" ]] && [[ "${KUBE_PROXY_DISABLE:-}" != "true" ]]; then
if [ -n "${CUSTOM_KUBE_PROXY_YAML:-}" ]; then

View File

@ -1124,7 +1124,6 @@ KUBE_PROXY_MODE: $(yaml-quote "${KUBE_PROXY_MODE:-iptables}")
DETECT_LOCAL_MODE: $(yaml-quote "${DETECT_LOCAL_MODE:-}")
NODE_PROBLEM_DETECTOR_TOKEN: $(yaml-quote "${NODE_PROBLEM_DETECTOR_TOKEN:-}")
ADMISSION_CONTROL: $(yaml-quote "${ADMISSION_CONTROL:-}")
ENABLE_POD_SECURITY_POLICY: $(yaml-quote "${ENABLE_POD_SECURITY_POLICY:-}")
MASTER_IP_RANGE: $(yaml-quote "${MASTER_IP_RANGE}")
RUNTIME_CONFIG: $(yaml-quote "${RUNTIME_CONFIG}")
CA_CERT: $(yaml-quote "${CA_CERT_BASE64:-}")

View File

@ -26,7 +26,6 @@ export DOCKER=(docker "${DOCKER_OPTS[@]}")
DOCKER_ROOT=${DOCKER_ROOT:-""}
ALLOW_PRIVILEGED=${ALLOW_PRIVILEGED:-""}
DENY_SECURITY_CONTEXT_ADMISSION=${DENY_SECURITY_CONTEXT_ADMISSION:-""}
PSP_ADMISSION=${PSP_ADMISSION:-""}
RUNTIME_CONFIG=${RUNTIME_CONFIG:-""}
KUBELET_AUTHORIZATION_WEBHOOK=${KUBELET_AUTHORIZATION_WEBHOOK:-""}
KUBELET_AUTHENTICATION_WEBHOOK=${KUBELET_AUTHENTICATION_WEBHOOK:-""}
@ -480,9 +479,6 @@ function start_apiserver {
if [[ -n "${DENY_SECURITY_CONTEXT_ADMISSION}" ]]; then
security_admission=",SecurityContextDeny"
fi
if [[ -n "${PSP_ADMISSION}" ]]; then
security_admission=",PodSecurityPolicy"
fi
# Append security_admission plugin
ENABLE_ADMISSION_PLUGINS="${ENABLE_ADMISSION_PLUGINS}${security_admission}"
@ -939,13 +935,6 @@ function start_csi_snapshotter {
fi
}
function create_psp_policy {
echo "Create podsecuritypolicy policies for RBAC."
${KUBECTL} --kubeconfig="${CERT_DIR}/admin.kubeconfig" create -f "${KUBE_ROOT}/examples/podsecuritypolicy/rbac/policies.yaml"
${KUBECTL} --kubeconfig="${CERT_DIR}/admin.kubeconfig" create -f "${KUBE_ROOT}/examples/podsecuritypolicy/rbac/roles.yaml"
${KUBECTL} --kubeconfig="${CERT_DIR}/admin.kubeconfig" create -f "${KUBE_ROOT}/examples/podsecuritypolicy/rbac/bindings.yaml"
}
function create_storage_class {
if [ -z "${CLOUD_PROVIDER}" ]; then
CLASS_FILE=${KUBE_ROOT}/cluster/addons/storage-class/local/default.yaml
@ -1208,10 +1197,6 @@ if [[ "${START_MODE}" != "kubeletonly" ]]; then
fi
fi
if [[ -n "${PSP_ADMISSION}" && "${AUTHORIZATION_MODE}" = *RBAC* ]]; then
create_psp_policy
fi
if [[ "${DEFAULT_STORAGE_CLASS}" = "true" ]]; then
create_storage_class
fi

View File

@ -33,8 +33,15 @@ import (
core "k8s.io/kubernetes/pkg/apis/core"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
)
const (
// AllowAny is the wildcard used to allow any profile.
seccompAllowAny = "*"
// DefaultProfileAnnotationKey specifies the default seccomp profile.
seccompDefaultProfileAnnotationKey = "seccomp.security.alpha.kubernetes.io/defaultProfileName"
// AllowedProfilesAnnotationKey specifies the allowed seccomp profiles.
seccompAllowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
)
// ValidatePodDisruptionBudget validates a PodDisruptionBudget and returns an ErrorList
@ -149,15 +156,15 @@ func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string,
}
}
if p := annotations[seccomp.DefaultProfileAnnotationKey]; p != "" {
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.DefaultProfileAnnotationKey))...)
if p := annotations[seccompDefaultProfileAnnotationKey]; p != "" {
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccompDefaultProfileAnnotationKey))...)
}
if allowed := annotations[seccomp.AllowedProfilesAnnotationKey]; allowed != "" {
if allowed := annotations[seccompAllowedProfilesAnnotationKey]; allowed != "" {
for _, p := range strings.Split(allowed, ",") {
if p == seccomp.AllowAny {
if p == seccompAllowAny {
continue
}
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.AllowedProfilesAnnotationKey))...)
allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccompAllowedProfilesAnnotationKey))...)
}
}
return allErrs
@ -321,7 +328,7 @@ func validatePSPSupplementalGroup(fldPath *field.Path, groupOptions *policy.Supp
// validatePodSecurityPolicyVolumes validates the volume fields of PodSecurityPolicy.
func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []policy.FSType) field.ErrorList {
allErrs := field.ErrorList{}
allowed := psputil.GetAllFSTypesAsSet()
allowed := getAllFSTypesAsSet()
// add in the * value since that is a pseudo type that is not included by default
allowed.Insert(string(policy.All))
for _, v := range volumes {
@ -332,6 +339,44 @@ func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []policy.FSTy
return allErrs
}
// getAllFSTypesAsSet returns all actual volume types, regardless
// of feature gates. The special policy.All pseudo type is not included.
func getAllFSTypesAsSet() sets.String {
fstypes := sets.NewString()
fstypes.Insert(
string(policy.HostPath),
string(policy.AzureFile),
string(policy.Flocker),
string(policy.FlexVolume),
string(policy.EmptyDir),
string(policy.GCEPersistentDisk),
string(policy.AWSElasticBlockStore),
string(policy.GitRepo),
string(policy.Secret),
string(policy.NFS),
string(policy.ISCSI),
string(policy.Glusterfs),
string(policy.PersistentVolumeClaim),
string(policy.RBD),
string(policy.Cinder),
string(policy.CephFS),
string(policy.DownwardAPI),
string(policy.FC),
string(policy.ConfigMap),
string(policy.VsphereVolume),
string(policy.Quobyte),
string(policy.AzureDisk),
string(policy.PhotonPersistentDisk),
string(policy.StorageOS),
string(policy.Projected),
string(policy.PortworxVolume),
string(policy.ScaleIO),
string(policy.CSI),
string(policy.Ephemeral),
)
return fstypes
}
// validatePSPDefaultAllowPrivilegeEscalation validates the DefaultAllowPrivilegeEscalation field against the AllowPrivilegeEscalation field of a PodSecurityPolicy.
func validatePSPDefaultAllowPrivilegeEscalation(fldPath *field.Path, defaultAllowPrivilegeEscalation *bool, allowPrivilegeEscalation bool) field.ErrorList {
allErrs := field.ErrorList{}

View File

@ -30,8 +30,6 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
"k8s.io/utils/pointer"
)
@ -373,15 +371,15 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
invalidSeccompDefault := validPSP()
invalidSeccompDefault.Annotations = map[string]string{
seccomp.DefaultProfileAnnotationKey: "not-good",
seccompDefaultProfileAnnotationKey: "not-good",
}
invalidSeccompAllowAnyDefault := validPSP()
invalidSeccompAllowAnyDefault.Annotations = map[string]string{
seccomp.DefaultProfileAnnotationKey: "*",
seccompDefaultProfileAnnotationKey: "*",
}
invalidSeccompAllowed := validPSP()
invalidSeccompAllowed.Annotations = map[string]string{
seccomp.AllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",not-good",
seccompAllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",not-good",
}
invalidAllowedHostPathMissingPath := validPSP()
@ -660,8 +658,8 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
validSeccomp := validPSP()
validSeccomp.Annotations = map[string]string{
seccomp.DefaultProfileAnnotationKey: api.SeccompProfileRuntimeDefault,
seccomp.AllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",unconfined,localhost/foo,*",
seccompDefaultProfileAnnotationKey: api.SeccompProfileRuntimeDefault,
seccompAllowedProfilesAnnotationKey: api.SeccompProfileRuntimeDefault + ",unconfined,localhost/foo,*",
}
validDefaultAllowPrivilegeEscalation := validPSP()
@ -779,7 +777,7 @@ func TestValidatePSPVolumes(t *testing.T) {
}
}
volumes := psputil.GetAllFSTypesAsSet()
volumes := getAllFSTypesAsSet()
// add in the * value since that is a pseudo type that is not included by default
volumes.Insert(string(policy.All))

View File

@ -45,7 +45,6 @@ import (
podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass"
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy"
"k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
"k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label"
@ -75,8 +74,7 @@ var AllOrderedPlugins = []string{
nodetaint.PluginName, // TaintNodesByCondition
alwayspullimages.PluginName, // AlwaysPullImages
imagepolicy.PluginName, // ImagePolicyWebhook
podsecurity.PluginName, // PodSecurity - before PodSecurityPolicy so audit/warn get exercised even if PodSecurityPolicy denies
podsecuritypolicy.PluginName, // PodSecurityPolicy
podsecurity.PluginName, // PodSecurity
podnodeselector.PluginName, // PodNodeSelector
podpriority.PluginName, // Priority
defaulttolerationseconds.PluginName, // DefaultTolerationSeconds
@ -129,7 +127,6 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
runtimeclass.Register(plugins)
resourcequota.Register(plugins)
podsecurity.Register(plugins)
podsecuritypolicy.Register(plugins)
podpriority.Register(plugins)
scdeny.Register(plugins)
serviceaccount.Register(plugins)

View File

@ -108,7 +108,6 @@ import (
"k8s.io/kubernetes/pkg/kubelet/util/sliceutils"
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
"k8s.io/kubernetes/pkg/security/apparmor"
sysctlallowlist "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
"k8s.io/kubernetes/pkg/util/oom"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/csi"
@ -776,7 +775,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
// Safe, allowed sysctls can always be used as unsafe sysctls in the spec.
// Hence, we concatenate those two lists.
safeAndUnsafeSysctls := append(sysctlallowlist.SafeSysctlAllowlist(), allowedUnsafeSysctls...)
safeAndUnsafeSysctls := append(sysctl.SafeSysctlAllowlist(), allowedUnsafeSysctls...)
sysctlsAllowlist, err := sysctl.NewAllowlist(safeAndUnsafeSysctls)
if err != nil {
return nil, err

View File

@ -18,8 +18,6 @@ package sysctl
import (
"testing"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
)
func TestNewAllowlist(t *testing.T) {
@ -37,7 +35,7 @@ func TestNewAllowlist(t *testing.T) {
{sysctls: []string{"net.*/foo"}, err: true},
{sysctls: []string{"foo"}, err: true},
} {
_, err := NewAllowlist(append(sysctl.SafeSysctlAllowlist(), test.sysctls...))
_, err := NewAllowlist(append(SafeSysctlAllowlist(), test.sysctls...))
if test.err && err == nil {
t.Errorf("expected an error creating a allowlist for %v", test.sysctls)
} else if !test.err && err != nil {
@ -69,7 +67,7 @@ func TestAllowlist(t *testing.T) {
{sysctl: "kernel.sem", hostIPC: true},
}
w, err := NewAllowlist(append(sysctl.SafeSysctlAllowlist(), "kernel.msg*", "kernel.sem"))
w, err := NewAllowlist(append(SafeSysctlAllowlist(), "kernel.msg*", "kernel.sem"))
if err != nil {
t.Fatalf("failed to create allowlist: %v", err)
}

View File

@ -16,13 +16,17 @@ limitations under the License.
package sysctl
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// SysctlsStrategy defines the interface for all sysctl strategies.
type SysctlsStrategy interface {
// Validate ensures that the specified values fall within the range of the strategy.
Validate(pod *api.Pod) field.ErrorList
// SafeSysctlAllowlist returns the allowlist of safe sysctls and safe sysctl patterns (ending in *).
//
// A sysctl is called safe iff
// - it is namespaced in the container or the pod
// - it is isolated, i.e. has no influence on any other pod on the same node.
func SafeSysctlAllowlist() []string {
return []string{
"kernel.shm_rmid_forced",
"net.ipv4.ip_local_port_range",
"net.ipv4.tcp_syncookies",
"net.ipv4.ping_group_range",
"net.ipv4.ip_unprivileged_port_start",
}
}

View File

@ -1,8 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-auth-policy-approvers
reviewers:
- sig-auth-policy-reviewers
labels:
- sig/auth

View File

@ -1,111 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apparmor
import (
"fmt"
"strings"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/util/maps"
)
// Strategy defines the interface for all AppArmor constraint strategies.
type Strategy interface {
// Generate updates the annotations based on constraint rules. The updates are applied to a copy
// of the annotations, and returned.
Generate(annotations map[string]string, container *api.Container) (map[string]string, error)
// Validate ensures that the specified values fall within the range of the strategy.
Validate(pod *api.Pod, container *api.Container) field.ErrorList
}
type strategy struct {
defaultProfile string
allowedProfiles map[string]bool
// For printing error messages (preserves order).
allowedProfilesString string
}
var _ Strategy = &strategy{}
// NewStrategy creates a new strategy that enforces AppArmor profile constraints.
func NewStrategy(pspAnnotations map[string]string) Strategy {
var allowedProfiles map[string]bool
if allowed, ok := pspAnnotations[v1.AppArmorBetaAllowedProfilesAnnotationKey]; ok {
profiles := strings.Split(allowed, ",")
allowedProfiles = make(map[string]bool, len(profiles))
for _, p := range profiles {
allowedProfiles[p] = true
}
}
return &strategy{
defaultProfile: pspAnnotations[v1.AppArmorBetaDefaultProfileAnnotationKey],
allowedProfiles: allowedProfiles,
allowedProfilesString: pspAnnotations[v1.AppArmorBetaAllowedProfilesAnnotationKey],
}
}
func (s *strategy) Generate(annotations map[string]string, container *api.Container) (map[string]string, error) {
copy := maps.CopySS(annotations)
if annotations[v1.AppArmorBetaContainerAnnotationKeyPrefix+container.Name] != "" {
// Profile already set, nothing to do.
return copy, nil
}
if s.defaultProfile == "" {
// No default set.
return copy, nil
}
if copy == nil {
copy = map[string]string{}
}
// Add the default profile.
copy[v1.AppArmorBetaContainerAnnotationKeyPrefix+container.Name] = s.defaultProfile
return copy, nil
}
func (s *strategy) Validate(pod *api.Pod, container *api.Container) field.ErrorList {
if s.allowedProfiles == nil {
// Unrestricted: allow all.
return nil
}
allErrs := field.ErrorList{}
fieldPath := field.NewPath("pod", "metadata", "annotations").Key(v1.AppArmorBetaContainerAnnotationKeyPrefix + container.Name)
profile := apparmor.GetProfileNameFromPodAnnotations(pod.Annotations, container.Name)
if profile == "" {
if len(s.allowedProfiles) > 0 {
allErrs = append(allErrs, field.Forbidden(fieldPath, "AppArmor profile must be set"))
return allErrs
}
return nil
}
if !s.allowedProfiles[profile] {
msg := fmt.Sprintf("%s is not an allowed profile. Allowed values: %q", profile, s.allowedProfilesString)
allErrs = append(allErrs, field.Forbidden(fieldPath, msg))
}
return allErrs
}

View File

@ -1,174 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apparmor
import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/util/maps"
)
const (
containerName = "test-c"
)
var (
withoutAppArmor = map[string]string{"foo": "bar"}
withDefault = map[string]string{
"foo": "bar",
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileRuntimeDefault,
}
withLocal = map[string]string{
"foo": "bar",
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileNamePrefix + "foo",
}
withDisallowed = map[string]string{
"foo": "bar",
v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName: v1.AppArmorBetaProfileNamePrefix + "bad",
}
noAppArmor = map[string]string{"foo": "bar"}
unconstrainedWithDefault = map[string]string{
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
}
constrained = map[string]string{
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault + "," +
v1.AppArmorBetaProfileNamePrefix + "foo",
}
constrainedWithDefault = map[string]string{
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault + "," +
v1.AppArmorBetaProfileNamePrefix + "foo",
}
container = api.Container{
Name: containerName,
Image: "busybox",
}
)
func TestGenerate(t *testing.T) {
type testcase struct {
pspAnnotations map[string]string
podAnnotations map[string]string
expected map[string]string
}
tests := []testcase{{
pspAnnotations: noAppArmor,
podAnnotations: withoutAppArmor,
expected: withoutAppArmor,
}, {
pspAnnotations: unconstrainedWithDefault,
podAnnotations: withoutAppArmor,
expected: withDefault,
}, {
pspAnnotations: constrained,
podAnnotations: withoutAppArmor,
expected: withoutAppArmor,
}, {
pspAnnotations: constrainedWithDefault,
podAnnotations: withoutAppArmor,
expected: withDefault,
}}
// Add unchanging permutations.
for _, podAnnotations := range []map[string]string{withDefault, withLocal} {
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} {
tests = append(tests, testcase{
pspAnnotations: pspAnnotations,
podAnnotations: podAnnotations,
expected: podAnnotations,
})
}
}
for i, test := range tests {
s := NewStrategy(test.pspAnnotations)
msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)}
actual, err := s.Generate(test.podAnnotations, &container)
assert.NoError(t, err, msgAndArgs...)
assert.Equal(t, test.expected, actual, msgAndArgs...)
}
}
func TestValidate(t *testing.T) {
type testcase struct {
pspAnnotations map[string]string
podAnnotations map[string]string
expectErr bool
}
tests := []testcase{}
// Valid combinations
for _, podAnnotations := range []map[string]string{withDefault, withLocal} {
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} {
tests = append(tests, testcase{
pspAnnotations: pspAnnotations,
podAnnotations: podAnnotations,
expectErr: false,
})
}
}
for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} {
for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault} {
tests = append(tests, testcase{
pspAnnotations: pspAnnotations,
podAnnotations: podAnnotations,
expectErr: false,
})
}
}
// Invalid combinations
for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} {
for _, pspAnnotations := range []map[string]string{constrained, constrainedWithDefault} {
tests = append(tests, testcase{
pspAnnotations: pspAnnotations,
podAnnotations: podAnnotations,
expectErr: true,
})
}
}
for i, test := range tests {
s := NewStrategy(test.pspAnnotations)
pod, container := makeTestPod(test.podAnnotations)
msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)}
errs := s.Validate(pod, container)
if test.expectErr {
assert.Len(t, errs, 1, msgAndArgs...)
} else {
assert.Len(t, errs, 0, msgAndArgs...)
}
}
}
func makeTestPod(annotations map[string]string) (*api.Pod, *api.Container) {
return &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Annotations: maps.CopySS(annotations),
},
Spec: api.PodSpec{
Containers: []api.Container{container},
},
}, &container
}

View File

@ -1,165 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package capabilities
import (
"fmt"
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// defaultCapabilities implements the Strategy interface
type defaultCapabilities struct {
defaultAddCapabilities []api.Capability
requiredDropCapabilities []api.Capability
allowedCaps []api.Capability
}
var _ Strategy = &defaultCapabilities{}
// NewDefaultCapabilities creates a new defaultCapabilities strategy that will provide defaults and validation
// based on the configured initial caps and allowed caps.
func NewDefaultCapabilities(defaultAddCapabilities, requiredDropCapabilities, allowedCaps []corev1.Capability) (Strategy, error) {
internalDefaultAddCaps := make([]api.Capability, len(defaultAddCapabilities))
for i, capability := range defaultAddCapabilities {
internalDefaultAddCaps[i] = api.Capability(capability)
}
internalRequiredDropCaps := make([]api.Capability, len(requiredDropCapabilities))
for i, capability := range requiredDropCapabilities {
internalRequiredDropCaps[i] = api.Capability(capability)
}
internalAllowedCaps := make([]api.Capability, len(allowedCaps))
for i, capability := range allowedCaps {
internalAllowedCaps[i] = api.Capability(capability)
}
return &defaultCapabilities{
defaultAddCapabilities: internalDefaultAddCaps,
requiredDropCapabilities: internalRequiredDropCaps,
allowedCaps: internalAllowedCaps,
}, nil
}
// Generate creates the capabilities based on policy rules. Generate will produce the following:
// 1. a capabilities.Add set containing all the required adds (unless the
// container specifically is dropping the cap) and container requested adds
// 2. a capabilities.Drop set containing all the required drops and container requested drops
//
// Returns the original container capabilities if no changes are required.
func (s *defaultCapabilities) Generate(pod *api.Pod, container *api.Container) (*api.Capabilities, error) {
defaultAdd := makeCapSet(s.defaultAddCapabilities)
requiredDrop := makeCapSet(s.requiredDropCapabilities)
containerAdd := sets.NewString()
containerDrop := sets.NewString()
var containerCapabilities *api.Capabilities
if container.SecurityContext != nil && container.SecurityContext.Capabilities != nil {
containerCapabilities = container.SecurityContext.Capabilities
containerAdd = makeCapSet(container.SecurityContext.Capabilities.Add)
containerDrop = makeCapSet(container.SecurityContext.Capabilities.Drop)
}
// remove any default adds that the container is specifically dropping
defaultAdd = defaultAdd.Difference(containerDrop)
combinedAdd := defaultAdd.Union(containerAdd)
combinedDrop := requiredDrop.Union(containerDrop)
// no changes? return the original capabilities
if (len(combinedAdd) == len(containerAdd)) && (len(combinedDrop) == len(containerDrop)) {
return containerCapabilities, nil
}
return &api.Capabilities{
Add: capabilityFromStringSlice(combinedAdd.List()),
Drop: capabilityFromStringSlice(combinedDrop.List()),
}, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *defaultCapabilities) Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, capabilities *api.Capabilities) field.ErrorList {
allErrs := field.ErrorList{}
if capabilities == nil {
// if container.SC.Caps is nil then nothing was defaulted by the strategy or requested by the pod author
// if there are no required caps on the strategy and nothing is requested on the pod
// then we can safely return here without further validation.
if len(s.defaultAddCapabilities) == 0 && len(s.requiredDropCapabilities) == 0 {
return allErrs
}
// container has no requested caps but we have required caps. We should have something in
// at least the drops on the container.
allErrs = append(allErrs, field.Invalid(fldPath, capabilities,
"required capabilities are not set on the securityContext"))
return allErrs
}
allowedAdd := makeCapSet(s.allowedCaps)
allowAllCaps := allowedAdd.Has(string(policy.AllowAllCapabilities))
if allowAllCaps {
// skip validation against allowed/defaultAdd/requiredDrop because all capabilities are allowed by a wildcard
return allErrs
}
// validate that anything being added is in the default or allowed sets
defaultAdd := makeCapSet(s.defaultAddCapabilities)
for _, cap := range capabilities.Add {
sCap := string(cap)
if !defaultAdd.Has(sCap) && !allowedAdd.Has(sCap) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("add"), sCap, "capability may not be added"))
}
}
// validate that anything that is required to be dropped is in the drop set
containerDrops := makeCapSet(capabilities.Drop)
for _, requiredDrop := range s.requiredDropCapabilities {
sDrop := string(requiredDrop)
if !containerDrops.Has(sDrop) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("drop"), capabilities.Drop,
fmt.Sprintf("%s is required to be dropped but was not found", sDrop)))
}
}
return allErrs
}
// capabilityFromStringSlice creates a capability slice from a string slice.
func capabilityFromStringSlice(slice []string) []api.Capability {
if len(slice) == 0 {
return nil
}
caps := []api.Capability{}
for _, c := range slice {
caps = append(caps, api.Capability(c))
}
return caps
}
// makeCapSet makes a string set from capabilities.
func makeCapSet(caps []api.Capability) sets.String {
s := sets.NewString()
for _, c := range caps {
s.Insert(string(c))
}
return s
}

View File

@ -1,412 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package capabilities
import (
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestGenerateAdds(t *testing.T) {
tests := map[string]struct {
defaultAddCaps []corev1.Capability
containerCaps *api.Capabilities
expectedCaps *api.Capabilities
}{
"no required, no container requests": {},
"no required, no container requests, non-nil": {
containerCaps: &api.Capabilities{},
expectedCaps: &api.Capabilities{},
},
"required, no container requests": {
defaultAddCaps: []corev1.Capability{"foo"},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"required, container requests add required": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"multiple required, container requests add required": {
defaultAddCaps: []corev1.Capability{"foo", "bar", "baz"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"bar", "baz", "foo"},
},
},
"required, container requests add non-required": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"bar"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"bar", "foo"},
},
},
"generation does not mutate unnecessarily": {
defaultAddCaps: []corev1.Capability{"foo", "bar"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo", "foo", "bar", "baz"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"foo", "foo", "bar", "baz"},
},
},
"generation dedupes": {
defaultAddCaps: []corev1.Capability{"foo", "bar"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo", "baz"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"bar", "baz", "foo"},
},
},
"generation is case sensitive - will not dedupe": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"FOO"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"FOO", "foo"},
},
},
}
for k, v := range tests {
container := &api.Container{
SecurityContext: &api.SecurityContext{
Capabilities: v.containerCaps,
},
}
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, nil, nil)
if err != nil {
t.Errorf("%s failed: %v", k, err)
continue
}
generatedCaps, err := strategy.Generate(nil, container)
if err != nil {
t.Errorf("%s failed generating: %v", k, err)
continue
}
if v.expectedCaps == nil && generatedCaps != nil {
t.Errorf("%s expected nil caps to be generated but got %v", k, generatedCaps)
continue
}
if !reflect.DeepEqual(v.expectedCaps, generatedCaps) {
t.Errorf("%s did not generate correctly. Expected: %#v, Actual: %#v", k, v.expectedCaps, generatedCaps)
}
}
}
func TestGenerateDrops(t *testing.T) {
tests := map[string]struct {
defaultAddCaps []corev1.Capability
requiredDropCaps []corev1.Capability
containerCaps *api.Capabilities
expectedCaps *api.Capabilities
}{
"no required, no container requests": {
expectedCaps: nil,
},
"no required, no container requests, non-nil": {
containerCaps: &api.Capabilities{},
expectedCaps: &api.Capabilities{},
},
"required drops are defaulted": {
requiredDropCaps: []corev1.Capability{"foo"},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"foo"},
},
},
"required drops are defaulted when making container requests": {
requiredDropCaps: []corev1.Capability{"baz"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"foo", "bar"},
},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"bar", "baz", "foo"},
},
},
"required drops do not mutate unnecessarily": {
requiredDropCaps: []corev1.Capability{"baz"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"foo", "bar", "baz"},
},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"foo", "bar", "baz"},
},
},
"can drop a required add": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"foo"},
},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"foo"},
},
},
"can drop non-required add": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"bar"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
Drop: []api.Capability{"bar"},
},
},
"defaulting adds and drops, dropping a required add": {
defaultAddCaps: []corev1.Capability{"foo", "bar", "baz"},
requiredDropCaps: []corev1.Capability{"abc"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"foo"},
},
expectedCaps: &api.Capabilities{
Add: []api.Capability{"bar", "baz"},
Drop: []api.Capability{"abc", "foo"},
},
},
"generation dedupes": {
requiredDropCaps: []corev1.Capability{"baz", "foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"bar", "foo"},
},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"bar", "baz", "foo"},
},
},
"generation is case sensitive - will not dedupe": {
requiredDropCaps: []corev1.Capability{"bar"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"BAR"},
},
expectedCaps: &api.Capabilities{
Drop: []api.Capability{"BAR", "bar"},
},
},
}
for k, v := range tests {
container := &api.Container{
SecurityContext: &api.SecurityContext{
Capabilities: v.containerCaps,
},
}
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, v.requiredDropCaps, nil)
if err != nil {
t.Errorf("%s failed: %v", k, err)
continue
}
generatedCaps, err := strategy.Generate(nil, container)
if err != nil {
t.Errorf("%s failed generating: %v", k, err)
continue
}
if v.expectedCaps == nil && generatedCaps != nil {
t.Errorf("%s expected nil caps to be generated but got %#v", k, generatedCaps)
continue
}
if !reflect.DeepEqual(v.expectedCaps, generatedCaps) {
t.Errorf("%s did not generate correctly. Expected: %#v, Actual: %#v", k, v.expectedCaps, generatedCaps)
}
}
}
func TestValidateAdds(t *testing.T) {
tests := map[string]struct {
defaultAddCaps []corev1.Capability
allowedCaps []corev1.Capability
containerCaps *api.Capabilities
expectedError string
}{
// no container requests
"no required, no allowed, no container requests": {},
"no required, allowed, no container requests": {
allowedCaps: []corev1.Capability{"foo"},
},
"required, no allowed, no container requests": {
defaultAddCaps: []corev1.Capability{"foo"},
expectedError: `capabilities: Invalid value: "null": required capabilities are not set on the securityContext`,
},
// container requests match required
"required, no allowed, container requests valid": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"required, no allowed, container requests invalid": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"bar"},
},
expectedError: `capabilities.add: Invalid value: "bar": capability may not be added`,
},
// container requests match allowed
"no required, allowed, container requests valid": {
allowedCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"no required, all allowed, container requests valid": {
allowedCaps: []corev1.Capability{policy.AllowAllCapabilities},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"no required, allowed, container requests invalid": {
allowedCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"bar"},
},
expectedError: `capabilities.add: Invalid value: "bar": capability may not be added`,
},
// required and allowed
"required, allowed, container requests valid required": {
defaultAddCaps: []corev1.Capability{"foo"},
allowedCaps: []corev1.Capability{"bar"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"foo"},
},
},
"required, allowed, container requests valid allowed": {
defaultAddCaps: []corev1.Capability{"foo"},
allowedCaps: []corev1.Capability{"bar"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"bar"},
},
},
"required, allowed, container requests invalid": {
defaultAddCaps: []corev1.Capability{"foo"},
allowedCaps: []corev1.Capability{"bar"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"baz"},
},
expectedError: `capabilities.add: Invalid value: "baz": capability may not be added`,
},
"validation is case sensitive": {
defaultAddCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Add: []api.Capability{"FOO"},
},
expectedError: `capabilities.add: Invalid value: "FOO": capability may not be added`,
},
}
for k, v := range tests {
strategy, err := NewDefaultCapabilities(v.defaultAddCaps, nil, v.allowedCaps)
if err != nil {
t.Errorf("%s failed: %v", k, err)
continue
}
errs := strategy.Validate(field.NewPath("capabilities"), nil, nil, v.containerCaps)
if v.expectedError == "" && len(errs) > 0 {
t.Errorf("%s should have passed but had errors %v", k, errs)
continue
}
if v.expectedError != "" && len(errs) == 0 {
t.Errorf("%s should have failed but received no errors", k)
continue
}
if len(errs) == 1 && errs[0].Error() != v.expectedError {
t.Errorf("%s should have failed with %v but received %v", k, v.expectedError, errs[0])
continue
}
if len(errs) > 1 {
t.Errorf("%s should have failed with at most one error, but received %v: %v", k, len(errs), errs)
}
}
}
func TestValidateDrops(t *testing.T) {
tests := map[string]struct {
requiredDropCaps []corev1.Capability
containerCaps *api.Capabilities
expectedError string
}{
// no container requests
"no required, no container requests": {},
"required, no container requests": {
requiredDropCaps: []corev1.Capability{"foo"},
expectedError: `capabilities: Invalid value: "null": required capabilities are not set on the securityContext`,
},
// container requests match required
"required, container requests valid": {
requiredDropCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"foo"},
},
},
"required, container requests invalid": {
requiredDropCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"bar"},
},
expectedError: `capabilities.drop: Invalid value: []core.Capability{"bar"}: foo is required to be dropped but was not found`,
},
"validation is case sensitive": {
requiredDropCaps: []corev1.Capability{"foo"},
containerCaps: &api.Capabilities{
Drop: []api.Capability{"FOO"},
},
expectedError: `capabilities.drop: Invalid value: []core.Capability{"FOO"}: foo is required to be dropped but was not found`,
},
}
for k, v := range tests {
strategy, err := NewDefaultCapabilities(nil, v.requiredDropCaps, nil)
if err != nil {
t.Errorf("%s failed: %v", k, err)
continue
}
errs := strategy.Validate(field.NewPath("capabilities"), nil, nil, v.containerCaps)
if v.expectedError == "" && len(errs) > 0 {
t.Errorf("%s should have passed but had errors %v", k, errs)
continue
}
if v.expectedError != "" && len(errs) == 0 {
t.Errorf("%s should have failed but received no errors", k)
continue
}
if len(errs) == 1 && errs[0].Error() != v.expectedError {
t.Errorf("%s should have failed with %v but received %v", k, v.expectedError, errs[0])
continue
}
if len(errs) > 1 {
t.Errorf("%s should have failed with at most one error, but received %v: %v", k, len(errs), errs)
}
}
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package capabilities contains code for validating and defaulting a pod's
// kernel capabilities according to a security policy.
package capabilities

View File

@ -1,30 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package capabilities
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// Strategy defines the interface for all cap constraint strategies.
type Strategy interface {
// Generate creates the capabilities based on policy rules.
Generate(pod *api.Pod, container *api.Container) (*api.Capabilities, error)
// Validate ensures that the specified values fall within the range of the strategy.
Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, capabilities *api.Capabilities) field.ErrorList
}

View File

@ -1,20 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package podsecuritypolicy contains code for validating and defaulting the
// security context of a pod and its containers according to a security
// policy.
package podsecuritypolicy

View File

@ -1,196 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podsecuritypolicy
import (
"fmt"
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
)
type simpleStrategyFactory struct{}
var _ StrategyFactory = &simpleStrategyFactory{}
func NewSimpleStrategyFactory() StrategyFactory {
return &simpleStrategyFactory{}
}
func (f *simpleStrategyFactory) CreateStrategies(psp *policy.PodSecurityPolicy, namespace string) (*ProviderStrategies, error) {
errs := []error{}
userStrat, err := createUserStrategy(&psp.Spec.RunAsUser)
if err != nil {
errs = append(errs, err)
}
var groupStrat group.GroupStrategy
groupStrat, err = createRunAsGroupStrategy(psp.Spec.RunAsGroup)
if err != nil {
errs = append(errs, err)
}
seLinuxStrat, err := createSELinuxStrategy(&psp.Spec.SELinux)
if err != nil {
errs = append(errs, err)
}
appArmorStrat, err := createAppArmorStrategy(psp)
if err != nil {
errs = append(errs, err)
}
seccompStrat, err := createSeccompStrategy(psp)
if err != nil {
errs = append(errs, err)
}
fsGroupStrat, err := createFSGroupStrategy(&psp.Spec.FSGroup)
if err != nil {
errs = append(errs, err)
}
supGroupStrat, err := createSupplementalGroupStrategy(&psp.Spec.SupplementalGroups)
if err != nil {
errs = append(errs, err)
}
capStrat, err := createCapabilitiesStrategy(psp.Spec.DefaultAddCapabilities, psp.Spec.RequiredDropCapabilities, psp.Spec.AllowedCapabilities)
if err != nil {
errs = append(errs, err)
}
sysctlsStrat := createSysctlsStrategy(sysctl.SafeSysctlAllowlist(), psp.Spec.AllowedUnsafeSysctls, psp.Spec.ForbiddenSysctls)
if len(errs) > 0 {
return nil, errors.NewAggregate(errs)
}
strategies := &ProviderStrategies{
RunAsUserStrategy: userStrat,
RunAsGroupStrategy: groupStrat,
SELinuxStrategy: seLinuxStrat,
AppArmorStrategy: appArmorStrat,
FSGroupStrategy: fsGroupStrat,
SupplementalGroupStrategy: supGroupStrat,
CapabilitiesStrategy: capStrat,
SeccompStrategy: seccompStrat,
SysctlsStrategy: sysctlsStrat,
}
return strategies, nil
}
// createUserStrategy creates a new user strategy.
func createUserStrategy(opts *policy.RunAsUserStrategyOptions) (user.RunAsUserStrategy, error) {
switch opts.Rule {
case policy.RunAsUserStrategyMustRunAs:
return user.NewMustRunAs(opts)
case policy.RunAsUserStrategyMustRunAsNonRoot:
return user.NewRunAsNonRoot(opts)
case policy.RunAsUserStrategyRunAsAny:
return user.NewRunAsAny(opts)
default:
return nil, fmt.Errorf("Unrecognized RunAsUser strategy type %s", opts.Rule)
}
}
// createRunAsGroupStrategy creates a new group strategy.
func createRunAsGroupStrategy(opts *policy.RunAsGroupStrategyOptions) (group.GroupStrategy, error) {
if opts == nil {
return group.NewRunAsAny()
}
switch opts.Rule {
case policy.RunAsGroupStrategyMustRunAs:
return group.NewMustRunAs(opts.Ranges)
case policy.RunAsGroupStrategyRunAsAny:
return group.NewRunAsAny()
case policy.RunAsGroupStrategyMayRunAs:
return group.NewMayRunAs(opts.Ranges)
default:
return nil, fmt.Errorf("Unrecognized RunAsGroup strategy type %s", opts.Rule)
}
}
// createSELinuxStrategy creates a new selinux strategy.
func createSELinuxStrategy(opts *policy.SELinuxStrategyOptions) (selinux.SELinuxStrategy, error) {
switch opts.Rule {
case policy.SELinuxStrategyMustRunAs:
return selinux.NewMustRunAs(opts)
case policy.SELinuxStrategyRunAsAny:
return selinux.NewRunAsAny(opts)
default:
return nil, fmt.Errorf("Unrecognized SELinuxContext strategy type %s", opts.Rule)
}
}
// createAppArmorStrategy creates a new AppArmor strategy.
func createAppArmorStrategy(psp *policy.PodSecurityPolicy) (apparmor.Strategy, error) {
return apparmor.NewStrategy(psp.Annotations), nil
}
// createSeccompStrategy creates a new seccomp strategy.
func createSeccompStrategy(psp *policy.PodSecurityPolicy) (seccomp.Strategy, error) {
return seccomp.NewStrategy(psp.Annotations), nil
}
// createFSGroupStrategy creates a new fsgroup strategy
func createFSGroupStrategy(opts *policy.FSGroupStrategyOptions) (group.GroupStrategy, error) {
switch opts.Rule {
case policy.FSGroupStrategyRunAsAny:
return group.NewRunAsAny()
case policy.FSGroupStrategyMayRunAs:
return group.NewMayRunAs(opts.Ranges)
case policy.FSGroupStrategyMustRunAs:
return group.NewMustRunAs(opts.Ranges)
default:
return nil, fmt.Errorf("Unrecognized FSGroup strategy type %s", opts.Rule)
}
}
// createSupplementalGroupStrategy creates a new supplemental group strategy
func createSupplementalGroupStrategy(opts *policy.SupplementalGroupsStrategyOptions) (group.GroupStrategy, error) {
switch opts.Rule {
case policy.SupplementalGroupsStrategyRunAsAny:
return group.NewRunAsAny()
case policy.SupplementalGroupsStrategyMayRunAs:
return group.NewMayRunAs(opts.Ranges)
case policy.SupplementalGroupsStrategyMustRunAs:
return group.NewMustRunAs(opts.Ranges)
default:
return nil, fmt.Errorf("Unrecognized SupplementalGroups strategy type %s", opts.Rule)
}
}
// createCapabilitiesStrategy creates a new capabilities strategy.
func createCapabilitiesStrategy(defaultAddCaps, requiredDropCaps, allowedCaps []corev1.Capability) (capabilities.Strategy, error) {
return capabilities.NewDefaultCapabilities(defaultAddCaps, requiredDropCaps, allowedCaps)
}
// createSysctlsStrategy creates a new sysctls strategy.
func createSysctlsStrategy(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls []string) sysctl.SysctlsStrategy {
return sysctl.NewMustMatchPatterns(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls)
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package group contains code for validating and defaulting the FSGroup and
// supplemental groups of a pod according to a security policy.
package group

View File

@ -1,46 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"fmt"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
)
func ValidateGroupsInRanges(fldPath *field.Path, ranges []policy.IDRange, groups []int64) field.ErrorList {
allErrs := field.ErrorList{}
for _, group := range groups {
if !isGroupInRanges(group, ranges) {
detail := fmt.Sprintf("group %d must be in the ranges: %v", group, ranges)
allErrs = append(allErrs, field.Invalid(fldPath, groups, detail))
}
}
return allErrs
}
func isGroupInRanges(group int64, ranges []policy.IDRange) bool {
for _, rng := range ranges {
if psputil.GroupFallsInRange(group, rng) {
return true
}
}
return false
}

View File

@ -1,59 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"fmt"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// mayRunAs implements the GroupStrategy interface.
type mayRunAs struct {
ranges []policy.IDRange
}
var _ GroupStrategy = &mayRunAs{}
// NewMayRunAs provides a new MayRunAs strategy.
func NewMayRunAs(ranges []policy.IDRange) (GroupStrategy, error) {
if len(ranges) == 0 {
return nil, fmt.Errorf("ranges must be supplied for MayRunAs")
}
return &mayRunAs{
ranges: ranges,
}, nil
}
// Generate creates the group based on policy rules. This strategy returns an empty slice.
func (s *mayRunAs) Generate(_ *api.Pod) ([]int64, error) {
return nil, nil
}
// Generate a single value to be applied. This is used for FSGroup. This strategy returns nil.
func (s *mayRunAs) GenerateSingle(_ *api.Pod) (*int64, error) {
return nil, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
// Groups are passed in here to allow this strategy to support multiple group fields (fsgroup and
// supplemental groups).
func (s *mayRunAs) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
return ValidateGroupsInRanges(fldPath, s.ranges, groups)
}

View File

@ -1,185 +0,0 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"fmt"
"strings"
"testing"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestMayRunAsOptions(t *testing.T) {
tests := map[string]struct {
ranges []policy.IDRange
pass bool
}{
"empty": {
ranges: []policy.IDRange{},
},
"ranges": {
ranges: []policy.IDRange{
{Min: 1, Max: 1},
},
pass: true,
},
}
for k, v := range tests {
_, err := NewMayRunAs(v.ranges)
if v.pass && err != nil {
t.Errorf("error creating strategy for %s: %v", k, err)
}
if !v.pass && err == nil {
t.Errorf("expected error for %s but got none", k)
}
}
}
func TestMayRunAsValidate(t *testing.T) {
tests := map[string]struct {
ranges []policy.IDRange
groups []int64
expectedErrors []string
}{
"empty groups": {
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"not in range": {
groups: []int64{5},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
{Min: 4, Max: 4},
},
expectedErrors: []string{"group 5 must be in the ranges: [{1 3} {4 4}]"},
},
"not in ranges - multiple groups": {
groups: []int64{5, 10, 2020},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
{Min: 15, Max: 70},
},
expectedErrors: []string{
"group 5 must be in the ranges: [{1 3} {15 70}]",
"group 10 must be in the ranges: [{1 3} {15 70}]",
"group 2020 must be in the ranges: [{1 3} {15 70}]",
},
},
"not in ranges - one of multiple groups does not match": {
groups: []int64{5, 10, 2020},
ranges: []policy.IDRange{
{Min: 1, Max: 5},
{Min: 8, Max: 12},
{Min: 15, Max: 70},
},
expectedErrors: []string{
"group 2020 must be in the ranges: [{1 5} {8 12} {15 70}]",
},
},
"in range 1": {
groups: []int64{2},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"in range boundary min": {
groups: []int64{1},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"in range boundary max": {
groups: []int64{3},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"singular range": {
groups: []int64{4},
ranges: []policy.IDRange{
{Min: 4, Max: 4},
},
},
"in one of multiple ranges": {
groups: []int64{4},
ranges: []policy.IDRange{
{Min: 1, Max: 4},
{Min: 10, Max: 15},
},
},
"multiple groups matches one range": {
groups: []int64{4, 8, 12},
ranges: []policy.IDRange{
{Min: 1, Max: 20},
},
},
"multiple groups match multiple ranges": {
groups: []int64{4, 8, 12},
ranges: []policy.IDRange{
{Min: 1, Max: 4},
{Min: 200, Max: 2000},
{Min: 7, Max: 11},
{Min: 5, Max: 7},
{Min: 17, Max: 53},
{Min: 12, Max: 71},
},
},
}
for k, v := range tests {
s, err := NewMayRunAs(v.ranges)
if err != nil {
t.Errorf("error creating strategy for %s: %v", k, err)
}
errs := s.Validate(field.NewPath(""), nil, v.groups)
if len(v.expectedErrors) != len(errs) {
// number of expected errors is different from actual, includes cases when we expected errors and they appeared or vice versa
t.Errorf("number of expected errors for '%s' does not match with errors received:\n"+
"expected:\n%s\nbut got:\n%s",
k, concatenateStrings(v.expectedErrors), concatenateErrors(errs))
} else if len(v.expectedErrors) > 0 {
// check that the errors received match the expectations
for i, s := range v.expectedErrors {
if !strings.Contains(errs[i].Error(), s) {
t.Errorf("expected errors in particular order for '%s':\n%s\nbut got:\n%s",
k, concatenateStrings(v.expectedErrors), concatenateErrors(errs))
break
}
}
}
}
}
func concatenateErrors(errs field.ErrorList) string {
var errStrings []string
for _, e := range errs {
errStrings = append(errStrings, e.Error())
}
return concatenateStrings(errStrings)
}
func concatenateStrings(ss []string) string {
var ret string
for i, v := range ss {
ret += fmt.Sprintf("%d: %s\n", i+1, v)
}
return ret
}

View File

@ -1,71 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"fmt"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// mustRunAs implements the GroupStrategy interface
type mustRunAs struct {
ranges []policy.IDRange
}
var _ GroupStrategy = &mustRunAs{}
// NewMustRunAs provides a new MustRunAs strategy based on ranges.
func NewMustRunAs(ranges []policy.IDRange) (GroupStrategy, error) {
if len(ranges) == 0 {
return nil, fmt.Errorf("ranges must be supplied for MustRunAs")
}
return &mustRunAs{
ranges: ranges,
}, nil
}
// Generate creates the group based on policy rules. By default this returns the first group of the
// first range (min val).
func (s *mustRunAs) Generate(_ *api.Pod) ([]int64, error) {
return []int64{s.ranges[0].Min}, nil
}
// Generate a single value to be applied. This is used for FSGroup. This strategy will return
// the first group of the first range (min val).
func (s *mustRunAs) GenerateSingle(_ *api.Pod) (*int64, error) {
single := new(int64)
*single = s.ranges[0].Min
return single, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
// Groups are passed in here to allow this strategy to support multiple group fields (fsgroup and
// supplemental groups).
func (s *mustRunAs) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
allErrs := field.ErrorList{}
if len(groups) == 0 && len(s.ranges) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, groups, "unable to validate empty groups against required ranges"))
}
allErrs = append(allErrs, ValidateGroupsInRanges(fldPath, s.ranges, groups)...)
return allErrs
}

View File

@ -1,180 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"strings"
"testing"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestMustRunAsOptions(t *testing.T) {
tests := map[string]struct {
ranges []policy.IDRange
pass bool
}{
"empty": {
ranges: []policy.IDRange{},
},
"ranges": {
ranges: []policy.IDRange{
{Min: 1, Max: 1},
},
pass: true,
},
}
for k, v := range tests {
_, err := NewMustRunAs(v.ranges)
if v.pass && err != nil {
t.Errorf("error creating strategy for %s: %v", k, err)
}
if !v.pass && err == nil {
t.Errorf("expected error for %s but got none", k)
}
}
}
func TestGenerate(t *testing.T) {
tests := map[string]struct {
ranges []policy.IDRange
expected []int64
}{
"multi value": {
ranges: []policy.IDRange{
{Min: 1, Max: 2},
},
expected: []int64{1},
},
"single value": {
ranges: []policy.IDRange{
{Min: 1, Max: 1},
},
expected: []int64{1},
},
"multi range": {
ranges: []policy.IDRange{
{Min: 1, Max: 1},
{Min: 2, Max: 500},
},
expected: []int64{1},
},
}
for k, v := range tests {
s, err := NewMustRunAs(v.ranges)
if err != nil {
t.Errorf("error creating strategy for %s: %v", k, err)
}
actual, err := s.Generate(nil)
if err != nil {
t.Errorf("unexpected error for %s: %v", k, err)
}
if len(actual) != len(v.expected) {
t.Errorf("unexpected generated values. Expected %v, got %v", v.expected, actual)
continue
}
if len(actual) > 0 && len(v.expected) > 0 {
if actual[0] != v.expected[0] {
t.Errorf("unexpected generated values. Expected %v, got %v", v.expected, actual)
}
}
single, err := s.GenerateSingle(nil)
if err != nil {
t.Errorf("unexpected error for %s: %v", k, err)
}
if single == nil {
t.Errorf("unexpected nil generated value for %s: %v", k, single)
}
if *single != v.expected[0] {
t.Errorf("unexpected generated single value. Expected %v, got %v", v.expected, actual)
}
}
}
func TestValidate(t *testing.T) {
tests := map[string]struct {
ranges []policy.IDRange
groups []int64
expectedError string
}{
"nil security context": {
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
expectedError: "unable to validate empty groups against required ranges",
},
"empty groups": {
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
expectedError: "unable to validate empty groups against required ranges",
},
"not in range": {
groups: []int64{5},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
{Min: 4, Max: 4},
},
expectedError: "group 5 must be in the ranges: [{1 3} {4 4}]",
},
"in range 1": {
groups: []int64{2},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"in range boundary min": {
groups: []int64{1},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"in range boundary max": {
groups: []int64{3},
ranges: []policy.IDRange{
{Min: 1, Max: 3},
},
},
"singular range": {
groups: []int64{4},
ranges: []policy.IDRange{
{Min: 4, Max: 4},
},
},
}
for k, v := range tests {
s, err := NewMustRunAs(v.ranges)
if err != nil {
t.Errorf("error creating strategy for %s: %v", k, err)
}
errs := s.Validate(field.NewPath(""), nil, v.groups)
if v.expectedError == "" && len(errs) > 0 {
t.Errorf("unexpected errors for %s: %v", k, errs)
}
if v.expectedError != "" && len(errs) == 0 {
t.Errorf("expected errors for %s but got: %v", k, errs)
}
if v.expectedError != "" && len(errs) > 0 && !strings.Contains(errs[0].Error(), v.expectedError) {
t.Errorf("expected error for %s: %v, but got: %v", k, v.expectedError, errs[0])
}
}
}

View File

@ -1,49 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// runAsAny implements the GroupStrategy interface.
type runAsAny struct {
}
var _ GroupStrategy = &runAsAny{}
// NewRunAsAny provides a new RunAsAny strategy.
func NewRunAsAny() (GroupStrategy, error) {
return &runAsAny{}, nil
}
// Generate creates the group based on policy rules. This strategy returns an empty slice.
func (s *runAsAny) Generate(_ *api.Pod) ([]int64, error) {
return nil, nil
}
// Generate a single value to be applied. This is used for FSGroup. This strategy returns nil.
func (s *runAsAny) GenerateSingle(_ *api.Pod) (*int64, error) {
return nil, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *runAsAny) Validate(fldPath *field.Path, _ *api.Pod, groups []int64) field.ErrorList {
return field.ErrorList{}
}

View File

@ -1,62 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestRunAsAnyGenerate(t *testing.T) {
s, err := NewRunAsAny()
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
groups, err := s.Generate(nil)
if len(groups) > 0 {
t.Errorf("expected empty but got %v", groups)
}
if err != nil {
t.Errorf("unexpected error generating groups: %v", err)
}
}
func TestRunAsAnyGenerateSingle(t *testing.T) {
s, err := NewRunAsAny()
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
group, err := s.GenerateSingle(nil)
if group != nil {
t.Errorf("expected empty but got %v", group)
}
if err != nil {
t.Errorf("unexpected error generating groups: %v", err)
}
}
func TestRunAsAnyValidate(t *testing.T) {
s, err := NewRunAsAny()
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
errs := s.Validate(field.NewPath(""), nil, nil)
if len(errs) != 0 {
t.Errorf("unexpected errors: %v", errs)
}
}

View File

@ -1,35 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package group
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// GroupStrategy defines the interface for all group constraint strategies.
type GroupStrategy interface {
// Generate creates the group based on policy rules. The underlying implementation can
// decide whether it will return a full range of values or a subset of values from the
// configured ranges.
Generate(pod *api.Pod) ([]int64, error)
// Generate a single value to be applied. The underlying implementation decides which
// value to return if configured with multiple ranges. This is used for FSGroup.
GenerateSingle(pod *api.Pod) (*int64, error)
// Validate ensures that the specified values fall within the range of the strategy.
Validate(fldPath *field.Path, pod *api.Pod, groups []int64) field.ErrorList
}

View File

@ -1,459 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podsecuritypolicy
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/pods"
"k8s.io/kubernetes/pkg/features"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
"k8s.io/kubernetes/pkg/securitycontext"
)
// simpleProvider is the default implementation of Provider.
type simpleProvider struct {
psp *policy.PodSecurityPolicy
strategies *ProviderStrategies
}
// ensure we implement the interface correctly.
var _ Provider = &simpleProvider{}
// NewSimpleProvider creates a new Provider instance.
func NewSimpleProvider(psp *policy.PodSecurityPolicy, namespace string, strategyFactory StrategyFactory) (Provider, error) {
if psp == nil {
return nil, fmt.Errorf("NewSimpleProvider requires a PodSecurityPolicy")
}
if strategyFactory == nil {
return nil, fmt.Errorf("NewSimpleProvider requires a StrategyFactory")
}
strategies, err := strategyFactory.CreateStrategies(psp, namespace)
if err != nil {
return nil, err
}
return &simpleProvider{
psp: psp,
strategies: strategies,
}, nil
}
// MutatePod sets the default values of the required but not filled fields.
// Validation should be used after the context is defaulted to ensure it
// complies with the required restrictions.
func (s *simpleProvider) MutatePod(pod *api.Pod) error {
sc := securitycontext.NewPodSecurityContextMutator(pod.Spec.SecurityContext)
if sc.SupplementalGroups() == nil {
supGroups, err := s.strategies.SupplementalGroupStrategy.Generate(pod)
if err != nil {
return err
}
sc.SetSupplementalGroups(supGroups)
}
if sc.FSGroup() == nil {
fsGroup, err := s.strategies.FSGroupStrategy.GenerateSingle(pod)
if err != nil {
return err
}
sc.SetFSGroup(fsGroup)
}
if sc.SELinuxOptions() == nil {
seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, nil)
if err != nil {
return err
}
sc.SetSELinuxOptions(seLinux)
}
// This is only generated on the pod level. Containers inherit the pod's profile. If the
// container has a specific profile set then it will be caught in the validation step.
seccompProfile, err := s.strategies.SeccompStrategy.Generate(pod.Annotations, pod)
if err != nil {
return err
}
if seccompProfile != "" {
if pod.Annotations == nil {
pod.Annotations = map[string]string{}
}
pod.Annotations[api.SeccompPodAnnotationKey] = seccompProfile
}
pod.Spec.SecurityContext = sc.PodSecurityContext()
if s.psp.Spec.RuntimeClass != nil && pod.Spec.RuntimeClassName == nil {
pod.Spec.RuntimeClassName = s.psp.Spec.RuntimeClass.DefaultRuntimeClassName
}
var retErr error
podutil.VisitContainers(&pod.Spec, podutil.AllContainers, func(c *api.Container, containerType podutil.ContainerType) bool {
retErr = s.mutateContainer(pod, c)
if retErr != nil {
return false
}
return true
})
return retErr
}
// mutateContainer sets the default values of the required but not filled fields.
// It modifies the SecurityContext of the container and annotations of the pod. Validation should
// be used after the context is defaulted to ensure it complies with the required restrictions.
func (s *simpleProvider) mutateContainer(pod *api.Pod, container *api.Container) error {
sc := securitycontext.NewEffectiveContainerSecurityContextMutator(
securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext),
securitycontext.NewContainerSecurityContextMutator(container.SecurityContext),
)
if sc.RunAsUser() == nil {
uid, err := s.strategies.RunAsUserStrategy.Generate(pod, container)
if err != nil {
return err
}
sc.SetRunAsUser(uid)
}
if sc.RunAsGroup() == nil {
gid, err := s.strategies.RunAsGroupStrategy.GenerateSingle(pod)
if err != nil {
return err
}
sc.SetRunAsGroup(gid)
}
if sc.SELinuxOptions() == nil {
seLinux, err := s.strategies.SELinuxStrategy.Generate(pod, container)
if err != nil {
return err
}
sc.SetSELinuxOptions(seLinux)
}
annotations, err := s.strategies.AppArmorStrategy.Generate(pod.Annotations, container)
if err != nil {
return err
}
// if we're using the non-root strategy set the marker that this container should not be
// run as root which will signal to the kubelet to do a final check either on the runAsUser
// or, if runAsUser is not set, the image UID will be checked.
if sc.RunAsNonRoot() == nil && sc.RunAsUser() == nil && s.psp.Spec.RunAsUser.Rule == policy.RunAsUserStrategyMustRunAsNonRoot {
nonRoot := true
sc.SetRunAsNonRoot(&nonRoot)
}
caps, err := s.strategies.CapabilitiesStrategy.Generate(pod, container)
if err != nil {
return err
}
sc.SetCapabilities(caps)
// if the PSP requires a read only root filesystem and the container has not made a specific
// request then default ReadOnlyRootFilesystem to true.
if s.psp.Spec.ReadOnlyRootFilesystem && sc.ReadOnlyRootFilesystem() == nil {
readOnlyRootFS := true
sc.SetReadOnlyRootFilesystem(&readOnlyRootFS)
}
// if the PSP sets DefaultAllowPrivilegeEscalation and the container security context
// allowPrivilegeEscalation is not set, then default to that set by the PSP.
if s.psp.Spec.DefaultAllowPrivilegeEscalation != nil && sc.AllowPrivilegeEscalation() == nil {
sc.SetAllowPrivilegeEscalation(s.psp.Spec.DefaultAllowPrivilegeEscalation)
}
// if the PSP sets psp.AllowPrivilegeEscalation to false, set that as the default
if !*s.psp.Spec.AllowPrivilegeEscalation && sc.AllowPrivilegeEscalation() == nil {
sc.SetAllowPrivilegeEscalation(s.psp.Spec.AllowPrivilegeEscalation)
}
pod.Annotations = annotations
container.SecurityContext = sc.ContainerSecurityContext()
return nil
}
// ValidatePod ensure a pod is in compliance with the given constraints.
func (s *simpleProvider) ValidatePod(pod *api.Pod) field.ErrorList {
allErrs := field.ErrorList{}
sc := securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext)
scPath := field.NewPath("spec", "securityContext")
var fsGroups []int64
if fsGroup := sc.FSGroup(); fsGroup != nil {
fsGroups = []int64{*fsGroup}
}
allErrs = append(allErrs, s.strategies.FSGroupStrategy.Validate(scPath.Child("fsGroup"), pod, fsGroups)...)
allErrs = append(allErrs, s.strategies.SupplementalGroupStrategy.Validate(scPath.Child("supplementalGroups"), pod, sc.SupplementalGroups())...)
allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidatePod(pod)...)
allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(scPath.Child("seLinuxOptions"), pod, nil, sc.SELinuxOptions())...)
if !s.psp.Spec.HostNetwork && sc.HostNetwork() {
allErrs = append(allErrs, field.Invalid(scPath.Child("hostNetwork"), sc.HostNetwork(), "Host network is not allowed to be used"))
}
if !s.psp.Spec.HostPID && sc.HostPID() {
allErrs = append(allErrs, field.Invalid(scPath.Child("hostPID"), sc.HostPID(), "Host PID is not allowed to be used"))
}
if !s.psp.Spec.HostIPC && sc.HostIPC() {
allErrs = append(allErrs, field.Invalid(scPath.Child("hostIPC"), sc.HostIPC(), "Host IPC is not allowed to be used"))
}
allErrs = append(allErrs, s.strategies.SysctlsStrategy.Validate(pod)...)
allErrs = append(allErrs, s.validatePodVolumes(pod)...)
if s.psp.Spec.RuntimeClass != nil {
allErrs = append(allErrs, validateRuntimeClassName(pod.Spec.RuntimeClassName, s.psp.Spec.RuntimeClass.AllowedRuntimeClassNames)...)
}
pods.VisitContainersWithPath(&pod.Spec, field.NewPath("spec"), func(c *api.Container, p *field.Path) bool {
allErrs = append(allErrs, s.validateContainer(pod, c, p)...)
return true
})
return allErrs
}
func (s *simpleProvider) validatePodVolumes(pod *api.Pod) field.ErrorList {
allErrs := field.ErrorList{}
if len(pod.Spec.Volumes) > 0 {
allowsAllVolumeTypes := psputil.PSPAllowsAllVolumes(s.psp)
allowedVolumes := psputil.FSTypeToStringSet(s.psp.Spec.Volumes)
for i, v := range pod.Spec.Volumes {
fsType, err := psputil.GetVolumeFSType(v)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "volumes").Index(i), string(fsType), err.Error()))
continue
}
if !allowsAllVolumeTypes && !allowsVolumeType(allowedVolumes, fsType, v) {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "volumes").Index(i), string(fsType),
fmt.Sprintf("%s volumes are not allowed to be used", string(fsType))))
continue
}
switch fsType {
case policy.HostPath:
allows, mustBeReadOnly := psputil.AllowsHostVolumePath(s.psp, v.HostPath.Path)
if !allows {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "volumes").Index(i).Child("hostPath", "pathPrefix"), v.HostPath.Path,
fmt.Sprintf("is not allowed to be used")))
} else if mustBeReadOnly {
// Ensure all the VolumeMounts that use this volume are read-only
pods.VisitContainersWithPath(&pod.Spec, field.NewPath("spec"), func(c *api.Container, p *field.Path) bool {
for i, cv := range c.VolumeMounts {
if cv.Name == v.Name && !cv.ReadOnly {
allErrs = append(allErrs, field.Invalid(p.Child("volumeMounts").Index(i).Child("readOnly"), cv.ReadOnly, "must be read-only"))
}
}
return true
})
}
case policy.FlexVolume:
if len(s.psp.Spec.AllowedFlexVolumes) > 0 {
found := false
driver := v.FlexVolume.Driver
for _, allowedFlexVolume := range s.psp.Spec.AllowedFlexVolumes {
if driver == allowedFlexVolume.Driver {
found = true
break
}
}
if !found {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "volumes").Index(i).Child("driver"), driver,
"Flexvolume driver is not allowed to be used"))
}
}
case policy.CSI:
if utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
if len(s.psp.Spec.AllowedCSIDrivers) > 0 {
found := false
driver := v.CSI.Driver
for _, allowedCSIDriver := range s.psp.Spec.AllowedCSIDrivers {
if driver == allowedCSIDriver.Name {
found = true
break
}
}
if !found {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "volumes").Index(i).Child("csi", "driver"), driver,
"Inline CSI driver is not allowed to be used"))
}
}
}
}
}
}
return allErrs
}
// Ensure a container's SecurityContext is in compliance with the given constraints
func (s *simpleProvider) validateContainer(pod *api.Pod, container *api.Container, containerPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
podSC := securitycontext.NewPodSecurityContextAccessor(pod.Spec.SecurityContext)
sc := securitycontext.NewEffectiveContainerSecurityContextAccessor(podSC, securitycontext.NewContainerSecurityContextMutator(container.SecurityContext))
scPath := containerPath.Child("securityContext")
allErrs = append(allErrs, s.strategies.RunAsUserStrategy.Validate(scPath, pod, container, sc.RunAsNonRoot(), sc.RunAsUser())...)
var runAsGroups []int64
if sc.RunAsGroup() != nil {
runAsGroups = []int64{*sc.RunAsGroup()}
}
allErrs = append(allErrs, s.strategies.RunAsGroupStrategy.Validate(scPath, pod, runAsGroups)...)
allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(scPath.Child("seLinuxOptions"), pod, container, sc.SELinuxOptions())...)
allErrs = append(allErrs, s.strategies.AppArmorStrategy.Validate(pod, container)...)
allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidateContainer(pod, container)...)
privileged := sc.Privileged()
if !s.psp.Spec.Privileged && privileged != nil && *privileged {
allErrs = append(allErrs, field.Invalid(scPath.Child("privileged"), *privileged, "Privileged containers are not allowed"))
}
procMount := sc.ProcMount()
allowedProcMounts := s.psp.Spec.AllowedProcMountTypes
if len(allowedProcMounts) == 0 {
allowedProcMounts = []corev1.ProcMountType{corev1.DefaultProcMount}
}
foundProcMountType := false
for _, pm := range allowedProcMounts {
if string(pm) == string(procMount) {
foundProcMountType = true
}
}
if !foundProcMountType {
allErrs = append(allErrs, field.Invalid(scPath.Child("procMount"), procMount, "ProcMountType is not allowed"))
}
allErrs = append(allErrs, s.strategies.CapabilitiesStrategy.Validate(scPath.Child("capabilities"), pod, container, sc.Capabilities())...)
allErrs = append(allErrs, s.hasInvalidHostPort(container, containerPath)...)
if s.psp.Spec.ReadOnlyRootFilesystem {
readOnly := sc.ReadOnlyRootFilesystem()
if readOnly == nil {
allErrs = append(allErrs, field.Invalid(scPath.Child("readOnlyRootFilesystem"), readOnly, "ReadOnlyRootFilesystem may not be nil and must be set to true"))
} else if !*readOnly {
allErrs = append(allErrs, field.Invalid(scPath.Child("readOnlyRootFilesystem"), *readOnly, "ReadOnlyRootFilesystem must be set to true"))
}
}
allowEscalation := sc.AllowPrivilegeEscalation()
if !*s.psp.Spec.AllowPrivilegeEscalation && (allowEscalation == nil || *allowEscalation) {
allErrs = append(allErrs, field.Invalid(scPath.Child("allowPrivilegeEscalation"), allowEscalation, "Allowing privilege escalation for containers is not allowed"))
}
return allErrs
}
// hasInvalidHostPort checks whether the port definitions on the container fall outside of the ranges allowed by the PSP.
func (s *simpleProvider) hasInvalidHostPort(container *api.Container, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for _, cp := range container.Ports {
if cp.HostPort > 0 && !s.isValidHostPort(cp.HostPort) {
detail := fmt.Sprintf("Host port %d is not allowed to be used. Allowed ports: [%s]", cp.HostPort, hostPortRangesToString(s.psp.Spec.HostPorts))
allErrs = append(allErrs, field.Invalid(fldPath.Child("hostPort"), cp.HostPort, detail))
}
}
return allErrs
}
// isValidHostPort returns true if the port falls in any range allowed by the PSP.
func (s *simpleProvider) isValidHostPort(port int32) bool {
for _, hostPortRange := range s.psp.Spec.HostPorts {
if port >= hostPortRange.Min && port <= hostPortRange.Max {
return true
}
}
return false
}
// Get the name of the PSP that this provider was initialized with.
func (s *simpleProvider) GetPSPName() string {
return s.psp.Name
}
func hostPortRangesToString(ranges []policy.HostPortRange) string {
formattedString := ""
if ranges != nil {
strRanges := []string{}
for _, r := range ranges {
if r.Min == r.Max {
strRanges = append(strRanges, fmt.Sprintf("%d", r.Min))
} else {
strRanges = append(strRanges, fmt.Sprintf("%d-%d", r.Min, r.Max))
}
}
formattedString = strings.Join(strRanges, ",")
}
return formattedString
}
// validates that the actual RuntimeClassName is contained in the list of valid names.
func validateRuntimeClassName(actual *string, validNames []string) field.ErrorList {
if actual == nil {
return nil // An unset RuntimeClassName is always allowed.
}
for _, valid := range validNames {
if valid == policy.AllowAllRuntimeClassNames {
return nil
}
if *actual == valid {
return nil
}
}
return field.ErrorList{field.Invalid(field.NewPath("spec", "runtimeClassName"), *actual, "")}
}
func allowsVolumeType(allowedVolumes sets.String, fsType policy.FSType, volume api.Volume) bool {
if allowedVolumes.Has(string(fsType)) {
return true
}
// if secret volume is allowed, all the projected volume sources that projected service account token volumes expose are allowed, regardless of psp.
if allowedVolumes.Has(string(policy.Secret)) && fsType == policy.Projected && psputil.IsOnlyServiceAccountTokenSources(volume.VolumeSource.Projected) {
return true
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -1,178 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package seccomp
import (
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core"
)
const (
// AllowAny is the wildcard used to allow any profile.
AllowAny = "*"
// DefaultProfileAnnotationKey specifies the default seccomp profile.
DefaultProfileAnnotationKey = "seccomp.security.alpha.kubernetes.io/defaultProfileName"
// AllowedProfilesAnnotationKey specifies the allowed seccomp profiles.
AllowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
)
// Strategy defines the interface for all seccomp constraint strategies.
type Strategy interface {
// Generate returns a profile based on constraint rules.
Generate(annotations map[string]string, pod *api.Pod) (string, error)
// Validate ensures that the specified values fall within the range of the strategy.
ValidatePod(pod *api.Pod) field.ErrorList
// Validate ensures that the specified values fall within the range of the strategy.
ValidateContainer(pod *api.Pod, container *api.Container) field.ErrorList
}
type strategy struct {
defaultProfile string
allowedProfiles map[string]bool
// For printing error messages (preserves order).
allowedProfilesString string
// does the strategy allow any profile (wildcard)
allowAnyProfile bool
}
var _ Strategy = &strategy{}
// NewStrategy creates a new strategy that enforces seccomp profile constraints.
func NewStrategy(pspAnnotations map[string]string) Strategy {
var allowedProfiles map[string]bool
allowAnyProfile := false
if allowed, ok := pspAnnotations[AllowedProfilesAnnotationKey]; ok {
profiles := strings.Split(allowed, ",")
allowedProfiles = make(map[string]bool, len(profiles))
for _, p := range profiles {
if p == AllowAny {
allowAnyProfile = true
continue
}
// With the graduation of seccomp to GA we automatically convert
// the deprecated seccomp profile annotation `docker/default` to
// `runtime/default`. This means that we now have to automatically
// allow `runtime/default` if a user specifies `docker/default` and
// vice versa in a PSP.
if p == v1.DeprecatedSeccompProfileDockerDefault || p == v1.SeccompProfileRuntimeDefault {
allowedProfiles[v1.SeccompProfileRuntimeDefault] = true
allowedProfiles[v1.DeprecatedSeccompProfileDockerDefault] = true
}
allowedProfiles[p] = true
}
}
return &strategy{
defaultProfile: pspAnnotations[DefaultProfileAnnotationKey],
allowedProfiles: allowedProfiles,
allowedProfilesString: pspAnnotations[AllowedProfilesAnnotationKey],
allowAnyProfile: allowAnyProfile,
}
}
// Generate returns a profile based on constraint rules.
func (s *strategy) Generate(annotations map[string]string, pod *api.Pod) (string, error) {
if annotations[api.SeccompPodAnnotationKey] != "" {
// Profile already set, nothing to do.
return annotations[api.SeccompPodAnnotationKey], nil
}
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
// Profile field already set, translate to annotation
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile), nil
}
return s.defaultProfile, nil
}
// ValidatePod ensures that the specified values on the pod fall within the range
// of the strategy.
func (s *strategy) ValidatePod(pod *api.Pod) field.ErrorList {
allErrs := field.ErrorList{}
podSpecFieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompPodAnnotationKey)
podProfile := pod.Annotations[api.SeccompPodAnnotationKey]
// if the annotation is not set, see if the field is set and derive the corresponding annotation value
if len(podProfile) == 0 && pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
podProfile = podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
}
if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && podProfile != "" {
allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, "seccomp may not be set"))
return allErrs
}
if !s.profileAllowed(podProfile) {
msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", podProfile, s.allowedProfilesString)
allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, msg))
}
return allErrs
}
// ValidateContainer ensures that the specified values on the container fall within
// the range of the strategy.
func (s *strategy) ValidateContainer(pod *api.Pod, container *api.Container) field.ErrorList {
allErrs := field.ErrorList{}
fieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompContainerAnnotationKeyPrefix + container.Name)
containerProfile := profileForContainer(pod, container)
if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && containerProfile != "" {
allErrs = append(allErrs, field.Forbidden(fieldPath, "seccomp may not be set"))
return allErrs
}
if !s.profileAllowed(containerProfile) {
msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", containerProfile, s.allowedProfilesString)
allErrs = append(allErrs, field.Forbidden(fieldPath, msg))
}
return allErrs
}
// profileAllowed checks if profile is in allowedProfiles or if allowedProfiles
// contains the wildcard.
func (s *strategy) profileAllowed(profile string) bool {
// for backwards compatibility and PSPs without a defined list of allowed profiles.
// If a PSP does not have allowedProfiles set then we should allow an empty profile.
// This will mean that the runtime default is used.
if len(s.allowedProfiles) == 0 && profile == "" {
return true
}
return s.allowAnyProfile || s.allowedProfiles[profile]
}
// profileForContainer returns the container profile if set, otherwise the pod profile.
func profileForContainer(pod *api.Pod, container *api.Container) string {
if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil {
// derive the annotation value from the container field
return podutil.SeccompAnnotationForField(container.SecurityContext.SeccompProfile)
}
containerProfile, ok := pod.Annotations[api.SeccompContainerAnnotationKeyPrefix+container.Name]
if ok {
// return the existing container annotation
return containerProfile
}
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
// derive the annotation value from the pod field
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
}
// return the existing pod annotation
return pod.Annotations[api.SeccompPodAnnotationKey]
}

View File

@ -1,419 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package seccomp
import (
"reflect"
"strings"
"testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "k8s.io/kubernetes/pkg/apis/core"
)
var (
withoutSeccomp = map[string]string{"foo": "bar"}
allowAnyNoDefault = map[string]string{
AllowedProfilesAnnotationKey: "*",
}
allowAnyDefault = map[string]string{
AllowedProfilesAnnotationKey: "*",
DefaultProfileAnnotationKey: "foo",
}
allowAnyAndSpecificDefault = map[string]string{
AllowedProfilesAnnotationKey: "*,bar",
DefaultProfileAnnotationKey: "foo",
}
allowSpecific = map[string]string{
AllowedProfilesAnnotationKey: "foo",
}
allowSpecificLocalhost = map[string]string{
AllowedProfilesAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + "foo",
}
allowSpecificDockerDefault = map[string]string{
AllowedProfilesAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault,
}
allowSpecificRuntimeDefault = map[string]string{
AllowedProfilesAnnotationKey: v1.SeccompProfileRuntimeDefault,
}
)
func TestNewStrategy(t *testing.T) {
tests := map[string]struct {
annotations map[string]string
expectedAllowedProfilesString string
expectedAllowAny bool
expectedAllowedProfiles map[string]bool
expectedDefaultProfile string
}{
"no seccomp": {
annotations: withoutSeccomp,
expectedAllowAny: false,
expectedAllowedProfilesString: "",
expectedAllowedProfiles: nil,
expectedDefaultProfile: "",
},
"allow any, no default": {
annotations: allowAnyNoDefault,
expectedAllowAny: true,
expectedAllowedProfilesString: "*",
expectedAllowedProfiles: map[string]bool{},
expectedDefaultProfile: "",
},
"allow any, default": {
annotations: allowAnyDefault,
expectedAllowAny: true,
expectedAllowedProfilesString: "*",
expectedAllowedProfiles: map[string]bool{},
expectedDefaultProfile: "foo",
},
"allow any and specific, default": {
annotations: allowAnyAndSpecificDefault,
expectedAllowAny: true,
expectedAllowedProfilesString: "*,bar",
expectedAllowedProfiles: map[string]bool{
"bar": true,
},
expectedDefaultProfile: "foo",
},
}
for k, v := range tests {
s := NewStrategy(v.annotations)
internalStrat, _ := s.(*strategy)
if internalStrat.allowAnyProfile != v.expectedAllowAny {
t.Errorf("%s expected allowAnyProfile to be %t but found %t", k, v.expectedAllowAny, internalStrat.allowAnyProfile)
}
if internalStrat.allowedProfilesString != v.expectedAllowedProfilesString {
t.Errorf("%s expected allowedProfilesString to be %s but found %s", k, v.expectedAllowedProfilesString, internalStrat.allowedProfilesString)
}
if internalStrat.defaultProfile != v.expectedDefaultProfile {
t.Errorf("%s expected defaultProfile to be %s but found %s", k, v.expectedDefaultProfile, internalStrat.defaultProfile)
}
if !reflect.DeepEqual(v.expectedAllowedProfiles, internalStrat.allowedProfiles) {
t.Errorf("%s expected expectedAllowedProfiles to be %#v but found %#v", k, v.expectedAllowedProfiles, internalStrat.allowedProfiles)
}
}
}
func TestGenerate(t *testing.T) {
bar := "bar"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedProfile string
}{
"no seccomp, no pod annotations": {
pspAnnotations: withoutSeccomp,
podAnnotations: nil,
expectedProfile: "",
},
"no seccomp, pod annotations": {
pspAnnotations: withoutSeccomp,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedProfile: "foo",
},
"seccomp with no default, no pod annotations": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: nil,
expectedProfile: "",
},
"seccomp with no default, pod annotations": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedProfile: "foo",
},
"seccomp with default, no pod annotations": {
pspAnnotations: allowAnyDefault,
podAnnotations: nil,
expectedProfile: "foo",
},
"seccomp with default, pod annotations": {
pspAnnotations: allowAnyDefault,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "bar",
},
expectedProfile: "bar",
},
"seccomp with default, pod field": {
pspAnnotations: allowAnyDefault,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &bar,
},
expectedProfile: "localhost/bar",
},
}
for k, v := range tests {
s := NewStrategy(v.pspAnnotations)
actual, err := s.Generate(v.podAnnotations, &api.Pod{
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
SeccompProfile: v.seccompProfile,
},
},
})
if err != nil {
t.Errorf("%s received error during generation %#v", k, err)
continue
}
if actual != v.expectedProfile {
t.Errorf("%s expected profile %s but received %s", k, v.expectedProfile, actual)
}
}
}
func TestValidatePod(t *testing.T) {
foo := "foo"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedError string
}{
"no pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: nil,
expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo",
},
"no pod annotations, no required profiles": {
pspAnnotations: withoutSeccomp,
podAnnotations: nil,
expectedError: "",
},
"valid pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedError: "",
},
"invalid pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "bar",
},
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
},
"pod annotations, no required profiles": {
pspAnnotations: withoutSeccomp,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedError: "Forbidden: seccomp may not be set",
},
"pod annotations, allow any": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedError: "",
},
"no pod annotations, allow any": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: nil,
expectedError: "",
},
"valid pod annotations and field, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
"valid pod field and no annotation, required profiles": {
pspAnnotations: allowSpecific,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "Forbidden: localhost/foo is not an allowed seccomp profile. Valid values are foo",
},
"valid pod field and no annotation, required profiles (localhost)": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
"docker/default PSP annotation automatically allows runtime/default pods": {
pspAnnotations: allowSpecificDockerDefault,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault,
},
expectedError: "",
},
"runtime/default PSP annotation automatically allows docker/default pods": {
pspAnnotations: allowSpecificRuntimeDefault,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault,
},
expectedError: "",
},
}
for k, v := range tests {
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: v.podAnnotations,
},
Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{
SeccompProfile: v.seccompProfile,
},
},
}
s := NewStrategy(v.pspAnnotations)
errs := s.ValidatePod(pod)
if v.expectedError == "" && len(errs) != 0 {
t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error())
}
if v.expectedError != "" && len(errs) == 0 {
t.Errorf("%s expected error %s but received none", k, v.expectedError)
}
if v.expectedError != "" && len(errs) > 1 {
t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error())
}
if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) {
t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error())
}
}
}
func TestValidateContainer(t *testing.T) {
foo := "foo"
bar := "bar"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
seccompProfile *api.SeccompProfile
expectedError string
}{
"no pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: nil,
expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo",
},
"no pod annotations, no required profiles": {
pspAnnotations: withoutSeccomp,
podAnnotations: nil,
expectedError: "",
},
"valid pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
},
expectedError: "",
},
"invalid pod annotations, required profiles": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompContainerAnnotationKeyPrefix + "container": "bar",
},
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
},
"pod annotations, no required profiles": {
pspAnnotations: withoutSeccomp,
podAnnotations: map[string]string{
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
},
expectedError: "Forbidden: seccomp may not be set",
},
"pod annotations, allow any": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: map[string]string{
api.SeccompContainerAnnotationKeyPrefix + "container": "foo",
},
expectedError: "",
},
"no pod annotations, allow any": {
pspAnnotations: allowAnyNoDefault,
podAnnotations: nil,
expectedError: "",
},
"container inherits valid pod annotation": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "foo",
},
expectedError: "",
},
"container inherits invalid pod annotation": {
pspAnnotations: allowSpecific,
podAnnotations: map[string]string{
api.SeccompPodAnnotationKey: "bar",
},
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
},
"valid container field and no annotation, required profiles": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &foo,
},
expectedError: "",
},
"invalid container field and no annotation, required profiles": {
pspAnnotations: allowSpecificLocalhost,
seccompProfile: &api.SeccompProfile{
Type: api.SeccompProfileTypeLocalhost,
LocalhostProfile: &bar,
},
expectedError: "Forbidden: localhost/bar is not an allowed seccomp profile. Valid values are localhost/foo",
},
}
for k, v := range tests {
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: v.podAnnotations,
},
}
container := &api.Container{
Name: "container",
SecurityContext: &api.SecurityContext{
SeccompProfile: v.seccompProfile,
},
}
s := NewStrategy(v.pspAnnotations)
errs := s.ValidateContainer(pod, container)
if v.expectedError == "" && len(errs) != 0 {
t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error())
}
if v.expectedError != "" && len(errs) == 0 {
t.Errorf("%s expected error %s but received none", k, v.expectedError)
}
if v.expectedError != "" && len(errs) > 1 {
t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error())
}
if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) {
t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error())
}
}
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package selinux contains code for validating and defaulting the SELinux
// context of a pod according to a security policy.
package selinux

View File

@ -1,126 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selinux
import (
"fmt"
"sort"
"strings"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
)
type mustRunAs struct {
opts *api.SELinuxOptions
}
var _ SELinuxStrategy = &mustRunAs{}
func NewMustRunAs(options *policy.SELinuxStrategyOptions) (SELinuxStrategy, error) {
if options == nil {
return nil, fmt.Errorf("MustRunAs requires SELinuxContextStrategyOptions")
}
if options.SELinuxOptions == nil {
return nil, fmt.Errorf("MustRunAs requires SELinuxOptions")
}
internalSELinuxOptions := &api.SELinuxOptions{}
if err := v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(options.SELinuxOptions, internalSELinuxOptions, nil); err != nil {
return nil, err
}
return &mustRunAs{
opts: internalSELinuxOptions,
}, nil
}
// Generate creates the SELinuxOptions based on constraint rules.
func (s *mustRunAs) Generate(_ *api.Pod, _ *api.Container) (*api.SELinuxOptions, error) {
return s.opts, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *mustRunAs) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, seLinux *api.SELinuxOptions) field.ErrorList {
allErrs := field.ErrorList{}
if seLinux == nil {
allErrs = append(allErrs, field.Required(fldPath, ""))
return allErrs
}
if !equalLevels(s.opts.Level, seLinux.Level) {
detail := fmt.Sprintf("must be %s", s.opts.Level)
allErrs = append(allErrs, field.Invalid(fldPath.Child("level"), seLinux.Level, detail))
}
if seLinux.Role != s.opts.Role {
detail := fmt.Sprintf("must be %s", s.opts.Role)
allErrs = append(allErrs, field.Invalid(fldPath.Child("role"), seLinux.Role, detail))
}
if seLinux.Type != s.opts.Type {
detail := fmt.Sprintf("must be %s", s.opts.Type)
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), seLinux.Type, detail))
}
if seLinux.User != s.opts.User {
detail := fmt.Sprintf("must be %s", s.opts.User)
allErrs = append(allErrs, field.Invalid(fldPath.Child("user"), seLinux.User, detail))
}
return allErrs
}
// equalLevels compares SELinux levels for equality.
func equalLevels(expected, actual string) bool {
if expected == actual {
return true
}
// "s0:c6,c0" => [ "s0", "c6,c0" ]
expectedParts := strings.SplitN(expected, ":", 2)
actualParts := strings.SplitN(actual, ":", 2)
// both SELinux levels must be in a format "sX:cY"
if len(expectedParts) != 2 || len(actualParts) != 2 {
return false
}
if !equalSensitivity(expectedParts[0], actualParts[0]) {
return false
}
if !equalCategories(expectedParts[1], actualParts[1]) {
return false
}
return true
}
// equalSensitivity compares sensitivities of the SELinux levels for equality.
func equalSensitivity(expected, actual string) bool {
return expected == actual
}
// equalCategories compares categories of the SELinux levels for equality.
func equalCategories(expected, actual string) bool {
expectedCategories := strings.Split(expected, ",")
actualCategories := strings.Split(actual, ",")
sort.Strings(expectedCategories)
sort.Strings(actualCategories)
return util.EqualStringSlices(expectedCategories, actualCategories)
}

View File

@ -1,183 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selinux
import (
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/v1"
"reflect"
"strings"
"testing"
)
func TestMustRunAsOptions(t *testing.T) {
tests := map[string]struct {
opts *policy.SELinuxStrategyOptions
pass bool
}{
"nil opts": {
opts: nil,
pass: false,
},
"invalid opts": {
opts: &policy.SELinuxStrategyOptions{},
pass: false,
},
"valid opts": {
opts: &policy.SELinuxStrategyOptions{SELinuxOptions: &corev1.SELinuxOptions{}},
pass: true,
},
}
for name, tc := range tests {
_, err := NewMustRunAs(tc.opts)
if err != nil && tc.pass {
t.Errorf("%s expected to pass but received error %#v", name, err)
}
if err == nil && !tc.pass {
t.Errorf("%s expected to fail but did not receive an error", name)
}
}
}
func TestMustRunAsGenerate(t *testing.T) {
opts := &policy.SELinuxStrategyOptions{
SELinuxOptions: &corev1.SELinuxOptions{
User: "user",
Role: "role",
Type: "type",
Level: "level",
},
}
mustRunAs, err := NewMustRunAs(opts)
if err != nil {
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
}
generated, err := mustRunAs.Generate(nil, nil)
if err != nil {
t.Fatalf("unexpected error generating selinux %v", err)
}
internalSELinuxOptions := &api.SELinuxOptions{}
v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(opts.SELinuxOptions, internalSELinuxOptions, nil)
if !reflect.DeepEqual(generated, internalSELinuxOptions) {
t.Errorf("generated selinux does not equal configured selinux")
}
}
func TestMustRunAsValidate(t *testing.T) {
newValidOpts := func() *corev1.SELinuxOptions {
return &corev1.SELinuxOptions{
User: "user",
Role: "role",
Level: "s0:c0,c6",
Type: "type",
}
}
newValidOptsWithLevel := func(level string) *corev1.SELinuxOptions {
opts := newValidOpts()
opts.Level = level
return opts
}
role := newValidOpts()
role.Role = "invalid"
user := newValidOpts()
user.User = "invalid"
seType := newValidOpts()
seType.Type = "invalid"
validOpts := newValidOpts()
tests := map[string]struct {
podSeLinux *corev1.SELinuxOptions
pspSeLinux *corev1.SELinuxOptions
expectedMsg string
}{
"invalid role": {
podSeLinux: role,
pspSeLinux: validOpts,
expectedMsg: "role: Invalid value",
},
"invalid user": {
podSeLinux: user,
pspSeLinux: validOpts,
expectedMsg: "user: Invalid value",
},
"levels are not equal": {
podSeLinux: newValidOptsWithLevel("s0"),
pspSeLinux: newValidOptsWithLevel("s0:c1,c2"),
expectedMsg: "level: Invalid value",
},
"levels differ by sensitivity": {
podSeLinux: newValidOptsWithLevel("s0:c6"),
pspSeLinux: newValidOptsWithLevel("s1:c6"),
expectedMsg: "level: Invalid value",
},
"levels differ by categories": {
podSeLinux: newValidOptsWithLevel("s0:c0,c8"),
pspSeLinux: newValidOptsWithLevel("s0:c1,c7"),
expectedMsg: "level: Invalid value",
},
"valid": {
podSeLinux: validOpts,
pspSeLinux: validOpts,
expectedMsg: "",
},
"valid with different order of categories": {
podSeLinux: newValidOptsWithLevel("s0:c6,c0"),
pspSeLinux: validOpts,
expectedMsg: "",
},
}
for name, tc := range tests {
opts := &policy.SELinuxStrategyOptions{
SELinuxOptions: tc.pspSeLinux,
}
mustRunAs, err := NewMustRunAs(opts)
if err != nil {
t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err)
continue
}
internalSELinuxOptions := api.SELinuxOptions{}
v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(tc.podSeLinux, &internalSELinuxOptions, nil)
errs := mustRunAs.Validate(nil, nil, nil, &internalSELinuxOptions)
//should've passed but didn't
if len(tc.expectedMsg) == 0 && len(errs) > 0 {
t.Errorf("%s expected no errors but received %v", name, errs)
}
//should've failed but didn't
if len(tc.expectedMsg) != 0 && len(errs) == 0 {
t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg)
}
//failed with additional messages
if len(tc.expectedMsg) != 0 && len(errs) > 1 {
t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
}
//check that we got the right message
if len(tc.expectedMsg) != 0 && len(errs) == 1 {
if !strings.Contains(errs[0].Error(), tc.expectedMsg) {
t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
}
}
}
}

View File

@ -1,43 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selinux
import (
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// runAsAny implements the SELinuxStrategy interface.
type runAsAny struct{}
var _ SELinuxStrategy = &runAsAny{}
// NewRunAsAny provides a strategy that will return the configured se linux context or nil.
func NewRunAsAny(options *policy.SELinuxStrategyOptions) (SELinuxStrategy, error) {
return &runAsAny{}, nil
}
// Generate creates the SELinuxOptions based on constraint rules.
func (s *runAsAny) Generate(pod *api.Pod, container *api.Container) (*api.SELinuxOptions, error) {
return nil, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *runAsAny) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, options *api.SELinuxOptions) field.ErrorList {
return field.ErrorList{}
}

View File

@ -1,72 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selinux
import (
corev1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"testing"
)
func TestRunAsAnyOptions(t *testing.T) {
_, err := NewRunAsAny(nil)
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
_, err = NewRunAsAny(&policy.SELinuxStrategyOptions{})
if err != nil {
t.Errorf("unexpected error initializing NewRunAsAny %v", err)
}
}
func TestRunAsAnyGenerate(t *testing.T) {
s, err := NewRunAsAny(&policy.SELinuxStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
uid, err := s.Generate(nil, nil)
if uid != nil {
t.Errorf("expected nil uid but got %v", *uid)
}
if err != nil {
t.Errorf("unexpected error generating uid %v", err)
}
}
func TestRunAsAnyValidate(t *testing.T) {
s, err := NewRunAsAny(&policy.SELinuxStrategyOptions{
SELinuxOptions: &corev1.SELinuxOptions{
Level: "foo",
},
},
)
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
errs := s.Validate(nil, nil, nil, nil)
if len(errs) != 0 {
t.Errorf("unexpected errors validating with ")
}
s, err = NewRunAsAny(&policy.SELinuxStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
errs = s.Validate(nil, nil, nil, nil)
if len(errs) != 0 {
t.Errorf("unexpected errors validating %v", errs)
}
}

View File

@ -1,30 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package selinux
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// SELinuxStrategy defines the interface for all SELinux constraint strategies.
type SELinuxStrategy interface {
// Generate creates the SELinuxOptions based on constraint rules.
Generate(pod *api.Pod, container *api.Container) (*api.SELinuxOptions, error)
// Validate ensures that the specified values fall within the range of the strategy.
Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, options *api.SELinuxOptions) field.ErrorList
}

View File

@ -1,126 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sysctl
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// SafeSysctlAllowlist returns the allowlist of safe sysctls and safe sysctl patterns (ending in *).
//
// A sysctl is called safe iff
// - it is namespaced in the container or the pod
// - it is isolated, i.e. has no influence on any other pod on the same node.
func SafeSysctlAllowlist() []string {
return []string{
"kernel.shm_rmid_forced",
"net.ipv4.ip_local_port_range",
"net.ipv4.tcp_syncookies",
"net.ipv4.ping_group_range",
"net.ipv4.ip_unprivileged_port_start",
}
}
// mustMatchPatterns implements the SysctlsStrategy interface
type mustMatchPatterns struct {
safeAllowlist []string
allowedUnsafeSysctls []string
forbiddenSysctls []string
}
var (
_ SysctlsStrategy = &mustMatchPatterns{}
)
// NewMustMatchPatterns creates a new mustMatchPatterns strategy that will provide validation.
// Passing nil means the default pattern, passing an empty list means to disallow all sysctls.
func NewMustMatchPatterns(safeAllowlist, allowedUnsafeSysctls, forbiddenSysctls []string) SysctlsStrategy {
return &mustMatchPatterns{
safeAllowlist: safeAllowlist,
allowedUnsafeSysctls: allowedUnsafeSysctls,
forbiddenSysctls: forbiddenSysctls,
}
}
func (s *mustMatchPatterns) isForbidden(sysctlName string) bool {
// Is the sysctl forbidden?
for _, s := range s.forbiddenSysctls {
if strings.HasSuffix(s, "*") {
prefix := strings.TrimSuffix(s, "*")
if strings.HasPrefix(sysctlName, prefix) {
return true
}
} else if sysctlName == s {
return true
}
}
return false
}
func (s *mustMatchPatterns) isSafe(sysctlName string) bool {
for _, ws := range s.safeAllowlist {
if sysctlName == ws {
return true
}
}
return false
}
func (s *mustMatchPatterns) isAllowedUnsafe(sysctlName string) bool {
for _, s := range s.allowedUnsafeSysctls {
if strings.HasSuffix(s, "*") {
prefix := strings.TrimSuffix(s, "*")
if strings.HasPrefix(sysctlName, prefix) {
return true
}
} else if sysctlName == s {
return true
}
}
return false
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *mustMatchPatterns) Validate(pod *api.Pod) field.ErrorList {
allErrs := field.ErrorList{}
var sysctls []api.Sysctl
if pod.Spec.SecurityContext != nil {
sysctls = pod.Spec.SecurityContext.Sysctls
}
fieldPath := field.NewPath("pod", "spec", "securityContext").Child("sysctls")
for i, sysctl := range sysctls {
switch {
case s.isForbidden(sysctl.Name):
allErrs = append(allErrs, field.ErrorList{field.Forbidden(fieldPath.Index(i), fmt.Sprintf("sysctl %q is not allowed", sysctl.Name))}...)
case s.isSafe(sysctl.Name):
continue
case s.isAllowedUnsafe(sysctl.Name):
continue
default:
allErrs = append(allErrs, field.ErrorList{field.Forbidden(fieldPath.Index(i), fmt.Sprintf("unsafe sysctl %q is not allowed", sysctl.Name))}...)
}
}
return allErrs
}

View File

@ -1,104 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sysctl
import (
"testing"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestValidate(t *testing.T) {
tests := map[string]struct {
allowlist []string
forbiddenSafe []string
allowedUnsafe []string
allowed []string
disallowed []string
}{
// no container requests
"with allow all": {
allowlist: []string{"foo"},
allowed: []string{"foo"},
},
"empty": {
allowlist: []string{"foo"},
forbiddenSafe: []string{"*"},
disallowed: []string{"foo"},
},
"without wildcard": {
allowlist: []string{"a", "a.b"},
allowed: []string{"a", "a.b"},
disallowed: []string{"b"},
},
"with catch-all wildcard and non-wildcard": {
allowedUnsafe: []string{"a.b.c", "*"},
allowed: []string{"a", "a.b", "a.b.c", "b"},
},
"without catch-all wildcard": {
allowedUnsafe: []string{"a.*", "b.*", "c.d.e", "d.e.f.*"},
allowed: []string{"a.b", "b.c", "c.d.e", "d.e.f.g.h"},
disallowed: []string{"a", "b", "c", "c.d", "d.e", "d.e.f"},
},
}
for k, v := range tests {
strategy := NewMustMatchPatterns(v.allowlist, v.allowedUnsafe, v.forbiddenSafe)
pod := &api.Pod{}
errs := strategy.Validate(pod)
if len(errs) != 0 {
t.Errorf("%s: unexpected validaton errors for empty sysctls: %v", k, errs)
}
testAllowed := func() {
sysctls := []api.Sysctl{}
for _, s := range v.allowed {
sysctls = append(sysctls, api.Sysctl{
Name: s,
Value: "dummy",
})
}
pod.Spec.SecurityContext = &api.PodSecurityContext{
Sysctls: sysctls,
}
errs = strategy.Validate(pod)
if len(errs) != 0 {
t.Errorf("%s: unexpected validaton errors for sysctls: %v", k, errs)
}
}
testDisallowed := func() {
for _, s := range v.disallowed {
pod.Spec.SecurityContext = &api.PodSecurityContext{
Sysctls: []api.Sysctl{
{
Name: s,
Value: "dummy",
},
},
}
errs = strategy.Validate(pod)
if len(errs) == 0 {
t.Errorf("%s: expected error for sysctl %q", k, s)
}
}
}
testAllowed()
testDisallowed()
}
}

View File

@ -1,67 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podsecuritypolicy
import (
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
)
// Provider provides the implementation to generate a new security
// context based on constraints or validate an existing security context against constraints.
type Provider interface {
// MutatePod sets the default values of the required but not filled fields of the pod and all
// containers in the pod.
MutatePod(pod *api.Pod) error
// ValidatePod ensures a pod and all its containers are in compliance with the given constraints.
// ValidatePod MUST NOT mutate the pod.
ValidatePod(pod *api.Pod) field.ErrorList
// Get the name of the PSP that this provider was initialized with.
GetPSPName() string
}
// StrategyFactory abstracts how the strategies are created from the provider so that you may
// implement your own custom strategies that may pull information from other resources as necessary.
// For example, if you would like to populate the strategies with values from namespace annotations
// you may create a factory with a client that can pull the namespace and populate the appropriate
// values.
type StrategyFactory interface {
// CreateStrategies creates the strategies that a provider will use. The namespace argument
// should be the namespace of the object being checked (the pod's namespace).
CreateStrategies(psp *policy.PodSecurityPolicy, namespace string) (*ProviderStrategies, error)
}
// ProviderStrategies is a holder for all strategies that the provider requires to be populated.
type ProviderStrategies struct {
RunAsUserStrategy user.RunAsUserStrategy
RunAsGroupStrategy group.GroupStrategy
SELinuxStrategy selinux.SELinuxStrategy
AppArmorStrategy apparmor.Strategy
FSGroupStrategy group.GroupStrategy
SupplementalGroupStrategy group.GroupStrategy
CapabilitiesStrategy capabilities.Strategy
SysctlsStrategy sysctl.SysctlsStrategy
SeccompStrategy seccomp.Strategy
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package user contains code for validating and defaulting the UID of a pod
// or container according to a security policy.
package user

View File

@ -1,74 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
"fmt"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
)
// mustRunAs implements the RunAsUserStrategy interface
type mustRunAs struct {
opts *policy.RunAsUserStrategyOptions
}
// NewMustRunAs provides a strategy that requires the container to run as a specific UID in a range.
func NewMustRunAs(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
if options == nil {
return nil, fmt.Errorf("MustRunAs requires run as user options")
}
if len(options.Ranges) == 0 {
return nil, fmt.Errorf("MustRunAs requires at least one range")
}
return &mustRunAs{
opts: options,
}, nil
}
// Generate creates the uid based on policy rules. MustRunAs returns the first range's Min.
func (s *mustRunAs) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
return &s.opts.Ranges[0].Min, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *mustRunAs) Validate(scPath *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
allErrs := field.ErrorList{}
if runAsUser == nil {
allErrs = append(allErrs, field.Required(scPath.Child("runAsUser"), ""))
return allErrs
}
if !s.isValidUID(*runAsUser) {
detail := fmt.Sprintf("must be in the ranges: %v", s.opts.Ranges)
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsUser"), *runAsUser, detail))
}
return allErrs
}
func (s *mustRunAs) isValidUID(id int64) bool {
for _, rng := range s.opts.Ranges {
if psputil.UserFallsInRange(id, rng) {
return true
}
}
return false
}

View File

@ -1,144 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
policy "k8s.io/api/policy/v1beta1"
api "k8s.io/kubernetes/pkg/apis/core"
"strings"
"testing"
)
func TestNewMustRunAs(t *testing.T) {
tests := map[string]struct {
opts *policy.RunAsUserStrategyOptions
pass bool
}{
"nil opts": {
opts: nil,
pass: false,
},
"invalid opts": {
opts: &policy.RunAsUserStrategyOptions{},
pass: false,
},
"valid opts": {
opts: &policy.RunAsUserStrategyOptions{
Ranges: []policy.IDRange{
{Min: 1, Max: 1},
},
},
pass: true,
},
}
for name, tc := range tests {
_, err := NewMustRunAs(tc.opts)
if err != nil && tc.pass {
t.Errorf("%s expected to pass but received error %#v", name, err)
}
if err == nil && !tc.pass {
t.Errorf("%s expected to fail but did not receive an error", name)
}
}
}
func TestGenerate(t *testing.T) {
opts := &policy.RunAsUserStrategyOptions{
Ranges: []policy.IDRange{
{Min: 1, Max: 1},
},
}
mustRunAs, err := NewMustRunAs(opts)
if err != nil {
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
}
generated, err := mustRunAs.Generate(nil, nil)
if err != nil {
t.Fatalf("unexpected error generating runAsUser %v", err)
}
if *generated != opts.Ranges[0].Min {
t.Errorf("generated runAsUser does not equal configured runAsUser")
}
}
func TestValidate(t *testing.T) {
opts := &policy.RunAsUserStrategyOptions{
Ranges: []policy.IDRange{
{Min: 1, Max: 1},
{Min: 10, Max: 20},
},
}
validID := int64(15)
invalidID := int64(21)
tests := map[string]struct {
container *api.Container
expectedMsg string
}{
"good container": {
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsUser: &validID,
},
},
},
"nil run as user": {
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsUser: nil,
},
},
expectedMsg: "runAsUser: Required",
},
"invalid id": {
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsUser: &invalidID,
},
},
expectedMsg: "runAsUser: Invalid",
},
}
for name, tc := range tests {
mustRunAs, err := NewMustRunAs(opts)
if err != nil {
t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err)
continue
}
errs := mustRunAs.Validate(nil, nil, nil, tc.container.SecurityContext.RunAsNonRoot, tc.container.SecurityContext.RunAsUser)
//should've passed but didn't
if len(tc.expectedMsg) == 0 && len(errs) > 0 {
t.Errorf("%s expected no errors but received %v", name, errs)
}
//should've failed but didn't
if len(tc.expectedMsg) != 0 && len(errs) == 0 {
t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg)
}
//failed with additional messages
if len(tc.expectedMsg) != 0 && len(errs) > 1 {
t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
}
//check that we got the right message
if len(tc.expectedMsg) != 0 && len(errs) == 1 {
if !strings.Contains(errs[0].Error(), tc.expectedMsg) {
t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
}
}
}
}

View File

@ -1,59 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
type nonRoot struct{}
var _ RunAsUserStrategy = &nonRoot{}
func NewRunAsNonRoot(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
return &nonRoot{}, nil
}
// Generate creates the uid based on policy rules. This strategy does return a UID. It assumes
// that the user will specify a UID or the container image specifies a UID.
func (s *nonRoot) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
return nil, nil
}
// Validate ensures that the specified values fall within the range of the strategy. Validation
// of this will pass if either the UID is not set, assuming that the image will provided the UID
// or if the UID is set it is not root. Validation will fail if RunAsNonRoot is set to false.
// In order to work properly this assumes that the kubelet performs a final check on runAsUser
// or the image UID when runAsUser is nil.
func (s *nonRoot) Validate(scPath *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
allErrs := field.ErrorList{}
if runAsNonRoot == nil && runAsUser == nil {
allErrs = append(allErrs, field.Required(scPath.Child("runAsNonRoot"), "must be true"))
return allErrs
}
if runAsNonRoot != nil && *runAsNonRoot == false {
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsNonRoot"), *runAsNonRoot, "must be true"))
return allErrs
}
if runAsUser != nil && *runAsUser == 0 {
allErrs = append(allErrs, field.Invalid(scPath.Child("runAsUser"), *runAsUser, "running with the root UID is forbidden"))
return allErrs
}
return allErrs
}

View File

@ -1,119 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
api "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
"testing"
)
func TestNonRootOptions(t *testing.T) {
_, err := NewRunAsNonRoot(nil)
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsNonRoot %v", err)
}
_, err = NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Errorf("unexpected error initializing NewRunAsNonRoot %v", err)
}
}
func TestNonRootGenerate(t *testing.T) {
s, err := NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsNonRoot %v", err)
}
uid, err := s.Generate(nil, nil)
if uid != nil {
t.Errorf("expected nil uid but got %d", *uid)
}
if err != nil {
t.Errorf("unexpected error generating uid %v", err)
}
}
func TestNonRootValidate(t *testing.T) {
goodUID := int64(1)
badUID := int64(0)
untrue := false
unfalse := true
s, err := NewRunAsNonRoot(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewMustRunAs %v", err)
}
tests := []struct {
container *api.Container
expectedErr bool
msg string
}{
{
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsUser: &badUID,
},
},
expectedErr: true,
msg: "in test case %d, expected errors from root uid but got none: %v",
},
{
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsUser: &goodUID,
},
},
expectedErr: false,
msg: "in test case %d, expected no errors from non-root uid but got %v",
},
{
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsNonRoot: &untrue,
},
},
expectedErr: true,
msg: "in test case %d, expected errors from RunAsNonRoot but got none: %v",
},
{
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsNonRoot: &unfalse,
RunAsUser: &goodUID,
},
},
expectedErr: false,
msg: "in test case %d, expected no errors from non-root uid but got %v",
},
{
container: &api.Container{
SecurityContext: &api.SecurityContext{
RunAsNonRoot: nil,
RunAsUser: nil,
},
},
expectedErr: true,
msg: "in test case %d, expected errors from nil runAsNonRoot and nil runAsUser but got %v",
},
}
for i, tc := range tests {
errs := s.Validate(nil, nil, nil, tc.container.SecurityContext.RunAsNonRoot, tc.container.SecurityContext.RunAsUser)
if (len(errs) == 0) == tc.expectedErr {
t.Errorf(tc.msg, i, errs)
}
}
}

View File

@ -1,43 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// runAsAny implements the interface RunAsUserStrategy.
type runAsAny struct{}
var _ RunAsUserStrategy = &runAsAny{}
// NewRunAsAny provides a strategy that will return nil.
func NewRunAsAny(options *policy.RunAsUserStrategyOptions) (RunAsUserStrategy, error) {
return &runAsAny{}, nil
}
// Generate creates the uid based on policy rules.
func (s *runAsAny) Generate(pod *api.Pod, container *api.Container) (*int64, error) {
return nil, nil
}
// Validate ensures that the specified values fall within the range of the strategy.
func (s *runAsAny) Validate(_ *field.Path, _ *api.Pod, _ *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList {
return field.ErrorList{}
}

View File

@ -1,59 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
"testing"
policy "k8s.io/api/policy/v1beta1"
)
func TestRunAsAnyOptions(t *testing.T) {
_, err := NewRunAsAny(nil)
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
_, err = NewRunAsAny(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Errorf("unexpected error initializing NewRunAsAny %v", err)
}
}
func TestRunAsAnyGenerate(t *testing.T) {
s, err := NewRunAsAny(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
uid, err := s.Generate(nil, nil)
if uid != nil {
t.Errorf("expected nil uid but got %d", *uid)
}
if err != nil {
t.Errorf("unexpected error generating uid %v", err)
}
}
func TestRunAsAnyValidate(t *testing.T) {
s, err := NewRunAsAny(&policy.RunAsUserStrategyOptions{})
if err != nil {
t.Fatalf("unexpected error initializing NewRunAsAny %v", err)
}
errs := s.Validate(nil, nil, nil, nil, nil)
if len(errs) != 0 {
t.Errorf("unexpected errors validating with ")
}
}

View File

@ -1,31 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
"k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core"
)
// RunAsUserStrategy defines the interface for all uid constraint strategies.
type RunAsUserStrategy interface {
// Generate creates the uid based on policy rules.
Generate(pod *api.Pod, container *api.Container) (*int64, error)
// Validate ensures that the specified values fall within the range of the strategy.
// scPath is the field path to the container's security context
Validate(scPath *field.Path, pod *api.Pod, container *api.Container, runAsNonRoot *bool, runAsUser *int64) field.ErrorList
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package util contains utility code shared amongst different parts of the
// pod security policy apparatus.
package util

View File

@ -1,276 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"fmt"
"strings"
policy "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
api "k8s.io/kubernetes/pkg/apis/core"
)
const (
ValidatedPSPAnnotation = "kubernetes.io/psp"
)
// GetAllFSTypesExcept returns the result of GetAllFSTypesAsSet minus
// the given exceptions.
func GetAllFSTypesExcept(exceptions ...string) sets.String {
fstypes := GetAllFSTypesAsSet()
for _, e := range exceptions {
fstypes.Delete(e)
}
return fstypes
}
// GetAllFSTypesAsSet returns all actual volume types, regardless
// of feature gates. The special policy.All pseudo type is not included.
func GetAllFSTypesAsSet() sets.String {
fstypes := sets.NewString()
fstypes.Insert(
string(policy.HostPath),
string(policy.AzureFile),
string(policy.Flocker),
string(policy.FlexVolume),
string(policy.EmptyDir),
string(policy.GCEPersistentDisk),
string(policy.AWSElasticBlockStore),
string(policy.GitRepo),
string(policy.Secret),
string(policy.NFS),
string(policy.ISCSI),
string(policy.Glusterfs),
string(policy.PersistentVolumeClaim),
string(policy.RBD),
string(policy.Cinder),
string(policy.CephFS),
string(policy.DownwardAPI),
string(policy.FC),
string(policy.ConfigMap),
string(policy.VsphereVolume),
string(policy.Quobyte),
string(policy.AzureDisk),
string(policy.PhotonPersistentDisk),
string(policy.StorageOS),
string(policy.Projected),
string(policy.PortworxVolume),
string(policy.ScaleIO),
string(policy.CSI),
string(policy.Ephemeral),
)
return fstypes
}
// getVolumeFSType gets the FSType for a volume.
func GetVolumeFSType(v api.Volume) (policy.FSType, error) {
switch {
case v.HostPath != nil:
return policy.HostPath, nil
case v.EmptyDir != nil:
return policy.EmptyDir, nil
case v.GCEPersistentDisk != nil:
return policy.GCEPersistentDisk, nil
case v.AWSElasticBlockStore != nil:
return policy.AWSElasticBlockStore, nil
case v.GitRepo != nil:
return policy.GitRepo, nil
case v.Secret != nil:
return policy.Secret, nil
case v.NFS != nil:
return policy.NFS, nil
case v.ISCSI != nil:
return policy.ISCSI, nil
case v.Glusterfs != nil:
return policy.Glusterfs, nil
case v.PersistentVolumeClaim != nil:
return policy.PersistentVolumeClaim, nil
case v.RBD != nil:
return policy.RBD, nil
case v.FlexVolume != nil:
return policy.FlexVolume, nil
case v.Cinder != nil:
return policy.Cinder, nil
case v.CephFS != nil:
return policy.CephFS, nil
case v.Flocker != nil:
return policy.Flocker, nil
case v.DownwardAPI != nil:
return policy.DownwardAPI, nil
case v.FC != nil:
return policy.FC, nil
case v.AzureFile != nil:
return policy.AzureFile, nil
case v.ConfigMap != nil:
return policy.ConfigMap, nil
case v.VsphereVolume != nil:
return policy.VsphereVolume, nil
case v.Quobyte != nil:
return policy.Quobyte, nil
case v.AzureDisk != nil:
return policy.AzureDisk, nil
case v.PhotonPersistentDisk != nil:
return policy.PhotonPersistentDisk, nil
case v.StorageOS != nil:
return policy.StorageOS, nil
case v.Projected != nil:
return policy.Projected, nil
case v.PortworxVolume != nil:
return policy.PortworxVolume, nil
case v.ScaleIO != nil:
return policy.ScaleIO, nil
case v.CSI != nil:
return policy.CSI, nil
case v.Ephemeral != nil:
return policy.Ephemeral, nil
}
return "", fmt.Errorf("unknown volume type for volume: %#v", v)
}
// FSTypeToStringSet converts an FSType slice to a string set.
func FSTypeToStringSet(fsTypes []policy.FSType) sets.String {
set := sets.NewString()
for _, v := range fsTypes {
set.Insert(string(v))
}
return set
}
// PSPAllowsAllVolumes checks for FSTypeAll in the psp's allowed volumes.
func PSPAllowsAllVolumes(psp *policy.PodSecurityPolicy) bool {
return PSPAllowsFSType(psp, policy.All)
}
// PSPAllowsFSType is a utility for checking if a PSP allows a particular FSType.
// If all volumes are allowed then this will return true for any FSType passed.
func PSPAllowsFSType(psp *policy.PodSecurityPolicy, fsType policy.FSType) bool {
if psp == nil {
return false
}
for _, v := range psp.Spec.Volumes {
if v == fsType || v == policy.All {
return true
}
}
return false
}
// UserFallsInRange is a utility to determine it the id falls in the valid range.
func UserFallsInRange(id int64, rng policy.IDRange) bool {
return id >= rng.Min && id <= rng.Max
}
// GroupFallsInRange is a utility to determine it the id falls in the valid range.
func GroupFallsInRange(id int64, rng policy.IDRange) bool {
return id >= rng.Min && id <= rng.Max
}
// AllowsHostVolumePath is a utility for checking if a PSP allows the host volume path.
// This only checks the path. You should still check to make sure the host volume fs type is allowed.
func AllowsHostVolumePath(psp *policy.PodSecurityPolicy, hostPath string) (pathIsAllowed, mustBeReadOnly bool) {
if psp == nil {
return false, false
}
// If no allowed paths are specified then allow any path
if len(psp.Spec.AllowedHostPaths) == 0 {
return true, false
}
for _, allowedPath := range psp.Spec.AllowedHostPaths {
if hasPathPrefix(hostPath, allowedPath.PathPrefix) {
if !allowedPath.ReadOnly {
return true, allowedPath.ReadOnly
}
pathIsAllowed = true
mustBeReadOnly = true
}
}
return pathIsAllowed, mustBeReadOnly
}
// hasPathPrefix returns true if the string matches pathPrefix exactly, or if is prefixed with pathPrefix at a path segment boundary
// the string and pathPrefix are both normalized to remove trailing slashes prior to checking.
func hasPathPrefix(s, pathPrefix string) bool {
s = strings.TrimSuffix(s, "/")
pathPrefix = strings.TrimSuffix(pathPrefix, "/")
// Short circuit if s doesn't contain the prefix at all
if !strings.HasPrefix(s, pathPrefix) {
return false
}
pathPrefixLength := len(pathPrefix)
if len(s) == pathPrefixLength {
// Exact match
return true
}
if s[pathPrefixLength:pathPrefixLength+1] == "/" {
// The next character in s is a path segment boundary
// Check this instead of normalizing pathPrefix to avoid allocating on every call
// Example where this check applies: s=/foo/bar and pathPrefix=/foo
return true
}
return false
}
// EqualStringSlices compares string slices for equality. Slices are equal when
// their sizes and elements on similar positions are equal.
func EqualStringSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func IsOnlyServiceAccountTokenSources(v *api.ProjectedVolumeSource) bool {
for _, s := range v.Sources {
// reject any projected source that does not match any of our expected source types
if s.ServiceAccountToken == nil && s.ConfigMap == nil && s.DownwardAPI == nil {
return false
}
if t := s.ServiceAccountToken; t != nil && (t.Path != "token" || t.Audience != "") {
return false
}
if s.ConfigMap != nil && s.ConfigMap.LocalObjectReference.Name != "kube-root-ca.crt" {
return false
}
if s.DownwardAPI != nil {
for _, d := range s.DownwardAPI.Items {
if d.Path != "namespace" || d.FieldRef == nil || d.FieldRef.APIVersion != "v1" || d.FieldRef.FieldPath != "metadata.namespace" {
return false
}
}
}
}
return true
}

View File

@ -1,471 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"reflect"
"testing"
policy "k8s.io/api/policy/v1beta1"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/serviceaccount"
)
// TestVolumeSourceFSTypeDrift ensures that for every known type of volume source (by the fields on
// a VolumeSource object that GetVolumeFSType is returning a good value. This ensures both that we're
// returning an FSType for the VolumeSource field (protect the GetVolumeFSType method) and that we
// haven't drifted (ensure new fields in VolumeSource are covered).
func TestVolumeSourceFSTypeDrift(t *testing.T) {
allFSTypes := GetAllFSTypesAsSet()
val := reflect.ValueOf(api.VolumeSource{})
for i := 0; i < val.NumField(); i++ {
fieldVal := val.Type().Field(i)
volumeSource := api.VolumeSource{}
volumeSourceVolume := reflect.New(fieldVal.Type.Elem())
reflect.ValueOf(&volumeSource).Elem().FieldByName(fieldVal.Name).Set(volumeSourceVolume)
fsType, err := GetVolumeFSType(api.Volume{VolumeSource: volumeSource})
if err != nil {
t.Errorf("error getting fstype for field %s. This likely means that drift has occurred between FSType and VolumeSource. Please update the api and getVolumeFSType", fieldVal.Name)
}
if !allFSTypes.Has(string(fsType)) {
t.Errorf("%s was missing from GetFSTypesAsSet", fsType)
}
}
}
func TestPSPAllowsFSType(t *testing.T) {
tests := map[string]struct {
psp *policy.PodSecurityPolicy
fsType policy.FSType
allows bool
}{
"nil psp": {
psp: nil,
fsType: policy.HostPath,
allows: false,
},
"empty volumes": {
psp: &policy.PodSecurityPolicy{},
fsType: policy.HostPath,
allows: false,
},
"non-matching": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
Volumes: []policy.FSType{policy.AWSElasticBlockStore},
},
},
fsType: policy.HostPath,
allows: false,
},
"match on FSTypeAll": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
Volumes: []policy.FSType{policy.All},
},
},
fsType: policy.HostPath,
allows: true,
},
"match on direct match": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
Volumes: []policy.FSType{policy.HostPath},
},
},
fsType: policy.HostPath,
allows: true,
},
}
for k, v := range tests {
allows := PSPAllowsFSType(v.psp, v.fsType)
if v.allows != allows {
t.Errorf("%s expected PSPAllowsFSType to return %t but got %t", k, v.allows, allows)
}
}
}
func TestAllowsHostVolumePath(t *testing.T) {
tests := map[string]struct {
psp *policy.PodSecurityPolicy
path string
allows bool
mustBeReadOnly bool
}{
"nil psp": {
psp: nil,
path: "/test",
allows: false,
mustBeReadOnly: false,
},
"empty allowed paths": {
psp: &policy.PodSecurityPolicy{},
path: "/test",
allows: true,
mustBeReadOnly: false,
},
"non-matching": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{
PathPrefix: "/foo",
ReadOnly: true,
},
},
},
},
path: "/foobar",
allows: false,
mustBeReadOnly: false,
},
"match on direct match": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{
PathPrefix: "/foo",
ReadOnly: true,
},
},
},
},
path: "/foo",
allows: true,
mustBeReadOnly: true,
},
"match with trailing slash on host path": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo"},
},
},
},
path: "/foo/",
allows: true,
mustBeReadOnly: false,
},
"match with trailing slash on allowed path": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo/"},
},
},
},
path: "/foo",
allows: true,
mustBeReadOnly: false,
},
"match child directory": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{
PathPrefix: "/foo/",
ReadOnly: true,
},
},
},
},
path: "/foo/bar",
allows: true,
mustBeReadOnly: true,
},
"non-matching parent directory": {
psp: &policy.PodSecurityPolicy{
Spec: policy.PodSecurityPolicySpec{
AllowedHostPaths: []policy.AllowedHostPath{
{PathPrefix: "/foo/bar"},
},
},
},
path: "/foo",
allows: false,
mustBeReadOnly: false,
},
}
for k, v := range tests {
allows, mustBeReadOnly := AllowsHostVolumePath(v.psp, v.path)
if v.allows != allows {
t.Errorf("allows: %s expected %t but got %t", k, v.allows, allows)
}
if v.mustBeReadOnly != mustBeReadOnly {
t.Errorf("mustBeReadOnly: %s expected %t but got %t", k, v.mustBeReadOnly, mustBeReadOnly)
}
}
}
func TestEqualStringSlices(t *testing.T) {
tests := map[string]struct {
arg1 []string
arg2 []string
expectedResult bool
}{
"nil equals to nil": {
arg1: nil,
arg2: nil,
expectedResult: true,
},
"equal by size": {
arg1: []string{"1", "1"},
arg2: []string{"1", "1"},
expectedResult: true,
},
"not equal by size": {
arg1: []string{"1"},
arg2: []string{"1", "1"},
expectedResult: false,
},
"not equal by elements": {
arg1: []string{"1", "1"},
arg2: []string{"1", "2"},
expectedResult: false,
},
}
for k, v := range tests {
if result := EqualStringSlices(v.arg1, v.arg2); result != v.expectedResult {
t.Errorf("%s expected to return %t but got %t", k, v.expectedResult, result)
}
}
}
func TestIsOnlyServiceAccountTokenSources(t *testing.T) {
serviceAccountToken := api.VolumeProjection{
ServiceAccountToken: &api.ServiceAccountTokenProjection{
Path: "token",
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
}}
configMap := api.VolumeProjection{
ConfigMap: &api.ConfigMapProjection{
LocalObjectReference: api.LocalObjectReference{
Name: "kube-root-ca.crt",
},
Items: []api.KeyToPath{
{
Key: "ca.crt",
Path: "ca.crt",
},
},
},
}
downwardAPI := api.VolumeProjection{
DownwardAPI: &api.DownwardAPIProjection{
Items: []api.DownwardAPIVolumeFile{
{
Path: "namespace",
FieldRef: &api.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.namespace",
},
},
},
},
}
tests := []struct {
desc string
volume *api.ProjectedVolumeSource
want bool
}{
{
desc: "deny if ServiceAccountToken has wrong path",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
{ServiceAccountToken: &api.ServiceAccountTokenProjection{
Path: "notatoken",
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
}},
configMap,
downwardAPI,
},
},
},
{
desc: "deny if ServiceAccountToken has wrong audience",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
{ServiceAccountToken: &api.ServiceAccountTokenProjection{
Path: "token",
Audience: "not api server",
ExpirationSeconds: serviceaccount.WarnOnlyBoundTokenExpirationSeconds,
}},
configMap,
downwardAPI,
},
},
},
{
desc: "deny if CondigMap has wrong LocalObjectReference.Name",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
serviceAccountToken,
{
ConfigMap: &api.ConfigMapProjection{
LocalObjectReference: api.LocalObjectReference{
Name: "foo-ca.crt",
},
Items: []api.KeyToPath{
{
Key: "ca.crt",
Path: "ca.crt",
},
},
},
},
downwardAPI,
},
},
},
{
desc: "deny if DownwardAPI has wrong path",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
serviceAccountToken,
configMap,
{
DownwardAPI: &api.DownwardAPIProjection{
Items: []api.DownwardAPIVolumeFile{
{
Path: "foo",
FieldRef: &api.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.namespace",
},
},
},
},
},
},
},
},
{
desc: "deny if DownwardAPI has nil field ref",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
serviceAccountToken,
configMap,
{
DownwardAPI: &api.DownwardAPIProjection{
Items: []api.DownwardAPIVolumeFile{
{
Path: "namespace",
},
},
},
},
},
},
},
{
desc: "deny if DownwardAPI has wrong api version",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
serviceAccountToken,
configMap,
{
DownwardAPI: &api.DownwardAPIProjection{
Items: []api.DownwardAPIVolumeFile{
{
Path: "namespace",
FieldRef: &api.ObjectFieldSelector{
APIVersion: "v1beta1",
FieldPath: "metadata.namespace",
},
},
},
},
},
},
},
},
{
desc: "deny if DownwardAPI has wrong field path",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
serviceAccountToken,
configMap,
{
DownwardAPI: &api.DownwardAPIProjection{
Items: []api.DownwardAPIVolumeFile{
{
Path: "namespace",
FieldRef: &api.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "metadata.foo",
},
},
},
},
},
},
},
},
{
desc: "deny if Secret exists",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
{
Secret: &api.SecretProjection{},
},
configMap,
downwardAPI,
serviceAccountToken,
},
},
},
{
desc: "deny if none of ServiceAccountToken, ConfigMap and DownwardAPI exist",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
{},
},
},
},
{
desc: "allow if any of ServiceAccountToken, ConfigMap and DownwardAPI matches",
volume: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{
configMap,
downwardAPI,
serviceAccountToken,
},
},
want: true,
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
if got := IsOnlyServiceAccountTokenSources(test.volume); got != test.want {
t.Errorf("IsOnlyServiceAccountTokenSources(%+v) = %v, want %v", test.volume, got, test.want)
}
})
}
}

View File

@ -1,8 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-auth-policy-approvers
reviewers:
- sig-auth-policy-reviewers
labels:
- sig/auth

View File

@ -1,380 +0,0 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package podsecuritypolicy
import (
"context"
"fmt"
"io"
"sort"
"strings"
"k8s.io/klog/v2"
policyv1beta1 "k8s.io/api/policy/v1beta1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/informers"
policylisters "k8s.io/client-go/listers/policy/v1beta1"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/apis/policy"
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
)
// PluginName is a string with the name of the plugin
const PluginName = "PodSecurityPolicy"
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
plugin := newPlugin(psp.NewSimpleStrategyFactory(), true)
return plugin, nil
})
}
// Plugin holds state for and implements the admission plugin.
type Plugin struct {
*admission.Handler
strategyFactory psp.StrategyFactory
failOnNoPolicies bool
authz authorizer.Authorizer
lister policylisters.PodSecurityPolicyLister
}
// SetAuthorizer sets the authorizer.
func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) {
p.authz = authz
}
// ValidateInitialization ensures an authorizer is set.
func (p *Plugin) ValidateInitialization() error {
if p.authz == nil {
return fmt.Errorf("%s requires an authorizer", PluginName)
}
if p.lister == nil {
return fmt.Errorf("%s requires a lister", PluginName)
}
return nil
}
var _ admission.MutationInterface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{}
var _ genericadmissioninit.WantsAuthorizer = &Plugin{}
var _ genericadmissioninit.WantsExternalKubeInformerFactory = &Plugin{}
var auditKeyPrefix = strings.ToLower(PluginName) + "." + policy.GroupName + ".k8s.io"
// newPlugin creates a new PSP admission plugin.
func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *Plugin {
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
strategyFactory: strategyFactory,
failOnNoPolicies: failOnNoPolicies,
}
}
// SetExternalKubeInformerFactory registers an informer
func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
podSecurityPolicyInformer := f.Policy().V1beta1().PodSecurityPolicies()
p.lister = podSecurityPolicyInformer.Lister()
p.SetReadyFunc(podSecurityPolicyInformer.Informer().HasSynced)
}
// Admit determines if the pod should be admitted based on the requested security context
// and the available PSPs.
//
// 1. Find available PSPs.
// 2. Create the providers, includes setting pre-allocated values if necessary.
// 3. Try to generate and validate a PSP with providers. If we find one then admit the pod
// with the validated PSP. If we don't find any reject the pod and give all errors from the
// failed attempts.
func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
if ignore, err := shouldIgnore(a); err != nil {
return err
} else if ignore {
return nil
}
// only mutate if this is a CREATE request. On updates we only validate.
if a.GetOperation() != admission.Create {
return nil
}
pod := a.GetObject().(*api.Pod)
// compute the context. Mutation is allowed. ValidatedPSPAnnotation is not taken into account.
allowedPod, pspName, validationErrs, err := p.computeSecurityContext(ctx, a, pod, true, "")
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: %w", err))
}
if allowedPod != nil {
*pod = *allowedPod
// annotate and accept the pod
klog.V(4).Infof("pod %s (generate: %s) in namespace %s validated against provider %s", pod.Name, pod.GenerateName, a.GetNamespace(), pspName)
if pod.ObjectMeta.Annotations == nil {
pod.ObjectMeta.Annotations = map[string]string{}
}
pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = pspName
key := auditKeyPrefix + "/" + "admit-policy"
if err := a.AddAnnotation(key, pspName); err != nil {
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
}
return nil
}
// we didn't validate against any provider, reject the pod and give the errors for each attempt
klog.V(4).Infof("unable to admit pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: unable to admit pod: %v", validationErrs))
}
// Validate verifies attributes against the PodSecurityPolicy
func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
if ignore, err := shouldIgnore(a); err != nil {
return err
} else if ignore {
return nil
}
pod := a.GetObject().(*api.Pod)
// compute the context. Mutation is not allowed. ValidatedPSPAnnotation is used as a hint to gain same speed-up.
allowedPod, pspName, validationErrs, err := p.computeSecurityContext(ctx, a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: %w", err))
}
if apiequality.Semantic.DeepEqual(pod, allowedPod) {
key := auditKeyPrefix + "/" + "validate-policy"
if err := a.AddAnnotation(key, pspName); err != nil {
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
}
return nil
}
// we didn't validate against any provider, reject the pod and give the errors for each attempt
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
return admission.NewForbidden(a, fmt.Errorf("PodSecurityPolicy: unable to validate pod: %v", validationErrs))
}
func shouldIgnore(a admission.Attributes) (bool, error) {
if a.GetResource().GroupResource() != api.Resource("pods") {
return true, nil
}
if len(a.GetSubresource()) != 0 {
return true, nil
}
// if we can't convert then fail closed since we've already checked that this is supposed to be a pod object.
// this shouldn't normally happen during admission but could happen if an integrator passes a versioned
// pod object rather than an internal object.
if _, ok := a.GetObject().(*api.Pod); !ok {
return false, admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// if this is an update, see if we are only updating the ownerRef/finalizers. Garbage collection does this
// and we should allow it in general, since you had the power to update and the power to delete.
// The worst that happens is that you delete something, but you aren't controlling the privileged object itself
if a.GetOperation() == admission.Update && rbacregistry.IsOnlyMutatingGCFields(a.GetObject(), a.GetOldObject(), apiequality.Semantic) {
return true, nil
}
return false, nil
}
// computeSecurityContext derives a valid security context while trying to avoid any changes to the given pod. I.e.
// if there is a matching policy with the same security context as given, it will be reused. If there is no
// matching policy the returned pod will be nil and the pspName empty. validatedPSPHint is the validated psp name
// saved in kubernetes.io/psp annotation. This psp is usually the one we are looking for.
func (p *Plugin) computeSecurityContext(ctx context.Context, a admission.Attributes, pod *api.Pod, specMutationAllowed bool, validatedPSPHint string) (*api.Pod, string, field.ErrorList, error) {
// get all constraints that are usable by the user
klog.V(4).Infof("getting pod security policies for pod %s (generate: %s)", pod.Name, pod.GenerateName)
var saInfo user.Info
if len(pod.Spec.ServiceAccountName) > 0 {
saInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "")
}
policies, err := p.lister.List(labels.Everything())
if err != nil {
return nil, "", nil, err
}
// if we have no policies and want to succeed then return. Otherwise we'll end up with no
// providers and fail with "unable to validate against any pod security policy" below.
if len(policies) == 0 && !p.failOnNoPolicies {
return pod, "", nil, nil
}
// sort policies by name to make order deterministic
// If mutation is not allowed and validatedPSPHint is provided, check the validated policy first.
sort.SliceStable(policies, func(i, j int) bool {
if !specMutationAllowed {
if policies[i].Name == validatedPSPHint {
return true
}
if policies[j].Name == validatedPSPHint {
return false
}
}
return strings.Compare(policies[i].Name, policies[j].Name) < 0
})
providers, errs := p.createProvidersFromPolicies(policies, pod.Namespace)
for _, err := range errs {
klog.V(4).Infof("provider creation error: %v", err)
}
if len(providers) == 0 {
return nil, "", nil, fmt.Errorf("no providers available to validate pod request")
}
var (
allowedMutatedPod *api.Pod
allowingMutatingPSP string
// Map of PSP name to associated validation errors.
validationErrs = map[string]field.ErrorList{}
)
for _, provider := range providers {
podCopy := pod.DeepCopy()
if errs := assignSecurityContext(provider, podCopy); len(errs) > 0 {
validationErrs[provider.GetPSPName()] = errs
continue
}
// the entire pod validated
mutated := !apiequality.Semantic.DeepEqual(pod, podCopy)
if mutated && !specMutationAllowed {
continue
}
if !isAuthorizedForPolicy(ctx, a.GetUserInfo(), saInfo, a.GetNamespace(), provider.GetPSPName(), p.authz) {
continue
}
switch {
case !mutated:
// if it validated without mutating anything, use this result
return podCopy, provider.GetPSPName(), nil, nil
case specMutationAllowed && allowedMutatedPod == nil:
// if mutation is allowed and this is the first PSP to allow the pod, remember it,
// but continue to see if another PSP allows without mutating
allowedMutatedPod = podCopy
allowingMutatingPSP = provider.GetPSPName()
}
}
if allowedMutatedPod != nil {
return allowedMutatedPod, allowingMutatingPSP, nil, nil
}
// Pod is rejected. Filter the validation errors to only include errors from authorized PSPs.
aggregate := field.ErrorList{}
for psp, errs := range validationErrs {
if isAuthorizedForPolicy(ctx, a.GetUserInfo(), saInfo, a.GetNamespace(), psp, p.authz) {
aggregate = append(aggregate, errs...)
}
}
return nil, "", aggregate, nil
}
// assignSecurityContext creates a security context for each container in the pod
// and validates that the sc falls within the psp constraints. All containers must validate against
// the same psp or is not considered valid.
func assignSecurityContext(provider psp.Provider, pod *api.Pod) field.ErrorList {
errs := field.ErrorList{}
if err := provider.MutatePod(pod); err != nil {
// TODO(tallclair): MutatePod should return a field.ErrorList
errs = append(errs, field.Invalid(field.NewPath(""), pod, err.Error()))
}
errs = append(errs, provider.ValidatePod(pod)...)
return errs
}
// createProvidersFromPolicies creates providers from the constraints supplied.
func (p *Plugin) createProvidersFromPolicies(psps []*policyv1beta1.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
var (
// collected providers
providers []psp.Provider
// collected errors to return
errs []error
)
for _, constraint := range psps {
provider, err := psp.NewSimpleProvider(constraint, namespace, p.strategyFactory)
if err != nil {
errs = append(errs, fmt.Errorf("error creating provider for PSP %s: %v", constraint.Name, err))
continue
}
providers = append(providers, provider)
}
return providers, errs
}
func isAuthorizedForPolicy(ctx context.Context, user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
// Check the service account first, as that is the more common use case.
return authorizedForPolicy(ctx, sa, namespace, policyName, authz) ||
authorizedForPolicy(ctx, user, namespace, policyName, authz)
}
// authorizedForPolicy returns true if info is authorized to perform the "use" verb on the policy resource.
// TODO: check against only the policy group when PSP will be completely moved out of the extensions
func authorizedForPolicy(ctx context.Context, info user.Info, namespace string, policyName string, authz authorizer.Authorizer) bool {
// Check against extensions API group for backward compatibility
return authorizedForPolicyInAPIGroup(ctx, info, namespace, policyName, policy.GroupName, authz) ||
authorizedForPolicyInAPIGroup(ctx, info, namespace, policyName, extensions.GroupName, authz)
}
// authorizedForPolicyInAPIGroup returns true if info is authorized to perform the "use" verb on the policy resource in the specified API group.
func authorizedForPolicyInAPIGroup(ctx context.Context, info user.Info, namespace, policyName, apiGroupName string, authz authorizer.Authorizer) bool {
if info == nil {
return false
}
attr := buildAttributes(info, namespace, policyName, apiGroupName)
decision, reason, err := authz.Authorize(ctx, attr)
if err != nil {
klog.V(5).Infof("cannot authorize for policy: %v,%v", reason, err)
}
return (decision == authorizer.DecisionAllow)
}
// buildAttributes builds an attributes record for a SAR based on the user info and policy.
func buildAttributes(info user.Info, namespace, policyName, apiGroupName string) authorizer.Attributes {
// check against the namespace that the pod is being created in to allow per-namespace PSP grants.
attr := authorizer.AttributesRecord{
User: info,
Verb: "use",
Namespace: namespace,
Name: policyName,
APIGroup: apiGroupName,
APIVersion: "*",
Resource: "podsecuritypolicies",
ResourceRequest: true,
}
return attr
}

View File

@ -420,17 +420,74 @@ run_deprecated_api_tests() {
set -o nounset
set -o errexit
create_and_use_new_namespace
kube::log::status "Testing deprecated APIs"
# Create deprecated CRD
kubectl "${kube_flags_with_token[@]:?}" create -f - << __EOF__
{
"kind": "CustomResourceDefinition",
"apiVersion": "apiextensions.k8s.io/v1",
"metadata": {
"name": "deprecated.example.com"
},
"spec": {
"group": "example.com",
"scope": "Namespaced",
"names": {
"plural": "deprecated",
"kind": "DeprecatedKind"
},
"versions": [
{
"name": "v1",
"served": true,
"storage": true,
"schema": {
"openAPIV3Schema": {
"x-kubernetes-preserve-unknown-fields": true,
"type": "object"
}
}
},
{
"name": "v1beta1",
"deprecated": true,
"served": true,
"storage": false,
"schema": {
"openAPIV3Schema": {
"x-kubernetes-preserve-unknown-fields": true,
"type": "object"
}
}
}
]
}
}
__EOF__
# Ensure the API server has recognized and started serving the associated CR API
local tries=5
for i in $(seq 1 $tries); do
local output
output=$(kubectl "${kube_flags[@]:?}" api-resources --api-group example.com -oname)
if kube::test::if_has_string "$output" deprecated.example.com; then
break
fi
echo "${i}: Waiting for CR API to be available"
sleep "$i"
done
# Test deprecated API request output
# TODO(liggitt): switch this to a custom deprecated resource once CRDs support marking versions as deprecated
output_message=$(kubectl get podsecuritypolicies.v1beta1.policy 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'PodSecurityPolicy is deprecated'
output_message=$(! kubectl get podsecuritypolicies.v1beta1.policy --warnings-as-errors 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'PodSecurityPolicy is deprecated'
output_message=$(kubectl get deprecated.v1beta1.example.com 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'example.com/v1beta1 DeprecatedKind is deprecated'
output_message=$(! kubectl get deprecated.v1beta1.example.com --warnings-as-errors 2>&1 "${kube_flags[@]}")
kube::test::if_has_string "${output_message}" 'example.com/v1beta1 DeprecatedKind is deprecated'
kube::test::if_has_string "${output_message}" 'error: 1 warning received'
# Delete deprecated CRD
kubectl delete "${kube_flags[@]}" crd deprecated.example.com
set +o nounset
set +o errexit
}

View File

@ -90,7 +90,6 @@ nodes="nodes"
persistentvolumeclaims="persistentvolumeclaims"
persistentvolumes="persistentvolumes"
pods="pods"
podsecuritypolicies="podsecuritypolicies"
podtemplates="podtemplates"
replicasets="replicasets"
replicationcontrollers="replicationcontrollers"
@ -934,7 +933,7 @@ runTests() {
# Kubectl deprecated APIs #
############################
if kube::test::if_supports_resource "${podsecuritypolicies}" ; then
if kube::test::if_supports_resource "${customresourcedefinitions}" ; then
record_command run_deprecated_api_tests
fi

View File

@ -1,367 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth
import (
"context"
"fmt"
v1 "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
"k8s.io/kubernetes/test/e2e/framework"
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
imageutils "k8s.io/kubernetes/test/utils/image"
admissionapi "k8s.io/pod-security-admission/api"
utilpointer "k8s.io/utils/pointer"
"github.com/onsi/ginkgo"
)
const nobodyUser = int64(65534)
var _ = SIGDescribe("PodSecurityPolicy [Feature:PodSecurityPolicy]", func() {
f := framework.NewDefaultFramework("podsecuritypolicy")
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged
f.SkipPrivilegedPSPBinding = true
// Client that will impersonate the default service account, in order to run
// with reduced privileges.
var c clientset.Interface
var ns string // Test namespace, for convenience
ginkgo.BeforeEach(func() {
if !framework.IsPodSecurityPolicyEnabled(f.ClientSet) {
framework.Failf("PodSecurityPolicy not enabled")
return
}
if !e2eauth.IsRBACEnabled(f.ClientSet.RbacV1()) {
e2eskipper.Skipf("RBAC not enabled")
}
ns = f.Namespace.Name
ginkgo.By("Creating a kubernetes client that impersonates the default service account")
config, err := framework.LoadConfig()
framework.ExpectNoError(err)
config.Impersonate = restclient.ImpersonationConfig{
UserName: serviceaccount.MakeUsername(ns, "default"),
Groups: serviceaccount.MakeGroupNames(ns),
}
c, err = clientset.NewForConfig(config)
framework.ExpectNoError(err)
ginkgo.By("Binding the edit role to the default SA")
err = e2eauth.BindClusterRole(f.ClientSet.RbacV1(), "edit", ns,
rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Namespace: ns, Name: "default"})
framework.ExpectNoError(err)
})
ginkgo.It("should forbid pod creation when no PSP is available", func() {
ginkgo.By("Running a restricted pod")
_, err := c.CoreV1().Pods(ns).Create(context.TODO(), restrictedPod("restricted"), metav1.CreateOptions{})
expectForbidden(err)
})
ginkgo.It("should enforce the restricted policy.PodSecurityPolicy", func() {
ginkgo.By("Creating & Binding a restricted policy for the test service account")
_, cleanup := createAndBindPSP(f, restrictedPSP("restrictive"))
defer cleanup()
ginkgo.By("Running a restricted pod")
pod, err := c.CoreV1().Pods(ns).Create(context.TODO(), restrictedPod("allowed"), metav1.CreateOptions{})
framework.ExpectNoError(err)
framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(c, pod.Name, pod.Namespace))
testPrivilegedPods(func(pod *v1.Pod) {
_, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
expectForbidden(err)
})
})
ginkgo.It("should allow pods under the privileged policy.PodSecurityPolicy", func() {
ginkgo.By("Creating & Binding a privileged policy for the test service account")
// Ensure that the permissive policy is used even in the presence of the restricted policy.
_, cleanup := createAndBindPSP(f, restrictedPSP("restrictive"))
defer cleanup()
expectedPSP, cleanup := createAndBindPSP(f, privilegedPSP("permissive"))
defer cleanup()
testPrivilegedPods(func(pod *v1.Pod) {
p, err := c.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
framework.ExpectNoError(err)
framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(c, p.Name, p.Namespace))
// Verify expected PSP was used.
p, err = c.CoreV1().Pods(ns).Get(context.TODO(), p.Name, metav1.GetOptions{})
framework.ExpectNoError(err)
validated, found := p.Annotations[psputil.ValidatedPSPAnnotation]
framework.ExpectEqual(found, true, "PSP annotation not found")
framework.ExpectEqual(validated, expectedPSP.Name, "Unexpected validated PSP")
})
})
})
func expectForbidden(err error) {
framework.ExpectError(err, "should be forbidden")
framework.ExpectEqual(apierrors.IsForbidden(err), true, "should be forbidden error")
}
func testPrivilegedPods(tester func(pod *v1.Pod)) {
ginkgo.By("Running a privileged pod", func() {
privileged := restrictedPod("privileged")
privileged.Spec.Containers[0].SecurityContext.Privileged = boolPtr(true)
privileged.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation = nil
tester(privileged)
})
ginkgo.By("Running a HostPath pod", func() {
hostpath := restrictedPod("hostpath")
hostpath.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{{
Name: "hp",
MountPath: "/hp",
}}
hostpath.Spec.Volumes = []v1.Volume{{
Name: "hp",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "/tmp"},
},
}}
tester(hostpath)
})
ginkgo.By("Running a HostNetwork pod", func() {
hostnet := restrictedPod("hostnet")
hostnet.Spec.HostNetwork = true
tester(hostnet)
})
ginkgo.By("Running a HostPID pod", func() {
hostpid := restrictedPod("hostpid")
hostpid.Spec.HostPID = true
tester(hostpid)
})
ginkgo.By("Running a HostIPC pod", func() {
hostipc := restrictedPod("hostipc")
hostipc.Spec.HostIPC = true
tester(hostipc)
})
ginkgo.By("Running an unconfined Seccomp pod", func() {
unconfined := restrictedPod("seccomp")
unconfined.Annotations[v1.SeccompPodAnnotationKey] = "unconfined"
tester(unconfined)
})
ginkgo.By("Running a SYS_ADMIN pod", func() {
sysadmin := restrictedPod("sysadmin")
sysadmin.Spec.Containers[0].SecurityContext.Capabilities = &v1.Capabilities{
Add: []v1.Capability{"SYS_ADMIN"},
}
sysadmin.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation = nil
tester(sysadmin)
})
ginkgo.By("Running a RunAsGroup pod", func() {
sysadmin := restrictedPod("runasgroup")
gid := int64(0)
sysadmin.Spec.Containers[0].SecurityContext.RunAsGroup = &gid
tester(sysadmin)
})
ginkgo.By("Running a RunAsUser pod", func() {
sysadmin := restrictedPod("runasuser")
uid := int64(0)
sysadmin.Spec.Containers[0].SecurityContext.RunAsUser = &uid
tester(sysadmin)
})
}
// createAndBindPSP creates a PSP in the policy API group.
func createAndBindPSP(f *framework.Framework, pspTemplate *policyv1beta1.PodSecurityPolicy) (psp *policyv1beta1.PodSecurityPolicy, cleanup func()) {
// Create the PodSecurityPolicy object.
psp = pspTemplate.DeepCopy()
// Add the namespace to the name to ensure uniqueness and tie it to the namespace.
ns := f.Namespace.Name
name := fmt.Sprintf("%s-%s", ns, psp.Name)
psp.Name = name
psp, err := f.ClientSet.PolicyV1beta1().PodSecurityPolicies().Create(context.TODO(), psp, metav1.CreateOptions{})
framework.ExpectNoError(err, "Failed to create PSP")
// Create the Role to bind it to the namespace.
_, err = f.ClientSet.RbacV1().Roles(ns).Create(context.TODO(), &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Rules: []rbacv1.PolicyRule{{
APIGroups: []string{"policy"},
Resources: []string{"podsecuritypolicies"},
ResourceNames: []string{name},
Verbs: []string{"use"},
}},
}, metav1.CreateOptions{})
framework.ExpectNoError(err, "Failed to create PSP role")
// Bind the role to the namespace.
err = e2eauth.BindRoleInNamespace(f.ClientSet.RbacV1(), name, ns, rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Namespace: ns,
Name: "default",
})
framework.ExpectNoError(err)
framework.ExpectNoError(e2eauth.WaitForNamedAuthorizationUpdate(f.ClientSet.AuthorizationV1(),
serviceaccount.MakeUsername(ns, "default"), ns, "use", name,
schema.GroupResource{Group: "policy", Resource: "podsecuritypolicies"}, true))
return psp, func() {
// Cleanup non-namespaced PSP object.
f.ClientSet.PolicyV1beta1().PodSecurityPolicies().Delete(context.TODO(), name, metav1.DeleteOptions{})
}
}
func restrictedPod(name string) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{
v1.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault,
v1.AppArmorBetaContainerAnnotationKeyPrefix + "pause": v1.AppArmorBetaProfileRuntimeDefault,
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{{
Name: "pause",
Image: imageutils.GetPauseImageName(),
SecurityContext: &v1.SecurityContext{
AllowPrivilegeEscalation: boolPtr(false),
RunAsUser: utilpointer.Int64Ptr(nobodyUser),
RunAsGroup: utilpointer.Int64Ptr(nobodyUser),
},
}},
},
}
}
// privilegedPSPInPolicy creates a PodSecurityPolicy (in the "policy" API Group) that allows everything.
func privilegedPSP(name string) *policyv1beta1.PodSecurityPolicy {
return &policyv1beta1.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny},
},
Spec: policyv1beta1.PodSecurityPolicySpec{
Privileged: true,
AllowPrivilegeEscalation: utilpointer.BoolPtr(true),
AllowedCapabilities: []v1.Capability{"*"},
Volumes: []policyv1beta1.FSType{policyv1beta1.All},
HostNetwork: true,
HostPorts: []policyv1beta1.HostPortRange{{Min: 0, Max: 65535}},
HostIPC: true,
HostPID: true,
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
Rule: policyv1beta1.RunAsUserStrategyRunAsAny,
},
RunAsGroup: &policyv1beta1.RunAsGroupStrategyOptions{
Rule: policyv1beta1.RunAsGroupStrategyRunAsAny,
},
SELinux: policyv1beta1.SELinuxStrategyOptions{
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
},
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
},
FSGroup: policyv1beta1.FSGroupStrategyOptions{
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
},
ReadOnlyRootFilesystem: false,
},
}
}
// restrictedPSPInPolicy creates a PodSecurityPolicy (in the "policy" API Group) that is most strict.
func restrictedPSP(name string) *policyv1beta1.PodSecurityPolicy {
return &policyv1beta1.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: v1.SeccompProfileRuntimeDefault,
seccomp.DefaultProfileAnnotationKey: v1.SeccompProfileRuntimeDefault,
v1.AppArmorBetaAllowedProfilesAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
v1.AppArmorBetaDefaultProfileAnnotationKey: v1.AppArmorBetaProfileRuntimeDefault,
},
},
Spec: policyv1beta1.PodSecurityPolicySpec{
Privileged: false,
AllowPrivilegeEscalation: utilpointer.BoolPtr(false),
RequiredDropCapabilities: []v1.Capability{
"AUDIT_WRITE",
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"FSETID",
"KILL",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SYS_CHROOT",
},
Volumes: []policyv1beta1.FSType{
policyv1beta1.ConfigMap,
policyv1beta1.EmptyDir,
policyv1beta1.PersistentVolumeClaim,
"projected",
policyv1beta1.Secret,
},
HostNetwork: false,
HostIPC: false,
HostPID: false,
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
Rule: policyv1beta1.RunAsUserStrategyMustRunAsNonRoot,
},
RunAsGroup: &policyv1beta1.RunAsGroupStrategyOptions{
Rule: policyv1beta1.RunAsGroupStrategyMustRunAs,
Ranges: []policyv1beta1.IDRange{
{Min: nobodyUser, Max: nobodyUser}},
},
SELinux: policyv1beta1.SELinuxStrategyOptions{
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
},
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
},
FSGroup: policyv1beta1.FSGroupStrategyOptions{
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
},
ReadOnlyRootFilesystem: false,
},
}
}
func boolPtr(b bool) *bool {
return &b
}

View File

@ -83,7 +83,6 @@ type Framework struct {
Namespace *v1.Namespace // Every test has at least one namespace unless creation is skipped
namespacesToDelete []*v1.Namespace // Some tests have more than one.
NamespaceDeletionTimeout time.Duration
SkipPrivilegedPSPBinding bool // Whether to skip creating a binding to the privileged PSP in the test namespace
NamespacePodSecurityEnforceLevel admissionapi.Level // The pod security enforcement level for namespaces to be applied.
gatherer *ContainerResourceGatherer
@ -545,10 +544,6 @@ func (f *Framework) CreateNamespace(baseName string, labels map[string]string) (
// fail to create serviceAccount in it.
f.AddNamespacesToDelete(ns)
if err == nil && !f.SkipPrivilegedPSPBinding {
CreatePrivilegedPSPBinding(f.ClientSet, ns.Name)
}
return ns, err
}

View File

@ -1,192 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package framework
import (
"context"
"fmt"
"strings"
"sync"
v1 "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
clientset "k8s.io/client-go/kubernetes"
imageutils "k8s.io/kubernetes/test/utils/image"
"github.com/onsi/ginkgo"
// TODO: Remove the following imports (ref: https://github.com/kubernetes/kubernetes/issues/81245)
e2eauth "k8s.io/kubernetes/test/e2e/framework/auth"
)
const (
podSecurityPolicyPrivileged = "e2e-test-privileged-psp"
// allowAny is the wildcard used to allow any profile.
allowAny = "*"
// allowedProfilesAnnotationKey specifies the allowed seccomp profiles.
allowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames"
)
var (
isPSPEnabledOnce sync.Once
isPSPEnabled bool
)
// privilegedPSP creates a PodSecurityPolicy that allows everything.
func privilegedPSP(name string) *policyv1beta1.PodSecurityPolicy {
allowPrivilegeEscalation := true
return &policyv1beta1.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{allowedProfilesAnnotationKey: allowAny},
},
Spec: policyv1beta1.PodSecurityPolicySpec{
Privileged: true,
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
AllowedCapabilities: []v1.Capability{"*"},
Volumes: []policyv1beta1.FSType{policyv1beta1.All},
HostNetwork: true,
HostPorts: []policyv1beta1.HostPortRange{{Min: 0, Max: 65535}},
HostIPC: true,
HostPID: true,
RunAsUser: policyv1beta1.RunAsUserStrategyOptions{
Rule: policyv1beta1.RunAsUserStrategyRunAsAny,
},
SELinux: policyv1beta1.SELinuxStrategyOptions{
Rule: policyv1beta1.SELinuxStrategyRunAsAny,
},
SupplementalGroups: policyv1beta1.SupplementalGroupsStrategyOptions{
Rule: policyv1beta1.SupplementalGroupsStrategyRunAsAny,
},
FSGroup: policyv1beta1.FSGroupStrategyOptions{
Rule: policyv1beta1.FSGroupStrategyRunAsAny,
},
ReadOnlyRootFilesystem: false,
AllowedUnsafeSysctls: []string{"*"},
},
}
}
// IsPodSecurityPolicyEnabled returns true if PodSecurityPolicy is enabled. Otherwise false.
func IsPodSecurityPolicyEnabled(kubeClient clientset.Interface) bool {
isPSPEnabledOnce.Do(func() {
psps, err := kubeClient.PolicyV1beta1().PodSecurityPolicies().List(context.TODO(), metav1.ListOptions{})
if err != nil {
Logf("Error listing PodSecurityPolicies; assuming PodSecurityPolicy is disabled: %v", err)
return
}
if psps == nil || len(psps.Items) == 0 {
Logf("No PodSecurityPolicies found; assuming PodSecurityPolicy is disabled.")
return
}
Logf("Found PodSecurityPolicies; testing pod creation to see if PodSecurityPolicy is enabled")
testPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{GenerateName: "psp-test-pod-"},
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "test", Image: imageutils.GetPauseImageName()}}},
}
dryRunPod, err := kubeClient.CoreV1().Pods("kube-system").Create(context.TODO(), testPod, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
if err != nil {
if strings.Contains(err.Error(), "PodSecurityPolicy") {
Logf("PodSecurityPolicy error creating dryrun pod; assuming PodSecurityPolicy is enabled: %v", err)
isPSPEnabled = true
} else {
Logf("Error creating dryrun pod; assuming PodSecurityPolicy is disabled: %v", err)
}
return
}
pspAnnotation, pspAnnotationExists := dryRunPod.Annotations["kubernetes.io/psp"]
if !pspAnnotationExists {
Logf("No PSP annotation exists on dry run pod; assuming PodSecurityPolicy is disabled")
return
}
Logf("PSP annotation exists on dry run pod: %q; assuming PodSecurityPolicy is enabled", pspAnnotation)
isPSPEnabled = true
})
return isPSPEnabled
}
var (
privilegedPSPOnce sync.Once
)
// CreatePrivilegedPSPBinding creates the privileged PSP & role
func CreatePrivilegedPSPBinding(kubeClient clientset.Interface, namespace string) {
if !IsPodSecurityPolicyEnabled(kubeClient) {
return
}
// Create the privileged PSP & role
privilegedPSPOnce.Do(func() {
_, err := kubeClient.PolicyV1beta1().PodSecurityPolicies().Get(context.TODO(), podSecurityPolicyPrivileged, metav1.GetOptions{})
if !apierrors.IsNotFound(err) {
// Privileged PSP was already created.
ExpectNoError(err, "Failed to get PodSecurityPolicy %s", podSecurityPolicyPrivileged)
return
}
psp := privilegedPSP(podSecurityPolicyPrivileged)
_, err = kubeClient.PolicyV1beta1().PodSecurityPolicies().Create(context.TODO(), psp, metav1.CreateOptions{})
if !apierrors.IsAlreadyExists(err) {
ExpectNoError(err, "Failed to create PSP %s", podSecurityPolicyPrivileged)
}
if e2eauth.IsRBACEnabled(kubeClient.RbacV1()) {
// Create the Role to bind it to the namespace.
_, err = kubeClient.RbacV1().ClusterRoles().Create(context.TODO(), &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: podSecurityPolicyPrivileged},
Rules: []rbacv1.PolicyRule{{
APIGroups: []string{"extensions"},
Resources: []string{"podsecuritypolicies"},
ResourceNames: []string{podSecurityPolicyPrivileged},
Verbs: []string{"use"},
}},
}, metav1.CreateOptions{})
if !apierrors.IsAlreadyExists(err) {
ExpectNoError(err, "Failed to create PSP role")
}
}
})
if e2eauth.IsRBACEnabled(kubeClient.RbacV1()) {
ginkgo.By(fmt.Sprintf("Binding the %s PodSecurityPolicy to the default service account in %s",
podSecurityPolicyPrivileged, namespace))
err := e2eauth.BindClusterRoleInNamespace(kubeClient.RbacV1(),
podSecurityPolicyPrivileged,
namespace,
rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Namespace: namespace,
Name: "default",
},
rbacv1.Subject{
Kind: rbacv1.GroupKind,
APIGroup: rbacv1.GroupName,
Name: "system:serviceaccounts:" + namespace,
},
)
ExpectNoError(err)
ExpectNoError(e2eauth.WaitForNamedAuthorizationUpdate(kubeClient.AuthorizationV1(),
serviceaccount.MakeUsername(namespace, "default"), namespace, "use", podSecurityPolicyPrivileged,
schema.GroupResource{Group: "extensions", Resource: "podsecuritypolicies"}, true))
}
}

View File

@ -32,14 +32,12 @@ import (
"github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/test/e2e/framework"
@ -65,11 +63,6 @@ const (
maxValidSize string = "10Ei"
)
const (
// ClusterRole name for e2e test Priveledged Pod Security Policy User
podSecurityPolicyPrivilegedClusterRoleName = "e2e-test-privileged-psp"
)
// VerifyFSGroupInPod verifies that the passed in filePath contains the expectedFSGroup
func VerifyFSGroupInPod(f *framework.Framework, filePath, expectedFSGroup string, pod *v1.Pod) {
cmd := fmt.Sprintf("ls -l %s", filePath)
@ -417,54 +410,6 @@ func StartExternalProvisioner(c clientset.Interface, ns string, externalPluginNa
return pod
}
// PrivilegedTestPSPClusterRoleBinding test Pod Security Policy Role bindings
func PrivilegedTestPSPClusterRoleBinding(client clientset.Interface,
namespace string,
teardown bool,
saNames []string) {
bindingString := "Binding"
if teardown {
bindingString = "Unbinding"
}
roleBindingClient := client.RbacV1().RoleBindings(namespace)
for _, saName := range saNames {
ginkgo.By(fmt.Sprintf("%v priviledged Pod Security Policy to the service account %s", bindingString, saName))
binding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "psp-" + saName,
Namespace: namespace,
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.ServiceAccountKind,
Name: saName,
Namespace: namespace,
},
},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
Name: podSecurityPolicyPrivilegedClusterRoleName,
APIGroup: "rbac.authorization.k8s.io",
},
}
roleBindingClient.Delete(context.TODO(), binding.GetName(), metav1.DeleteOptions{})
err := wait.Poll(2*time.Second, 2*time.Minute, func() (bool, error) {
_, err := roleBindingClient.Get(context.TODO(), binding.GetName(), metav1.GetOptions{})
return apierrors.IsNotFound(err), nil
})
framework.ExpectNoError(err, "Timed out waiting for RBAC binding %s deletion: %v", binding.GetName(), err)
if teardown {
continue
}
_, err = roleBindingClient.Create(context.TODO(), binding, metav1.CreateOptions{})
framework.ExpectNoError(err, "Failed to create %s role binding: %v", binding.GetName(), err)
}
}
func isSudoPresent(nodeIP string, provider string) bool {
framework.Logf("Checking if sudo command is present")
sshResult, err := e2essh.SSH("sudo --version", nodeIP, provider)