diff --git a/pkg/kubelet/container/runtime.go b/pkg/kubelet/container/runtime.go index 04b999e092..10ebfc326a 100644 --- a/pkg/kubelet/container/runtime.go +++ b/pkg/kubelet/container/runtime.go @@ -238,6 +238,8 @@ type Mount struct { HostPath string // Whether the mount is read-only. ReadOnly bool + // Whether the mount needs SELinux relabeling + SELinuxRelabel bool } type PortMapping struct { @@ -273,7 +275,16 @@ type RunContainerOptions struct { CgroupParent string } -type VolumeMap map[string]volume.Volume +// VolumeInfo contains information about the volume. +type VolumeInfo struct { + // Builder is the volume's builder + Builder volume.Builder + // SELinuxLabeled indicates whether this volume has had the + // pod's SELinux label applied to it or not + SELinuxLabeled bool +} + +type VolumeMap map[string]VolumeInfo type Pods []*Pod diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index 7a6ec0c38f..14fe49e0bc 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -606,13 +606,29 @@ func makeEnvList(envs []kubecontainer.EnvVar) (result []string) { // can be understood by docker. // Each element in the string is in the form of: // ':', or -// '::ro', if the path is read only. -func makeMountBindings(mounts []kubecontainer.Mount) (result []string) { +// '::ro', if the path is read only, or +// '::Z', if the volume requires SELinux +// relabeling and the pod provides an SELinux label +func makeMountBindings(mounts []kubecontainer.Mount, podHasSELinuxLabel bool) (result []string) { for _, m := range mounts { bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath) if m.ReadOnly { bind += ":ro" } + // Only request relabeling if the pod provides an + // SELinux context. If the pod does not provide an + // SELinux context relabeling will label the volume + // with the container's randomly allocated MCS label. + // This would restrict access to the volume to the + // container which mounts it first. + if m.SELinuxRelabel && podHasSELinuxLabel { + if m.ReadOnly { + bind += ",Z" + } else { + bind += ":Z" + } + + } result = append(result, bind) } return @@ -766,7 +782,8 @@ func (dm *DockerManager) runContainer( dm.recorder.Eventf(ref, "Created", "Created with docker id %v", util.ShortenString(dockerContainer.ID, 12)) } - binds := makeMountBindings(opts.Mounts) + podHasSELinuxLabel := pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SELinuxOptions != nil + binds := makeMountBindings(opts.Mounts, podHasSELinuxLabel) // The reason we create and mount the log file in here (not in kubelet) is because // the file's location depends on the ID of the container, and we need to create and diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 97201976ce..f8d7118303 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -29,6 +29,7 @@ import ( "net/http" "os" "path" + "path/filepath" "sort" "strings" "sync" @@ -62,6 +63,7 @@ import ( kubeletutil "k8s.io/kubernetes/pkg/kubelet/util" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/securitycontext" "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/bandwidth" @@ -73,6 +75,7 @@ import ( nodeutil "k8s.io/kubernetes/pkg/util/node" "k8s.io/kubernetes/pkg/util/oom" "k8s.io/kubernetes/pkg/util/procfs" + "k8s.io/kubernetes/pkg/util/selinux" "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/pkg/volume" @@ -975,6 +978,47 @@ func (kl *Kubelet) syncNodeStatus() { } } +// relabelVolumes relabels SELinux volumes to match the pod's +// SELinuxOptions specification. This is only needed if the pod uses +// hostPID or hostIPC. Otherwise relabeling is delegated to docker. +func (kl *Kubelet) relabelVolumes(pod *api.Pod, volumes kubecontainer.VolumeMap) error { + if pod.Spec.SecurityContext.SELinuxOptions == nil { + return nil + } + + rootDirContext, err := kl.getRootDirContext() + if err != nil { + return err + } + + chconRunner := selinux.NewChconRunner() + // Apply the pod's Level to the rootDirContext + rootDirSELinuxOptions, err := securitycontext.ParseSELinuxOptions(rootDirContext) + if err != nil { + return err + } + + rootDirSELinuxOptions.Level = pod.Spec.SecurityContext.SELinuxOptions.Level + volumeContext := fmt.Sprintf("%s:%s:%s:%s", rootDirSELinuxOptions.User, rootDirSELinuxOptions.Role, rootDirSELinuxOptions.Type, rootDirSELinuxOptions.Level) + + for _, volume := range volumes { + if volume.Builder.SupportsSELinux() && !volume.Builder.IsReadOnly() { + // Relabel the volume and its content to match the 'Level' of the pod + err := filepath.Walk(volume.Builder.GetPath(), func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + return chconRunner.SetContext(path, volumeContext) + }) + if err != nil { + return err + } + volume.SELinuxLabeled = true + } + } + return nil +} + func makeMounts(pod *api.Pod, podDir string, container *api.Container, podVolumes kubecontainer.VolumeMap) ([]kubecontainer.Mount, error) { // Kubernetes only mounts on /etc/hosts if : // - container does not use hostNetwork and @@ -991,11 +1035,21 @@ func makeMounts(pod *api.Pod, podDir string, container *api.Container, podVolume glog.Warningf("Mount cannot be satisified for container %q, because the volume is missing: %q", container.Name, mount) continue } + + relabelVolume := false + // If the volume supports SELinux and it has not been + // relabeled already and it is not a read-only volume, + // relabel it and mark it as labeled + if vol.Builder.SupportsSELinux() && !vol.SELinuxLabeled && !vol.Builder.IsReadOnly() { + vol.SELinuxLabeled = true + relabelVolume = true + } mounts = append(mounts, kubecontainer.Mount{ - Name: mount.Name, - ContainerPath: mount.MountPath, - HostPath: vol.GetPath(), - ReadOnly: mount.ReadOnly, + Name: mount.Name, + ContainerPath: mount.MountPath, + HostPath: vol.Builder.GetPath(), + ReadOnly: mount.ReadOnly, + SELinuxRelabel: relabelVolume, }) } if mountEtcHostsFile { @@ -1080,6 +1134,16 @@ func (kl *Kubelet) GenerateRunContainerOptions(pod *api.Pod, container *api.Cont } opts.PortMappings = makePortMappings(container) + // Docker does not relabel volumes if the container is running + // in the host pid or ipc namespaces so the kubelet must + // relabel the volumes + if pod.Spec.SecurityContext != nil && (pod.Spec.SecurityContext.HostIPC || pod.Spec.SecurityContext.HostPID) { + err = kl.relabelVolumes(pod, vol) + if err != nil { + return nil, err + } + } + opts.Mounts, err = makeMounts(pod, kl.getPodDir(pod.UID), container, vol) if err != nil { return nil, err diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 9bf141b432..391d0c99f3 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -510,6 +510,26 @@ func (f *stubVolume) GetPath() string { return f.path } +func (f *stubVolume) IsReadOnly() bool { + return false +} + +func (f *stubVolume) SetUp() error { + return nil +} + +func (f *stubVolume) SetUpAt(dir string) error { + return nil +} + +func (f *stubVolume) SupportsSELinux() bool { + return false +} + +func (f *stubVolume) SupportsOwnershipManagement() bool { + return false +} + func TestMakeVolumeMounts(t *testing.T) { container := api.Container{ VolumeMounts: []api.VolumeMount{ @@ -537,9 +557,9 @@ func TestMakeVolumeMounts(t *testing.T) { } podVolumes := kubecontainer.VolumeMap{ - "disk": &stubVolume{"/mnt/disk"}, - "disk4": &stubVolume{"/mnt/host"}, - "disk5": &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"}, + "disk": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/disk"}}, + "disk4": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/host"}}, + "disk5": kubecontainer.VolumeInfo{Builder: &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"}}, } pod := api.Pod{ @@ -558,24 +578,28 @@ func TestMakeVolumeMounts(t *testing.T) { "/etc/hosts", "/mnt/disk", false, + false, }, { "disk", "/mnt/path3", "/mnt/disk", true, + false, }, { "disk4", "/mnt/path4", "/mnt/host", false, + false, }, { "disk5", "/mnt/path5", "/var/lib/kubelet/podID/volumes/empty/disk5", false, + false, }, } if !reflect.DeepEqual(mounts, expectedMounts) { diff --git a/pkg/kubelet/rkt/rkt.go b/pkg/kubelet/rkt/rkt.go index 93cdbbcd81..315b08a1ee 100644 --- a/pkg/kubelet/rkt/rkt.go +++ b/pkg/kubelet/rkt/rkt.go @@ -488,7 +488,7 @@ func (r *Runtime) makePodManifest(pod *api.Pod, pullSecrets []api.Secret) (*appc manifest.Volumes = append(manifest.Volumes, appctypes.Volume{ Name: *volName, Kind: "host", - Source: volume.GetPath(), + Source: volume.Builder.GetPath(), }) } diff --git a/pkg/kubelet/root_context_linux.go b/pkg/kubelet/root_context_linux.go index d6aed1780e..5916dbe7da 100644 --- a/pkg/kubelet/root_context_linux.go +++ b/pkg/kubelet/root_context_linux.go @@ -22,7 +22,7 @@ import ( "github.com/docker/libcontainer/selinux" ) -// getRootContext gets the SELinux context of the kubelet rootDir +// getRootDirContext gets the SELinux context of the kubelet rootDir // or returns an error. func (kl *Kubelet) getRootDirContext() (string, error) { // If SELinux is not enabled, return an empty string diff --git a/pkg/kubelet/volumes.go b/pkg/kubelet/volumes.go index aaba8846b3..e788e3c895 100644 --- a/pkg/kubelet/volumes.go +++ b/pkg/kubelet/volumes.go @@ -146,7 +146,7 @@ func (kl *Kubelet) mountExternalVolumes(pod *api.Pod) (kubecontainer.VolumeMap, return nil, err } } - podVolumes[volSpec.Name] = builder + podVolumes[volSpec.Name] = kubecontainer.VolumeInfo{Builder: builder} } return podVolumes, nil } diff --git a/pkg/util/selinux/doc.go b/pkg/util/selinux/doc.go new file mode 100644 index 0000000000..9b99015044 --- /dev/null +++ b/pkg/util/selinux/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package selinux contains selinux utility functions. +package selinux diff --git a/pkg/volume/empty_dir/chcon_runner.go b/pkg/util/selinux/selinux.go similarity index 90% rename from pkg/volume/empty_dir/chcon_runner.go rename to pkg/util/selinux/selinux.go index e18aefce6b..a1ae9dfb8a 100644 --- a/pkg/volume/empty_dir/chcon_runner.go +++ b/pkg/util/selinux/selinux.go @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package empty_dir +package selinux // chconRunner knows how to chcon a directory. -type chconRunner interface { +type ChconRunner interface { SetContext(dir, context string) error } // newChconRunner returns a new chconRunner. -func newChconRunner() chconRunner { +func NewChconRunner() ChconRunner { return &realChconRunner{} } diff --git a/pkg/volume/empty_dir/chcon_runner_linux.go b/pkg/util/selinux/selinux_linux.go similarity index 98% rename from pkg/volume/empty_dir/chcon_runner_linux.go rename to pkg/util/selinux/selinux_linux.go index 42abba2501..e051e25a5f 100644 --- a/pkg/volume/empty_dir/chcon_runner_linux.go +++ b/pkg/util/selinux/selinux_linux.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package empty_dir +package selinux import ( "github.com/docker/libcontainer/selinux" diff --git a/pkg/volume/empty_dir/chcon_runner_unsupported.go b/pkg/util/selinux/selinux_unsupported.go similarity index 97% rename from pkg/volume/empty_dir/chcon_runner_unsupported.go rename to pkg/util/selinux/selinux_unsupported.go index 4b75ef9d30..08d531133d 100644 --- a/pkg/volume/empty_dir/chcon_runner_unsupported.go +++ b/pkg/util/selinux/selinux_unsupported.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package empty_dir +package selinux type realChconRunner struct{} diff --git a/pkg/volume/aws_ebs/aws_ebs.go b/pkg/volume/aws_ebs/aws_ebs.go index a72b820767..bc9bd7eb57 100644 --- a/pkg/volume/aws_ebs/aws_ebs.go +++ b/pkg/volume/aws_ebs/aws_ebs.go @@ -250,6 +250,10 @@ func (b *awsElasticBlockStoreBuilder) IsReadOnly() bool { return b.readOnly } +func (b *awsElasticBlockStoreBuilder) SupportsSELinux() bool { + return true +} + func makeGlobalPDPath(host volume.VolumeHost, volumeID string) string { // Clean up the URI to be more fs-friendly name := volumeID diff --git a/pkg/volume/cephfs/cephfs.go b/pkg/volume/cephfs/cephfs.go index 28e53554d3..97c150600f 100644 --- a/pkg/volume/cephfs/cephfs.go +++ b/pkg/volume/cephfs/cephfs.go @@ -187,6 +187,10 @@ func (cephfsVolume *cephfsBuilder) IsReadOnly() bool { return cephfsVolume.readonly } +func (cephfsVolume *cephfsBuilder) SupportsSELinux() bool { + return false +} + type cephfsCleaner struct { *cephfs } diff --git a/pkg/volume/cinder/cinder.go b/pkg/volume/cinder/cinder.go index 7d335d0efc..6e670b34ef 100644 --- a/pkg/volume/cinder/cinder.go +++ b/pkg/volume/cinder/cinder.go @@ -225,6 +225,10 @@ func (b *cinderVolumeBuilder) IsReadOnly() bool { return b.readOnly } +func (b *cinderVolumeBuilder) SupportsSELinux() bool { + return true +} + func makeGlobalPDName(host volume.VolumeHost, devName string) string { return path.Join(host.GetPluginDir(cinderVolumePluginName), "mounts", devName) } diff --git a/pkg/volume/downwardapi/downwardapi.go b/pkg/volume/downwardapi/downwardapi.go index ea96cd2a74..fb7e79f30d 100644 --- a/pkg/volume/downwardapi/downwardapi.go +++ b/pkg/volume/downwardapi/downwardapi.go @@ -157,6 +157,10 @@ func (d *downwardAPIVolume) IsReadOnly() bool { return true } +func (d *downwardAPIVolume) SupportsSELinux() bool { + return true +} + // collectData collects requested downwardAPI in data map. // Map's key is the requested name of file to dump // Map's value is the (sorted) content of the field to be dumped in the file. diff --git a/pkg/volume/empty_dir/empty_dir.go b/pkg/volume/empty_dir/empty_dir.go index e9a8a29c3a..5415390889 100644 --- a/pkg/volume/empty_dir/empty_dir.go +++ b/pkg/volume/empty_dir/empty_dir.go @@ -70,10 +70,10 @@ func (plugin *emptyDirPlugin) CanSupport(spec *volume.Spec) bool { } func (plugin *emptyDirPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Builder, error) { - return plugin.newBuilderInternal(spec, pod, plugin.host.GetMounter(), &realMountDetector{plugin.host.GetMounter()}, opts, newChconRunner()) + return plugin.newBuilderInternal(spec, pod, plugin.host.GetMounter(), &realMountDetector{plugin.host.GetMounter()}, opts) } -func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions, chconRunner chconRunner) (volume.Builder, error) { +func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions) (volume.Builder, error) { medium := api.StorageMediumDefault if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir. medium = spec.Volume.EmptyDir.Medium @@ -86,7 +86,6 @@ func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod mountDetector: mountDetector, plugin: plugin, rootContext: opts.RootContext, - chconRunner: chconRunner, }, nil } @@ -134,7 +133,6 @@ type emptyDir struct { mountDetector mountDetector plugin *emptyDirPlugin rootContext string - chconRunner chconRunner } func (_ *emptyDir) SupportsOwnershipManagement() bool { @@ -175,7 +173,7 @@ func (ed *emptyDir) SetUpAt(dir string) error { switch ed.medium { case api.StorageMediumDefault: - err = ed.setupDir(dir, securityContext) + err = ed.setupDir(dir) case api.StorageMediumMemory: err = ed.setupTmpfs(dir, securityContext) default: @@ -193,13 +191,17 @@ func (ed *emptyDir) IsReadOnly() bool { return false } +func (ed *emptyDir) SupportsSELinux() bool { + return true +} + // setupTmpfs creates a tmpfs mount at the specified directory with the // specified SELinux context. func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error { if ed.mounter == nil { return fmt.Errorf("memory storage requested, but mounter is nil") } - if err := ed.setupDir(dir, selinuxContext); err != nil { + if err := ed.setupDir(dir); err != nil { return err } // Make SetUp idempotent. @@ -228,7 +230,7 @@ func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error { // setupDir creates the directory with the specified SELinux context and // the default permissions specified by the perm constant. -func (ed *emptyDir) setupDir(dir, selinuxContext string) error { +func (ed *emptyDir) setupDir(dir string) error { // Create the directory if it doesn't already exist. if err := os.MkdirAll(dir, perm); err != nil { return err @@ -262,12 +264,6 @@ func (ed *emptyDir) setupDir(dir, selinuxContext string) error { } } - // Set the context on the directory, if appropriate - if selinuxContext != "" { - glog.V(3).Infof("Setting SELinux context for %v to %v", dir, selinuxContext) - return ed.chconRunner.SetContext(dir, selinuxContext) - } - return nil } diff --git a/pkg/volume/empty_dir/empty_dir_test.go b/pkg/volume/empty_dir/empty_dir_test.go index 9c3fe4452c..e28ff9339c 100644 --- a/pkg/volume/empty_dir/empty_dir_test.go +++ b/pkg/volume/empty_dir/empty_dir_test.go @@ -69,30 +69,10 @@ func (fake *fakeMountDetector) GetMountMedium(path string) (storageMedium, bool, return fake.medium, fake.isMount, nil } -type fakeChconRequest struct { - dir string - context string -} - -type fakeChconRunner struct { - requests []fakeChconRequest -} - -func newFakeChconRunner() *fakeChconRunner { - return &fakeChconRunner{} -} - -func (f *fakeChconRunner) SetContext(dir, context string) error { - f.requests = append(f.requests, fakeChconRequest{dir, context}) - - return nil -} - func TestPluginEmptyRootContext(t *testing.T) { doTestPlugin(t, pluginTestConfig{ medium: api.StorageMediumDefault, rootContext: "", - expectedChcons: 0, expectedSetupMounts: 0, expectedTeardownMounts: 0}) } @@ -106,7 +86,6 @@ func TestPluginRootContextSet(t *testing.T) { medium: api.StorageMediumDefault, rootContext: "user:role:type:range", expectedSELinuxContext: "user:role:type:range", - expectedChcons: 1, expectedSetupMounts: 0, expectedTeardownMounts: 0}) } @@ -120,7 +99,6 @@ func TestPluginTmpfs(t *testing.T) { medium: api.StorageMediumMemory, rootContext: "user:role:type:range", expectedSELinuxContext: "user:role:type:range", - expectedChcons: 1, expectedSetupMounts: 1, shouldBeMountedBeforeTeardown: true, expectedTeardownMounts: 1}) @@ -132,7 +110,6 @@ type pluginTestConfig struct { SELinuxOptions *api.SELinuxOptions idempotent bool expectedSELinuxContext string - expectedChcons int expectedSetupMounts int shouldBeMountedBeforeTeardown bool expectedTeardownMounts int @@ -160,7 +137,6 @@ func doTestPlugin(t *testing.T, config pluginTestConfig) { mounter = mount.FakeMounter{} mountDetector = fakeMountDetector{} pod = &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} - fakeChconRnr = &fakeChconRunner{} ) // Set up the SELinux options on the pod @@ -194,8 +170,7 @@ func doTestPlugin(t *testing.T, config pluginTestConfig) { pod, &mounter, &mountDetector, - volume.VolumeOptions{RootContext: config.rootContext}, - fakeChconRnr) + volume.VolumeOptions{RootContext: config.rootContext}) if err != nil { t.Errorf("Failed to make a new Builder: %v", err) } @@ -231,19 +206,6 @@ func doTestPlugin(t *testing.T, config pluginTestConfig) { t.Errorf("Volume directory was created unexpectedly") } - // Check the number of chcons during setup - if e, a := config.expectedChcons, len(fakeChconRnr.requests); e != a { - t.Errorf("Expected %v chcon calls, got %v", e, a) - } - if config.expectedChcons == 1 { - if e, a := config.expectedSELinuxContext, fakeChconRnr.requests[0].context; e != a { - t.Errorf("Unexpected chcon context argument; expected: %v, got: %v", e, a) - } - if e, a := volPath, fakeChconRnr.requests[0].dir; e != a { - t.Errorf("Unexpected chcon path argument: expected: %v, got: %v", e, a) - } - } - // Check the number of mounts performed during setup if e, a := config.expectedSetupMounts, len(mounter.Log); e != a { t.Errorf("Expected %v mounter calls during setup, got %v", e, a) diff --git a/pkg/volume/fc/fc.go b/pkg/volume/fc/fc.go index d9d438bea2..32cc382ddc 100644 --- a/pkg/volume/fc/fc.go +++ b/pkg/volume/fc/fc.go @@ -191,6 +191,10 @@ func (b *fcDiskBuilder) IsReadOnly() bool { return b.readOnly } +func (b *fcDiskBuilder) SupportsSELinux() bool { + return true +} + // Unmounts the bind mount, and detaches the disk only if the disk // resource was the last reference to that disk on the kubelet. func (c *fcDiskCleaner) TearDown() error { diff --git a/pkg/volume/flocker/plugin.go b/pkg/volume/flocker/plugin.go index f525c51ff3..995d4b43a0 100644 --- a/pkg/volume/flocker/plugin.go +++ b/pkg/volume/flocker/plugin.go @@ -205,6 +205,10 @@ func (b flockerBuilder) IsReadOnly() bool { return b.readOnly } +func (b flockerBuilder) SupportsSELinux() bool { + return false +} + // updateDatasetPrimary will update the primary in Flocker and wait for it to // be ready. If it never gets to ready state it will timeout and error. func (b flockerBuilder) updateDatasetPrimary(datasetID, primaryUUID string) error { diff --git a/pkg/volume/gce_pd/gce_pd.go b/pkg/volume/gce_pd/gce_pd.go index b5687c2813..fce652254e 100644 --- a/pkg/volume/gce_pd/gce_pd.go +++ b/pkg/volume/gce_pd/gce_pd.go @@ -238,6 +238,10 @@ func (b *gcePersistentDiskBuilder) IsReadOnly() bool { return b.readOnly } +func (b *gcePersistentDiskBuilder) SupportsSELinux() bool { + return true +} + func makeGlobalPDName(host volume.VolumeHost, devName string) string { return path.Join(host.GetPluginDir(gcePersistentDiskPluginName), "mounts", devName) } diff --git a/pkg/volume/git_repo/git_repo.go b/pkg/volume/git_repo/git_repo.go index 38ceef3402..1e1c1531d2 100644 --- a/pkg/volume/git_repo/git_repo.go +++ b/pkg/volume/git_repo/git_repo.go @@ -122,6 +122,10 @@ func (b *gitRepoVolumeBuilder) IsReadOnly() bool { return false } +func (b *gitRepoVolumeBuilder) SupportsSELinux() bool { + return true +} + // This is the spec for the volume that this plugin wraps. var wrappedVolumeSpec = &volume.Spec{ Volume: &api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, diff --git a/pkg/volume/glusterfs/glusterfs.go b/pkg/volume/glusterfs/glusterfs.go index c2ebb0d9cd..c9cc63c6fa 100644 --- a/pkg/volume/glusterfs/glusterfs.go +++ b/pkg/volume/glusterfs/glusterfs.go @@ -189,6 +189,10 @@ func (b *glusterfsBuilder) IsReadOnly() bool { return b.readOnly } +func (b *glusterfsBuilder) SupportsSELinux() bool { + return false +} + func (glusterfsVolume *glusterfs) GetPath() string { name := glusterfsPluginName return glusterfsVolume.plugin.host.GetPodVolumeDir(glusterfsVolume.pod.UID, util.EscapeQualifiedNameForDisk(name), glusterfsVolume.volName) diff --git a/pkg/volume/host_path/host_path.go b/pkg/volume/host_path/host_path.go index 36065cb7e7..28f151188a 100644 --- a/pkg/volume/host_path/host_path.go +++ b/pkg/volume/host_path/host_path.go @@ -185,6 +185,10 @@ func (b *hostPathBuilder) IsReadOnly() bool { return b.readOnly } +func (b *hostPathBuilder) SupportsSELinux() bool { + return false +} + func (b *hostPathBuilder) GetPath() string { return b.path } diff --git a/pkg/volume/iscsi/iscsi.go b/pkg/volume/iscsi/iscsi.go index c1d25b234a..a448da31e7 100644 --- a/pkg/volume/iscsi/iscsi.go +++ b/pkg/volume/iscsi/iscsi.go @@ -185,6 +185,10 @@ func (b *iscsiDiskBuilder) IsReadOnly() bool { return b.readOnly } +func (b *iscsiDiskBuilder) SupportsSELinux() bool { + return true +} + // Unmounts the bind mount, and detaches the disk only if the disk // resource was the last reference to that disk on the kubelet. func (c *iscsiDiskCleaner) TearDown() error { diff --git a/pkg/volume/nfs/nfs.go b/pkg/volume/nfs/nfs.go index ea6c0bea13..d30af4b501 100644 --- a/pkg/volume/nfs/nfs.go +++ b/pkg/volume/nfs/nfs.go @@ -226,6 +226,10 @@ func (b *nfsBuilder) IsReadOnly() bool { return b.readOnly } +func (b *nfsBuilder) SupportsSELinux() bool { + return false +} + // //func (c *nfsCleaner) GetPath() string { // name := nfsPluginName diff --git a/pkg/volume/rbd/rbd.go b/pkg/volume/rbd/rbd.go index 84996cbe2f..6b828c3a52 100644 --- a/pkg/volume/rbd/rbd.go +++ b/pkg/volume/rbd/rbd.go @@ -219,6 +219,10 @@ func (b *rbd) IsReadOnly() bool { return b.ReadOnly } +func (b *rbd) SupportsSELinux() bool { + return true +} + // Unmounts the bind mount, and detaches the disk only if the disk // resource was the last reference to that disk on the kubelet. func (c *rbdCleaner) TearDown() error { diff --git a/pkg/volume/secret/secret.go b/pkg/volume/secret/secret.go index c5feb3bce7..514e9e3046 100644 --- a/pkg/volume/secret/secret.go +++ b/pkg/volume/secret/secret.go @@ -176,6 +176,10 @@ func (sv *secretVolume) IsReadOnly() bool { return false } +func (sv *secretVolume) SupportsSELinux() bool { + return true +} + func totalSecretBytes(secret *api.Secret) int { totalSize := 0 for _, bytes := range secret.Data { diff --git a/pkg/volume/testing.go b/pkg/volume/testing.go index e4cd84fa32..0be6b1e86e 100644 --- a/pkg/volume/testing.go +++ b/pkg/volume/testing.go @@ -172,6 +172,10 @@ func (fv *FakeVolume) IsReadOnly() bool { return false } +func (fv *FakeVolume) SupportsSELinux() bool { + return false +} + func (fv *FakeVolume) GetPath() string { return path.Join(fv.Plugin.Host.GetPodVolumeDir(fv.PodUID, util.EscapeQualifiedNameForDisk(fv.Plugin.PluginName), fv.VolName)) } diff --git a/pkg/volume/volume.go b/pkg/volume/volume.go index 220fea7d48..cc2f6d93cd 100644 --- a/pkg/volume/volume.go +++ b/pkg/volume/volume.go @@ -53,6 +53,10 @@ type Builder interface { // 2. Set the setgid bit is set (new files created in the volume will be owned by FSGroup) // 3. Logical OR the permission bits with rw-rw---- SupportsOwnershipManagement() bool + // SupportsSELinux reports whether the given builder supports + // SELinux and would like the kubelet to relabel the volume to + // match the pod to which it will be attached. + SupportsSELinux() bool } // Cleaner interface provides methods to cleanup/unmount the volumes. diff --git a/test/e2e/security_context.go b/test/e2e/security_context.go index e3313937c9..5f7f661396 100644 --- a/test/e2e/security_context.go +++ b/test/e2e/security_context.go @@ -29,9 +29,10 @@ import ( "k8s.io/kubernetes/pkg/util" . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) -func scTestPod() *api.Pod { +func scTestPod(hostIPC bool, hostPID bool) *api.Pod { podName := "security-context-" + string(util.NewUUID()) pod := &api.Pod{ ObjectMeta: api.ObjectMeta{ @@ -39,7 +40,10 @@ func scTestPod() *api.Pod { Labels: map[string]string{"name": podName}, }, Spec: api.PodSpec{ - SecurityContext: &api.PodSecurityContext{}, + SecurityContext: &api.PodSecurityContext{ + HostIPC: hostIPC, + HostPID: hostPID, + }, Containers: []api.Container{ { Name: "test-container", @@ -57,7 +61,7 @@ var _ = Describe("[Skipped] Security Context", func() { framework := NewFramework("security-context") It("should support pod.Spec.SecurityContext.SupplementalGroups", func() { - pod := scTestPod() + pod := scTestPod(false, false) pod.Spec.Containers[0].Command = []string{"id", "-G"} pod.Spec.SecurityContext.SupplementalGroups = []int64{1234, 5678} groups := []string{"1234", "5678"} @@ -65,7 +69,7 @@ var _ = Describe("[Skipped] Security Context", func() { }) It("should support pod.Spec.SecurityContext.RunAsUser", func() { - pod := scTestPod() + pod := scTestPod(false, false) var uid int64 = 1001 pod.Spec.SecurityContext.RunAsUser = &uid pod.Spec.Containers[0].Command = []string{"sh", "-c", "id -u"} @@ -76,7 +80,7 @@ var _ = Describe("[Skipped] Security Context", func() { }) It("should support container.SecurityContext.RunAsUser", func() { - pod := scTestPod() + pod := scTestPod(false, false) var uid int64 = 1001 var overrideUid int64 = 1002 pod.Spec.SecurityContext.RunAsUser = &uid @@ -88,4 +92,112 @@ var _ = Describe("[Skipped] Security Context", func() { fmt.Sprintf("%v", overrideUid), }) }) + + It("should support volume SELinux relabeling", func() { + testPodSELinuxLabeling(framework, false, false) + }) + + It("should support volume SELinux relabeling when using hostIPC", func() { + testPodSELinuxLabeling(framework, true, false) + }) + + It("should support volume SELinux relabeling when using hostPID", func() { + testPodSELinuxLabeling(framework, false, true) + }) + }) + +func testPodSELinuxLabeling(framework *Framework, hostIPC bool, hostPID bool) { + // Write and read a file with an empty_dir volume + // with a pod with the MCS label s0:c0,c1 + pod := scTestPod(hostIPC, hostPID) + volumeName := "test-volume" + mountPath := "/mounted_volume" + pod.Spec.Containers[0].VolumeMounts = []api.VolumeMount{ + { + Name: volumeName, + MountPath: mountPath, + }, + } + pod.Spec.Volumes = []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{ + Medium: api.StorageMediumDefault, + }, + }, + }, + } + pod.Spec.SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "s0:c0,c1", + } + pod.Spec.Containers[0].Command = []string{"sleep", "6000"} + + client := framework.Client.Pods(framework.Namespace.Name) + _, err := client.Create(pod) + + expectNoError(err, "Error creating pod %v", pod) + defer client.Delete(pod.Name, nil) + expectNoError(waitForPodRunningInNamespace(framework.Client, pod.Name, framework.Namespace.Name)) + + testContent := "hello" + testFilePath := mountPath + "/TEST" + err = framework.WriteFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, testFilePath, testContent) + Expect(err).To(BeNil()) + content, err := framework.ReadFileViaContainer(pod.Name, pod.Spec.Containers[0].Name, testFilePath) + Expect(err).To(BeNil()) + Expect(content).To(ContainSubstring(testContent)) + + foundPod, err := framework.Client.Pods(framework.Namespace.Name).Get(pod.Name) + Expect(err).NotTo(HaveOccurred()) + + // Confirm that the file can be accessed from a second + // pod using host_path with the same MCS label + volumeHostPath := fmt.Sprintf("/var/lib/kubelet/pods/%s/volumes/kubernetes.io~empty-dir/%s", foundPod.UID, volumeName) + By("confirming a container with the same label can read the file") + pod = scTestPod(hostIPC, hostPID) + pod.Spec.NodeName = foundPod.Spec.NodeName + volumeMounts := []api.VolumeMount{ + { + Name: volumeName, + MountPath: mountPath, + }, + } + volumes := []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: volumeHostPath, + }, + }, + }, + } + pod.Spec.Containers[0].VolumeMounts = volumeMounts + pod.Spec.Volumes = volumes + pod.Spec.Containers[0].Command = []string{"cat", testFilePath} + pod.Spec.SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "s0:c0,c1", + } + + framework.TestContainerOutput("Pod with same MCS label reading test file", pod, 0, []string{testContent}) + // Confirm that the same pod with a different MCS + // label cannot access the volume + pod = scTestPod(hostIPC, hostPID) + pod.Spec.Volumes = volumes + pod.Spec.Containers[0].VolumeMounts = volumeMounts + pod.Spec.Containers[0].Command = []string{"sleep", "6000"} + pod.Spec.SecurityContext.SELinuxOptions = &api.SELinuxOptions{ + Level: "s0:c2,c3", + } + _, err = client.Create(pod) + expectNoError(err, "Error creating pod %v", pod) + defer client.Delete(pod.Name, nil) + + err = framework.WaitForPodRunning(pod.Name) + expectNoError(err, "Error waiting for pod to run %v", pod) + + content, err = framework.ReadFileViaContainer(pod.Name, "test-container", testFilePath) + Expect(content).NotTo(ContainSubstring(testContent)) +}