diff --git a/pkg/podimpersonation/podimpersonation.go b/pkg/podimpersonation/podimpersonation.go index 9a8b14c5..c01da0f4 100644 --- a/pkg/podimpersonation/podimpersonation.go +++ b/pkg/podimpersonation/podimpersonation.go @@ -512,10 +512,12 @@ func (s *PodImpersonation) adminKubeConfig(user user.Info, role *rbacv1.ClusterR func (s *PodImpersonation) augmentPod(pod *v1.Pod, sa *v1.ServiceAccount, secret *v1.Secret, imageOverride string) *v1.Pod { var ( - zero = int64(0) - t = true - f = false - m = int32(420) + zero = int64(0) + t = true + f = false + m = int32(0o644) + m2 = int32(0o600) + shellUser = 1000 ) pod = pod.DeepCopy() @@ -535,11 +537,18 @@ func (s *PodImpersonation) augmentPod(pod *v1.Pod, sa *v1.ServiceAccount, secret }, v1.Volume{ Name: "user-kubeconfig", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + v1.Volume{ + Name: "user-kube-configmap", VolumeSource: v1.VolumeSource{ ConfigMap: &v1.ConfigMapVolumeSource{ LocalObjectReference: v1.LocalObjectReference{ Name: s.userConfigName(), }, + DefaultMode: &m2, }, }, }, @@ -553,15 +562,45 @@ func (s *PodImpersonation) augmentPod(pod *v1.Pod, sa *v1.ServiceAccount, secret }, }) + image := imageOverride + if image == "" { + image = s.imageName() + } + for i, container := range pod.Spec.Containers { for _, envvar := range container.Env { if envvar.Name != "KUBECONFIG" { continue } + //This mounts two volumes, one configMap and one emptyDir. + //The reason for this is that we need to change the permissions on the kubeconfig file + //and, since a configMap volume is always read-only, we need an emptyDir volume as well. + vmount := v1.VolumeMount{ + Name: "user-kubeconfig", + MountPath: "/tmp/.kube", + } + cfgVMount := v1.VolumeMount{ + Name: "user-kube-configmap", + MountPath: "/home/.kube/config", + SubPath: "config", + } + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, v1.Container{ + Name: "init-kubeconfig-volume", + Image: image, + Command: []string{"sh", "-c", fmt.Sprintf("cp %s %s && chown %d %s/config", cfgVMount.MountPath, vmount.MountPath, shellUser, vmount.MountPath)}, + ImagePullPolicy: v1.PullIfNotPresent, + SecurityContext: &v1.SecurityContext{ + RunAsUser: &zero, + RunAsGroup: &zero, + }, + VolumeMounts: []v1.VolumeMount{cfgVMount, vmount}, + }, + ) + pod.Spec.Containers[i].VolumeMounts = append(container.VolumeMounts, v1.VolumeMount{ Name: "user-kubeconfig", - ReadOnly: true, MountPath: envvar.Value, SubPath: "config", }) @@ -569,11 +608,6 @@ func (s *PodImpersonation) augmentPod(pod *v1.Pod, sa *v1.ServiceAccount, secret } } - image := imageOverride - if image == "" { - image = s.imageName() - } - pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{ Name: "proxy", Image: image, diff --git a/pkg/podimpersonation/podimpersonation_test.go b/pkg/podimpersonation/podimpersonation_test.go new file mode 100644 index 00000000..1669e005 --- /dev/null +++ b/pkg/podimpersonation/podimpersonation_test.go @@ -0,0 +1,91 @@ +package podimpersonation + +import ( + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + "time" +) + +func TestAugmentPod(t *testing.T) { + var ( + zero = int64(0) + ) + testCases := []struct { + name string + imageOverride string + envVars []v1.EnvVar + }{ + { + name: "Should mount volume to container, create an init container and use regular image", + imageOverride: "", + envVars: []v1.EnvVar{{Name: "KUBECONFIG", Value: ".kube/config"}}, + }, + { + name: "Should mount volume to container, create an init container and use overridden image", + imageOverride: "rancher/notShell:v1.0.0", + envVars: []v1.EnvVar{{Name: "KUBECONFIG", Value: ".kube/config"}}, + }, + { + name: "Should not create init container if there's no KUBECONFIG envVar", + imageOverride: "", + envVars: []v1.EnvVar{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := newPod(tc.envVars) + impersonator := New("", nil, time.Minute, func() string { return "rancher/shell:v0.1.22" }) + pod := impersonator.augmentPod(p, nil, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "s"}}, tc.imageOverride) + + assert.Len(t, pod.Spec.Volumes, len(p.Spec.Volumes)+4, "expected four new volumes") + if len(tc.envVars) != 0 { + assert.Len(t, pod.Spec.Containers[0].VolumeMounts, len(p.Spec.Containers[0].VolumeMounts)+1, "expected kubeconfig volume to be mounted") + assert.Len(t, pod.Spec.InitContainers, len(p.Spec.InitContainers)+1, "expected an init container to be created") + if tc.imageOverride != "" { + assert.Equal(t, pod.Spec.InitContainers[len(pod.Spec.InitContainers)-1].Image, tc.imageOverride, "expected image to be the one received as parameter") + } else { + assert.Equal(t, pod.Spec.InitContainers[len(pod.Spec.InitContainers)-1].Image, impersonator.imageName(), "expected image to be the impersonator image") + } + assert.Equal(t, pod.Spec.InitContainers[len(pod.Spec.InitContainers)-1].SecurityContext.RunAsUser, &zero, "expected init container to run as user zero") + assert.Equal(t, pod.Spec.InitContainers[len(pod.Spec.InitContainers)-1].SecurityContext.RunAsGroup, &zero, "expected init container to run as group zero") + } else { + assert.Len(t, pod.Spec.InitContainers, len(p.Spec.InitContainers), "expected no init container to be created") + } + assert.Equal(t, pod.Spec.Containers[len(pod.Spec.Containers)-1].Name, "proxy", "expected the container proxy to be created") + }) + } +} + +func newPod(env []v1.EnvVar) *v1.Pod { + return &v1.Pod{ + Spec: v1.PodSpec{ + Volumes: []v1.Volume{{ + Name: "volume1", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "cfgMap", + }, + }, + }, + }}, + Containers: []v1.Container{ + { + Name: "shell", + Image: "rancher/shell:v0.1.22", + Env: env, + VolumeMounts: []v1.VolumeMount{{ + Name: "volume1", + MountPath: "/home/vol", + }}, + }, + }, + ServiceAccountName: "svc-account-1", + AutomountServiceAccountToken: nil, + SecurityContext: nil, + }, + } +}