Support volume relabling for pods which specify an SELinux label

pull/6/head
Sami Wagiaalla 2015-10-07 15:19:06 -04:00
parent 1524d7490a
commit 1d352a16b8
30 changed files with 344 additions and 76 deletions

View File

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

View File

@ -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:
// '<HostPath>:<ContainerPath>', or
// '<HostPath>:<ContainerPath>:ro', if the path is read only.
func makeMountBindings(mounts []kubecontainer.Mount) (result []string) {
// '<HostPath>:<ContainerPath>:ro', if the path is read only, or
// '<HostPath>:<ContainerPath>: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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

18
pkg/util/selinux/doc.go Normal file
View File

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

View File

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

View File

@ -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"

View File

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

View File

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

View File

@ -187,6 +187,10 @@ func (cephfsVolume *cephfsBuilder) IsReadOnly() bool {
return cephfsVolume.readonly
}
func (cephfsVolume *cephfsBuilder) SupportsSELinux() bool {
return false
}
type cephfsCleaner struct {
*cephfs
}

View File

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

View File

@ -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.

View File

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

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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.

View File

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