security context initial implementation - squash

pull/6/head
Paul Weil 2015-05-05 12:37:23 -04:00
parent 20ea35105d
commit 982bf19c20
47 changed files with 2359 additions and 606 deletions

View File

@ -72,4 +72,4 @@ DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota

View File

@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1
ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}"
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota

View File

@ -111,4 +111,4 @@ DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota,

View File

@ -70,4 +70,4 @@ DNS_SERVER_IP="10.0.0.10"
DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,ResourceQuota
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota

View File

@ -50,7 +50,7 @@ MASTER_USER=vagrant
MASTER_PASSWD=vagrant
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
# Optional: Install node monitoring.
ENABLE_NODE_MONITORING=true

View File

@ -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/scdeny"
)

View File

@ -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,SecurityContextDeny,ResourceQuota
APISERVER_LOG=/tmp/kube-apiserver.log
sudo -E "${GO_OUT}/kube-apiserver" \

View File

@ -204,6 +204,17 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
ev.ValueFrom.FieldRef.FieldPath = c.RandString()
}
},
func(sc *api.SecurityContext, c fuzz.Continue) {
c.FuzzNoCustom(sc) // fuzz self without calling this function again
priv := c.RandBool()
sc.Privileged = &priv
sc.Capabilities = &api.Capabilities{
Add: make([]api.CapabilityType, 0),
Drop: make([]api.CapabilityType, 0),
}
c.Fuzz(&sc.Capabilities.Add)
c.Fuzz(&sc.Capabilities.Drop)
},
func(e *api.Event, c fuzz.Continue) {
c.FuzzNoCustom(e) // fuzz self without calling this function again
// Fix event count to 1, otherwise, if a v1beta1 or v1beta2 event has a count set arbitrarily, it's count is ignored

View File

@ -623,12 +623,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"`
// Optional: SecurityContext defines the security options the pod should be run with
SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"`
}
// Handler defines a specific action that should be taken
@ -1876,3 +1874,37 @@ type ComponentStatusList struct {
Items []ComponentStatus `json:"items"`
}
// SecurityContext holds security configuration that will be applied to a container. SecurityContext
// contains duplication of some existing fields from the Container resource. These duplicate fields
// will be populated based on the Container configuration if they are not set. Defining them on
// both the Container AND the SecurityContext will result in an error.
type SecurityContext struct {
// Capabilities are the capabilities to add/drop when running the container
Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"`
// Run the container in privileged mode
Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"`
// SELinuxOptions are the labels to be applied to the container
// and volumes
SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"`
// RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"`
}
// SELinuxOptions are the labels to be applied to the container.
type SELinuxOptions struct {
// SELinux user label
User string `json:"user,omitempty" description:"the user label to apply to the container"`
// SELinux role label
Role string `json:"role,omitempty" description:"the role label to apply to the container"`
// SELinux type label
Type string `json:"type,omitempty" description:"the type label to apply to the container"`
// SELinux level label.
Level string `json:"level,omitempty" description:"the level label to apply to the container"`
}

View File

@ -18,6 +18,7 @@ package v1
import (
"fmt"
"reflect"
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
@ -237,9 +238,22 @@ func init() {
return err
}
out.TerminationMessagePath = in.TerminationMessagePath
out.Privileged = in.Privileged
out.ImagePullPolicy = newer.PullPolicy(in.ImagePullPolicy)
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if in.SecurityContext != nil {
if in.SecurityContext.Capabilities != nil {
if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) ||
!reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) {
return fmt.Errorf("container capability settings do not match security context settings, cannot convert")
}
}
if in.SecurityContext.Privileged != nil {
if in.Privileged != *in.SecurityContext.Privileged {
return fmt.Errorf("container privileged settings do not match security context settings, cannot convert")
}
}
}
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
return nil
@ -297,11 +311,19 @@ func init() {
return err
}
out.TerminationMessagePath = in.TerminationMessagePath
out.Privileged = in.Privileged
out.ImagePullPolicy = PullPolicy(in.ImagePullPolicy)
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Privileged != nil {
out.Privileged = *out.SecurityContext.Privileged
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil {
out.Capabilities = *out.SecurityContext.Capabilities
}
return nil
},
func(in *ContainerPort, out *newer.ContainerPort, s conversion.Scope) error {

View File

@ -45,3 +45,62 @@ func TestNodeConversion(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBadSecurityContextConversion(t *testing.T) {
priv := false
testCases := map[string]struct {
c *current.Container
err string
}{
// this use case must use true for the container and false for the sc. Otherwise the defaulter
// will assume privileged was left undefined (since it is the default value) and copy the
// sc setting upwards
"mismatched privileged": {
c: &current.Container{
Privileged: true,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
err: "container privileged settings do not match security context settings, cannot convert",
},
"mismatched caps add": {
c: &current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
"mismatched caps drop": {
c: &current.Container{
Capabilities: current.Capabilities{
Drop: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Drop: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
}
for k, v := range testCases {
got := newer.Container{}
err := newer.Scheme.Convert(v.c, &got)
if err == nil {
t.Errorf("expected error for case %s but got none", k)
} else {
if err.Error() != v.err {
t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error())
}
}
}
}

View File

@ -19,6 +19,8 @@ package v1
import (
"strings"
"github.com/golang/glog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
@ -66,6 +68,7 @@ func init() {
if obj.TerminationMessagePath == "" {
obj.TerminationMessagePath = TerminationMessagePathDefault
}
defaultSecurityContext(obj)
},
func(obj *ServiceSpec) {
if obj.SessionAffinity == "" {
@ -156,3 +159,44 @@ func defaultHostNetworkPorts(containers *[]Container) {
}
}
}
// defaultSecurityContext performs the downward and upward merges of a pod definition
func defaultSecurityContext(container *Container) {
if container.SecurityContext == nil {
glog.V(4).Infof("creating security context for container %s", container.Name)
container.SecurityContext = &SecurityContext{}
}
// if there are no capabilities defined on the SecurityContext then copy the container settings
if container.SecurityContext.Capabilities == nil {
glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name)
container.SecurityContext.Capabilities = &container.Capabilities
} else {
// if there are capabilities defined on the security context and the container setting is
// empty then assume that it was left off the pod definition and ensure that the container
// settings match the security context settings (checked by the convert functions). If
// there are settings in both then don't touch it, the converter will error if they don't
// match
if len(container.Capabilities.Add) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name)
container.Capabilities.Add = container.SecurityContext.Capabilities.Add
}
if len(container.Capabilities.Drop) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name)
container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop
}
}
// if there are no privileged settings on the security context then copy the container settings
if container.SecurityContext.Privileged == nil {
glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name)
container.SecurityContext.Privileged = &container.Privileged
} else {
// we don't have a good way to know if container.Privileged was set or just defaulted to false
// so the best we can do here is check if the securityContext is set to true and the
// container is set to false and assume that the Privileged field was left off the container
// definition and not an intentional mismatch
if *container.SecurityContext.Privileged && !container.Privileged {
glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name)
container.Privileged = *container.SecurityContext.Privileged
}
}
}

View File

@ -349,3 +349,104 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
t.Errorf("Expected default APIVersion v1, got: %v", apiVersion)
}
}
func TestSetDefaultSecurityContext(t *testing.T) {
priv := false
privTrue := true
testCases := map[string]struct {
c current.Container
}{
"downward defaulting caps": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
},
"downward defaulting priv": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
"upward defaulting caps": {
c: current.Container{
Privileged: false,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"biz"},
Drop: []current.CapabilityType{"baz"},
},
},
},
},
"upward defaulting priv": {
c: current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &privTrue,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
}
pod := &current.Pod{
Spec: current.PodSpec{},
}
for k, v := range testCases {
pod.Spec.Containers = []current.Container{v.c}
obj := roundTrip(t, runtime.Object(pod))
defaultedPod := obj.(*current.Pod)
c := defaultedPod.Spec.Containers[0]
if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual {
t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues)
}
}
}
func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) {
issues := make([]string, 0)
equal := true
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil {
equal = false
issues = append(issues, "Expected non nil settings for SecurityContext")
return equal, issues
}
if *c.SecurityContext.Privileged != c.Privileged {
equal = false
issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value")
}
if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings")
}
if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings")
}
return equal, issues
}

View File

@ -636,12 +636,14 @@ 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"`
// Deprecated - see SecurityContext. Optional: Default to false.
Privileged bool `json:"privileged,omitempty" description:"hether or not the container is granted privileged status; defaults to false; cannot be updated; deprecated; See SecurityContext"`
// 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"`
// Deprecated - see SecurityContext. Optional: Capabilities for container.
Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"`
// Optional: SecurityContext defines the security options the pod should be run with
SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"`
}
// Handler defines a specific action that should be taken
@ -1735,3 +1737,39 @@ type ComponentStatusList struct {
Items []ComponentStatus `json:"items" description:"list of component status objects"`
}
// SecurityContext holds security configuration that will be applied to a container. SecurityContext
// contains duplication of some existing fields from the Container resource. These duplicate fields
// will be populated based on the Container configuration if they are not set. Defining them on
// both the Container AND the SecurityContext will result in an error.
type SecurityContext struct {
// Capabilities are the capabilities to add/drop when running the container
// Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset
Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"`
// Run the container in privileged mode
// Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset
Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"`
// SELinuxOptions are the labels to be applied to the container
// and volumes
SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"`
// RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"`
}
// SELinuxOptions are the labels to be applied to the container
type SELinuxOptions struct {
// SELinux user label
User string `json:"user,omitempty" description:"the user label to apply to the container"`
// SELinux role label
Role string `json:"role,omitempty" description:"the role label to apply to the container"`
// SELinux type label
Type string `json:"type,omitempty" description:"the type label to apply to the container"`
// SELinux level label.
Level string `json:"level,omitempty" description:"the level label to apply to the container"`
}

View File

@ -19,6 +19,7 @@ package v1beta1
import (
"fmt"
"net"
"reflect"
"strconv"
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
@ -579,15 +580,20 @@ func init() {
if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil {
return err
}
if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil {
return err
}
if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil {
return err
}
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Privileged != nil {
out.Privileged = *out.SecurityContext.Privileged
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil {
out.Capabilities = *out.SecurityContext.Capabilities
}
return nil
},
// Internal API does not support CPU to be specified via an explicit field.
@ -665,13 +671,23 @@ func init() {
if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil {
return err
}
if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil {
return err
}
if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil {
return err
}
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if in.SecurityContext != nil {
if in.SecurityContext.Capabilities != nil {
if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) ||
!reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) {
return fmt.Errorf("container capability settings do not match security context settings, cannot convert")
}
}
if in.SecurityContext.Privileged != nil {
if in.Privileged != *in.SecurityContext.Privileged {
return fmt.Errorf("container privileged settings do not match security context settings, cannot convert")
}
}
}
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
return nil

View File

@ -749,3 +749,63 @@ func TestSecretVolumeSourceConversion(t *testing.T) {
t.Errorf("Expected %v; got %v", given, got2)
}
}
func TestBadSecurityContextConversion(t *testing.T) {
priv := false
testCases := map[string]struct {
c *current.Container
err string
}{
// this use case must use true for the container and false for the sc. Otherwise the defaulter
// will assume privileged was left undefined (since it is the default value) and copy the
// sc setting upwards
"mismatched privileged": {
c: &current.Container{
Privileged: true,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
err: "container privileged settings do not match security context settings, cannot convert",
},
"mismatched caps add": {
c: &current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
"mismatched caps drop": {
c: &current.Container{
Capabilities: current.Capabilities{
Drop: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Drop: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
}
for k, v := range testCases {
got := newer.Container{}
err := Convert(v.c, &got)
if err == nil {
t.Errorf("expected error for case %s but got none", k)
} else {
if err.Error() != v.err {
t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error())
}
}
}
}

View File

@ -62,6 +62,7 @@ func init() {
if obj.TerminationMessagePath == "" {
obj.TerminationMessagePath = TerminationMessagePathDefault
}
defaultSecurityContext(obj)
},
func(obj *RestartPolicy) {
if util.AllPtrFieldsNil(obj) {
@ -194,3 +195,44 @@ func defaultHostNetworkPorts(containers *[]Container) {
}
}
}
// defaultSecurityContext performs the downward and upward merges of a pod definition
func defaultSecurityContext(container *Container) {
if container.SecurityContext == nil {
glog.V(4).Infof("creating security context for container %s", container.Name)
container.SecurityContext = &SecurityContext{}
}
// if there are no capabilities defined on the SecurityContext then copy the container settings
if container.SecurityContext.Capabilities == nil {
glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name)
container.SecurityContext.Capabilities = &container.Capabilities
} else {
// if there are capabilities defined on the security context and the container setting is
// empty then assume that it was left off the pod definition and ensure that the container
// settings match the security context settings (checked by the convert functions). If
// there are settings in both then don't touch it, the converter will error if they don't
// match
if len(container.Capabilities.Add) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name)
container.Capabilities.Add = container.SecurityContext.Capabilities.Add
}
if len(container.Capabilities.Drop) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name)
container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop
}
}
// if there are no privileged settings on the security context then copy the container settings
if container.SecurityContext.Privileged == nil {
glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name)
container.SecurityContext.Privileged = &container.Privileged
} else {
// we don't have a good way to know if container.Privileged was set or just defaulted to false
// so the best we can do here is check if the securityContext is set to true and the
// container is set to false and assume that the Privileged field was left off the container
// definition and not an intentional mismatch
if *container.SecurityContext.Privileged && !container.Privileged {
glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name)
container.Privileged = *container.SecurityContext.Privileged
}
}
}

View File

@ -340,3 +340,106 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
t.Errorf("Expected default APIVersion v1beta1, got: %v", apiVersion)
}
}
func TestSetDefaultSecurityContext(t *testing.T) {
priv := false
privTrue := true
testCases := map[string]struct {
c current.Container
}{
"downward defaulting caps": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
},
"downward defaulting priv": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
"upward defaulting caps": {
c: current.Container{
Privileged: false,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"biz"},
Drop: []current.CapabilityType{"baz"},
},
},
},
},
"upward defaulting priv": {
c: current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &privTrue,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
}
pod := &current.Pod{
DesiredState: current.PodState{
Manifest: current.ContainerManifest{},
},
}
for k, v := range testCases {
pod.DesiredState.Manifest.Containers = []current.Container{v.c}
obj := roundTrip(t, runtime.Object(pod))
defaultedPod := obj.(*current.Pod)
c := defaultedPod.DesiredState.Manifest.Containers[0]
if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual {
t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues)
}
}
}
func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) {
issues := make([]string, 0)
equal := true
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil {
equal = false
issues = append(issues, "Expected non nil settings for SecurityContext")
return equal, issues
}
if *c.SecurityContext.Privileged != c.Privileged {
equal = false
issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value")
}
if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings")
}
if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings")
}
return equal, issues
}

View File

@ -525,12 +525,14 @@ 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"`
// Deprecated - see SecurityContext. 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; deprecated; See SecurityContext"`
// 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"`
// Deprecated - see SecurityContext. Optional: Capabilities for container.
Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"`
// Optional: SecurityContext defines the security options the pod should be run with
SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"`
}
// Handler defines a specific action that should be taken
@ -1655,3 +1657,39 @@ type ComponentStatusList struct {
Items []ComponentStatus `json:"items" description:"list of component status objects"`
}
// SecurityContext holds security configuration that will be applied to a container. SecurityContext
// contains duplication of some existing fields from the Container resource. These duplicate fields
// will be populated based on the Container configuration if they are not set. Defining them on
// both the Container AND the SecurityContext will result in an error.
type SecurityContext struct {
// Capabilities are the capabilities to add/drop when running the container
// Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset
Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"`
// Run the container in privileged mode
// Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset
Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"`
// SELinuxOptions are the labels to be applied to the container
// and volumes
SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"`
// RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"`
}
// SELinuxOptions are the labels to be applied to the container.
type SELinuxOptions struct {
// SELinux user label
User string `json:"user,omitempty" description:"the user label to apply to the container"`
// SELinux role label
Role string `json:"role,omitempty" description:"the role label to apply to the container"`
// SELinux type label
Type string `json:"type,omitempty" description:"the type label to apply to the container"`
// SELinux level label.
Level string `json:"level,omitempty" description:"the level label to apply to the container"`
}

View File

@ -19,6 +19,7 @@ package v1beta2
import (
"fmt"
"net"
"reflect"
"strconv"
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
@ -357,15 +358,20 @@ func init() {
if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil {
return err
}
if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil {
return err
}
if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil {
return err
}
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Privileged != nil {
out.Privileged = *out.SecurityContext.Privileged
}
// now that we've converted set the container field from security context
if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil {
out.Capabilities = *out.SecurityContext.Capabilities
}
return nil
},
// Internal API does not support CPU to be specified via an explicit field.
@ -445,13 +451,23 @@ func init() {
if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil {
return err
}
if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil {
return err
}
if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil {
return err
}
if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil {
if in.SecurityContext != nil {
if in.SecurityContext.Capabilities != nil {
if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) ||
!reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) {
return fmt.Errorf("container capability settings do not match security context settings, cannot convert")
}
}
if in.SecurityContext.Privileged != nil {
if in.Privileged != *in.SecurityContext.Privileged {
return fmt.Errorf("container privileged settings do not match security context settings, cannot convert")
}
}
}
if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil {
return err
}
return nil

View File

@ -564,3 +564,63 @@ func TestSecretVolumeSourceConversion(t *testing.T) {
t.Errorf("Expected %v; got %v", given, got2)
}
}
func TestBadSecurityContextConversion(t *testing.T) {
priv := false
testCases := map[string]struct {
c *current.Container
err string
}{
// this use case must use true for the container and false for the sc. Otherwise the defaulter
// will assume privileged was left undefined (since it is the default value) and copy the
// sc setting upwards
"mismatched privileged": {
c: &current.Container{
Privileged: true,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
err: "container privileged settings do not match security context settings, cannot convert",
},
"mismatched caps add": {
c: &current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
"mismatched caps drop": {
c: &current.Container{
Capabilities: current.Capabilities{
Drop: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Drop: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
}
for k, v := range testCases {
got := newer.Container{}
err := newer.Scheme.Convert(v.c, &got)
if err == nil {
t.Errorf("expected error for case %s but got none", k)
} else {
if err.Error() != v.err {
t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error())
}
}
}
}

View File

@ -63,6 +63,7 @@ func init() {
if obj.TerminationMessagePath == "" {
obj.TerminationMessagePath = TerminationMessagePathDefault
}
defaultSecurityContext(obj)
},
func(obj *RestartPolicy) {
if util.AllPtrFieldsNil(obj) {
@ -195,3 +196,44 @@ func defaultHostNetworkPorts(containers *[]Container) {
}
}
}
// defaultSecurityContext performs the downward and upward merges of a pod definition
func defaultSecurityContext(container *Container) {
if container.SecurityContext == nil {
glog.V(4).Infof("creating security context for container %s", container.Name)
container.SecurityContext = &SecurityContext{}
}
// if there are no capabilities defined on the SecurityContext then copy the container settings
if container.SecurityContext.Capabilities == nil {
glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name)
container.SecurityContext.Capabilities = &container.Capabilities
} else {
// if there are capabilities defined on the security context and the container setting is
// empty then assume that it was left off the pod definition and ensure that the container
// settings match the security context settings (checked by the convert functions). If
// there are settings in both then don't touch it, the converter will error if they don't
// match
if len(container.Capabilities.Add) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name)
container.Capabilities.Add = container.SecurityContext.Capabilities.Add
}
if len(container.Capabilities.Drop) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name)
container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop
}
}
// if there are no privileged settings on the security context then copy the container settings
if container.SecurityContext.Privileged == nil {
glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name)
container.SecurityContext.Privileged = &container.Privileged
} else {
// we don't have a good way to know if container.Privileged was set or just defaulted to false
// so the best we can do here is check if the securityContext is set to true and the
// container is set to false and assume that the Privileged field was left off the container
// definition and not an intentional mismatch
if *container.SecurityContext.Privileged && !container.Privileged {
glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name)
container.Privileged = *container.SecurityContext.Privileged
}
}
}

View File

@ -339,3 +339,106 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
t.Errorf("Expected default APIVersion v1beta2, got: %v", apiVersion)
}
}
func TestSetDefaultSecurityContext(t *testing.T) {
priv := false
privTrue := true
testCases := map[string]struct {
c current.Container
}{
"downward defaulting caps": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
},
"downward defaulting priv": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
"upward defaulting caps": {
c: current.Container{
Privileged: false,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"biz"},
Drop: []current.CapabilityType{"baz"},
},
},
},
},
"upward defaulting priv": {
c: current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &privTrue,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
}
pod := &current.Pod{
DesiredState: current.PodState{
Manifest: current.ContainerManifest{},
},
}
for k, v := range testCases {
pod.DesiredState.Manifest.Containers = []current.Container{v.c}
obj := roundTrip(t, runtime.Object(pod))
defaultedPod := obj.(*current.Pod)
c := defaultedPod.DesiredState.Manifest.Containers[0]
if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual {
t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues)
}
}
}
func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) {
issues := make([]string, 0)
equal := true
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil {
equal = false
issues = append(issues, "Expected non nil settings for SecurityContext")
return equal, issues
}
if *c.SecurityContext.Privileged != c.Privileged {
equal = false
issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value")
}
if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings")
}
if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings")
}
return equal, issues
}

View File

@ -513,12 +513,14 @@ 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"`
// Deprecated - see SecurityContext. 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; deprecated; See SecurityContext"`
// 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"`
// Deprecated - see SecurityContext. Optional: Capabilities for container.
Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"`
// Optional: SecurityContext defines the security options the pod should be run with
SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"`
}
const (
@ -1717,3 +1719,39 @@ type ComponentStatusList struct {
Items []ComponentStatus `json:"items" description:"list of component status objects"`
}
// SecurityContext holds security configuration that will be applied to a container. SecurityContext
// contains duplication of some existing fields from the Container resource. These duplicate fields
// will be populated based on the Container configuration if they are not set. Defining them on
// both the Container AND the SecurityContext will result in an error.
type SecurityContext struct {
// Capabilities are the capabilities to add/drop when running the container
// Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset
Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"`
// Run the container in privileged mode
// Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset
Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"`
// SELinuxOptions are the labels to be applied to the container
// and volumes
SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"`
// RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"`
}
// SELinuxOptions are the labels to be applied to the container.
type SELinuxOptions struct {
// SELinux user label
User string `json:"user,omitempty" description:"the user label to apply to the container"`
// SELinux role label
Role string `json:"role,omitempty" description:"the role label to apply to the container"`
// SELinux type label
Type string `json:"type,omitempty" description:"the type label to apply to the container"`
// SELinux level label.
Level string `json:"level,omitempty" description:"the level label to apply to the container"`
}

File diff suppressed because it is too large Load Diff

View File

@ -45,3 +45,63 @@ func TestNodeConversion(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBadSecurityContextConversion(t *testing.T) {
priv := false
testCases := map[string]struct {
c *current.Container
err string
}{
// this use case must use true for the container and false for the sc. Otherwise the defaulter
// will assume privileged was left undefined (since it is the default value) and copy the
// sc setting upwards
"mismatched privileged": {
c: &current.Container{
Privileged: true,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
err: "container privileged settings do not match security context settings, cannot convert",
},
"mismatched caps add": {
c: &current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
"mismatched caps drop": {
c: &current.Container{
Capabilities: current.Capabilities{
Drop: []current.CapabilityType{"foo"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Drop: []current.CapabilityType{"bar"},
},
},
},
err: "container capability settings do not match security context settings, cannot convert",
},
}
for k, v := range testCases {
got := newer.Container{}
err := newer.Scheme.Convert(v.c, &got)
if err == nil {
t.Errorf("expected error for case %s but got none", k)
} else {
if err.Error() != v.err {
t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error())
}
}
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/golang/glog"
)
func init() {
@ -66,6 +67,7 @@ func init() {
if obj.TerminationMessagePath == "" {
obj.TerminationMessagePath = TerminationMessagePathDefault
}
defaultSecurityContext(obj)
},
func(obj *ServiceSpec) {
if obj.SessionAffinity == "" {
@ -156,3 +158,44 @@ func defaultHostNetworkPorts(containers *[]Container) {
}
}
}
// defaultSecurityContext performs the downward and upward merges of a pod definition
func defaultSecurityContext(container *Container) {
if container.SecurityContext == nil {
glog.V(4).Infof("creating security context for container %s", container.Name)
container.SecurityContext = &SecurityContext{}
}
// if there are no capabilities defined on the SecurityContext then copy the container settings
if container.SecurityContext.Capabilities == nil {
glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name)
container.SecurityContext.Capabilities = &container.Capabilities
} else {
// if there are capabilities defined on the security context and the container setting is
// empty then assume that it was left off the pod definition and ensure that the container
// settings match the security context settings (checked by the convert functions). If
// there are settings in both then don't touch it, the converter will error if they don't
// match
if len(container.Capabilities.Add) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name)
container.Capabilities.Add = container.SecurityContext.Capabilities.Add
}
if len(container.Capabilities.Drop) == 0 {
glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name)
container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop
}
}
// if there are no privileged settings on the security context then copy the container settings
if container.SecurityContext.Privileged == nil {
glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name)
container.SecurityContext.Privileged = &container.Privileged
} else {
// we don't have a good way to know if container.Privileged was set or just defaulted to false
// so the best we can do here is check if the securityContext is set to true and the
// container is set to false and assume that the Privileged field was left off the container
// definition and not an intentional mismatch
if *container.SecurityContext.Privileged && !container.Privileged {
glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name)
container.Privileged = *container.SecurityContext.Privileged
}
}
}

View File

@ -349,3 +349,104 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) {
t.Errorf("Expected default APIVersion v1beta3, got: %v", apiVersion)
}
}
func TestSetDefaultSecurityContext(t *testing.T) {
priv := false
privTrue := true
testCases := map[string]struct {
c current.Container
}{
"downward defaulting caps": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &priv,
},
},
},
"downward defaulting priv": {
c: current.Container{
Privileged: false,
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
"upward defaulting caps": {
c: current.Container{
Privileged: false,
SecurityContext: &current.SecurityContext{
Privileged: &priv,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"biz"},
Drop: []current.CapabilityType{"baz"},
},
},
},
},
"upward defaulting priv": {
c: current.Container{
Capabilities: current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
SecurityContext: &current.SecurityContext{
Privileged: &privTrue,
Capabilities: &current.Capabilities{
Add: []current.CapabilityType{"foo"},
Drop: []current.CapabilityType{"bar"},
},
},
},
},
}
pod := &current.Pod{
Spec: current.PodSpec{},
}
for k, v := range testCases {
pod.Spec.Containers = []current.Container{v.c}
obj := roundTrip(t, runtime.Object(pod))
defaultedPod := obj.(*current.Pod)
c := defaultedPod.Spec.Containers[0]
if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual {
t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues)
}
}
}
func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) {
issues := make([]string, 0)
equal := true
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil {
equal = false
issues = append(issues, "Expected non nil settings for SecurityContext")
return equal, issues
}
if *c.SecurityContext.Privileged != c.Privileged {
equal = false
issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value")
}
if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings")
}
if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) {
equal = false
issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings")
}
return equal, issues
}

View File

@ -636,12 +636,14 @@ 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"`
// Deprecated - see SecurityContext. 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; deprecated; See SecurityContext."`
// 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"`
// Deprecated - see SecurityContext. Optional: Capabilities for container.
Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext."`
// Optional: SecurityContext defines the security options the pod should be run with
SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"`
}
// Handler defines a specific action that should be taken
@ -1735,3 +1737,39 @@ type ComponentStatusList struct {
Items []ComponentStatus `json:"items" description:"list of component status objects"`
}
// SecurityContext holds security configuration that will be applied to a container. SecurityContext
// contains duplication of some existing fields from the Container resource. These duplicate fields
// will be populated based on the Container configuration if they are not set. Defining them on
// both the Container AND the SecurityContext will result in an error.
type SecurityContext struct {
// Capabilities are the capabilities to add/drop when running the container
// Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset
Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"`
// Run the container in privileged mode
// Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset
Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"`
// SELinuxOptions are the labels to be applied to the container
// and volumes
SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"`
// RunAsUser is the UID to run the entrypoint of the container process.
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"`
}
// SELinuxOptions are the labels to be applied to the container.
type SELinuxOptions struct {
// SELinux user label
User string `json:"user,omitempty" description:"the user label to apply to the container"`
// SELinux role label
Role string `json:"role,omitempty" description:"the role label to apply to the container"`
// SELinux type label
Type string `json:"type,omitempty" description:"the type label to apply to the container"`
// SELinux level label.
Level string `json:"level,omitempty" description:"the level label to apply to the container"`
}

View File

@ -776,15 +776,12 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs
allNames := util.StringSet{}
for i, ctr := range containers {
cErrs := errs.ValidationErrorList{}
capabilities := capabilities.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 {
cErrs = append(cErrs, errs.NewFieldForbidden("privileged", ctr.Privileged))
} else {
allNames.Insert(ctr.Name)
}
@ -801,6 +798,7 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs
cErrs = append(cErrs, validateVolumeMounts(ctr.VolumeMounts, volumes).Prefix("volumeMounts")...)
cErrs = append(cErrs, validatePullPolicy(&ctr).Prefix("pullPolicy")...)
cErrs = append(cErrs, ValidateResourceRequirements(&ctr.Resources).Prefix("resources")...)
cErrs = append(cErrs, ValidateSecurityContext(ctr.SecurityContext).Prefix("securityContext")...)
allErrs = append(allErrs, cErrs.PrefixIndex(i)...)
}
// Check for colliding ports across all containers.
@ -1481,3 +1479,25 @@ func ValidateEndpointsUpdate(oldEndpoints, newEndpoints *api.Endpoints) errs.Val
allErrs = append(allErrs, validateEndpointSubsets(newEndpoints.Subsets).Prefix("subsets")...)
return allErrs
}
// ValidateSecurityContext ensure the security context contains valid settings
func ValidateSecurityContext(sc *api.SecurityContext) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
//this should only be true for testing since SecurityContext is defaulted by the api
if sc == nil {
return allErrs
}
if sc.Privileged != nil {
if *sc.Privileged && !capabilities.Get().AllowPrivileged {
allErrs = append(allErrs, errs.NewFieldForbidden("privileged", sc.Privileged))
}
}
if sc.RunAsUser != nil {
if *sc.RunAsUser < 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("runAsUser", *sc.RunAsUser, "runAsUser cannot be negative"))
}
}
return allErrs
}

View File

@ -901,7 +901,7 @@ func TestValidateContainers(t *testing.T) {
},
ImagePullPolicy: "IfNotPresent",
},
{Name: "abc-1234", Image: "image", Privileged: true, ImagePullPolicy: "IfNotPresent"},
{Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", SecurityContext: fakeValidSecurityContext(true)},
}
if errs := validateContainers(successCase, volumes); len(errs) != 0 {
t.Errorf("expected success: %v", errs)
@ -1015,7 +1015,7 @@ func TestValidateContainers(t *testing.T) {
},
},
"privilege disabled": {
{Name: "abc", Image: "image", Privileged: true},
{Name: "abc", Image: "image", SecurityContext: fakeValidSecurityContext(true)},
},
"invalid compute resource": {
{
@ -3180,3 +3180,89 @@ func TestValidateEndpoints(t *testing.T) {
}
}
}
func TestValidateSecurityContext(t *testing.T) {
priv := false
var runAsUser int64 = 1
fullValidSC := func() *api.SecurityContext {
return &api.SecurityContext{
Privileged: &priv,
Capabilities: &api.Capabilities{
Add: []api.CapabilityType{"foo"},
Drop: []api.CapabilityType{"bar"},
},
SELinuxOptions: &api.SELinuxOptions{
User: "user",
Role: "role",
Type: "type",
Level: "level",
},
RunAsUser: &runAsUser,
}
}
//setup data
allSettings := fullValidSC()
noCaps := fullValidSC()
noCaps.Capabilities = nil
noSELinux := fullValidSC()
noSELinux.SELinuxOptions = nil
noPrivRequest := fullValidSC()
noPrivRequest.Privileged = nil
noRunAsUser := fullValidSC()
noRunAsUser.RunAsUser = nil
successCases := map[string]struct {
sc *api.SecurityContext
}{
"all settings": {allSettings},
"no capabilities": {noCaps},
"no selinux": {noSELinux},
"no priv request": {noPrivRequest},
"no run as user": {noRunAsUser},
}
for k, v := range successCases {
if errs := ValidateSecurityContext(v.sc); len(errs) != 0 {
t.Errorf("Expected success for %s, got %v", k, errs)
}
}
privRequestWithGlobalDeny := fullValidSC()
requestPrivileged := true
privRequestWithGlobalDeny.Privileged = &requestPrivileged
negativeRunAsUser := fullValidSC()
var negativeUser int64 = -1
negativeRunAsUser.RunAsUser = &negativeUser
errorCases := map[string]struct {
sc *api.SecurityContext
errorType fielderrors.ValidationErrorType
errorDetail string
}{
"request privileged when capabilities forbids": {
sc: privRequestWithGlobalDeny,
errorType: "FieldValueForbidden",
errorDetail: "",
},
"negative RunAsUser": {
sc: negativeRunAsUser,
errorType: "FieldValueInvalid",
errorDetail: "runAsUser cannot be negative",
},
}
for k, v := range errorCases {
if errs := ValidateSecurityContext(v.sc); len(errs) == 0 || errs[0].(*errors.ValidationError).Type != v.errorType || errs[0].(*errors.ValidationError).Detail != v.errorDetail {
t.Errorf("Expected error type %s with detail %s for %s, got %v", v.errorType, v.errorDetail, k, errs)
}
}
}
func fakeValidSecurityContext(priv bool) *api.SecurityContext {
return &api.SecurityContext{
Privileged: &priv,
}
}

View File

@ -35,6 +35,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
@ -112,6 +113,7 @@ func newReplicationController(replicas int) *api.ReplicationController {
Image: "foo/bar",
TerminationMessagePath: api.TerminationMessagePathDefault,
ImagePullPolicy: api.PullIfNotPresent,
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
RestartPolicy: api.RestartPolicyAlways,

View File

@ -22,6 +22,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/ghodss/yaml"
)
@ -46,6 +47,7 @@ func TestDecodeSinglePod(t *testing.T) {
Image: "test/image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePath: "/dev/termination-log",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
}},
},
}
@ -108,6 +110,7 @@ func TestDecodePodList(t *testing.T) {
Image: "test/image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePath: "/dev/termination-log",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
}},
},
}

View File

@ -23,6 +23,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
)
@ -62,7 +63,14 @@ func CreateValidPod(name, namespace, source string) *api.Pod {
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
Containers: []api.Container{
{
Name: "ctr",
Image: "image",
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
},
}
}

View File

@ -31,6 +31,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
)
@ -105,7 +106,8 @@ func TestReadContainerManifestFromFile(t *testing.T) {
Name: "image",
Image: "test/image",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -131,7 +133,8 @@ func TestReadContainerManifestFromFile(t *testing.T) {
Name: "image",
Image: "test/image",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -182,7 +185,7 @@ func TestReadPodsFromFile(t *testing.T) {
Namespace: "mynamespace",
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "image", Image: "test/image"}},
Containers: []api.Container{{Name: "image", Image: "test/image", SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
},
expected: CreatePodUpdate(kubelet.SET, kubelet.FileSource, &api.Pod{
@ -200,7 +203,8 @@ func TestReadPodsFromFile(t *testing.T) {
Name: "image",
Image: "test/image",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "IfNotPresent"}},
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -216,7 +220,7 @@ func TestReadPodsFromFile(t *testing.T) {
UID: "12345",
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "image", Image: "test/image"}},
Containers: []api.Container{{Name: "image", Image: "test/image", SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
},
expected: CreatePodUpdate(kubelet.SET, kubelet.FileSource, &api.Pod{
@ -234,7 +238,8 @@ func TestReadPodsFromFile(t *testing.T) {
Name: "image",
Image: "test/image",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "IfNotPresent"}},
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},

View File

@ -28,6 +28,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
)
@ -151,7 +152,8 @@ func TestExtractManifestFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -177,7 +179,8 @@ func TestExtractManifestFromHTTP(t *testing.T) {
Name: "ctr",
Image: "image",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "IfNotPresent"}},
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -203,7 +206,8 @@ func TestExtractManifestFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -233,7 +237,8 @@ func TestExtractManifestFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
},
&api.Pod{
@ -252,7 +257,8 @@ func TestExtractManifestFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "IfNotPresent"}},
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -344,7 +350,8 @@ func TestExtractPodsFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},
@ -396,7 +403,8 @@ func TestExtractPodsFromHTTP(t *testing.T) {
Name: "1",
Image: "foo",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "Always"}},
ImagePullPolicy: "Always",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
},
&api.Pod{
@ -415,7 +423,8 @@ func TestExtractPodsFromHTTP(t *testing.T) {
Name: "2",
Image: "bar",
TerminationMessagePath: "/dev/termination-log",
ImagePullPolicy: "IfNotPresent"}},
ImagePullPolicy: "IfNotPresent",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}},
},
}),
},

View File

@ -38,6 +38,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/prober"
kubeletTypes "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/probe"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
docker "github.com/fsouza/go-dockerclient"
@ -524,6 +525,8 @@ 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)
securityContextProvider := securitycontext.NewSimpleSecurityContextProvider()
securityContextProvider.ModifyContainerConfig(pod, container, dockerOpts.Config)
dockerContainer, err := dm.client.CreateContainer(dockerOpts)
if err != nil {
if ref != nil {
@ -554,22 +557,15 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op
}
}
privileged := false
if capabilities.Get().AllowPrivileged {
privileged = container.Privileged
} else if container.Privileged {
if !capabilities.Get().AllowPrivileged && securitycontext.HasPrivilegedRequest(container) {
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,
}
if len(opts.DNS) > 0 {
hc.DNS = opts.DNS
@ -580,6 +576,7 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op
if len(opts.CgroupParent) > 0 {
hc.CgroupParent = opts.CgroupParent
}
securityContextProvider.ModifyHostConfig(pod, container, hc)
if err = dm.client.StartContainer(dockerContainer.ID, hc); err != nil {
if ref != nil {
@ -637,20 +634,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
}
// A helper function to get the KubeletContainerName and hash from a docker
// container.
func getDockerContainerNameInfo(c *docker.APIContainers) (*KubeletContainerName, uint64, error) {

View File

@ -38,6 +38,7 @@ import (
kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/prober"
"github.com/GoogleCloudPlatform/kubernetes/pkg/probe"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
@ -187,23 +188,29 @@ func rawValue(value string) *json.RawMessage {
}
// setIsolators overrides the isolators of the pod manifest if necessary.
// TODO need an apply config in security context for rkt
func setIsolators(app *appctypes.App, c *api.Container) error {
if len(c.Capabilities.Add) > 0 || len(c.Capabilities.Drop) > 0 || len(c.Resources.Limits) > 0 || len(c.Resources.Requests) > 0 {
hasCapRequests := securitycontext.HasCapabilitiesRequest(c)
if hasCapRequests || len(c.Resources.Limits) > 0 || len(c.Resources.Requests) > 0 {
app.Isolators = []appctypes.Isolator{}
}
// Retained capabilities/privileged.
privileged := false
if capabilities.Get().AllowPrivileged {
privileged = c.Privileged
} else if c.Privileged {
return fmt.Errorf("privileged is disallowed globally")
if !capabilities.Get().AllowPrivileged && securitycontext.HasPrivilegedRequest(c) {
return fmt.Errorf("container requested privileged mode, but it is disallowed globally.")
} else {
if c.SecurityContext != nil && c.SecurityContext.Privileged != nil {
privileged = *c.SecurityContext.Privileged
}
}
var addCaps string
if privileged {
addCaps = getAllCapabilities()
} else {
addCaps = getCapabilities(c.Capabilities.Add)
if hasCapRequests {
addCaps = getCapabilities(c.SecurityContext.Capabilities.Add)
}
}
if len(addCaps) > 0 {
// TODO(yifan): Replace with constructor, see:
@ -216,7 +223,10 @@ func setIsolators(app *appctypes.App, c *api.Container) error {
}
// Removed capabilities.
dropCaps := getCapabilities(c.Capabilities.Drop)
var dropCaps string
if hasCapRequests {
dropCaps = getCapabilities(c.SecurityContext.Capabilities.Drop)
}
if len(dropCaps) > 0 {
// TODO(yifan): Replace with constructor, see:
// https://github.com/appc/spec/issues/268

View File

@ -32,6 +32,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@ -68,6 +69,7 @@ func validNewPod() *api.Pod {
ImagePullPolicy: api.PullAlways,
TerminationMessagePath: api.TerminationMessagePathDefault,
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
},
@ -1108,8 +1110,9 @@ func TestEtcdUpdateScheduled(t *testing.T) {
Host: "machine",
Containers: []api.Container{
{
Name: "foobar",
Image: "foo:v1",
Name: "foobar",
Image: "foo:v1",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
},
@ -1131,6 +1134,7 @@ func TestEtcdUpdateScheduled(t *testing.T) {
Image: "foo:v2",
ImagePullPolicy: api.PullIfNotPresent,
TerminationMessagePath: api.TerminationMessagePathDefault,
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
RestartPolicy: api.RestartPolicyAlways,
@ -1169,7 +1173,8 @@ func TestEtcdUpdateStatus(t *testing.T) {
Host: "machine",
Containers: []api.Container{
{
Image: "foo:v1",
Image: "foo:v1",
SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(),
},
},
},

View File

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

View File

@ -0,0 +1,45 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
docker "github.com/fsouza/go-dockerclient"
)
// ValidSecurityContextWithContainerDefaults creates a valid security context provider based on
// empty container defaults. Used for testing.
func ValidSecurityContextWithContainerDefaults() *api.SecurityContext {
priv := false
return &api.SecurityContext{
Capabilities: &api.Capabilities{},
Privileged: &priv,
}
}
// NewFakeSecurityContextProvider creates a new, no-op security context provider.
func NewFakeSecurityContextProvider() SecurityContextProvider {
return FakeSecurityContextProvider{}
}
type FakeSecurityContextProvider struct{}
func (p FakeSecurityContextProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) {
}
func (p FakeSecurityContextProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) {
}

View File

@ -0,0 +1,97 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import (
"fmt"
"strconv"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
docker "github.com/fsouza/go-dockerclient"
)
// NewSimpleSecurityContextProvider creates a new SimpleSecurityContextProvider.
func NewSimpleSecurityContextProvider() SecurityContextProvider {
return SimpleSecurityContextProvider{}
}
// SimpleSecurityContextProvider is the default implementation of a SecurityContextProvider.
type SimpleSecurityContextProvider struct{}
// ModifyContainerConfig is called before the Docker createContainer call.
// The security context provider can make changes to the Config with which
// the container is created.
func (p SimpleSecurityContextProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) {
if container.SecurityContext == nil {
return
}
if container.SecurityContext.RunAsUser != nil {
config.User = strconv.FormatInt(*container.SecurityContext.RunAsUser, 10)
}
}
// 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.
func (p SimpleSecurityContextProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) {
if container.SecurityContext == nil {
return
}
if container.SecurityContext.Privileged != nil {
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 {
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)
}
}
// modifySecurityOption adds the security option of name to the config array with value in the form
// of name:value
func modifySecurityOption(config []string, name, value string) []string {
if len(value) > 0 {
config = append(config, fmt.Sprintf("%s:%s", name, value))
}
return config
}
// makeCapabilites creates string slices from CapabilityType slices
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
}

View File

@ -0,0 +1,181 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import (
"fmt"
"reflect"
"strconv"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
docker "github.com/fsouza/go-dockerclient"
)
func TestModifyContainerConfig(t *testing.T) {
var uid int64 = 1
testCases := map[string]struct {
securityContext *api.SecurityContext
expected *docker.Config
}{
"modify config, value set for user": {
securityContext: &api.SecurityContext{
RunAsUser: &uid,
},
expected: &docker.Config{
User: strconv.FormatInt(uid, 10),
},
},
"modify config, nil user value": {
securityContext: &api.SecurityContext{},
expected: &docker.Config{},
},
}
provider := NewSimpleSecurityContextProvider()
dummyContainer := &api.Container{}
for k, v := range testCases {
dummyContainer.SecurityContext = v.securityContext
dockerCfg := &docker.Config{}
provider.ModifyContainerConfig(nil, dummyContainer, dockerCfg)
if !reflect.DeepEqual(v.expected, dockerCfg) {
t.Errorf("unexpected modification of docker config for %s. Expected: %#v Got: %#v", k, v.expected, dockerCfg)
}
}
}
func TestModifyHostConfig(t *testing.T) {
nilPrivSC := fullValidSecurityContext()
nilPrivSC.Privileged = nil
nilPrivHC := fullValidHostConfig()
nilPrivHC.Privileged = false
nilCapsSC := fullValidSecurityContext()
nilCapsSC.Capabilities = nil
nilCapsHC := fullValidHostConfig()
nilCapsHC.CapAdd = *new([]string)
nilCapsHC.CapDrop = *new([]string)
nilSELinuxSC := fullValidSecurityContext()
nilSELinuxSC.SELinuxOptions = nil
nilSELinuxHC := fullValidHostConfig()
nilSELinuxHC.SecurityOpt = *new([]string)
seLinuxLabelsSC := fullValidSecurityContext()
seLinuxLabelsHC := fullValidHostConfig()
testCases := map[string]struct {
securityContext *api.SecurityContext
expected *docker.HostConfig
}{
"full settings": {
securityContext: fullValidSecurityContext(),
expected: fullValidHostConfig(),
},
"nil privileged": {
securityContext: nilPrivSC,
expected: nilPrivHC,
},
"nil capabilities": {
securityContext: nilCapsSC,
expected: nilCapsHC,
},
"nil selinux options": {
securityContext: nilSELinuxSC,
expected: nilSELinuxHC,
},
"selinux labels": {
securityContext: seLinuxLabelsSC,
expected: seLinuxLabelsHC,
},
}
provider := NewSimpleSecurityContextProvider()
dummyContainer := &api.Container{}
for k, v := range testCases {
dummyContainer.SecurityContext = v.securityContext
dockerCfg := &docker.HostConfig{}
provider.ModifyHostConfig(nil, dummyContainer, dockerCfg)
if !reflect.DeepEqual(v.expected, dockerCfg) {
t.Errorf("unexpected modification of host config for %s. Expected: %#v Got: %#v", k, v.expected, dockerCfg)
}
}
}
func TestModifySecurityOption(t *testing.T) {
testCases := []struct {
name string
config []string
optName string
optVal string
expected []string
}{
{
name: "Empty val",
config: []string{"a:b", "c:d"},
optName: "optA",
optVal: "",
expected: []string{"a:b", "c:d"},
},
{
name: "Valid",
config: []string{"a:b", "c:d"},
optName: "e",
optVal: "f",
expected: []string{"a:b", "c:d", "e:f"},
},
}
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 fullValidSecurityContext() *api.SecurityContext {
priv := true
return &api.SecurityContext{
Privileged: &priv,
Capabilities: &api.Capabilities{
Add: []api.CapabilityType{"addCapA", "addCapB"},
Drop: []api.CapabilityType{"dropCapA", "dropCapB"},
},
SELinuxOptions: &api.SELinuxOptions{
User: "user",
Role: "role",
Type: "type",
Level: "level",
},
}
}
func fullValidHostConfig() *docker.HostConfig {
return &docker.HostConfig{
Privileged: true,
CapAdd: []string{"addCapA", "addCapB"},
CapDrop: []string{"dropCapA", "dropCapB"},
SecurityOpt: []string{
fmt.Sprintf("%s:%s", dockerLabelUser, "user"),
fmt.Sprintf("%s:%s", dockerLabelRole, "role"),
fmt.Sprintf("%s:%s", dockerLabelType, "type"),
fmt.Sprintf("%s:%s", dockerLabelLevel, "level"),
},
}
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
docker "github.com/fsouza/go-dockerclient"
)
type SecurityContextProvider interface {
// ModifyContainerConfig is called before the Docker createContainer call.
// The security context provider can make changes to the Config with which
// the container is created.
ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config)
// 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"
)

View File

@ -0,0 +1,43 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package securitycontext
import "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
// HasPrivilegedRequest returns the value of SecurityContext.Privileged, taking into account
// the possibility of nils
func HasPrivilegedRequest(container *api.Container) bool {
if container.SecurityContext == nil {
return false
}
if container.SecurityContext.Privileged == nil {
return false
}
return *container.SecurityContext.Privileged
}
// HasCapabilitiesRequest returns true if Adds or Drops are defined in the security context
// capabilities, taking into account nils
func HasCapabilitiesRequest(container *api.Container) bool {
if container.SecurityContext == nil {
return false
}
if container.SecurityContext.Capabilities == nil {
return false
}
return len(container.SecurityContext.Capabilities.Add) > 0 || len(container.SecurityContext.Capabilities.Drop) > 0
}

View File

@ -0,0 +1,70 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package scdeny
import (
"fmt"
"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"
)
func init() {
admission.RegisterPlugin("SecurityContextDeny", func(client client.Interface, config io.Reader) (admission.Interface, error) {
return NewSecurityContextDeny(client), nil
})
}
// plugin contains the client used by the SecurityContextDeny admission controller
type plugin struct {
client client.Interface
}
// NewSecurityContextDeny creates a new instance of the SecurityContextDeny admission controller
func NewSecurityContextDeny(client client.Interface) admission.Interface {
return &plugin{client}
}
// Admit will deny any SecurityContext that defines options that were not previously available in the api.Container
// struct (Capabilities and Privileged)
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")
}
for _, v := range pod.Spec.Containers {
if v.SecurityContext != nil {
if v.SecurityContext.SELinuxOptions != nil {
return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden"))
}
if v.SecurityContext.RunAsUser != nil {
return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden"))
}
}
}
return nil
}

View File

@ -0,0 +1,65 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package scdeny
import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// ensures the SecurityContext is denied if it defines anything more than Caps or Privileged
func TestAdmission(t *testing.T) {
handler := NewSecurityContextDeny(nil)
var runAsUser int64 = 1
priv := true
successCases := map[string]*api.SecurityContext{
"no sc": nil,
"empty sc": {},
"valid sc": {Privileged: &priv, Capabilities: &api.Capabilities{}},
}
pod := api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{
{},
},
},
}
for k, v := range successCases {
pod.Spec.Containers[0].SecurityContext = v
err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored"))
if err != nil {
t.Errorf("Unexpected error returned from admission handler for case %s", k)
}
}
errorCases := map[string]*api.SecurityContext{
"run as user": {RunAsUser: &runAsUser},
"se linux optons": {SELinuxOptions: &api.SELinuxOptions{}},
"mixed settings": {Privileged: &priv, RunAsUser: &runAsUser, SELinuxOptions: &api.SELinuxOptions{}},
}
for k, v := range errorCases {
pod.Spec.Containers[0].SecurityContext = v
err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored"))
if err == nil {
t.Errorf("Expected error returned from admission handler for case %s", k)
}
}
}