From 4bcd969a9ecf364d230cab3ed71eeb739b8e3af3 Mon Sep 17 00:00:00 2001 From: Francis Laniel Date: Mon, 10 Jul 2023 14:21:39 +0200 Subject: [PATCH] kubectl debug: add sysadmin profile Add the sysadmin profile from KEP 1441 [1]. Signed-off-by: Francis Laniel [1]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/1441-kubectl-debug#debugging-profiles Kubernetes-commit: 4d6c0ba518d6c4037102736fe443490dde006fc2 --- pkg/cmd/debug/debug.go | 2 +- pkg/cmd/debug/debug_test.go | 19 +++ pkg/cmd/debug/profiles.go | 44 +++++++ pkg/cmd/debug/profiles_test.go | 234 +++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/debug/debug.go b/pkg/cmd/debug/debug.go index 456ebc4b..7a6bea2f 100644 --- a/pkg/cmd/debug/debug.go +++ b/pkg/cmd/debug/debug.go @@ -192,7 +192,7 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.ShareProcesses, "share-processes", o.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy.")) cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container.")) - cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Debugging profile. Options are "legacy", "general", "baseline", "netadmin", or "restricted".`)) + cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Options are "legacy", "general", "baseline", "netadmin", "restricted" or "sysadmin".`)) } // Complete finishes run-time initialization of debug.DebugOptions. diff --git a/pkg/cmd/debug/debug_test.go b/pkg/cmd/debug/debug_test.go index e3e72f21..035e7f6d 100644 --- a/pkg/cmd/debug/debug_test.go +++ b/pkg/cmd/debug/debug_test.go @@ -316,6 +316,25 @@ func TestGenerateDebugContainer(t *testing.T) { }, }, }, + { + name: "sysadmin profile", + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileSysadmin, + }, + expected: &corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() diff --git a/pkg/cmd/debug/profiles.go b/pkg/cmd/debug/profiles.go index ee338325..2e0f8621 100644 --- a/pkg/cmd/debug/profiles.go +++ b/pkg/cmd/debug/profiles.go @@ -54,6 +54,8 @@ const ( ProfileRestricted = "restricted" // ProfileNetadmin offers elevated privileges for network debugging. ProfileNetadmin = "netadmin" + // ProfileSysadmin offers elevated privileges for debugging. + ProfileSysadmin = "sysadmin" ) type ProfileApplier interface { @@ -74,6 +76,8 @@ func NewProfileApplier(profile string) (ProfileApplier, error) { return &restrictedProfile{}, nil case ProfileNetadmin: return &netadminProfile{}, nil + case ProfileSysadmin: + return &sysadminProfile{}, nil } return nil, fmt.Errorf("unknown profile: %s", profile) @@ -94,6 +98,9 @@ type restrictedProfile struct { type netadminProfile struct { } +type sysadminProfile struct { +} + func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { switch target.(type) { case *corev1.Pod: @@ -212,6 +219,29 @@ func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target ru return nil } +func (p *sysadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + style, err := getDebugStyle(pod, target) + if err != nil { + return fmt.Errorf("sysadmin profile: %s", err) + } + + setPrivileged(pod, containerName) + + switch style { + case node: + useHostNamespaces(pod) + mountRootPartition(pod, containerName) + + case podCopy: + // to mimic general, default and baseline + shareProcessNamespace(pod) + case ephemeral: + // no additional modifications needed + } + + return nil +} + // removeLabelsAndProbes removes labels from the pod and remove probes // from all containers of the pod. func removeLabelsAndProbes(p *corev1.Pod) { @@ -271,6 +301,20 @@ func clearSecurityContext(p *corev1.Pod, containerName string) { }) } +// setPrivileged configures the containers as privileged. +func setPrivileged(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + if c.SecurityContext == nil { + c.SecurityContext = &corev1.SecurityContext{} + } + c.SecurityContext.Privileged = pointer.Bool(true) + return false + }) +} + // disallowRoot configures the container to run as a non-root user. func disallowRoot(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { diff --git a/pkg/cmd/debug/profiles_test.go b/pkg/cmd/debug/profiles_test.go index 7b44a9f3..8d76ab64 100644 --- a/pkg/cmd/debug/profiles_test.go +++ b/pkg/cmd/debug/profiles_test.go @@ -678,3 +678,237 @@ func TestNetAdminProfile(t *testing.T) { }) } } + +func TestSysAdminProfile(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + }, + }, + }}, + } + + tests := []struct { + name string + pod *corev1.Pod + containerName string + target runtime.Object + expectPod *corev1.Pod + expectErr error + }{ + { + name: "nil target", + pod: pod, + containerName: "dbg", + target: nil, + expectErr: fmt.Errorf("sysadmin profile: objects of type are not supported"), + }, + { + name: "debug by ephemeral container", + pod: pod, + containerName: "dbg", + target: pod, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "dbg", Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + }}, + }, + }, + { + name: "debug by pod copy", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + }, + }, + }, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + { + name: "debug by pod copy preserve existing capability", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }, + }, + containerName: "dbg", + target: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + }, + }, + }, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "app", Image: "appimage"}, + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + ShareProcessNamespace: pointer.Bool(true), + }, + }, + }, + { + name: "debug by node", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "dbg", Image: "dbgimage"}, + }, + }, + }, + containerName: "dbg", + target: testNode, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + HostNetwork: true, + HostPID: true, + HostIPC: true, + Volumes: []corev1.Volume{ + { + Name: "host-root", + VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}}, + }, + }, + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}}, + }, + }, + }, + }, + }, + { + name: "debug by node preserve existing capability", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + }, + }, + }, + }, + containerName: "dbg", + target: testNode, + expectPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod"}, + Spec: corev1.PodSpec{ + HostNetwork: true, + HostPID: true, + HostIPC: true, + Volumes: []corev1.Volume{ + { + Name: "host-root", + VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}}, + }, + }, + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SYS_PTRACE"}, + }, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}}, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := (&sysadminProfile{}).Apply(test.pod, test.containerName, test.target) + if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) { + t.Fatalf("expect error: %v, got error: %v", test.expectErr, err) + } + if err != nil { + return + } + if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { + t.Error("unexpected diff in generated object: (-want +got):\n", diff) + } + }) + } +}