security context initial implementation - squash

This commit is contained in:
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

@@ -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,
}
}