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 HostPath string
// Whether the mount is read-only. // Whether the mount is read-only.
ReadOnly bool ReadOnly bool
// Whether the mount needs SELinux relabeling
SELinuxRelabel bool
} }
type PortMapping struct { type PortMapping struct {
@ -273,7 +275,16 @@ type RunContainerOptions struct {
CgroupParent string 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 type Pods []*Pod

View File

@ -606,13 +606,29 @@ func makeEnvList(envs []kubecontainer.EnvVar) (result []string) {
// can be understood by docker. // can be understood by docker.
// Each element in the string is in the form of: // Each element in the string is in the form of:
// '<HostPath>:<ContainerPath>', or // '<HostPath>:<ContainerPath>', or
// '<HostPath>:<ContainerPath>:ro', if the path is read only. // '<HostPath>:<ContainerPath>:ro', if the path is read only, or
func makeMountBindings(mounts []kubecontainer.Mount) (result []string) { // '<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 { for _, m := range mounts {
bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath) bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath)
if m.ReadOnly { if m.ReadOnly {
bind += ":ro" 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) result = append(result, bind)
} }
return return
@ -766,7 +782,8 @@ func (dm *DockerManager) runContainer(
dm.recorder.Eventf(ref, "Created", "Created with docker id %v", util.ShortenString(dockerContainer.ID, 12)) 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 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 // 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" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -62,6 +63,7 @@ import (
kubeletutil "k8s.io/kubernetes/pkg/kubelet/util" kubeletutil "k8s.io/kubernetes/pkg/kubelet/util"
"k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/securitycontext"
"k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/pkg/util/bandwidth" "k8s.io/kubernetes/pkg/util/bandwidth"
@ -73,6 +75,7 @@ import (
nodeutil "k8s.io/kubernetes/pkg/util/node" nodeutil "k8s.io/kubernetes/pkg/util/node"
"k8s.io/kubernetes/pkg/util/oom" "k8s.io/kubernetes/pkg/util/oom"
"k8s.io/kubernetes/pkg/util/procfs" "k8s.io/kubernetes/pkg/util/procfs"
"k8s.io/kubernetes/pkg/util/selinux"
"k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/pkg/version"
"k8s.io/kubernetes/pkg/volume" "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) { func makeMounts(pod *api.Pod, podDir string, container *api.Container, podVolumes kubecontainer.VolumeMap) ([]kubecontainer.Mount, error) {
// Kubernetes only mounts on /etc/hosts if : // Kubernetes only mounts on /etc/hosts if :
// - container does not use hostNetwork and // - 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) glog.Warningf("Mount cannot be satisified for container %q, because the volume is missing: %q", container.Name, mount)
continue 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{ mounts = append(mounts, kubecontainer.Mount{
Name: mount.Name, Name: mount.Name,
ContainerPath: mount.MountPath, ContainerPath: mount.MountPath,
HostPath: vol.GetPath(), HostPath: vol.Builder.GetPath(),
ReadOnly: mount.ReadOnly, ReadOnly: mount.ReadOnly,
SELinuxRelabel: relabelVolume,
}) })
} }
if mountEtcHostsFile { if mountEtcHostsFile {
@ -1080,6 +1134,16 @@ func (kl *Kubelet) GenerateRunContainerOptions(pod *api.Pod, container *api.Cont
} }
opts.PortMappings = makePortMappings(container) 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) opts.Mounts, err = makeMounts(pod, kl.getPodDir(pod.UID), container, vol)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -510,6 +510,26 @@ func (f *stubVolume) GetPath() string {
return f.path 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) { func TestMakeVolumeMounts(t *testing.T) {
container := api.Container{ container := api.Container{
VolumeMounts: []api.VolumeMount{ VolumeMounts: []api.VolumeMount{
@ -537,9 +557,9 @@ func TestMakeVolumeMounts(t *testing.T) {
} }
podVolumes := kubecontainer.VolumeMap{ podVolumes := kubecontainer.VolumeMap{
"disk": &stubVolume{"/mnt/disk"}, "disk": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/disk"}},
"disk4": &stubVolume{"/mnt/host"}, "disk4": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/host"}},
"disk5": &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"}, "disk5": kubecontainer.VolumeInfo{Builder: &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"}},
} }
pod := api.Pod{ pod := api.Pod{
@ -558,24 +578,28 @@ func TestMakeVolumeMounts(t *testing.T) {
"/etc/hosts", "/etc/hosts",
"/mnt/disk", "/mnt/disk",
false, false,
false,
}, },
{ {
"disk", "disk",
"/mnt/path3", "/mnt/path3",
"/mnt/disk", "/mnt/disk",
true, true,
false,
}, },
{ {
"disk4", "disk4",
"/mnt/path4", "/mnt/path4",
"/mnt/host", "/mnt/host",
false, false,
false,
}, },
{ {
"disk5", "disk5",
"/mnt/path5", "/mnt/path5",
"/var/lib/kubelet/podID/volumes/empty/disk5", "/var/lib/kubelet/podID/volumes/empty/disk5",
false, false,
false,
}, },
} }
if !reflect.DeepEqual(mounts, expectedMounts) { 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{ manifest.Volumes = append(manifest.Volumes, appctypes.Volume{
Name: *volName, Name: *volName,
Kind: "host", Kind: "host",
Source: volume.GetPath(), Source: volume.Builder.GetPath(),
}) })
} }

View File

@ -22,7 +22,7 @@ import (
"github.com/docker/libcontainer/selinux" "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. // or returns an error.
func (kl *Kubelet) getRootDirContext() (string, error) { func (kl *Kubelet) getRootDirContext() (string, error) {
// If SELinux is not enabled, return an empty string // 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 return nil, err
} }
} }
podVolumes[volSpec.Name] = builder podVolumes[volSpec.Name] = kubecontainer.VolumeInfo{Builder: builder}
} }
return podVolumes, nil 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. limitations under the License.
*/ */
package empty_dir package selinux
// chconRunner knows how to chcon a directory. // chconRunner knows how to chcon a directory.
type chconRunner interface { type ChconRunner interface {
SetContext(dir, context string) error SetContext(dir, context string) error
} }
// newChconRunner returns a new chconRunner. // newChconRunner returns a new chconRunner.
func newChconRunner() chconRunner { func NewChconRunner() ChconRunner {
return &realChconRunner{} return &realChconRunner{}
} }

View File

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package empty_dir package selinux
import ( import (
"github.com/docker/libcontainer/selinux" "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. limitations under the License.
*/ */
package empty_dir package selinux
type realChconRunner struct{} type realChconRunner struct{}

View File

@ -250,6 +250,10 @@ func (b *awsElasticBlockStoreBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *awsElasticBlockStoreBuilder) SupportsSELinux() bool {
return true
}
func makeGlobalPDPath(host volume.VolumeHost, volumeID string) string { func makeGlobalPDPath(host volume.VolumeHost, volumeID string) string {
// Clean up the URI to be more fs-friendly // Clean up the URI to be more fs-friendly
name := volumeID name := volumeID

View File

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

View File

@ -225,6 +225,10 @@ func (b *cinderVolumeBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *cinderVolumeBuilder) SupportsSELinux() bool {
return true
}
func makeGlobalPDName(host volume.VolumeHost, devName string) string { func makeGlobalPDName(host volume.VolumeHost, devName string) string {
return path.Join(host.GetPluginDir(cinderVolumePluginName), "mounts", devName) return path.Join(host.GetPluginDir(cinderVolumePluginName), "mounts", devName)
} }

View File

@ -157,6 +157,10 @@ func (d *downwardAPIVolume) IsReadOnly() bool {
return true return true
} }
func (d *downwardAPIVolume) SupportsSELinux() bool {
return true
}
// collectData collects requested downwardAPI in data map. // collectData collects requested downwardAPI in data map.
// Map's key is the requested name of file to dump // 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. // 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) { 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 medium := api.StorageMediumDefault
if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir. if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir.
medium = spec.Volume.EmptyDir.Medium medium = spec.Volume.EmptyDir.Medium
@ -86,7 +86,6 @@ func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod
mountDetector: mountDetector, mountDetector: mountDetector,
plugin: plugin, plugin: plugin,
rootContext: opts.RootContext, rootContext: opts.RootContext,
chconRunner: chconRunner,
}, nil }, nil
} }
@ -134,7 +133,6 @@ type emptyDir struct {
mountDetector mountDetector mountDetector mountDetector
plugin *emptyDirPlugin plugin *emptyDirPlugin
rootContext string rootContext string
chconRunner chconRunner
} }
func (_ *emptyDir) SupportsOwnershipManagement() bool { func (_ *emptyDir) SupportsOwnershipManagement() bool {
@ -175,7 +173,7 @@ func (ed *emptyDir) SetUpAt(dir string) error {
switch ed.medium { switch ed.medium {
case api.StorageMediumDefault: case api.StorageMediumDefault:
err = ed.setupDir(dir, securityContext) err = ed.setupDir(dir)
case api.StorageMediumMemory: case api.StorageMediumMemory:
err = ed.setupTmpfs(dir, securityContext) err = ed.setupTmpfs(dir, securityContext)
default: default:
@ -193,13 +191,17 @@ func (ed *emptyDir) IsReadOnly() bool {
return false return false
} }
func (ed *emptyDir) SupportsSELinux() bool {
return true
}
// setupTmpfs creates a tmpfs mount at the specified directory with the // setupTmpfs creates a tmpfs mount at the specified directory with the
// specified SELinux context. // specified SELinux context.
func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error { func (ed *emptyDir) setupTmpfs(dir string, selinuxContext string) error {
if ed.mounter == nil { if ed.mounter == nil {
return fmt.Errorf("memory storage requested, but mounter is 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 return err
} }
// Make SetUp idempotent. // 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 // setupDir creates the directory with the specified SELinux context and
// the default permissions specified by the perm constant. // 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. // Create the directory if it doesn't already exist.
if err := os.MkdirAll(dir, perm); err != nil { if err := os.MkdirAll(dir, perm); err != nil {
return err 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 return nil
} }

View File

@ -69,30 +69,10 @@ func (fake *fakeMountDetector) GetMountMedium(path string) (storageMedium, bool,
return fake.medium, fake.isMount, nil 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) { func TestPluginEmptyRootContext(t *testing.T) {
doTestPlugin(t, pluginTestConfig{ doTestPlugin(t, pluginTestConfig{
medium: api.StorageMediumDefault, medium: api.StorageMediumDefault,
rootContext: "", rootContext: "",
expectedChcons: 0,
expectedSetupMounts: 0, expectedSetupMounts: 0,
expectedTeardownMounts: 0}) expectedTeardownMounts: 0})
} }
@ -106,7 +86,6 @@ func TestPluginRootContextSet(t *testing.T) {
medium: api.StorageMediumDefault, medium: api.StorageMediumDefault,
rootContext: "user:role:type:range", rootContext: "user:role:type:range",
expectedSELinuxContext: "user:role:type:range", expectedSELinuxContext: "user:role:type:range",
expectedChcons: 1,
expectedSetupMounts: 0, expectedSetupMounts: 0,
expectedTeardownMounts: 0}) expectedTeardownMounts: 0})
} }
@ -120,7 +99,6 @@ func TestPluginTmpfs(t *testing.T) {
medium: api.StorageMediumMemory, medium: api.StorageMediumMemory,
rootContext: "user:role:type:range", rootContext: "user:role:type:range",
expectedSELinuxContext: "user:role:type:range", expectedSELinuxContext: "user:role:type:range",
expectedChcons: 1,
expectedSetupMounts: 1, expectedSetupMounts: 1,
shouldBeMountedBeforeTeardown: true, shouldBeMountedBeforeTeardown: true,
expectedTeardownMounts: 1}) expectedTeardownMounts: 1})
@ -132,7 +110,6 @@ type pluginTestConfig struct {
SELinuxOptions *api.SELinuxOptions SELinuxOptions *api.SELinuxOptions
idempotent bool idempotent bool
expectedSELinuxContext string expectedSELinuxContext string
expectedChcons int
expectedSetupMounts int expectedSetupMounts int
shouldBeMountedBeforeTeardown bool shouldBeMountedBeforeTeardown bool
expectedTeardownMounts int expectedTeardownMounts int
@ -160,7 +137,6 @@ func doTestPlugin(t *testing.T, config pluginTestConfig) {
mounter = mount.FakeMounter{} mounter = mount.FakeMounter{}
mountDetector = fakeMountDetector{} mountDetector = fakeMountDetector{}
pod = &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} pod = &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}}
fakeChconRnr = &fakeChconRunner{}
) )
// Set up the SELinux options on the pod // Set up the SELinux options on the pod
@ -194,8 +170,7 @@ func doTestPlugin(t *testing.T, config pluginTestConfig) {
pod, pod,
&mounter, &mounter,
&mountDetector, &mountDetector,
volume.VolumeOptions{RootContext: config.rootContext}, volume.VolumeOptions{RootContext: config.rootContext})
fakeChconRnr)
if err != nil { if err != nil {
t.Errorf("Failed to make a new Builder: %v", err) 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") 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 // Check the number of mounts performed during setup
if e, a := config.expectedSetupMounts, len(mounter.Log); e != a { if e, a := config.expectedSetupMounts, len(mounter.Log); e != a {
t.Errorf("Expected %v mounter calls during setup, got %v", 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 return b.readOnly
} }
func (b *fcDiskBuilder) SupportsSELinux() bool {
return true
}
// Unmounts the bind mount, and detaches the disk only if the disk // Unmounts the bind mount, and detaches the disk only if the disk
// resource was the last reference to that disk on the kubelet. // resource was the last reference to that disk on the kubelet.
func (c *fcDiskCleaner) TearDown() error { func (c *fcDiskCleaner) TearDown() error {

View File

@ -205,6 +205,10 @@ func (b flockerBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b flockerBuilder) SupportsSELinux() bool {
return false
}
// updateDatasetPrimary will update the primary in Flocker and wait for it to // 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. // be ready. If it never gets to ready state it will timeout and error.
func (b flockerBuilder) updateDatasetPrimary(datasetID, primaryUUID string) error { func (b flockerBuilder) updateDatasetPrimary(datasetID, primaryUUID string) error {

View File

@ -238,6 +238,10 @@ func (b *gcePersistentDiskBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *gcePersistentDiskBuilder) SupportsSELinux() bool {
return true
}
func makeGlobalPDName(host volume.VolumeHost, devName string) string { func makeGlobalPDName(host volume.VolumeHost, devName string) string {
return path.Join(host.GetPluginDir(gcePersistentDiskPluginName), "mounts", devName) return path.Join(host.GetPluginDir(gcePersistentDiskPluginName), "mounts", devName)
} }

View File

@ -122,6 +122,10 @@ func (b *gitRepoVolumeBuilder) IsReadOnly() bool {
return false return false
} }
func (b *gitRepoVolumeBuilder) SupportsSELinux() bool {
return true
}
// This is the spec for the volume that this plugin wraps. // This is the spec for the volume that this plugin wraps.
var wrappedVolumeSpec = &volume.Spec{ var wrappedVolumeSpec = &volume.Spec{
Volume: &api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, Volume: &api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},

View File

@ -189,6 +189,10 @@ func (b *glusterfsBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *glusterfsBuilder) SupportsSELinux() bool {
return false
}
func (glusterfsVolume *glusterfs) GetPath() string { func (glusterfsVolume *glusterfs) GetPath() string {
name := glusterfsPluginName name := glusterfsPluginName
return glusterfsVolume.plugin.host.GetPodVolumeDir(glusterfsVolume.pod.UID, util.EscapeQualifiedNameForDisk(name), glusterfsVolume.volName) 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 return b.readOnly
} }
func (b *hostPathBuilder) SupportsSELinux() bool {
return false
}
func (b *hostPathBuilder) GetPath() string { func (b *hostPathBuilder) GetPath() string {
return b.path return b.path
} }

View File

@ -185,6 +185,10 @@ func (b *iscsiDiskBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *iscsiDiskBuilder) SupportsSELinux() bool {
return true
}
// Unmounts the bind mount, and detaches the disk only if the disk // Unmounts the bind mount, and detaches the disk only if the disk
// resource was the last reference to that disk on the kubelet. // resource was the last reference to that disk on the kubelet.
func (c *iscsiDiskCleaner) TearDown() error { func (c *iscsiDiskCleaner) TearDown() error {

View File

@ -226,6 +226,10 @@ func (b *nfsBuilder) IsReadOnly() bool {
return b.readOnly return b.readOnly
} }
func (b *nfsBuilder) SupportsSELinux() bool {
return false
}
// //
//func (c *nfsCleaner) GetPath() string { //func (c *nfsCleaner) GetPath() string {
// name := nfsPluginName // name := nfsPluginName

View File

@ -219,6 +219,10 @@ func (b *rbd) IsReadOnly() bool {
return b.ReadOnly return b.ReadOnly
} }
func (b *rbd) SupportsSELinux() bool {
return true
}
// Unmounts the bind mount, and detaches the disk only if the disk // Unmounts the bind mount, and detaches the disk only if the disk
// resource was the last reference to that disk on the kubelet. // resource was the last reference to that disk on the kubelet.
func (c *rbdCleaner) TearDown() error { func (c *rbdCleaner) TearDown() error {

View File

@ -176,6 +176,10 @@ func (sv *secretVolume) IsReadOnly() bool {
return false return false
} }
func (sv *secretVolume) SupportsSELinux() bool {
return true
}
func totalSecretBytes(secret *api.Secret) int { func totalSecretBytes(secret *api.Secret) int {
totalSize := 0 totalSize := 0
for _, bytes := range secret.Data { for _, bytes := range secret.Data {

View File

@ -172,6 +172,10 @@ func (fv *FakeVolume) IsReadOnly() bool {
return false return false
} }
func (fv *FakeVolume) SupportsSELinux() bool {
return false
}
func (fv *FakeVolume) GetPath() string { func (fv *FakeVolume) GetPath() string {
return path.Join(fv.Plugin.Host.GetPodVolumeDir(fv.PodUID, util.EscapeQualifiedNameForDisk(fv.Plugin.PluginName), fv.VolName)) 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) // 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---- // 3. Logical OR the permission bits with rw-rw----
SupportsOwnershipManagement() bool 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. // Cleaner interface provides methods to cleanup/unmount the volumes.

View File

@ -29,9 +29,10 @@ import (
"k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util"
. "github.com/onsi/ginkgo" . "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()) podName := "security-context-" + string(util.NewUUID())
pod := &api.Pod{ pod := &api.Pod{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
@ -39,7 +40,10 @@ func scTestPod() *api.Pod {
Labels: map[string]string{"name": podName}, Labels: map[string]string{"name": podName},
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
SecurityContext: &api.PodSecurityContext{}, SecurityContext: &api.PodSecurityContext{
HostIPC: hostIPC,
HostPID: hostPID,
},
Containers: []api.Container{ Containers: []api.Container{
{ {
Name: "test-container", Name: "test-container",
@ -57,7 +61,7 @@ var _ = Describe("[Skipped] Security Context", func() {
framework := NewFramework("security-context") framework := NewFramework("security-context")
It("should support pod.Spec.SecurityContext.SupplementalGroups", func() { It("should support pod.Spec.SecurityContext.SupplementalGroups", func() {
pod := scTestPod() pod := scTestPod(false, false)
pod.Spec.Containers[0].Command = []string{"id", "-G"} pod.Spec.Containers[0].Command = []string{"id", "-G"}
pod.Spec.SecurityContext.SupplementalGroups = []int64{1234, 5678} pod.Spec.SecurityContext.SupplementalGroups = []int64{1234, 5678}
groups := []string{"1234", "5678"} groups := []string{"1234", "5678"}
@ -65,7 +69,7 @@ var _ = Describe("[Skipped] Security Context", func() {
}) })
It("should support pod.Spec.SecurityContext.RunAsUser", func() { It("should support pod.Spec.SecurityContext.RunAsUser", func() {
pod := scTestPod() pod := scTestPod(false, false)
var uid int64 = 1001 var uid int64 = 1001
pod.Spec.SecurityContext.RunAsUser = &uid pod.Spec.SecurityContext.RunAsUser = &uid
pod.Spec.Containers[0].Command = []string{"sh", "-c", "id -u"} 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() { It("should support container.SecurityContext.RunAsUser", func() {
pod := scTestPod() pod := scTestPod(false, false)
var uid int64 = 1001 var uid int64 = 1001
var overrideUid int64 = 1002 var overrideUid int64 = 1002
pod.Spec.SecurityContext.RunAsUser = &uid pod.Spec.SecurityContext.RunAsUser = &uid
@ -88,4 +92,112 @@ var _ = Describe("[Skipped] Security Context", func() {
fmt.Sprintf("%v", overrideUid), 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))
}