diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index 3b34695efa7a..016d2bf44660 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -36,4 +36,5 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext" ) diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index afdd53fe1cff..3993103ae04a 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -33,10 +33,10 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -203,7 +203,7 @@ func (s *APIServer) Run(_ []string) error { glog.Fatalf("specify either --etcd_servers or --etcd_config") } - capabilities.Initialize(capabilities.Capabilities{ + securitycontext.Initialize(api.SecurityConstraints{ AllowPrivileged: s.AllowPrivileged, // TODO(vmarmol): Implement support for HostNetworkSources. HostNetworkSources: []string{}, diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index a9c8d15bfbe3..d8bb03cfd762 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -30,7 +30,6 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/chaosclient" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" @@ -47,6 +46,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/golang/glog" "github.com/spf13/pflag" ) @@ -98,6 +98,7 @@ type KubeletServer struct { CertDirectory string NodeStatusUpdateFrequency time.Duration ResourceContainer string + SecurityContextProvider string // Flags intended for testing @@ -201,6 +202,7 @@ func (s *KubeletServer) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.CloudProvider, "cloud_provider", s.CloudProvider, "The provider for cloud services. Empty string for no provider.") fs.StringVar(&s.CloudConfigFile, "cloud_config", s.CloudConfigFile, "The path to the cloud provider configuration file. Empty string for no configuration file.") fs.StringVar(&s.ResourceContainer, "resource_container", s.ResourceContainer, "Absolute name of the resource-only container to create and run the Kubelet in (Default: /kubelet).") + fs.StringVar(&s.SecurityContextProvider, "security_context", s.SecurityContextProvider, "Name of the security context provider to use. Allowable values are |permit|restrict") // Flags intended for testing, not recommended used in production environments. fs.BoolVar(&s.ReallyCrashForTesting, "really_crash_for_testing", s.ReallyCrashForTesting, "If true, when panics occur crash. Intended for testing.") @@ -302,6 +304,8 @@ func (s *KubeletServer) Run(_ []string) error { ResourceContainer: s.ResourceContainer, } + InitializeSecurityContextProvider(&kcfg, s.SecurityContextProvider) + RunKubelet(&kcfg, nil) if s.HealthzPort > 0 { @@ -406,6 +410,7 @@ func SimpleKubelet(client *client.Client, Cloud: cloud, NodeStatusUpdateFrequency: 10 * time.Second, ResourceContainer: "/kubelet", + SecurityContextProvider: securitycontext.NewPermitSecurityContextProvider(securitycontext.Get()), } return &kcfg } @@ -415,6 +420,7 @@ func SimpleKubelet(client *client.Client, // 2 Kubelet binary // 3 Standalone 'kubernetes' binary // Eventually, #2 will be replaced with instances of #3 +// NOTE: please see the comments on InitializeSecurityContextProvider for important information about the SecurityContextProvider!! func RunKubelet(kcfg *KubeletConfig, builder KubeletBuilder) { kcfg.Hostname = util.GetHostname(kcfg.HostnameOverride) eventBroadcaster := record.NewBroadcaster() @@ -426,7 +432,10 @@ func RunKubelet(kcfg *KubeletConfig, builder KubeletBuilder) { } else { glog.Infof("No api server defined - no events will be sent to API server.") } - capabilities.Setup(kcfg.AllowPrivileged, kcfg.HostNetworkSources) + + //initialize the security context provider if it hasn't already happened + //NOTE: please see the comments on InitializeSecurityContextProvider! + InitializeSecurityContextProvider(kcfg, "") credentialprovider.SetPreferredDockercfgPath(kcfg.RootDirectory) @@ -449,6 +458,43 @@ func RunKubelet(kcfg *KubeletConfig, builder KubeletBuilder) { glog.Infof("Started kubelet") } +// InitializeSecurityContextProvider initializes a global security context and sets the security context provider. +// This should eventually be pulled as a resource but this is replacing the existing capabilities functionality for now. +// This method is called twice: once by Run and once by RunKubelet. +// +// Issue: the old global capabilities code has migrated into a security constraint. This is initialized only once +// for the system. We also want to pass that global context into the global provider until the security constraints +// are fetched at runtime based on the namespace. In order to have both items you MUST initialize the context and +// provider yourself before you use RunKubelet or SimpleKubelet since they set up a default security context provider +// to catch anything that is not set. (this is where the old capabilities code was originally called) unless you are ok +// with using the default permit provider which ignores the cap add/drop settings and priv container requests and +// allows them to be run. +// +// SimpleKubelet example: +// myKcfg := KubeletConfig{} +// InitializeSecurityContextProvider(myKcfg, type) +// simpleKubeletCfg := SimpleKubelet() +// simpleKubeletCfg.SecurityContextProvider = myKcfg.SecurityContextProvider +// RunKubelet example: +// myKcfg := KubeletConfig{} +// InitializeSecurityContextProvider(myKcfg, type) +// RunKubelet(myKcfg, ...) +// +// +// Using the Run method pre-initializes the provider base on the command line settings +func InitializeSecurityContextProvider(kcfg *KubeletConfig, providerType string) { + securitycontext.Setup(kcfg.AllowPrivileged, kcfg.HostNetworkSources) + + if kcfg.SecurityContextProvider == nil { + switch providerType { + case "restrict": + kcfg.SecurityContextProvider = securitycontext.NewRestrictSecurityContextProvider(securitycontext.Get()) + default: + kcfg.SecurityContextProvider = securitycontext.NewPermitSecurityContextProvider(securitycontext.Get()) + } + } +} + func startKubelet(k KubeletBootstrap, podCfg *config.PodConfig, kc *KubeletConfig) { // start the kubelet go util.Forever(func() { k.Run(podCfg.Updates()) }, 0) @@ -529,6 +575,7 @@ type KubeletConfig struct { Cloud cloudprovider.Interface NodeStatusUpdateFrequency time.Duration ResourceContainer string + SecurityContextProvider securitycontext.SecurityContextProvider } func createAndInitKubelet(kc *KubeletConfig) (k KubeletBootstrap, pc *config.PodConfig, err error) { @@ -572,7 +619,8 @@ func createAndInitKubelet(kc *KubeletConfig) (k KubeletBootstrap, pc *config.Pod kc.ImageGCPolicy, kc.Cloud, kc.NodeStatusUpdateFrequency, - kc.ResourceContainer) + kc.ResourceContainer, + kc.SecurityContextProvider) if err != nil { return nil, nil, err diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index a947cea14687..76d460387d9b 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -116,7 +116,7 @@ echo "Starting etcd" kube::etcd::start # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota,SecurityContext APISERVER_LOG=/tmp/kube-apiserver.log sudo -E "${GO_OUT}/kube-apiserver" \ @@ -149,6 +149,7 @@ sudo -E "${GO_OUT}/kubelet" \ --address="127.0.0.1" \ --api_servers="${API_HOST}:${API_PORT}" \ --auth_path="${KUBE_ROOT}/hack/.test-cmd-auth" \ + --security_context="restrict" \ --port="$KUBELET_PORT" >"${KUBELET_LOG}" 2>&1 & KUBELET_PID=$! diff --git a/pkg/api/types.go b/pkg/api/types.go index 1aeb63f9f437..ea62317973b3 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -599,12 +599,10 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Required. TerminationMessagePath string `json:"terminationMessagePath,omitempty"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty"` // Required: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty"` + // SecurityContext defines the security context the pod should run with + SecurityContext `json:",inline"` } // Handler defines a specific action that should be taken @@ -1776,6 +1774,110 @@ type SecretList struct { Items []Secret `json:"items"` } +// SecurityContext holds security configuration that will be applied to a container. If a security context +// is set on the container spec then it must comply with any constraints defined in the SecurityConstraints context +// it is running in. If a security context is not supplied and the pod is running under a SecurityConstraints context +// then a default SecurityContext may be applied. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // Run the container in privileged mode + Privileged bool `json:"privileged,omitempty"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty"` + + // RunAsUser is the UID to run the entrypoint of the container process. Corresponding option is --user or -u + RunAsUser int64 `json:"runAsUser,omitempty"` +} + +// SELinuxOptions are the labels to be applied to the container +type SELinuxOptions struct { + // User --security-opt="label:user:USER" + User string `json:"user,omitempty"` + + // Role --security-opt="label:role:ROLE" + Role string `json:"role,omitempty"` + + // Type --security-opt="label:type:TYPE" + Type string `json:"type,omitempty"` + + // Level --security-opt="label:level:LEVEL" + Level string `json:"level,omitempty"` + + // Disabled --security-opt="label:disable" + Disabled bool `json:"disabled,omitempty"` +} + +// SecurityConstraints provides the constraints that at security context provider will +// ensure that applied SecurityContext requests follow. When a setting is provided in the SecurityConstraints +// that conflicts with an actual request it will be implementation specific whether that request is +// ignored and the container is still run (without the requested constraint) or if the container will be failed +type SecurityConstraints struct { + // EnforcementPolicy will drive behavior for how the constraints are enforce + EnforcementPolicy SecurityConstraintPolicy `json:"enforcementPolicy,omitempty"` + + // AllowPrivileged indicates whether this context allows privileged mode containers + AllowPrivileged bool `json:"allowPrivileged,omitempty"` + + // SELinux provides the security constraint options for selinux + SELinux *SELinuxSecurityConstraints `json:"seLinux,omitempty"` + + // AllowCapabilities dictates if a container can request to add or drop capabilites + AllowCapabilities bool `json:"allowCapabilities,omitempty"` + + // AllowCapabilities dictates if a container can request to run the entry point process as a specific user + AllowRunAsUser bool `json:"allowRunAsUser,omitempty"` + + // Capabilities represents, if AllowCapabilities is true, the caps that requests + // are allowed to add or drop. + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // DefaultSecurityContext is applied to any container that does not have a security context set. It must + // also conform to the constraints defined in SecurityConstraints object + DefaultSecurityContext *SecurityContext `json:"defaultSecurityContext,omitempty"` + + // List of pod sources for which using host network is allowed. + HostNetworkSources []string +} + +// SELinuxSecurityConstraints defines what is allowed in SecurityContext requests with regards to SELinux label options +// that are currently supported by docker +type SELinuxSecurityConstraints struct { + // AllowUserLabel --security-opt="label:user:USER" + AllowUserLabel bool `json:"allowUserLabel,omitempty"` + + // AllowRoleLabel --security-opt="label:role:ROLE" + AllowRoleLabel bool `json:"allowRoleLabel,omitempty"` + + // AllowTypeLabel --security-opt="label:type:TYPE" + AllowTypeLabel bool `json:"allowTypeLabel,omitempty"` + + // AllowLevelLabel --security-opt="label:level:LEVEL" + AllowLevelLabel bool `json:"allowLevelLabel,omitempty"` + + // AllowDisable --security-opt="label:disable" + AllowDisable bool `json:"allowDisable,omitempty"` +} + +// SecurityConstraintPolicy dictates how the security context provider should behave with regards to contexts that +// do not meet the requirements of the policy. +type SecurityConstraintPolicy string + +const ( + // SecurityConstraintPolicyDisabled means that any containers that do not meet policy constraints are still + // allowed to run and will be given their requested permissions. This could be used if an admin needs to allow + // a pod to run for testing without deleting and recreating the policy. Implementation may log warnings for + // permission requests that do not comply with the policy that would be enforced + SecurityConstraintPolicyDisable = "Disable" + + // SecurityConstraintPolicyReject means that any containers that do not meet policy constraints will be rejected + // (in the case of the api server) or not run (in the case of the kubelet) + SecurityConstraintPolicyReject = "Reject" +) + // These constants are for remote command execution and port forwarding and are // used by both the client side and server side components. // diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 257f3e352fc4..2708b51c085f 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -579,6 +579,9 @@ func init() { if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { return err } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } return nil }, // Internal API does not support CPU to be specified via an explicit field. @@ -665,6 +668,9 @@ func init() { if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { return err } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } return nil }, func(in *newer.PodSpec, out *ContainerManifest, s conversion.Scope) error { diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 3947ac26f9a4..4a151d27f3fd 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -70,6 +70,8 @@ func init() { &PodProxyOptions{}, &ComponentStatus{}, &ComponentStatusList{}, + &SecurityContext{}, + &SecurityConstraints{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -114,3 +116,5 @@ func (*PodExecOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {} func (*ComponentStatus) IsAnAPIObject() {} func (*ComponentStatusList) IsAnAPIObject() {} +func (*SecurityContext) IsAnAPIObject() {} +func (*SecurityConstraints) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 493d59eedeb3..1a15864a9cdf 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -496,12 +496,10 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // SecurityContext defines the security context the pod should run with + SecurityContext `json:",inline"` } // Handler defines a specific action that should be taken @@ -1625,3 +1623,109 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. If a security context +// is set on the container spec then it must comply with any constraints defined in the SecurityConstraints context +// it is running in. If a security context is not supplied and the pod is running under a SecurityConstraints context +// then a default SecurityContext may be applied. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // TODO: will need to refactor this from the container spec to here + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // Run the container in privileged mode + // TODO: will need to refactor this from the container spec to here + Privileged bool `json:"privileged,omitempty"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty"` + + // RunAsUser is the UID to run the entrypoint of the container process. Corresponding option is --user or -u + RunAsUser int64 `json:"runAsUser,omitempty"` +} + +// SELinuxOptions are the labels to be applied to the container +type SELinuxOptions struct { + // User --security-opt="label:user:USER" + User string `json:"user,omitempty"` + + // Role --security-opt="label:role:ROLE" + Role string `json:"role,omitempty"` + + // Type --security-opt="label:type:TYPE" + Type string `json:"type,omitempty"` + + // Level --security-opt="label:level:LEVEL" + Level string `json:"level,omitempty"` + + // Disabled --security-opt="label:disable" + Disabled bool `json:"disabled,omitempty"` +} + +// SecurityConstraints provides the constraints that at security context provider will +// ensure that applied SecurityContext requests follow. When a setting is provided in the SecurityConstraints +// that conflicts with an actual request it will be implementation specific whether that request is +// ignored and the container is still run (without the requested constraint) or if the container will be failed +type SecurityConstraints struct { + // EnforcementPolicy will drive behavior for how the constraints are enforce + EnforcementPolicy SecurityConstraintPolicy `json:"enforcementPolicy,omitempty"` + + // AllowPrivileged indicates whether this context allows privileged mode containers + AllowPrivileged bool `json:"allowPrivileged,omitempty"` + + // SELinux provides the security constraint options for selinux + SELinux *SELinuxSecurityConstraints `json:"seLinux,omitempty"` + + // AllowCapabilities dictates if a container can request to add or drop capabilites + AllowCapabilities bool `json:"allowCapabilities,omitempty"` + + // AllowCapabilities dictates if a container can request to run the entry point process as a specific user + AllowRunAsUser bool `json:"allowRunAsUser,omitempty"` + + // Capabilities represents, if AllowCapabilities is true, the caps that requests + // are allowed to add or drop. + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // DefaultSecurityContext is applied to any container that does not have a security context set. It must + // also conform to the constraints defined in SecurityConstraints object + DefaultSecurityContext *SecurityContext `json:"defaultSecurityContext,omitempty"` + + // List of pod sources for which using host network is allowed. + HostNetworkSources []string +} + +// SELinuxSecurityConstraints defines what is allowed in SecurityContext requests with regards to SELinux label options +// that are currently supported by docker +type SELinuxSecurityConstraints struct { + // AllowUserLabel --security-opt="label:user:USER" + AllowUserLabel bool `json:"allowUserLabel,omitempty"` + + // AllowRoleLabel --security-opt="label:role:ROLE" + AllowRoleLabel bool `json:"allowRoleLabel,omitempty"` + + // AllowTypeLabel --security-opt="label:type:TYPE" + AllowTypeLabel bool `json:"allowTypeLabel,omitempty"` + + // AllowLevelLabel --security-opt="label:level:LEVEL" + AllowLevelLabel bool `json:"allowLevelLabel,omitempty"` + + // AllowDisable --security-opt="label:disable" + AllowDisable bool `json:"allowDisable,omitempty"` +} + +// SecurityConstraintPolicy dictates how the security context provider should behave with regards to contexts that +// do not meet the requirements of the policy. +type SecurityConstraintPolicy string + +const ( + // SecurityConstraintPolicyDisabled means that any containers that do not meet policy constraints are still + // allowed to run and will be given their requested permissions. This could be used if an admin needs to allow + // a pod to run for testing without deleting and recreating the policy. Implementation may log warnings for + // permission requests that do not comply with the policy that would be enforced + SecurityConstraintPolicyDisable = "Disable" + + // SecurityConstraintPolicyReject means that any containers that do not meet policy constraints will be rejected + // (in the case of the api server) or not run (in the case of the kubelet) + SecurityConstraintPolicyReject = "Reject" +) diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index f2f5815d45fc..b2ae7d48533d 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -366,6 +366,9 @@ func init() { if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { return err } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } return nil }, // Internal API does not support CPU to be specified via an explicit field. @@ -454,6 +457,9 @@ func init() { if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { return err } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } return nil }, func(in *newer.PodSpec, out *ContainerManifest, s conversion.Scope) error { diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 75393fd89c42..b09aeaa31f4e 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -70,6 +70,8 @@ func init() { &PodProxyOptions{}, &ComponentStatus{}, &ComponentStatusList{}, + &SecurityContext{}, + &SecurityConstraints{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -114,3 +116,5 @@ func (*PodExecOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {} func (*ComponentStatus) IsAnAPIObject() {} func (*ComponentStatusList) IsAnAPIObject() {} +func (*SecurityContext) IsAnAPIObject() {} +func (*SecurityConstraints) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 14fab56566ea..eac71a944d68 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -491,12 +491,10 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // SecurityContext defines the security context the pod should run with + SecurityContext `json:",inline"` } const ( @@ -1688,3 +1686,109 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. If a security context +// is set on the container spec then it must comply with any constraints defined in the SecurityConstraints context +// it is running in. If a security context is not supplied and the pod is running under a SecurityConstraints context +// then a default SecurityContext may be applied. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // TODO: will need to refactor this from the container spec to here + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // Run the container in privileged mode + // TODO: will need to refactor this from the container spec to here + Privileged bool `json:"privileged,omitempty"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty"` + + // RunAsUser is the UID to run the entrypoint of the container process. Corresponding option is --user or -u + RunAsUser int64 `json:"runAsUser,omitempty"` +} + +// SELinuxOptions are the labels to be applied to the container +type SELinuxOptions struct { + // User --security-opt="label:user:USER" + User string `json:"user,omitempty"` + + // Role --security-opt="label:role:ROLE" + Role string `json:"role,omitempty"` + + // Type --security-opt="label:type:TYPE" + Type string `json:"type,omitempty"` + + // Level --security-opt="label:level:LEVEL" + Level string `json:"level,omitempty"` + + // Disabled --security-opt="label:disable" + Disabled bool `json:"disabled,omitempty"` +} + +// SecurityConstraints provides the constraints that at security context provider will +// ensure that applied SecurityContext requests follow. When a setting is provided in the SecurityConstraints +// that conflicts with an actual request it will be implementation specific whether that request is +// ignored and the container is still run (without the requested constraint) or if the container will be failed +type SecurityConstraints struct { + // EnforcementPolicy will drive behavior for how the constraints are enforce + EnforcementPolicy SecurityConstraintPolicy `json:"enforcementPolicy,omitempty"` + + // AllowPrivileged indicates whether this context allows privileged mode containers + AllowPrivileged bool `json:"allowPrivileged,omitempty"` + + // SELinux provides the security constraint options for selinux + SELinux *SELinuxSecurityConstraints `json:"seLinux,omitempty"` + + // AllowCapabilities dictates if a container can request to add or drop capabilites + AllowCapabilities bool `json:"allowCapabilities,omitempty"` + + // AllowCapabilities dictates if a container can request to run the entry point process as a specific user + AllowRunAsUser bool `json:"allowRunAsUser,omitempty"` + + // Capabilities represents, if AllowCapabilities is true, the caps that requests + // are allowed to add or drop. + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // DefaultSecurityContext is applied to any container that does not have a security context set. It must + // also conform to the constraints defined in SecurityConstraints object + DefaultSecurityContext *SecurityContext `json:"defaultSecurityContext,omitempty"` + + // List of pod sources for which using host network is allowed. + HostNetworkSources []string +} + +// SELinuxSecurityConstraints defines what is allowed in SecurityContext requests with regards to SELinux label options +// that are currently supported by docker +type SELinuxSecurityConstraints struct { + // AllowUserLabel --security-opt="label:user:USER" + AllowUserLabel bool `json:"allowUserLabel,omitempty"` + + // AllowRoleLabel --security-opt="label:role:ROLE" + AllowRoleLabel bool `json:"allowRoleLabel,omitempty"` + + // AllowTypeLabel --security-opt="label:type:TYPE" + AllowTypeLabel bool `json:"allowTypeLabel,omitempty"` + + // AllowLevelLabel --security-opt="label:level:LEVEL" + AllowLevelLabel bool `json:"allowLevelLabel,omitempty"` + + // AllowDisable --security-opt="label:disable" + AllowDisable bool `json:"allowDisable,omitempty"` +} + +// SecurityConstraintPolicy dictates how the security context provider should behave with regards to contexts that +// do not meet the requirements of the policy. +type SecurityConstraintPolicy string + +const ( + // SecurityConstraintPolicyDisabled means that any containers that do not meet policy constraints are still + // allowed to run and will be given their requested permissions. This could be used if an admin needs to allow + // a pod to run for testing without deleting and recreating the policy. Implementation may log warnings for + // permission requests that do not comply with the policy that would be enforced + SecurityConstraintPolicyDisable = "Disable" + + // SecurityConstraintPolicyReject means that any containers that do not meet policy constraints will be rejected + // (in the case of the api server) or not run (in the case of the kubelet) + SecurityConstraintPolicyReject = "Reject" +) diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index ea168aaf83ac..1e34d740268b 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -63,6 +63,8 @@ func init() { &PodProxyOptions{}, &ComponentStatus{}, &ComponentStatusList{}, + &SecurityContext{}, + &SecurityConstraints{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -106,3 +108,5 @@ func (*PodExecOptions) IsAnAPIObject() {} func (*PodProxyOptions) IsAnAPIObject() {} func (*ComponentStatus) IsAnAPIObject() {} func (*ComponentStatusList) IsAnAPIObject() {} +func (*SecurityContext) IsAnAPIObject() {} +func (*SecurityConstraints) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 065fe3bb37d0..3020c0e80245 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -614,12 +614,10 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // SecurityContext defines the security context the pod should run with + SecurityContext `json:"securityContext,omitempty"` } // Handler defines a specific action that should be taken @@ -1706,3 +1704,109 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. If a security context +// is set on the container spec then it must comply with any constraints defined in the SecurityConstraints context +// it is running in. If a security context is not supplied and the pod is running under a SecurityConstraints context +// then a default SecurityContext may be applied. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // TODO: will need to refactor this from the container spec to here + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // Run the container in privileged mode + // TODO: will need to refactor this from the container spec to here + Privileged bool `json:"privileged,omitempty"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty"` + + // RunAsUser is the UID to run the entrypoint of the container process. Corresponding option is --user or -u + RunAsUser int64 `json:"runAsUser,omitempty"` +} + +// SELinuxOptions are the labels to be applied to the container +type SELinuxOptions struct { + // User --security-opt="label:user:USER" + User string `json:"user,omitempty"` + + // Role --security-opt="label:role:ROLE" + Role string `json:"role,omitempty"` + + // Type --security-opt="label:type:TYPE" + Type string `json:"type,omitempty"` + + // Level --security-opt="label:level:LEVEL" + Level string `json:"level,omitempty"` + + // Disabled --security-opt="label:disable" + Disabled bool `json:"disabled,omitempty"` +} + +// SecurityConstraints provides the constraints that at security context provider will +// ensure that applied SecurityContext requests follow. When a setting is provided in the SecurityConstraints +// that conflicts with an actual request it will be implementation specific whether that request is +// ignored and the container is still run (without the requested constraint) or if the container will be failed +type SecurityConstraints struct { + // EnforcementPolicy will drive behavior for how the constraints are enforce + EnforcementPolicy SecurityConstraintPolicy `json:"enforcementPolicy,omitempty"` + + // AllowPrivileged indicates whether this context allows privileged mode containers + AllowPrivileged bool `json:"allowPrivileged,omitempty"` + + // SELinux provides the security constraint options for selinux + SELinux *SELinuxSecurityConstraints `json:"seLinux,omitempty"` + + // AllowCapabilities dictates if a container can request to add or drop capabilites + AllowCapabilities bool `json:"allowCapabilities,omitempty"` + + // AllowCapabilities dictates if a container can request to run the entry point process as a specific user + AllowRunAsUser bool `json:"allowRunAsUser,omitempty"` + + // Capabilities represents, if AllowCapabilities is true, the caps that requests + // are allowed to add or drop. + Capabilities *Capabilities `json:"capabilities,omitempty"` + + // DefaultSecurityContext is applied to any container that does not have a security context set. It must + // also conform to the constraints defined in SecurityConstraints object + DefaultSecurityContext *SecurityContext `json:"defaultSecurityContext,omitempty"` + + // List of pod sources for which using host network is allowed. + HostNetworkSources []string +} + +// SELinuxSecurityConstraints defines what is allowed in SecurityContext requests with regards to SELinux label options +// that are currently supported by docker +type SELinuxSecurityConstraints struct { + // AllowUserLabel --security-opt="label:user:USER" + AllowUserLabel bool `json:"allowUserLabel,omitempty"` + + // AllowRoleLabel --security-opt="label:role:ROLE" + AllowRoleLabel bool `json:"allowRoleLabel,omitempty"` + + // AllowTypeLabel --security-opt="label:type:TYPE" + AllowTypeLabel bool `json:"allowTypeLabel,omitempty"` + + // AllowLevelLabel --security-opt="label:level:LEVEL" + AllowLevelLabel bool `json:"allowLevelLabel,omitempty"` + + // AllowDisable --security-opt="label:disable" + AllowDisable bool `json:"allowDisable,omitempty"` +} + +// SecurityConstraintPolicy dictates how the security context provider should behave with regards to contexts that +// do not meet the requirements of the policy. +type SecurityConstraintPolicy string + +const ( + // SecurityConstraintPolicyDisabled means that any containers that do not meet policy constraints are still + // allowed to run and will be given their requested permissions. This could be used if an admin needs to allow + // a pod to run for testing without deleting and recreating the policy. Implementation may log warnings for + // permission requests that do not comply with the policy that would be enforced + SecurityConstraintPolicyDisable = "Disable" + + // SecurityConstraintPolicyReject means that any containers that do not meet policy constraints will be rejected + // (in the case of the api server) or not run (in the case of the kubelet) + SecurityConstraintPolicyReject = "Reject" +) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index d2535dc1676d..ac3aab1beb90 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -25,12 +25,12 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" errs "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/golang/glog" ) @@ -716,14 +716,14 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs allNames := util.StringSet{} for i, ctr := range containers { cErrs := errs.ValidationErrorList{} - capabilities := capabilities.Get() + securityConstraints := securitycontext.Get() if len(ctr.Name) == 0 { cErrs = append(cErrs, errs.NewFieldRequired("name")) } else if !util.IsDNS1123Label(ctr.Name) { cErrs = append(cErrs, errs.NewFieldInvalid("name", ctr.Name, dns1123LabelErrorMsg)) } else if allNames.Has(ctr.Name) { cErrs = append(cErrs, errs.NewFieldDuplicate("name", ctr.Name)) - } else if ctr.Privileged && !capabilities.AllowPrivileged { + } else if ctr.Privileged && !securityConstraints.AllowPrivileged { cErrs = append(cErrs, errs.NewFieldForbidden("privileged", ctr.Privileged)) } else { allNames.Insert(ctr.Name) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index ca10ea7b83ac..5d9bd3996dc3 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -23,7 +23,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" @@ -786,7 +786,7 @@ func getResourceLimits(cpu, memory string) api.ResourceList { func TestValidateContainers(t *testing.T) { volumes := util.StringSet{} - capabilities.SetForTests(capabilities.Capabilities{ + securitycontext.SetForTests(api.SecurityConstraints{ AllowPrivileged: true, }) @@ -816,13 +816,13 @@ func TestValidateContainers(t *testing.T) { }, ImagePullPolicy: "IfNotPresent", }, - {Name: "abc-1234", Image: "image", Privileged: true, ImagePullPolicy: "IfNotPresent"}, + {Name: "abc-1234", Image: "image", SecurityContext: api.SecurityContext{Privileged: true}, ImagePullPolicy: "IfNotPresent"}, } if errs := validateContainers(successCase, volumes); len(errs) != 0 { t.Errorf("expected success: %v", errs) } - capabilities.SetForTests(capabilities.Capabilities{ + securitycontext.SetForTests(api.SecurityConstraints{ AllowPrivileged: false, }) errorCases := map[string][]api.Container{ @@ -930,7 +930,7 @@ func TestValidateContainers(t *testing.T) { }, }, "privilege disabled": { - {Name: "abc", Image: "image", Privileged: true}, + {Name: "abc", Image: "image", SecurityContext: api.SecurityContext{Privileged: true}}, }, "invalid compute resource": { { diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go deleted file mode 100644 index 0c188eaf1c70..000000000000 --- a/pkg/capabilities/capabilities.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package capabilities - -import ( - "sync" -) - -// Capabilities defines the set of capabilities available within the system. -// For now these are global. Eventually they may be per-user -type Capabilities struct { - AllowPrivileged bool - - // List of pod sources for which using host network is allowed. - HostNetworkSources []string -} - -var once sync.Once -var capabilities *Capabilities - -// Initialize the capability set. This can only be done once per binary, subsequent calls are ignored. -func Initialize(c Capabilities) { - // Only do this once - once.Do(func() { - capabilities = &c - }) -} - -// Setup the capability set. It wraps Initialize for improving usibility. -func Setup(allowPrivileged bool, hostNetworkSources []string) { - Initialize(Capabilities{ - AllowPrivileged: allowPrivileged, - HostNetworkSources: hostNetworkSources, - }) -} - -// SetCapabilitiesForTests. Convenience method for testing. This should only be called from tests. -func SetForTests(c Capabilities) { - capabilities = &c -} - -// Returns a read-only copy of the system capabilities. -func Get() Capabilities { - if capabilities == nil { - Initialize(Capabilities{ - AllowPrivileged: false, - HostNetworkSources: []string{}, - }) - } - return *capabilities -} diff --git a/pkg/kubelet/dockertools/docker_test.go b/pkg/kubelet/dockertools/docker_test.go index 514c2ceafa1d..782bc4ac7cef 100644 --- a/pkg/kubelet/dockertools/docker_test.go +++ b/pkg/kubelet/dockertools/docker_test.go @@ -27,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" docker "github.com/fsouza/go-dockerclient" @@ -392,7 +393,8 @@ func TestIsImagePresent(t *testing.T) { func TestGetRunningContainers(t *testing.T) { fakeDocker := &FakeDockerClient{Errors: make(map[string]error)} fakeRecorder := &record.FakeRecorder{} - containerManager := NewDockerManager(fakeDocker, fakeRecorder, PodInfraContainerImage, 0, 0) + fakeSecurityContextProvider := &securitycontext.FakeSecurityContextProvider{} + containerManager := NewDockerManager(fakeDocker, fakeRecorder, PodInfraContainerImage, 0, 0, fakeSecurityContextProvider) tests := []struct { containers map[string]*docker.Container inputIDs []string @@ -657,7 +659,8 @@ func TestFindContainersByPod(t *testing.T) { }, } fakeClient := &FakeDockerClient{} - containerManager := NewDockerManager(fakeClient, &record.FakeRecorder{}, PodInfraContainerImage, 0, 0) + fakeSecurityContextProvider := &securitycontext.FakeSecurityContextProvider{} + containerManager := NewDockerManager(fakeClient, &record.FakeRecorder{}, PodInfraContainerImage, 0, 0, fakeSecurityContextProvider) for i, test := range tests { fakeClient.ContainerList = test.containerList fakeClient.ExitedContainerList = test.exitedContainerList diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index 4016bd3d0a44..42d59baec646 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -28,12 +28,12 @@ import ( "sync" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/fsouza/go-dockerclient" + docker "github.com/fsouza/go-dockerclient" "github.com/golang/glog" "github.com/golang/groupcache/lru" ) @@ -64,20 +64,22 @@ type DockerManager struct { // container manager, then we can unexport this. Also at that time, we // use the concrete type so that we can record the pull failure and eliminate // the image checking in GetPodStatus(). - Puller DockerPuller + Puller DockerPuller + securityContextProvider securitycontext.SecurityContextProvider } // Ensures DockerManager implements ConatinerRunner. var _ kubecontainer.ContainerRunner = new(DockerManager) -func NewDockerManager(client DockerInterface, recorder record.EventRecorder, podInfraContainerImage string, qps float32, burst int) *DockerManager { +func NewDockerManager(client DockerInterface, recorder record.EventRecorder, podInfraContainerImage string, qps float32, burst int, securityContextProvider securitycontext.SecurityContextProvider) *DockerManager { reasonCache := stringCache{cache: lru.New(maxReasonCacheEntries)} return &DockerManager{ - client: client, - recorder: recorder, - PodInfraContainerImage: podInfraContainerImage, - reasonCache: reasonCache, - Puller: newDockerPuller(client, qps, burst), + client: client, + recorder: recorder, + PodInfraContainerImage: podInfraContainerImage, + reasonCache: reasonCache, + Puller: newDockerPuller(client, qps, burst), + securityContextProvider: securityContextProvider, } } @@ -443,6 +445,10 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op glog.V(3).Infof("Container %v/%v/%v: setting entrypoint \"%v\" and command \"%v\"", pod.Namespace, pod.Name, container.Name, dockerOpts.Config.Entrypoint, dockerOpts.Config.Cmd) + if err := dm.securityContextProvider.ModifyContainerConfig(pod, container, dockerOpts.Config); err != nil { + return "", err + } + dockerContainer, err := dm.client.CreateContainer(dockerOpts) if err != nil { if ref != nil { @@ -473,22 +479,13 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op } } - privileged := false - if capabilities.Get().AllowPrivileged { - privileged = container.Privileged - } else if container.Privileged { - return "", fmt.Errorf("container requested privileged mode, but it is disallowed globally.") - } - - capAdd, capDrop := makeCapabilites(container.Capabilities.Add, container.Capabilities.Drop) hc := &docker.HostConfig{ PortBindings: portBindings, Binds: opts.Binds, NetworkMode: opts.NetMode, IpcMode: opts.IpcMode, - Privileged: privileged, - CapAdd: capAdd, - CapDrop: capDrop, + //privileged is set by the security context provider below + //capabilities are modified by the security context provider below } if len(opts.DNS) > 0 { hc.DNS = opts.DNS @@ -497,6 +494,8 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op hc.DNSSearch = opts.DNSSearch } + dm.securityContextProvider.ModifyHostConfig(pod, container, hc) + if err = dm.client.StartContainer(dockerContainer.ID, hc); err != nil { if ref != nil { dm.recorder.Eventf(ref, "failed", @@ -553,20 +552,6 @@ func makePortsAndBindings(container *api.Container) (map[docker.Port]struct{}, m return exposedPorts, portBindings } -func makeCapabilites(capAdd []api.CapabilityType, capDrop []api.CapabilityType) ([]string, []string) { - var ( - addCaps []string - dropCaps []string - ) - for _, cap := range capAdd { - addCaps = append(addCaps, string(cap)) - } - for _, cap := range capDrop { - dropCaps = append(dropCaps, string(cap)) - } - return addCaps, dropCaps -} - func (dm *DockerManager) GetPods(all bool) ([]*kubecontainer.Pod, error) { pods := make(map[types.UID]*kubecontainer.Pod) var result []*kubecontainer.Pod diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 63f169f5f515..27ecc1f104c0 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -47,6 +47,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" utilErrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" @@ -123,7 +124,8 @@ func NewMainKubelet( imageGCPolicy ImageGCPolicy, cloud cloudprovider.Interface, nodeStatusUpdateFrequency time.Duration, - resourceContainer string) (*Kubelet, error) { + resourceContainer string, + securityContextProvider securitycontext.SecurityContextProvider) (*Kubelet, error) { if rootDirectory == "" { return nil, fmt.Errorf("invalid root directory %q", rootDirectory) } @@ -200,7 +202,7 @@ func NewMainKubelet( return nil, fmt.Errorf("failed to initialize image manager: %v", err) } statusManager := newStatusManager(kubeClient) - containerManager := dockertools.NewDockerManager(dockerClient, recorder, podInfraContainerImage, pullQPS, pullBurst) + containerManager := dockertools.NewDockerManager(dockerClient, recorder, podInfraContainerImage, pullQPS, pullBurst, securityContextProvider) klet := &Kubelet{ hostname: hostname, @@ -229,6 +231,7 @@ func NewMainKubelet( containerManager: containerManager, nodeStatusUpdateFrequency: nodeStatusUpdateFrequency, resourceContainer: resourceContainer, + securityContextProvider: securityContextProvider, } klet.podManager = newBasicPodManager(klet.kubeClient) @@ -369,6 +372,9 @@ type Kubelet struct { // The name of the resource-only container to run the Kubelet in (empty for no container). // Name must be absolute. resourceContainer string + + // securityContextProvider provides the ability to manipulate the container security options before running + securityContextProvider securitycontext.SecurityContextProvider } // getRootDir returns the full path to the directory under which kubelet can @@ -1191,7 +1197,7 @@ func (kl *Kubelet) syncPod(pod *api.Pod, mirrorPod *api.Pod, runningPod kubecont }() // Kill pods we can't run. - err := canRunPod(pod) + err := canRunPod(pod, kl.securityContextProvider) if err != nil { kl.killPod(runningPod) return err diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index eaa98870f612..68cf7ee88fff 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -36,7 +36,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor" @@ -45,6 +44,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/metrics" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" @@ -72,7 +72,10 @@ func newTestKubelet(t *testing.T) *TestKubelet { fakeDocker := &dockertools.FakeDockerClient{Errors: make(map[string]error), RemovedImages: util.StringSet{}} fakeRecorder := &record.FakeRecorder{} fakeKubeClient := &testclient.Fake{} + fakeSecurityContextProvider := &securitycontext.FakeSecurityContextProvider{} + kubelet := &Kubelet{} + kubelet.securityContextProvider = fakeSecurityContextProvider kubelet.dockerClient = fakeDocker kubelet.kubeClient = fakeKubeClient @@ -102,7 +105,7 @@ func newTestKubelet(t *testing.T) *TestKubelet { podManager, fakeMirrorClient := newFakePodManager() kubelet.podManager = podManager kubelet.containerRefManager = kubecontainer.NewRefManager() - kubelet.containerManager = dockertools.NewDockerManager(fakeDocker, fakeRecorder, dockertools.PodInfraContainerImage, 0, 0) + kubelet.containerManager = dockertools.NewDockerManager(fakeDocker, fakeRecorder, dockertools.PodInfraContainerImage, 0, 0, fakeSecurityContextProvider) kubelet.runtimeCache = kubecontainer.NewFakeRuntimeCache(kubelet.containerManager) kubelet.podWorkers = newPodWorkers( kubelet.runtimeCache, @@ -3727,7 +3730,7 @@ func TestHostNetworkAllowed(t *testing.T) { testKubelet := newTestKubelet(t) kubelet := testKubelet.kubelet - capabilities.SetForTests(capabilities.Capabilities{ + securitycontext.SetForTests(api.SecurityConstraints{ HostNetworkSources: []string{ApiserverSource, FileSource}, }) pod := &api.Pod{ @@ -3757,7 +3760,7 @@ func TestHostNetworkDisallowed(t *testing.T) { testKubelet := newTestKubelet(t) kubelet := testKubelet.kubelet - capabilities.SetForTests(capabilities.Capabilities{ + securitycontext.SetForTests(api.SecurityConstraints{ HostNetworkSources: []string{}, }) pod := &api.Pod{ diff --git a/pkg/kubelet/pod_workers_test.go b/pkg/kubelet/pod_workers_test.go index c9199849648d..9c5ba9110475 100644 --- a/pkg/kubelet/pod_workers_test.go +++ b/pkg/kubelet/pod_workers_test.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" ) @@ -40,7 +41,8 @@ func newPod(uid, name string) *api.Pod { func createPodWorkers() (*podWorkers, map[types.UID][]string) { fakeDocker := &dockertools.FakeDockerClient{} fakeRecorder := &record.FakeRecorder{} - fakeRuntimeCache := kubecontainer.NewFakeRuntimeCache(dockertools.NewDockerManager(fakeDocker, fakeRecorder, dockertools.PodInfraContainerImage, 0, 0)) + fakeSecurityContextProvider := &securitycontext.FakeSecurityContextProvider{} + fakeRuntimeCache := kubecontainer.NewFakeRuntimeCache(dockertools.NewDockerManager(fakeDocker, fakeRecorder, dockertools.PodInfraContainerImage, 0, 0, fakeSecurityContextProvider)) lock := sync.Mutex{} processed := make(map[types.UID][]string) diff --git a/pkg/kubelet/runonce_test.go b/pkg/kubelet/runonce_test.go index 264ff3b33650..7f7cfa924cc1 100644 --- a/pkg/kubelet/runonce_test.go +++ b/pkg/kubelet/runonce_test.go @@ -28,6 +28,7 @@ import ( kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/network" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" docker "github.com/fsouza/go-dockerclient" cadvisorApi "github.com/google/cadvisor/info/v1" ) @@ -74,18 +75,20 @@ func (d *testDocker) InspectContainer(id string) (*docker.Container, error) { func TestRunOnce(t *testing.T) { cadvisor := &cadvisor.Mock{} cadvisor.On("MachineInfo").Return(&cadvisorApi.MachineInfo{}, nil) + fakeSecurityContextProvider := &securitycontext.FakeSecurityContextProvider{} podManager, _ := newFakePodManager() kb := &Kubelet{ - rootDirectory: "/tmp/kubelet", - recorder: &record.FakeRecorder{}, - cadvisor: cadvisor, - nodeLister: testNodeLister{}, - statusManager: newStatusManager(nil), - containerRefManager: kubecontainer.NewRefManager(), - readinessManager: kubecontainer.NewReadinessManager(), - podManager: podManager, + rootDirectory: "/tmp/kubelet", + recorder: &record.FakeRecorder{}, + cadvisor: cadvisor, + nodeLister: testNodeLister{}, + statusManager: newStatusManager(nil), + containerRefManager: kubecontainer.NewRefManager(), + readinessManager: kubecontainer.NewReadinessManager(), + podManager: podManager, + securityContextProvider: fakeSecurityContextProvider, } kb.networkPlugin, _ = network.InitNetworkPlugin([]network.NetworkPlugin{}, "", network.NewFakeHost(nil)) @@ -145,7 +148,7 @@ func TestRunOnce(t *testing.T) { t: t, } - kb.containerManager = dockertools.NewDockerManager(kb.dockerClient, kb.recorder, dockertools.PodInfraContainerImage, 0, 0) + kb.containerManager = dockertools.NewDockerManager(kb.dockerClient, kb.recorder, dockertools.PodInfraContainerImage, 0, 0, fakeSecurityContextProvider) kb.containerManager.Puller = &dockertools.FakeDockerPuller{} pods := []*api.Pod{ diff --git a/pkg/kubelet/util.go b/pkg/kubelet/util.go index 73cbc9b01dfc..f70e99630e28 100644 --- a/pkg/kubelet/util.go +++ b/pkg/kubelet/util.go @@ -21,7 +21,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" cadvisorApi "github.com/google/cadvisor/info/v1" ) @@ -38,7 +38,7 @@ func CapacityFromMachineInfo(info *cadvisorApi.MachineInfo) api.ResourceList { } // Check whether we have the capabilities to run the specified pod. -func canRunPod(pod *api.Pod) error { +func canRunPod(pod *api.Pod, scp securitycontext.SecurityContextProvider) error { if pod.Spec.HostNetwork { allowed, err := allowHostNetwork(pod) if err != nil { @@ -49,6 +49,12 @@ func canRunPod(pod *api.Pod) error { } } // TODO(vmarmol): Check Privileged too. + + // Can't run if we aren't validated by the security context + if errs := scp.ValidateSecurityContext(pod); len(errs) > 0 { + return fmt.Errorf("pod with UID %q does not comply with the security context", pod.UID) + } + return nil } @@ -58,7 +64,7 @@ func allowHostNetwork(pod *api.Pod) (bool, error) { if err != nil { return false, err } - for _, source := range capabilities.Get().HostNetworkSources { + for _, source := range securitycontext.Get().HostNetworkSources { if source == podSource { return true, nil } diff --git a/pkg/securitycontext/doc.go b/pkg/securitycontext/doc.go new file mode 100644 index 000000000000..747ea8f46f28 --- /dev/null +++ b/pkg/securitycontext/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext contains the security context provider implementation +package securitycontext diff --git a/pkg/securitycontext/fake.go b/pkg/securitycontext/fake.go new file mode 100644 index 000000000000..5f6307a9188f --- /dev/null +++ b/pkg/securitycontext/fake.go @@ -0,0 +1,36 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" + + docker "github.com/fsouza/go-dockerclient" +) + +type FakeSecurityContextProvider struct{} + +func (p *FakeSecurityContextProvider) ApplySecurityContext(pod *api.Pod) {} +func (p *FakeSecurityContextProvider) ValidateSecurityContext(pod *api.Pod) fielderrors.ValidationErrorList { + return nil +} +func (p *FakeSecurityContextProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) { +} +func (p *FakeSecurityContextProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) error { + return nil +} diff --git a/pkg/securitycontext/permitprovider.go b/pkg/securitycontext/permitprovider.go new file mode 100644 index 000000000000..da01e01f0942 --- /dev/null +++ b/pkg/securitycontext/permitprovider.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + docker "github.com/fsouza/go-dockerclient" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +type permitProvider struct { + *api.SecurityConstraints +} + +func NewPermitSecurityContextProvider(securityConstraints api.SecurityConstraints) SecurityContextProvider { + securityConstraints.EnforcementPolicy = api.SecurityConstraintPolicyDisable + return &permitProvider{ + SecurityConstraints: &securityConstraints, + } +} + +func (p *permitProvider) ApplySecurityContext(pod *api.Pod) {} +func (p *permitProvider) ValidateSecurityContext(pod *api.Pod) fielderrors.ValidationErrorList { + return nil +} +func (p *permitProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) { +} +func (p *permitProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) error { + return nil +} diff --git a/pkg/securitycontext/restrictprovider.go b/pkg/securitycontext/restrictprovider.go new file mode 100644 index 000000000000..e00cfe71e26b --- /dev/null +++ b/pkg/securitycontext/restrictprovider.go @@ -0,0 +1,310 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + "fmt" + "strconv" + + docker "github.com/fsouza/go-dockerclient" + "github.com/golang/glog" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +type provider struct { + *api.SecurityConstraints +} + +// NewRestrictSecurityContextProvider creates a new security context provider with the default constraints +func NewRestrictSecurityContextProvider(securityConstraints api.SecurityConstraints) SecurityContextProvider { + return &provider{ + SecurityConstraints: &securityConstraints, + } +} + +// ApplySecurityContext ensures that each container in the pod has a security context set and that it +// complies with the policy defined by the SecurityConstraints of the provider +func (p *provider) ApplySecurityContext(pod *api.Pod) { + for idx := range pod.Spec.Containers { + c := &pod.Spec.Containers[idx] + glog.V(4).Infof("Applying security constraints to %s", c.Name) + p.applySecurityContextToContainer(c) + } +} + +// applySecurityContextToContainer applies each section of the security context to the container. As more options +// become available they should be added here with corresponding application methods. +func (p *provider) applySecurityContextToContainer(c *api.Container) { + p.applyPrivileged(c) + p.applyCapRequests(c) + p.applySELinux(c) + p.applyRunAsUser(c) +} + +// applyRunAsUser will set the RunAsUser to 0 or p.DefaultSecurityContext.RunAsUser (if DefaultSecurityContext is not nil) +// if the constraints do not allow run as user requests +func (p *provider) applyRunAsUser(c *api.Container) { + if !p.SecurityConstraints.AllowRunAsUser { + if p.DefaultSecurityContext != nil { + c.SecurityContext.RunAsUser = p.DefaultSecurityContext.RunAsUser + } else { + c.SecurityContext.RunAsUser = 0 + } + } +} + +// applySELinux will: +// 1. if there are not selinux options on the security constraints: take no action +// 2. if there are selinux options on the security constraints AND there are no options defined on the container +// AND there is a default security context then use all the default settings +// 3. if there are selinux options on the security constraints AND options on the container then check each +// setting individually. If the individual setting is not allowed then remove it or set it to the default if one exists +func (p *provider) applySELinux(container *api.Container) { + // no security context settings for SELinux + if p.SecurityConstraints.SELinux == nil { + return + } + + constraints := p.SecurityConstraints + hasDefault := constraints.DefaultSecurityContext != nil + hasDefaultSELinux := hasDefault && constraints.DefaultSecurityContext.SELinuxOptions != nil + + // if the container has not defined SELinux options then apply the default if it exists + if container.SecurityContext.SELinuxOptions == nil { + if hasDefault { + glog.V(4).Infof("Setting default SELinux options for container %s", container.Name) + container.SecurityContext.SELinuxOptions = constraints.DefaultSecurityContext.SELinuxOptions + } + return + } + + // check individual settings of the container's request + if !constraints.SELinux.AllowDisable && container.SecurityContext.SELinuxOptions.Disabled { + glog.V(4).Infof("Resetting SELinuxOptions.Disabled for %s", container.Name) + container.SecurityContext.SELinuxOptions.Disabled = false + } + + if !constraints.SELinux.AllowLevelLabel { + glog.V(4).Infof("Resetting SELinuxOptions.Level for %s", container.Name) + level := "" + if hasDefault && hasDefaultSELinux { + level = constraints.DefaultSecurityContext.SELinuxOptions.Level + } + container.SecurityContext.SELinuxOptions.Level = level + } + + if !constraints.SELinux.AllowRoleLabel { + glog.V(4).Infof("Resetting SELinuxOptions.Role for %s", container.Name) + role := "" + if hasDefault && hasDefaultSELinux { + role = constraints.DefaultSecurityContext.SELinuxOptions.Role + } + container.SecurityContext.SELinuxOptions.Role = role + } + + if !constraints.SELinux.AllowTypeLabel { + glog.V(4).Infof("Resetting SELinuxOptions.Type for %s", container.Name) + typeLabel := "" + if hasDefault && hasDefaultSELinux { + typeLabel = constraints.DefaultSecurityContext.SELinuxOptions.Type + } + container.SecurityContext.SELinuxOptions.Type = typeLabel + } + + if !constraints.SELinux.AllowUserLabel { + glog.V(4).Infof("Resetting SELinuxOptions.User for %s", container.Name) + user := "" + if hasDefault && hasDefaultSELinux { + user = constraints.DefaultSecurityContext.SELinuxOptions.User + } + container.SecurityContext.SELinuxOptions.User = user + } +} + +// applyCapRequests will take the following steps: +// 1. if the security context does not allow capability requests and the container has capability requests defined +// it will set them to the default settings. If no default settings exist it will remove all requests +// 2. if the security context allows capability requests it will remove any add/drop requests that are not in +// the allowed set of add/drops. +// +// NOTE: if cap requests are allowed and requests are defined you will not get the defaults in addition to the +// requested add/drop list. This is an override, not a additive operation. +func (p *provider) applyCapRequests(container *api.Container) { + context := p.SecurityConstraints + if !context.AllowCapabilities { + //if we don't allow cap requests and the container is requesting them then either use the default + //or remove them completely + if context.DefaultSecurityContext != nil && context.DefaultSecurityContext.Capabilities != nil { + glog.V(4).Infof("Resetting cap add/drop for %s to default settings", container.Name) + container.SecurityContext.Capabilities = context.DefaultSecurityContext.Capabilities + } else { + if container.SecurityContext.Capabilities != nil { + glog.V(4).Infof("Removing cap add/drop for %s", container.Name) + container.SecurityContext.Capabilities = &api.Capabilities{} + } + } + + } else { + //otherwise check each request to see if it is allowed. If we haven't defined any cap restrictions + //then there is nothing to do + if context.Capabilities != nil && container.SecurityContext.Capabilities != nil { + container.SecurityContext.Capabilities.Add = p.filterCapabilities(container.SecurityContext.Capabilities.Add, context.Capabilities.Add) + container.SecurityContext.Capabilities.Drop = p.filterCapabilities(container.SecurityContext.Capabilities.Drop, context.Capabilities.Drop) + } + } +} + +// applyPrivileged will ensure that if a container is not allowed to make privileged container requests +// the setting will be reset to false +func (p *provider) applyPrivileged(container *api.Container) { + if !p.SecurityConstraints.AllowPrivileged && container.SecurityContext.Privileged { + glog.V(4).Infof("Resetting privileged for %s", container.Name) + container.SecurityContext.Privileged = false + } +} + +// filterCapabilities filters the capability requests based on what is allowed. +func (p *provider) filterCapabilities(capRequests, allowedCaps []api.CapabilityType) []api.CapabilityType { + filteredCaps := make([]api.CapabilityType, 0) + +outer: + for _, cap := range capRequests { + for _, allowed := range allowedCaps { + if cap == allowed { + filteredCaps = append(filteredCaps, cap) + continue outer + } + } + } + return filteredCaps +} + +func (p *provider) ValidateSecurityContext(pod *api.Pod) fielderrors.ValidationErrorList { + if p.SecurityConstraints.EnforcementPolicy == api.SecurityConstraintPolicyDisable { + glog.V(4).Info("Security constraint policy is disabled, validation is skipped") + return nil + } + + allErrs := fielderrors.ValidationErrorList{} + + for _, container := range pod.Spec.Containers { + if !p.SecurityConstraints.AllowPrivileged && container.SecurityContext.Privileged { + field := fmt.Sprintf("%s.SecurityContext.Privileged", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.Privileged, "SecurityContext does not allow privileged containers")) + } + + if !p.SecurityConstraints.AllowCapabilities { + if container.SecurityContext.Capabilities != nil && + (len(container.SecurityContext.Capabilities.Add) > 0 || len(container.SecurityContext.Capabilities.Drop) > 0) { + field := fmt.Sprintf("%s.SecurityContext.Capabilities", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.Capabilities, "SecurityContext does not allow capability requests")) + } + } + + if !p.SecurityConstraints.AllowRunAsUser && container.SecurityContext.RunAsUser > 0 { + field := fmt.Sprintf("%s.SecurityContext.RunAsUser", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.RunAsUser, "SecurityContext does not allow run as user requests")) + } + + if p.SecurityConstraints.SELinux == nil { + if container.SecurityContext.SELinuxOptions != nil { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions, "SecurityContext does not allow SELinux requests")) + } + } else { + if container.SecurityContext.SELinuxOptions != nil { + if !p.SecurityConstraints.SELinux.AllowDisable && container.SecurityContext.SELinuxOptions.Disabled { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions.Disable", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions.Disabled, "SecurityContext does not allow this setting")) + } + + if !p.SecurityConstraints.SELinux.AllowLevelLabel && len(container.SecurityContext.SELinuxOptions.Level) > 0 { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions.Level", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions.Level, "SecurityContext does not allow this setting")) + } + + if !p.SecurityConstraints.SELinux.AllowRoleLabel && len(container.SecurityContext.SELinuxOptions.Role) > 0 { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions.Role", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions.Role, "SecurityContext does not allow this setting")) + } + + if !p.SecurityConstraints.SELinux.AllowTypeLabel && len(container.SecurityContext.SELinuxOptions.Type) > 0 { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions.Type", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions.Type, "SecurityContext does not allow this setting")) + } + + if !p.SecurityConstraints.SELinux.AllowUserLabel && len(container.SecurityContext.SELinuxOptions.User) > 0 { + field := fmt.Sprintf("%s.SecurityContext.SELinuxOptions.User", container.Name) + allErrs = append(allErrs, fielderrors.NewFieldInvalid(field, container.SecurityContext.SELinuxOptions.User, "SecurityContext does not allow this setting")) + } + } + } + } + + return allErrs +} + +func (p *provider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) error { + config.User = strconv.FormatInt(container.SecurityContext.RunAsUser, 10) + return nil +} + +func (p *provider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) { + hostConfig.Privileged = container.SecurityContext.Privileged + + if container.SecurityContext.Capabilities != nil { + add, drop := makeCapabilites(container.SecurityContext.Capabilities.Add, container.SecurityContext.Capabilities.Drop) + hostConfig.CapAdd = add + hostConfig.CapDrop = drop + } + + if container.SecurityContext.SELinuxOptions != nil { + if container.SecurityContext.SELinuxOptions.Disabled { + hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, dockerLabelDisable) + } else { + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelUser, container.SecurityContext.SELinuxOptions.User) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelRole, container.SecurityContext.SELinuxOptions.Role) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelType, container.SecurityContext.SELinuxOptions.Type) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelLevel, container.SecurityContext.SELinuxOptions.Level) + } + } +} + +func modifySecurityOption(config []string, name, value string) []string { + if len(name) > 0 && len(value) > 0 { + config = append(config, fmt.Sprintf("%s:%s", name, value)) + } + + return config +} + +//TODO copied from manager.go, manager.go can be updated since it will no longer need to apply caps itself +func makeCapabilites(capAdd []api.CapabilityType, capDrop []api.CapabilityType) ([]string, []string) { + var ( + addCaps []string + dropCaps []string + ) + for _, cap := range capAdd { + addCaps = append(addCaps, string(cap)) + } + for _, cap := range capDrop { + dropCaps = append(dropCaps, string(cap)) + } + return addCaps, dropCaps +} diff --git a/pkg/securitycontext/restrictprovider_test.go b/pkg/securitycontext/restrictprovider_test.go new file mode 100644 index 000000000000..ccea45cb5aac --- /dev/null +++ b/pkg/securitycontext/restrictprovider_test.go @@ -0,0 +1,570 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func TestFilterCapabilities(t *testing.T) { + testCases := []struct { + name string + allowed []api.CapabilityType + requested []api.CapabilityType + expected []api.CapabilityType + }{ + { + "Empty allowed", + []api.CapabilityType{}, + []api.CapabilityType{}, + []api.CapabilityType{}, + }, + { + "Remove disallowed", + []api.CapabilityType{"a"}, + []api.CapabilityType{"a", "b"}, + []api.CapabilityType{"a"}, + }, + { + "Filter multiple", + []api.CapabilityType{"a", "c"}, + []api.CapabilityType{"a", "b", "c", "d"}, + []api.CapabilityType{"a", "c"}, + }, + } + + scp := provider{} + + for _, tc := range testCases { + actual := scp.filterCapabilities(tc.requested, tc.allowed) + if !reflect.DeepEqual(tc.expected, actual) { + t.Errorf("Failed to filter caps correctly for tc: %s. Expected: %v but got %v", tc.name, tc.expected, actual) + } + } +} + +func TestApplySecurityOption(t *testing.T) { + testCases := []struct { + name string + config []string + optName string + optVal string + expected []string + }{ + { + "Empty name", + []string{"a", "b"}, + "", + "valA", + []string{"a", "b"}, + }, + { + "Empty val", + []string{"a", "b"}, + "optA", + "", + []string{"a", "b"}, + }, + { + "Valid", + []string{"a", "b"}, + "c", + "d", + []string{"a", "b", "c:d"}, + }, + } + + for _, tc := range testCases { + actual := modifySecurityOption(tc.config, tc.optName, tc.optVal) + if !reflect.DeepEqual(tc.expected, actual) { + t.Errorf("Failed to apply options correctly for tc: %S. Expected: %v but got %v", tc.name, tc.expected, actual) + } + } +} + +func TestApplyPrivileged(t *testing.T) { + testCases := []struct { + name string + constraintSetting bool + containerSetting bool + expectedResult bool + }{ + {"Constraint allowed, pod requested", true, true, true}, + {"Constraint allowed, pod not requested", true, false, false}, + {"Constraint disallowed, pod requested", false, true, false}, + {"Constraint disallowed, pod not requested", false, false, false}, + } + + scp := provider{&api.SecurityConstraints{}} + + for _, tc := range testCases { + scp.SecurityConstraints.AllowPrivileged = tc.constraintSetting + container := &api.Container{ + SecurityContext: api.SecurityContext{ + Privileged: tc.containerSetting, + }, + } + scp.applyPrivileged(container) + + if container.SecurityContext.Privileged != tc.expectedResult { + t.Errorf("Failed to set privileged correctly for tc: %s. Expected %s, got %s", tc.name, tc.expectedResult, container.SecurityContext.Privileged) + } + } +} + +func TestApplyCapRequests(t *testing.T) { + testCases := []struct { + name string + securityContraints *api.SecurityConstraints + containerSecurityContext api.SecurityContext + expectedSecurityContext api.SecurityContext + }{ + { + //not testing nil, the entry method (ApplySecurityContext) ensures that if the container has + //a nil context then the default one is added + name: "context that doesn't allow, no default caps, no container requests", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: false, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{}, + }, + { + name: "context that doesn't allow, no default caps, container requests", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: false, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{Capabilities: &api.Capabilities{}}, + }, + { + name: "context that doesn't allow, default caps, no container requests", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: false, + DefaultSecurityContext: &api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"a"}, + Drop: []api.CapabilityType{"b"}, + }, + }, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"a"}, + Drop: []api.CapabilityType{"b"}, + }, + }, + }, + { + name: "context that doesn't allow, default caps, container requests", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: false, + DefaultSecurityContext: &api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"a"}, + Drop: []api.CapabilityType{"b"}, + }, + }, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"a"}, + Drop: []api.CapabilityType{"b"}, + }, + }, + }, + { + name: "filter with no whitelists", //everything should be allowed + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: true, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + }, + { + name: "filter with empty whitelists", //everything should be filtered out since nothing is allowed + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: true, + Capabilities: &api.Capabilities{}, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{}, + Drop: []api.CapabilityType{}, + }, + }, + }, + { + name: "filter add whitelists", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: true, + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + }, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo", "anotherFoo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{}, + }, + }, + }, + { + name: "filter drop whitelists", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: true, + Capabilities: &api.Capabilities{ + Drop: []api.CapabilityType{"bar"}, + }, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar", "anotherBar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + }, + { + name: "filter both whitelists", + securityContraints: &api.SecurityConstraints{ + AllowCapabilities: true, + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + containerSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo", "anotherFoo"}, + Drop: []api.CapabilityType{"bar", "anotherBar"}, + }, + }, + expectedSecurityContext: api.SecurityContext{ + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + }, + }, + } + + scp := provider{} + container := api.Container{} + for _, tc := range testCases { + scp.SecurityConstraints = tc.securityContraints + container.SecurityContext = tc.containerSecurityContext + scp.applyCapRequests(&container) + + if !reflect.DeepEqual(tc.expectedSecurityContext.Capabilities, container.SecurityContext.Capabilities) { + t.Errorf("Unexpected capabilities result for tc: %s. Expected %+v, got %+v", tc.name, tc.expectedSecurityContext.Capabilities, container.SecurityContext.Capabilities) + } + } +} + +func TestApplySELinux(t *testing.T) { + testCases := []struct { + name string + securityContraints *api.SecurityConstraints + containerSecurityContext api.SecurityContext + expectedSecurityContext api.SecurityContext + }{ + { + name: "context that doesn't allow, no default, no container requests", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, false, false}, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{}, + }, + { + //everything should be removed + name: "context that doesn't allow, no default, container requests", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, false, false}, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"", "", "", "", false}, + }, + }, + { + name: "context doesn't allow, default, no container requests", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, false, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + { + name: "context doesn't allow, default, container requests", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, false, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"a", "b", "c", "d", true}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + { + name: "context allows, container requests disable", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, false, true}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"", "", "", "", true}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", true}, + }, + }, + { + name: "context allows, container requests level", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, false, true, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"", "", "", "test", false}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "test", false}, + }, + }, + { + name: "context allows, container requests role", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, true, false, false, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"", "test", "", "", false}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "test", "type", "level", false}, + }, + }, + { + name: "context allows, container requests type", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{false, false, true, false, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"", "", "test", "", false}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "test", "level", false}, + }, + }, + { + name: "context allows, container requests user", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{true, false, false, false, false}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"test", "", "", "", false}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"test", "role", "type", "level", false}, + }, + }, + { + name: "context allows, full override", + securityContraints: &api.SecurityConstraints{ + SELinux: &api.SELinuxSecurityConstraints{true, true, true, true, true}, + DefaultSecurityContext: &api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"user", "role", "type", "level", false}, + }, + }, + containerSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"test", "test", "test", "test", true}, + }, + expectedSecurityContext: api.SecurityContext{ + SELinuxOptions: &api.SELinuxOptions{"test", "test", "test", "test", true}, + }, + }, + } + + scp := provider{} + container := api.Container{} + for _, tc := range testCases { + scp.SecurityConstraints = tc.securityContraints + container.SecurityContext = tc.containerSecurityContext + scp.applySELinux(&container) + + if !reflect.DeepEqual(tc.expectedSecurityContext.SELinuxOptions, container.SecurityContext.SELinuxOptions) { + t.Errorf("Unexpected SELinux result for tc: %s. Expected %+v, got %+v", tc.name, tc.expectedSecurityContext.SELinuxOptions, container.SecurityContext.SELinuxOptions) + } + } +} + +func TestApplyRunAsUser(t *testing.T) { + testCases := []struct { + name string + securityContraints *api.SecurityConstraints + containerSecurityContext api.SecurityContext + expectedSecurityContext api.SecurityContext + }{ + { + name: "context that doesn't allow, no default, no container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: false, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 0}, + }, + { + name: "context that doesn't allow, no default, container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: false, + }, + containerSecurityContext: api.SecurityContext{RunAsUser: 888}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 0}, + }, + { + name: "context that doesn't allow, default, container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: false, + DefaultSecurityContext: &api.SecurityContext{RunAsUser: 999}, + }, + containerSecurityContext: api.SecurityContext{RunAsUser: 0}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 999}, + }, + { + name: "context that doesn't allow, default, container doesn't request", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: false, + DefaultSecurityContext: &api.SecurityContext{RunAsUser: 999}, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 999}, + }, + { + name: "context allows, no default, no container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: true, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 0}, + }, + { + name: "context allows, no default, container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: true, + }, + containerSecurityContext: api.SecurityContext{RunAsUser: 888}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 888}, + }, + { + name: "context allows, default, container requests", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: true, + DefaultSecurityContext: &api.SecurityContext{RunAsUser: 999}, + }, + containerSecurityContext: api.SecurityContext{RunAsUser: 888}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 888}, + }, + { + name: "context allows, default, container doesn't request", + securityContraints: &api.SecurityConstraints{ + AllowRunAsUser: true, + DefaultSecurityContext: &api.SecurityContext{RunAsUser: 999}, + }, + containerSecurityContext: api.SecurityContext{}, + expectedSecurityContext: api.SecurityContext{RunAsUser: 999}, + }, + } + + scp := provider{} + container := api.Container{} + for _, tc := range testCases { + scp.SecurityConstraints = tc.securityContraints + container.SecurityContext = tc.containerSecurityContext + scp.applySELinux(&container) + + if !reflect.DeepEqual(tc.expectedSecurityContext.SELinuxOptions, container.SecurityContext.SELinuxOptions) { + t.Errorf("Unexpected RunAsUser result for tc: %s. Expected %+v, got %+v", tc.name, tc.expectedSecurityContext.RunAsUser, container.SecurityContext.RunAsUser) + } + } +} diff --git a/pkg/securitycontext/system.go b/pkg/securitycontext/system.go new file mode 100644 index 000000000000..6bd066b239e0 --- /dev/null +++ b/pkg/securitycontext/system.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 Google Inc. 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. +*/ + +// This is a temporary holder for the security constraint settings so it can be initialized once and used throughout the +// system as we currently did with the capabilities package. Eventually this will belong to a namespace/service account +// with a system wide context for general settings. This replaces the Capabilities system settings that were used +// system wide for allowing/disallowing privileged container requests and setting up the pod sources that are allowed +// to use host networking +package securitycontext + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "sync" +) + +var once sync.Once +var securityConstraints *api.SecurityConstraints + +// Initialize the capability set. This can only be done once per binary, subsequent calls are ignored. +func Initialize(s api.SecurityConstraints) { + // Only do this once + once.Do(func() { + // we expect the security constraints to be initialized with the AllowCapabilities and AllowPrivileged options since + // they are preexisting system options. We will initialize everything else to some defaults for now unless they + // are already set. + if len(s.EnforcementPolicy) == 0 { + s.EnforcementPolicy = api.SecurityConstraintPolicyReject + } + if s.SELinux == nil { + s.SELinux = &api.SELinuxSecurityConstraints{ + AllowUserLabel: true, + AllowRoleLabel: true, + AllowTypeLabel: true, + AllowLevelLabel: true, + AllowDisable: true, + } + } + if s.DefaultSecurityContext == nil { + s.DefaultSecurityContext = &api.SecurityContext{ + Privileged: false, + } + } + + securityConstraints = &s + }) +} + +// Setup the capability set. It wraps Initialize for improving usibility. +func Setup(allowPrivileged bool, hostNetworkSources []string) { + Initialize(api.SecurityConstraints{ + AllowPrivileged: allowPrivileged, + HostNetworkSources: hostNetworkSources, + }) +} + +// SetCapabilitiesForTests. Convenience method for testing. This should only be called from tests. +func SetForTests(s api.SecurityConstraints) { + securityConstraints = &s +} + +// Returns a read-only copy of the system capabilities. +func Get() api.SecurityConstraints { + if securityConstraints == nil { + Initialize(api.SecurityConstraints{ + AllowPrivileged: false, + HostNetworkSources: []string{}, + }) + } + return *securityConstraints +} diff --git a/pkg/securitycontext/types.go b/pkg/securitycontext/types.go new file mode 100644 index 000000000000..0afc608bbc00 --- /dev/null +++ b/pkg/securitycontext/types.go @@ -0,0 +1,54 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" + + docker "github.com/fsouza/go-dockerclient" +) + +type SecurityContextProvider interface { + // ApplySecurityContext ensures that the security context for a pod is set + ApplySecurityContext(pod *api.Pod) + + // ValidateSecurityContext checks if the pod's containers comply with the security context + ValidateSecurityContext(pod *api.Pod) fielderrors.ValidationErrorList + + // ModifyContainerConfig is called before the Docker createContainer call. + // The security context provider can make changes to the Config with which + // the container is created. + // An error is returned if it's not possible to secure the container as + // requested with a security context. + ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) error + + // ModifyHostConfig is called before the Docker runContainer call. + // The security context provider can make changes to the HostConfig, affecting + // security options, whether the container is privileged, volume binds, etc. + // An error is returned if it's not possible to secure the container as requested + // with a security context. + ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) +} + +const ( + dockerLabelUser string = "label:user" + dockerLabelRole string = "label:role" + dockerLabelType string = "label:type" + dockerLabelLevel string = "label:level" + dockerLabelDisable string = "label:disable" +) diff --git a/plugin/pkg/admission/securitycontext/admission.go b/plugin/pkg/admission/securitycontext/admission.go new file mode 100644 index 000000000000..e2b8f2d47476 --- /dev/null +++ b/plugin/pkg/admission/securitycontext/admission.go @@ -0,0 +1,86 @@ +/* +Copyright 2014 Google Inc. 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 securitycontext + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + + "fmt" + scapi "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" + "github.com/golang/glog" +) + +func init() { + admission.RegisterPlugin("SecurityContext", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewSecurityContext(client), nil + }) +} + +type plugin struct { + client client.Interface +} + +func NewSecurityContext(client client.Interface) admission.Interface { + return &plugin{client} +} + +func (p *plugin) Admit(a admission.Attributes) (err error) { + if a.GetOperation() == "DELETE" { + return nil + } + if a.GetResource() != string(api.ResourcePods) { + return nil + } + + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + scp := p.getSecurityContextProvider(pod) + if scp == nil { + return nil + } + + errs := scp.ValidateSecurityContext(pod) + + //disable enforcement doesn't return errors so this won't fail even if policy is broken + if len(errs) > 0 { + glog.Warningf("Initial validation of pod %s/%s failed, applying defaults. Broken policy: %v", pod.Namespace, pod.Name, errs) + } + + //always apply defaults + scp.ApplySecurityContext(pod) + + //revalidate, if policy is still broken then do not admit + if errs := scp.ValidateSecurityContext(pod); len(errs) > 0 { + msg := fmt.Sprintf("Validation failed even after default security context was applied for pod %s/%s. Broken policy: %v", pod.Namespace, pod.Name, errs) + return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf(msg)) + } + + return nil +} + +func (q *plugin) getSecurityContextProvider(pod *api.Pod) scapi.SecurityContextProvider { + //TODO this should be retrieved from the service account + // return scapi.NewPermitSecurityContextProvider() + return scapi.NewRestrictSecurityContextProvider(scapi.Get()) +} diff --git a/pkg/capabilities/doc.go b/plugin/pkg/admission/securitycontext/admission_test.go similarity index 87% rename from pkg/capabilities/doc.go rename to plugin/pkg/admission/securitycontext/admission_test.go index 7ab1949869be..b6822c997d3d 100644 --- a/pkg/capabilities/doc.go +++ b/plugin/pkg/admission/securitycontext/admission_test.go @@ -14,5 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -// package capbabilities manages system level capabilities -package capabilities +package securitycontext diff --git a/plugin/pkg/admission/securitycontext/doc.go b/plugin/pkg/admission/securitycontext/doc.go new file mode 100644 index 000000000000..99059417f7b9 --- /dev/null +++ b/plugin/pkg/admission/securitycontext/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 Google Inc. 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. +*/ + +// securitycontext enforces all incoming requests against any applied +// security contexts +package securitycontext