mirror of https://github.com/k3s-io/k3s
Support volume relabling for pods which specify an SELinux label
parent
1524d7490a
commit
1d352a16b8
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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{}
|
||||
}
|
|
@ -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"
|
|
@ -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{}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -187,6 +187,10 @@ func (cephfsVolume *cephfsBuilder) IsReadOnly() bool {
|
|||
return cephfsVolume.readonly
|
||||
}
|
||||
|
||||
func (cephfsVolume *cephfsBuilder) SupportsSELinux() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type cephfsCleaner struct {
|
||||
*cephfs
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{}}},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue