From ed80c2b940b842a5cd28de123154de10bcdb0948 Mon Sep 17 00:00:00 2001 From: Paul Weil Date: Tue, 15 Sep 2015 12:43:59 -0400 Subject: [PATCH] pid mode --- api/swagger-spec/v1.json | 6 +- cluster/aws/config-default.sh | 2 +- cluster/aws/config-test.sh | 2 +- cluster/azure/config-default.sh | 2 +- cluster/centos/master/scripts/apiserver.sh | 2 +- cluster/gce/config-default.sh | 2 +- cluster/gce/config-test.sh | 2 +- cluster/mesos/docker/docker-compose.yml | 2 +- cluster/ubuntu/config-default.sh | 2 +- cluster/vagrant/config-default.sh | 2 +- cmd/kube-apiserver/app/plugins.go | 2 +- cmd/kube-apiserver/app/server.go | 1 + cmd/kubelet/app/server.go | 11 ++ docs/admin/admission-controllers.md | 19 +- .../high-availability/kube-apiserver.yaml | 2 +- docs/admin/kube-apiserver.md | 2 +- docs/admin/kubelet.md | 1 + .../coreos/cloud-configs/master.yaml | 2 +- hack/local-up-cluster.sh | 2 +- hack/verify-flags/known-flags.txt | 1 + pkg/api/deep_copy_generated.go | 1 + pkg/api/types.go | 5 +- pkg/api/v1/conversion.go | 2 + pkg/api/v1/deep_copy_generated.go | 1 + pkg/api/v1/types.go | 5 +- pkg/api/v1/types_swagger_doc_generated.go | 3 +- pkg/apis/experimental/deep_copy_generated.go | 1 + pkg/apis/experimental/v1/conversion.go | 2 + .../experimental/v1/deep_copy_generated.go | 1 + pkg/capabilities/capabilities.go | 4 + pkg/kubelet/dockertools/manager.go | 21 +- pkg/kubelet/dockertools/manager_test.go | 17 ++ pkg/kubelet/rkt/rkt.go | 2 + pkg/kubelet/util.go | 24 +++ .../exec/{denyprivileged => }/admission.go | 67 +++++-- plugin/pkg/admission/exec/admission_test.go | 183 ++++++++++++++++++ .../exec/denyprivileged/admission_test.go | 105 ---------- 37 files changed, 366 insertions(+), 145 deletions(-) rename plugin/pkg/admission/exec/{denyprivileged => }/admission.go (52%) create mode 100644 plugin/pkg/admission/exec/admission_test.go delete mode 100644 plugin/pkg/admission/exec/denyprivileged/admission_test.go diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index cfadfdf252e..1f7963887a6 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -12490,7 +12490,11 @@ }, "hostNetwork": { "type": "boolean", - "description": "Host networking requested for this pod. Uses the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false." + "description": "Host networking requested for this pod. Use the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false." + }, + "hostPID": { + "type": "boolean", + "description": "Use the host's pid namespace. Optional: Default to false." }, "imagePullSecrets": { "type": "array", diff --git a/cluster/aws/config-default.sh b/cluster/aws/config-default.sh index aca9cb13aea..6def372c67a 100644 --- a/cluster/aws/config-default.sh +++ b/cluster/aws/config-default.sh @@ -87,7 +87,7 @@ DNS_REPLICAS=1 ENABLE_CLUSTER_UI="${KUBE_ENABLE_CLUSTER_UI:-true}" # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # Optional: Enable/disable public IP assignment for minions. # Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes! diff --git a/cluster/aws/config-test.sh b/cluster/aws/config-test.sh index 9bce781d45b..ba08038c91a 100755 --- a/cluster/aws/config-test.sh +++ b/cluster/aws/config-test.sh @@ -83,7 +83,7 @@ DNS_REPLICAS=1 ENABLE_CLUSTER_UI="${KUBE_ENABLE_CLUSTER_UI:-true}" # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # Optional: Enable/disable public IP assignment for minions. # Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes! diff --git a/cluster/azure/config-default.sh b/cluster/azure/config-default.sh index aa86c0c6790..51cde0892d5 100644 --- a/cluster/azure/config-default.sh +++ b/cluster/azure/config-default.sh @@ -55,4 +55,4 @@ ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-influxdb}" ENABLE_CLUSTER_UI="${KUBE_ENABLE_CLUSTER_UI:-true}" # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota diff --git a/cluster/centos/master/scripts/apiserver.sh b/cluster/centos/master/scripts/apiserver.sh index 53c4e5fcce3..f56dd6431b4 100755 --- a/cluster/centos/master/scripts/apiserver.sh +++ b/cluster/centos/master/scripts/apiserver.sh @@ -51,7 +51,7 @@ KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=${SERVICE_CLUSTER_IP_RANGE}" # to do admission control of resources into cluster. # Comma-delimited list of: # LimitRanger, AlwaysDeny, SecurityContextDeny, NamespaceExists, -# NamespaceLifecycle, NamespaceAutoProvision, DenyExecOnPrivileged, +# NamespaceLifecycle, NamespaceAutoProvision, DenyEscalatingExec, # AlwaysAdmit, ServiceAccount, ResourceQuota #KUBE_ADMISSION_CONTROL="--admission-control=\"${ADMISSION_CONTROL}\"" diff --git a/cluster/gce/config-default.sh b/cluster/gce/config-default.sh index 6aae9232ebb..bad0776c227 100755 --- a/cluster/gce/config-default.sh +++ b/cluster/gce/config-default.sh @@ -104,7 +104,7 @@ if [[ "${ENABLE_HORIZONTAL_POD_AUTOSCALER}" == "true" ]]; then fi # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # Optional: if set to true kube-up will automatically check for existing resources and clean them up. KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false} diff --git a/cluster/gce/config-test.sh b/cluster/gce/config-test.sh index 1ca394370d2..37414f86ce4 100755 --- a/cluster/gce/config-test.sh +++ b/cluster/gce/config-test.sh @@ -112,7 +112,7 @@ if [[ "${ENABLE_HORIZONTAL_POD_AUTOSCALER}" == "true" ]]; then ENABLE_EXPERIMENTAL_API=true fi -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # Optional: if set to true kube-up will automatically check for existing resources and clean them up. KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false} diff --git a/cluster/mesos/docker/docker-compose.yml b/cluster/mesos/docker/docker-compose.yml index 94dd95d8d1a..ec337c53d7b 100644 --- a/cluster/mesos/docker/docker-compose.yml +++ b/cluster/mesos/docker/docker-compose.yml @@ -89,7 +89,7 @@ apiserver: --external-hostname=apiserver --etcd-servers=http://etcd:4001 --port=8888 - --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota + --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota --authorization-mode=AlwaysAllow --token-auth-file=/var/run/kubernetes/auth/token-users --basic-auth-file=/var/run/kubernetes/auth/basic-users diff --git a/cluster/ubuntu/config-default.sh b/cluster/ubuntu/config-default.sh index a6052108369..7be6ba9a706 100755 --- a/cluster/ubuntu/config-default.sh +++ b/cluster/ubuntu/config-default.sh @@ -35,7 +35,7 @@ export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24} # f export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16} # Admission Controllers to invoke prior to persisting objects in cluster -export ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,SecurityContextDeny +export ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,ResourceQuota,DenyEscalatingExec,SecurityContextDeny SERVICE_NODE_PORT_RANGE=${SERVICE_NODE_PORT_RANGE:-"30000-32767"} diff --git a/cluster/vagrant/config-default.sh b/cluster/vagrant/config-default.sh index 1f3b686200f..c3d5fb42f5e 100755 --- a/cluster/vagrant/config-default.sh +++ b/cluster/vagrant/config-default.sh @@ -53,7 +53,7 @@ MASTER_USER=vagrant MASTER_PASSWD=vagrant # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # Optional: Enable node logging. ENABLE_NODE_LOGGING=false diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index 20f690d4168..f998568b964 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -26,7 +26,7 @@ import ( // Admission policies _ "k8s.io/kubernetes/plugin/pkg/admission/admit" _ "k8s.io/kubernetes/plugin/pkg/admission/deny" - _ "k8s.io/kubernetes/plugin/pkg/admission/exec/denyprivileged" + _ "k8s.io/kubernetes/plugin/pkg/admission/exec" _ "k8s.io/kubernetes/plugin/pkg/admission/initialresources" _ "k8s.io/kubernetes/plugin/pkg/admission/limitranger" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision" diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index c421167bc02..4fef70e7b6f 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -293,6 +293,7 @@ func (s *APIServer) Run(_ []string) error { // TODO(vmarmol): Implement support for HostNetworkSources. PrivilegedSources: capabilities.PrivilegedSources{ HostNetworkSources: []string{}, + HostPIDSources: []string{}, }, PerConnectionBandwidthLimitBytesPerSec: s.MaxConnectionBytesPerSec, }) diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index c90453b6c52..1256b384211 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -93,6 +93,7 @@ type KubeletServer struct { HealthzPort int HostnameOverride string HostNetworkSources string + HostPIDSources string HTTPCheckFrequency time.Duration ImageGCHighThresholdPercent int ImageGCLowThresholdPercent int @@ -170,6 +171,7 @@ func NewKubeletServer() *KubeletServer { HealthzBindAddress: net.ParseIP("127.0.0.1"), HealthzPort: 10248, HostNetworkSources: kubelet.FileSource, + HostPIDSources: kubelet.FileSource, HTTPCheckFrequency: 20 * time.Second, ImageGCHighThresholdPercent: 90, ImageGCLowThresholdPercent: 80, @@ -222,6 +224,7 @@ func (s *KubeletServer) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.RootDirectory, "root-dir", s.RootDirectory, "Directory path for managing kubelet files (volume mounts,etc).") fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow containers to request privileged mode. [default=false]") fs.StringVar(&s.HostNetworkSources, "host-network-sources", s.HostNetworkSources, "Comma-separated list of sources from which the Kubelet allows pods to use of host network. For all sources use \"*\" [default=\"file\"]") + fs.StringVar(&s.HostPIDSources, "host-pid-sources", s.HostPIDSources, "Comma-separated list of sources from which the Kubelet allows pods to use the host pid namespace. For all sources use \"*\" [default=\"file\"]") fs.Float64Var(&s.RegistryPullQPS, "registry-qps", s.RegistryPullQPS, "If > 0, limit registry pull QPS to this value. If 0, unlimited. [default=0.0]") fs.IntVar(&s.RegistryBurst, "registry-burst", s.RegistryBurst, "Maximum size of a bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0") fs.Float32Var(&s.EventRecordQPS, "event-qps", s.EventRecordQPS, "If > 0, limit event creations per second to this value. If 0, unlimited. [default=0.0]") @@ -278,6 +281,11 @@ func (s *KubeletServer) KubeletConfig() (*KubeletConfig, error) { return nil, err } + hostPIDSources, err := kubelet.GetValidatedSources(strings.Split(s.HostPIDSources, ",")) + if err != nil { + return nil, err + } + mounter := mount.New() if s.Containerized { glog.V(2).Info("Running kubelet in containerized mode (experimental)") @@ -342,6 +350,7 @@ func (s *KubeletServer) KubeletConfig() (*KubeletConfig, error) { FileCheckFrequency: s.FileCheckFrequency, HostnameOverride: s.HostnameOverride, HostNetworkSources: hostNetworkSources, + HostPIDSources: hostPIDSources, HTTPCheckFrequency: s.HTTPCheckFrequency, ImageGCPolicy: imageGCPolicy, KubeClient: nil, @@ -673,6 +682,7 @@ func RunKubelet(kcfg *KubeletConfig, builder KubeletBuilder) error { privilegedSources := capabilities.PrivilegedSources{ HostNetworkSources: kcfg.HostNetworkSources, + HostPIDSources: kcfg.HostPIDSources, } capabilities.Setup(kcfg.AllowPrivileged, privilegedSources, 0) @@ -766,6 +776,7 @@ type KubeletConfig struct { Hostname string HostnameOverride string HostNetworkSources []string + HostPIDSources []string HTTPCheckFrequency time.Duration ImageGCPolicy kubelet.ImageGCPolicy KubeClient *client.Client diff --git a/docs/admin/admission-controllers.md b/docs/admin/admission-controllers.md index 9a4bfe41e57..f780e80e8d7 100644 --- a/docs/admin/admission-controllers.md +++ b/docs/admin/admission-controllers.md @@ -43,7 +43,8 @@ Documentation for other releases can be found at - [What does each plug-in do?](#what-does-each-plug-in-do) - [AlwaysAdmit](#alwaysadmit) - [AlwaysDeny](#alwaysdeny) - - [DenyExecOnPrivileged](#denyexeconprivileged) + - [DenyExecOnPrivileged (deprecated)](#denyexeconprivileged-deprecated) + - [DenyEscalatingExec](#denyescalatingexec) - [ServiceAccount](#serviceaccount) - [SecurityContextDeny](#securitycontextdeny) - [ResourceQuota](#resourcequota) @@ -92,13 +93,25 @@ Use this plugin by itself to pass-through all requests. Rejects all requests. Used for testing. -### DenyExecOnPrivileged +### DenyExecOnPrivileged (deprecated) This plug-in will intercept all requests to exec a command in a pod if that pod has a privileged container. If your cluster supports privileged containers, and you want to restrict the ability of end-users to exec commands in those containers, we strongly encourage enabling this plug-in. +This functionality has been merged into [DenyEscalatingExec](#denyescalatingexec). + +### DenyEscalatingExec + +This plug-in will deny exec and attach commands to pods that run with escalated privileges that +allow host access. This includes pods that run as privileged, have access to the host IPC namespace, and +have access to the host PID namespace. + +If your cluster supports containers that run with escalated privileges, and you want to +restrict the ability of end-users to exec commands in those containers, we strongly encourage +enabling this plug-in. + ### ServiceAccount This plug-in implements automation for [serviceAccounts](../user-guide/service-accounts.md). @@ -159,7 +172,7 @@ Yes. For Kubernetes 1.0, we strongly recommend running the following set of admission control plug-ins (order matters): ``` ---admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota +--admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota ``` diff --git a/docs/admin/high-availability/kube-apiserver.yaml b/docs/admin/high-availability/kube-apiserver.yaml index 33d5cff5cdc..68b10a4376e 100644 --- a/docs/admin/high-availability/kube-apiserver.yaml +++ b/docs/admin/high-availability/kube-apiserver.yaml @@ -11,7 +11,7 @@ spec: - /bin/sh - -c - /usr/local/bin/kube-apiserver --address=127.0.0.1 --etcd-servers=http://127.0.0.1:4001 - --cloud-provider=gce --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota + --cloud-provider=gce --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota --service-cluster-ip-range=10.0.0.0/16 --client-ca-file=/srv/kubernetes/ca.crt --basic-auth-file=/srv/kubernetes/basic_auth.csv --cluster-name=e2e-test-bburns --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key diff --git a/docs/admin/kube-apiserver.md b/docs/admin/kube-apiserver.md index 342de9490b6..7d70906dbd8 100644 --- a/docs/admin/kube-apiserver.md +++ b/docs/admin/kube-apiserver.md @@ -48,7 +48,7 @@ cluster's shared state through which all other components interact. ``` --address=: DEPRECATED: see --insecure-bind-address instead - --admission-control="": Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: AlwaysAdmit, AlwaysDeny, DenyExecOnPrivileged, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, ResourceQuota, SecurityContextDeny, ServiceAccount + --admission-control="": Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: AlwaysAdmit, AlwaysDeny, DenyExecOnPrivileged, DenyEscalatingExec, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, ResourceQuota, SecurityContextDeny, ServiceAccount --admission-control-config-file="": File with admission control configuration. --advertise-address=: The IP address on which to advertise the apiserver to members of the cluster. This address must be reachable by the rest of the cluster. If blank, the --bind-address will be used. If --bind-address is unspecified, the host's default interface will be used. --allow-privileged=false: If true, allow privileged containers. diff --git a/docs/admin/kubelet.md b/docs/admin/kubelet.md index 4478ee29205..21a00399fb3 100644 --- a/docs/admin/kubelet.md +++ b/docs/admin/kubelet.md @@ -84,6 +84,7 @@ HTTP server: The kubelet can also listen for HTTP and respond to a simple API --healthz-port=0: The port of the localhost healthz endpoint -h, --help=false: help for kubelet --host-network-sources="": Comma-separated list of sources from which the Kubelet allows pods to use of host network. For all sources use "*" [default="file"] + --host-pid-sources="": Comma-separated list of sources from which the Kubelet allows pods to use the host pid namespace. For all sources use "*" [default="file"] --hostname-override="": If non-empty, will use this string as identification instead of the actual hostname. --http-check-frequency=0: Duration between checking http for new data --image-gc-high-threshold=0: The percent of disk usage after which image garbage collection is always run. Default: 90%% diff --git a/docs/getting-started-guides/coreos/cloud-configs/master.yaml b/docs/getting-started-guides/coreos/cloud-configs/master.yaml index c3b703d1d79..f629b17f020 100644 --- a/docs/getting-started-guides/coreos/cloud-configs/master.yaml +++ b/docs/getting-started-guides/coreos/cloud-configs/master.yaml @@ -89,7 +89,7 @@ coreos: ExecStart=/opt/bin/kube-apiserver \ --service-account-key-file=/opt/bin/kube-serviceaccount.key \ --service-account-lookup=false \ - --admission-control=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota \ + --admission-control=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota \ --runtime-config=api/v1 \ --allow-privileged=true \ --insecure-bind-address=0.0.0.0 \ diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index f67ab1041aa..9de214e8299 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -201,7 +201,7 @@ function set_service_accounts { function start_apiserver { # Admission Controllers to invoke prior to persisting objects in cluster - ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota + ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,DenyEscalatingExec,ResourceQuota # This is the default dir and filename where the apiserver will generate a self-signed cert # which should be able to be used as the CA to verify itself diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 8f23af2de0f..5b251c10e25 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -103,6 +103,7 @@ healthz-port horizontal-pod-autoscaler-sync-period hostname-override host-network-sources +host-pid-sources http-check-frequency http-port ignore-not-found diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 53c4f28c3ae..ea35651eade 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -1431,6 +1431,7 @@ func deepCopy_api_PodSpec(in PodSpec, out *PodSpec, c *conversion.Cloner) error out.ServiceAccountName = in.ServiceAccountName out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/api/types.go b/pkg/api/types.go index ab0df0336fc..690bc6aacbc 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -980,10 +980,13 @@ type PodSpec struct { // the scheduler simply schedules this pod onto that node, assuming that it fits resource // requirements. NodeName string `json:"nodeName,omitempty"` - // Uses the host's network namespace. If this option is set, the ports that will be + // Use the host's network namespace. If this option is set, the ports that will be // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty"` + // Use the host's pid namespace. + // Optional: Default to false. + HostPID bool `json:"hostPID,omitempty"` // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. // If specified, these secrets will be passed to individual puller implementations for them to use. For example, // in the case of docker, only DockerConfig type secrets are honored. diff --git a/pkg/api/v1/conversion.go b/pkg/api/v1/conversion.go index 44a2ce80cfa..07ffd791cbe 100644 --- a/pkg/api/v1/conversion.go +++ b/pkg/api/v1/conversion.go @@ -282,6 +282,7 @@ func convert_api_PodSpec_To_v1_PodSpec(in *api.PodSpec, out *PodSpec, s conversi out.DeprecatedServiceAccount = in.ServiceAccountName out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { @@ -349,6 +350,7 @@ func convert_v1_PodSpec_To_api_PodSpec(in *PodSpec, out *api.PodSpec, s conversi } out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]api.LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 60cb5aff58d..7c89892da5f 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -1431,6 +1431,7 @@ func deepCopy_v1_PodSpec(in PodSpec, out *PodSpec, c *conversion.Cloner) error { out.DeprecatedServiceAccount = in.DeprecatedServiceAccount out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 0acc87d3346..1990e40b525 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1227,10 +1227,13 @@ type PodSpec struct { // the scheduler simply schedules this pod onto that node, assuming that it fits resource // requirements. NodeName string `json:"nodeName,omitempty"` - // Host networking requested for this pod. Uses the host's network namespace. + // Host networking requested for this pod. Use the host's network namespace. // If this option is set, the ports that will be used must be specified. // Default to false. HostNetwork bool `json:"hostNetwork,omitempty"` + // Use the host's pid namespace. + // Optional: Default to false. + HostPID bool `json:"hostPID,omitempty"` // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. // If specified, these secrets will be passed to individual puller implementations for them to use. For example, // in the case of docker, only DockerConfig type secrets are honored. diff --git a/pkg/api/v1/types_swagger_doc_generated.go b/pkg/api/v1/types_swagger_doc_generated.go index d56456dad2f..e31d9ffde2e 100644 --- a/pkg/api/v1/types_swagger_doc_generated.go +++ b/pkg/api/v1/types_swagger_doc_generated.go @@ -953,7 +953,8 @@ var map_PodSpec = map[string]string{ "serviceAccountName": "ServiceAccountName is the name of the ServiceAccount to use to run this pod. More info: http://releases.k8s.io/HEAD/docs/design/service_accounts.md", "serviceAccount": "DeprecatedServiceAccount is a depreciated alias for ServiceAccountName. Deprecated: Use serviceAccountName instead.", "nodeName": "NodeName is a request to schedule this pod onto a specific node. If it is non-empty, the scheduler simply schedules this pod onto that node, assuming that it fits resource requirements.", - "hostNetwork": "Host networking requested for this pod. Uses the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false.", + "hostNetwork": "Host networking requested for this pod. Use the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false.", + "hostPID": "Use the host's pid namespace. Optional: Default to false.", "imagePullSecrets": "ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. If specified, these secrets will be passed to individual puller implementations for them to use. For example, in the case of docker, only DockerConfig type secrets are honored. More info: http://releases.k8s.io/HEAD/docs/user-guide/images.md#specifying-imagepullsecrets-on-a-pod", } diff --git a/pkg/apis/experimental/deep_copy_generated.go b/pkg/apis/experimental/deep_copy_generated.go index a2a1340dfa6..bd616321adb 100644 --- a/pkg/apis/experimental/deep_copy_generated.go +++ b/pkg/apis/experimental/deep_copy_generated.go @@ -465,6 +465,7 @@ func deepCopy_api_PodSpec(in api.PodSpec, out *api.PodSpec, c *conversion.Cloner out.ServiceAccountName = in.ServiceAccountName out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]api.LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/apis/experimental/v1/conversion.go b/pkg/apis/experimental/v1/conversion.go index 480d1d52cee..1d08928fd03 100644 --- a/pkg/apis/experimental/v1/conversion.go +++ b/pkg/apis/experimental/v1/conversion.go @@ -98,6 +98,7 @@ func convert_api_PodSpec_To_v1_PodSpec(in *api.PodSpec, out *v1.PodSpec, s conve out.DeprecatedServiceAccount = in.ServiceAccountName out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]v1.LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { @@ -165,6 +166,7 @@ func convert_v1_PodSpec_To_api_PodSpec(in *v1.PodSpec, out *api.PodSpec, s conve } out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]api.LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/apis/experimental/v1/deep_copy_generated.go b/pkg/apis/experimental/v1/deep_copy_generated.go index bc58c0de5bb..1959d456825 100644 --- a/pkg/apis/experimental/v1/deep_copy_generated.go +++ b/pkg/apis/experimental/v1/deep_copy_generated.go @@ -483,6 +483,7 @@ func deepCopy_v1_PodSpec(in v1.PodSpec, out *v1.PodSpec, c *conversion.Cloner) e out.DeprecatedServiceAccount = in.DeprecatedServiceAccount out.NodeName = in.NodeName out.HostNetwork = in.HostNetwork + out.HostPID = in.HostPID if in.ImagePullSecrets != nil { out.ImagePullSecrets = make([]v1.LocalObjectReference, len(in.ImagePullSecrets)) for i := range in.ImagePullSecrets { diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go index 664d97dfbf4..1895e178c4d 100644 --- a/pkg/capabilities/capabilities.go +++ b/pkg/capabilities/capabilities.go @@ -38,6 +38,9 @@ type Capabilities struct { type PrivilegedSources struct { // List of pod sources for which using host network is allowed. HostNetworkSources []string + + // List of pod sources for which using host pid namespace is allowed. + HostPIDSources []string } // TODO: Clean these up into a singleton @@ -79,6 +82,7 @@ func Get() Capabilities { AllowPrivileged: false, PrivilegedSources: PrivilegedSources{ HostNetworkSources: []string{}, + HostPIDSources: []string{}, }, }) } diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index ee609749102..7d9ddf16c5b 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -603,7 +603,8 @@ func (dm *DockerManager) runContainer( ref *api.ObjectReference, netMode string, ipcMode string, - utsMode string) (string, error) { + utsMode string, + pidMode string) (string, error) { dockerName := KubeletContainerName{ PodFullName: kubecontainer.GetPodFullName(pod), @@ -720,6 +721,7 @@ func (dm *DockerManager) runContainer( NetworkMode: netMode, IpcMode: ipcMode, UTSMode: utsMode, + PidMode: pidMode, // Memory and CPU are set here for newer versions of Docker (1.6+). Memory: memoryLimit, MemorySwap: -1, @@ -1368,7 +1370,7 @@ func containerAndPodFromLabels(inspect *docker.Container) (pod *api.Pod, contain } // Run a single container from a pod. Returns the docker container ID -func (dm *DockerManager) runContainerInPod(pod *api.Pod, container *api.Container, netMode, ipcMode string) (kubeletTypes.DockerID, error) { +func (dm *DockerManager) runContainerInPod(pod *api.Pod, container *api.Container, netMode, ipcMode string, pidMode string) (kubeletTypes.DockerID, error) { start := time.Now() defer func() { metrics.ContainerManagerLatency.WithLabelValues("runContainerInPod").Observe(metrics.SinceInMicroseconds(start)) @@ -1388,7 +1390,7 @@ func (dm *DockerManager) runContainerInPod(pod *api.Pod, container *api.Containe if pod.Spec.HostNetwork { utsMode = "host" } - id, err := dm.runContainer(pod, container, opts, ref, netMode, ipcMode, utsMode) + id, err := dm.runContainer(pod, container, opts, ref, netMode, ipcMode, utsMode, pidMode) if err != nil { return "", err } @@ -1520,7 +1522,7 @@ func (dm *DockerManager) createPodInfraContainer(pod *api.Pod) (kubeletTypes.Doc return "", err } - id, err := dm.runContainerInPod(pod, container, netNamespace, "") + id, err := dm.runContainerInPod(pod, container, netNamespace, "", getPidMode(pod)) if err != nil { return "", err } @@ -1776,7 +1778,7 @@ func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, pod // TODO(dawnchen): Check RestartPolicy.DelaySeconds before restart a container namespaceMode := fmt.Sprintf("container:%v", podInfraContainerID) - _, err = dm.runContainerInPod(pod, container, namespaceMode, namespaceMode) + _, err = dm.runContainerInPod(pod, container, namespaceMode, namespaceMode, getPidMode(pod)) dm.updateReasonCache(pod, container, err) if err != nil { // TODO(bburns) : Perhaps blacklist a container after N failures? @@ -1889,3 +1891,12 @@ func (dm *DockerManager) doBackOff(pod *api.Pod, container *api.Container, podSt dm.clearReasonCache(pod, container) return false } + +// getPidMode returns the pid mode to use on the docker container based on pod.Spec.HostPID. +func getPidMode(pod *api.Pod) string { + pidMode := "" + if pod.Spec.HostPID { + pidMode = "host" + } + return pidMode +} diff --git a/pkg/kubelet/dockertools/manager_test.go b/pkg/kubelet/dockertools/manager_test.go index c711e7b268f..61dc7513415 100644 --- a/pkg/kubelet/dockertools/manager_test.go +++ b/pkg/kubelet/dockertools/manager_test.go @@ -2335,3 +2335,20 @@ func TestGetUidFromUser(t *testing.T) { } } } + +func TestGetPidMode(t *testing.T) { + // test false + pod := &api.Pod{} + pidMode := getPidMode(pod) + + if pidMode != "" { + t.Errorf("expected empty pid mode for pod but got %v", pidMode) + } + + // test true + pod.Spec.HostPID = true + pidMode = getPidMode(pod) + if pidMode != "host" { + t.Errorf("expected host pid mode for pod but got %v", pidMode) + } +} diff --git a/pkg/kubelet/rkt/rkt.go b/pkg/kubelet/rkt/rkt.go index 265c27018b1..33d558730ce 100644 --- a/pkg/kubelet/rkt/rkt.go +++ b/pkg/kubelet/rkt/rkt.go @@ -593,6 +593,8 @@ func (r *runtime) preparePod(pod *api.Pod, pullSecrets []api.Secret) (string, *k runPrepared = fmt.Sprintf("%s run-prepared --mds-register=false --private-net %s", r.rktBinAbsPath, uuid) } + // TODO handle pod.Spec.HostPID + units := []*unit.UnitOption{ newUnitOption(unitKubernetesSection, unitRktID, uuid), newUnitOption(unitKubernetesSection, unitPodName, string(b)), diff --git a/pkg/kubelet/util.go b/pkg/kubelet/util.go index 7740f0c3652..4499dc94858 100644 --- a/pkg/kubelet/util.go +++ b/pkg/kubelet/util.go @@ -50,6 +50,16 @@ func canRunPod(pod *api.Pod) error { } } + if pod.Spec.HostPID { + allowed, err := allowHostPID(pod) + if err != nil { + return err + } + if !allowed { + return fmt.Errorf("pod with UID %q specified host PID, but is disallowed", pod.UID) + } + } + if !capabilities.Get().AllowPrivileged { for _, container := range pod.Spec.Containers { if securitycontext.HasPrivilegedRequest(&container) { @@ -73,3 +83,17 @@ func allowHostNetwork(pod *api.Pod) (bool, error) { } return false, nil } + +// Determined whether the specified pod is allowed to use host networking +func allowHostPID(pod *api.Pod) (bool, error) { + podSource, err := getPodSource(pod) + if err != nil { + return false, err + } + for _, source := range capabilities.Get().PrivilegedSources.HostPIDSources { + if source == podSource { + return true, nil + } + } + return false, nil +} diff --git a/plugin/pkg/admission/exec/denyprivileged/admission.go b/plugin/pkg/admission/exec/admission.go similarity index 52% rename from plugin/pkg/admission/exec/denyprivileged/admission.go rename to plugin/pkg/admission/exec/admission.go index 9fa8ffb202a..762db266f46 100644 --- a/plugin/pkg/admission/exec/denyprivileged/admission.go +++ b/plugin/pkg/admission/exec/admission.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package denyprivileged +package exec import ( "fmt" @@ -28,19 +28,55 @@ import ( ) func init() { + admission.RegisterPlugin("DenyEscalatingExec", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewDenyEscalatingExec(client), nil + }) + + // This is for legacy support of the DenyExecOnPrivileged admission controller. Most + // of the time DenyEscalatingExec should be preferred. admission.RegisterPlugin("DenyExecOnPrivileged", func(client client.Interface, config io.Reader) (admission.Interface, error) { return NewDenyExecOnPrivileged(client), nil }) } -// denyExecOnPrivileged is an implementation of admission.Interface which says no to a pod/exec on -// a privileged pod -type denyExecOnPrivileged struct { +// denyExec is an implementation of admission.Interface which says no to a pod/exec on +// a pod using host based configurations. +type denyExec struct { *admission.Handler client client.Interface + + // these flags control which items will be checked to deny exec/attach + hostIPC bool + hostPID bool + privileged bool } -func (d *denyExecOnPrivileged) Admit(a admission.Attributes) (err error) { +// NewDenyEscalatingExec creates a new admission controller that denies an exec operation on a pod +// using host based configurations. +func NewDenyEscalatingExec(client client.Interface) admission.Interface { + return &denyExec{ + Handler: admission.NewHandler(admission.Connect), + client: client, + hostIPC: true, + hostPID: true, + privileged: true, + } +} + +// NewDenyExecOnPrivileged creates a new admission controller that is only checking the privileged +// option. This is for legacy support of the DenyExecOnPrivileged admission controller. Most +// of the time NewDenyEscalatingExec should be preferred. +func NewDenyExecOnPrivileged(client client.Interface) admission.Interface { + return &denyExec{ + Handler: admission.NewHandler(admission.Connect), + client: client, + hostIPC: false, + hostPID: false, + privileged: true, + } +} + +func (d *denyExec) Admit(a admission.Attributes) (err error) { connectRequest, ok := a.GetObject().(*rest.ConnectRequest) if !ok { return errors.NewBadRequest("a connect request was received, but could not convert the request object.") @@ -53,9 +89,20 @@ func (d *denyExecOnPrivileged) Admit(a admission.Attributes) (err error) { if err != nil { return admission.NewForbidden(a, err) } - if isPrivileged(pod) { + + if d.hostPID && pod.Spec.HostPID { + return admission.NewForbidden(a, fmt.Errorf("Cannot exec into or attach to a container using host pid")) + } + + //TODO uncomment when this feature lands https://github.com/kubernetes/kubernetes/pull/12470 + // if d.hostIPC && pod.Spec.HostIPC { + // return admission.NewForbidden(a, fmt.Errorf("Cannot exec into or attach to a container using host ipc")) + // } + + if d.privileged && isPrivileged(pod) { return admission.NewForbidden(a, fmt.Errorf("Cannot exec into or attach to a privileged container")) } + return nil } @@ -71,11 +118,3 @@ func isPrivileged(pod *api.Pod) bool { } return false } - -// NewDenyExecOnPrivileged creates a new admission controller that denies an exec operation on a privileged pod -func NewDenyExecOnPrivileged(client client.Interface) admission.Interface { - return &denyExecOnPrivileged{ - Handler: admission.NewHandler(admission.Connect), - client: client, - } -} diff --git a/plugin/pkg/admission/exec/admission_test.go b/plugin/pkg/admission/exec/admission_test.go new file mode 100644 index 00000000000..647d049018b --- /dev/null +++ b/plugin/pkg/admission/exec/admission_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exec + +import ( + "testing" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestAdmission(t *testing.T) { + privPod := validPod("privileged") + priv := true + privPod.Spec.Containers[0].SecurityContext = &api.SecurityContext{ + Privileged: &priv, + } + + hostPIDPod := validPod("hostPID") + hostPIDPod.Spec.HostPID = true + + // hostIPCPod := validPod("hostIPC") + // hostIPCPod.Spec.HostIPC = true + + testCases := map[string]struct { + pod *api.Pod + shouldAccept bool + }{ + "priv": { + shouldAccept: false, + pod: privPod, + }, + "hostPID": { + shouldAccept: false, + pod: hostPIDPod, + }, + // "hostIPC": { + // shouldAccept: false, + // pod: hostIPCPod, + // }, + "non privileged": { + shouldAccept: true, + pod: validPod("nonPrivileged"), + }, + } + + // use the same code as NewDenyEscalatingExec, using the direct object though to allow testAdmission to + // inject the client + handler := &denyExec{ + Handler: admission.NewHandler(admission.Connect), + hostIPC: true, + hostPID: true, + privileged: true, + } + + for _, tc := range testCases { + testAdmission(t, tc.pod, handler, tc.shouldAccept) + } + + // run with a permissive config and all cases should pass + handler.privileged = false + handler.hostPID = false + handler.hostIPC = false + + for _, tc := range testCases { + testAdmission(t, tc.pod, handler, true) + } +} + +func testAdmission(t *testing.T, pod *api.Pod, handler *denyExec, shouldAccept bool) { + mockClient := &testclient.Fake{} + mockClient.AddReactor("get", "pods", func(action testclient.Action) (bool, runtime.Object, error) { + if action.(testclient.GetAction).GetName() == pod.Name { + return true, pod, nil + } + t.Errorf("Unexpected API call: %#v", action) + return true, nil, nil + }) + + handler.client = mockClient + + // pods/exec + { + req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/exec"} + err := handler.Admit(admission.NewAttributesRecord(req, "Pod", "test", "name", "pods", "exec", admission.Connect, nil)) + if shouldAccept && err != nil { + t.Errorf("Unexpected error returned from admission handler: %v", err) + } + if !shouldAccept && err == nil { + t.Errorf("An error was expected from the admission handler. Received nil") + } + } + + // pods/attach + { + req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/attach"} + err := handler.Admit(admission.NewAttributesRecord(req, "Pod", "test", "name", "pods", "attach", admission.Connect, nil)) + if shouldAccept && err != nil { + t.Errorf("Unexpected error returned from admission handler: %v", err) + } + if !shouldAccept && err == nil { + t.Errorf("An error was expected from the admission handler. Received nil") + } + } +} + +// Test to ensure legacy admission controller works as expected. +func TestDenyExecOnPrivileged(t *testing.T) { + privPod := validPod("privileged") + priv := true + privPod.Spec.Containers[0].SecurityContext = &api.SecurityContext{ + Privileged: &priv, + } + + hostPIDPod := validPod("hostPID") + hostPIDPod.Spec.HostPID = true + + // hostIPCPod := validPod("hostIPC") + // hostIPCPod.Spec.HostIPC = true + + testCases := map[string]struct { + pod *api.Pod + shouldAccept bool + }{ + "priv": { + shouldAccept: false, + pod: privPod, + }, + "hostPID": { + shouldAccept: true, + pod: hostPIDPod, + }, + // "hostIPC": { + // shouldAccept: true, + // pod: hostIPCPod, + // }, + "non privileged": { + shouldAccept: true, + pod: validPod("nonPrivileged"), + }, + } + + // use the same code as NewDenyExecOnPrivileged, using the direct object though to allow testAdmission to + // inject the client + handler := &denyExec{ + Handler: admission.NewHandler(admission.Connect), + hostIPC: false, + hostPID: false, + privileged: true, + } + for _, tc := range testCases { + testAdmission(t, tc.pod, handler, tc.shouldAccept) + } +} + +func validPod(name string) *api.Pod { + return &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: name, Namespace: "test"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr1", Image: "image"}, + {Name: "ctr2", Image: "image2"}, + }, + }, + } +} diff --git a/plugin/pkg/admission/exec/denyprivileged/admission_test.go b/plugin/pkg/admission/exec/denyprivileged/admission_test.go deleted file mode 100644 index 4c0bc7115ec..00000000000 --- a/plugin/pkg/admission/exec/denyprivileged/admission_test.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright 2015 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package denyprivileged - -import ( - "testing" - - "k8s.io/kubernetes/pkg/admission" - "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/api/rest" - "k8s.io/kubernetes/pkg/client/unversioned/testclient" - "k8s.io/kubernetes/pkg/runtime" -) - -// TestAdmission verifies a namespace is created on create requests for namespace managed resources -func TestAdmissionAccept(t *testing.T) { - testAdmission(t, acceptPod("podname"), true) -} - -func TestAdmissionDeny(t *testing.T) { - testAdmission(t, denyPod("podname"), false) -} - -func testAdmission(t *testing.T, pod *api.Pod, shouldAccept bool) { - mockClient := &testclient.Fake{} - mockClient.AddReactor("get", "pods", func(action testclient.Action) (bool, runtime.Object, error) { - if action.(testclient.GetAction).GetName() == pod.Name { - return true, pod, nil - } - t.Errorf("Unexpected API call: %#v", action) - return true, nil, nil - }) - handler := &denyExecOnPrivileged{ - client: mockClient, - } - - // pods/exec - { - req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/exec"} - err := handler.Admit(admission.NewAttributesRecord(req, "Pod", "test", "name", "pods", "exec", admission.Connect, nil)) - if shouldAccept && err != nil { - t.Errorf("Unexpected error returned from admission handler: %v", err) - } - if !shouldAccept && err == nil { - t.Errorf("An error was expected from the admission handler. Received nil") - } - } - - // pods/attach - { - req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/attach"} - err := handler.Admit(admission.NewAttributesRecord(req, "Pod", "test", "name", "pods", "attach", admission.Connect, nil)) - if shouldAccept && err != nil { - t.Errorf("Unexpected error returned from admission handler: %v", err) - } - if !shouldAccept && err == nil { - t.Errorf("An error was expected from the admission handler. Received nil") - } - } -} - -func acceptPod(name string) *api.Pod { - return &api.Pod{ - ObjectMeta: api.ObjectMeta{Name: name, Namespace: "test"}, - Spec: api.PodSpec{ - Containers: []api.Container{ - {Name: "ctr1", Image: "image"}, - {Name: "ctr2", Image: "image2"}, - }, - }, - } -} - -func denyPod(name string) *api.Pod { - privileged := true - return &api.Pod{ - ObjectMeta: api.ObjectMeta{Name: name, Namespace: "test"}, - Spec: api.PodSpec{ - Containers: []api.Container{ - {Name: "ctr1", Image: "image"}, - { - Name: "ctr2", - Image: "image2", - SecurityContext: &api.SecurityContext{ - Privileged: &privileged, - }, - }, - }, - }, - } -}