From 14a012bb214cb207e887a10c5349026a8866803f Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 15 Dec 2022 09:22:52 -0800 Subject: [PATCH] kubectl debug: add netadmin profile Add the netadmin profile from KEP 1441 https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/1441-kubectl-debug#debugging-profiles Signed-off-by: Will Daly Kubernetes-commit: f5095bf34dba15c702b4618bcd24aae6fc2f47b2 --- pkg/cmd/debug/debug.go | 2 +- pkg/cmd/debug/debug_test.go | 21 +++++ pkg/cmd/debug/profiles.go | 70 +++++++++++++++-- pkg/cmd/debug/profiles_test.go | 138 +++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/debug/debug.go b/pkg/cmd/debug/debug.go index a95b2f13..02d91fab 100644 --- a/pkg/cmd/debug/debug.go +++ b/pkg/cmd/debug/debug.go @@ -190,7 +190,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", or "restricted".`)) + cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Debugging profile. Options are "legacy", "general", "baseline", "netadmin", or "restricted".`)) } // 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 84224670..80d32166 100644 --- a/pkg/cmd/debug/debug_test.go +++ b/pkg/cmd/debug/debug_test.go @@ -294,6 +294,27 @@ func TestGenerateDebugContainer(t *testing.T) { }, }, }, + { + name: "netadmin profile", + opts: &DebugOptions{ + Image: "busybox", + PullPolicy: corev1.PullIfNotPresent, + Profile: ProfileNetadmin, + }, + expected: &corev1.EphemeralContainer{ + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "debugger-1", + Image: "busybox", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageReadFile, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }, + }, + }, } { t.Run(tc.name, func(t *testing.T) { tc.opts.IOStreams = genericclioptions.NewTestIOStreamsDiscard() diff --git a/pkg/cmd/debug/profiles.go b/pkg/cmd/debug/profiles.go index 4100e52d..f2986c9a 100644 --- a/pkg/cmd/debug/profiles.go +++ b/pkg/cmd/debug/profiles.go @@ -52,6 +52,8 @@ const ( // ProfileRestricted is identical to "baseline" but adds configuration that's required // under the restricted security profile, such as requiring a non-root user and dropping all capabilities. ProfileRestricted = "restricted" + // ProfileNetadmin offers elevated privileges for network debugging. + ProfileNetadmin = "netadmin" ) type ProfileApplier interface { @@ -70,6 +72,8 @@ func NewProfileApplier(profile string) (ProfileApplier, error) { return &baselineProfile{}, nil case ProfileRestricted: return &restrictedProfile{}, nil + case ProfileNetadmin: + return &netadminProfile{}, nil } return nil, fmt.Errorf("unknown profile: %s", profile) @@ -87,6 +91,9 @@ type baselineProfile struct { type restrictedProfile struct { } +type netadminProfile struct { +} + func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { switch target.(type) { case *corev1.Pod: @@ -183,6 +190,26 @@ func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target return nil } +func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { + style, err := getDebugStyle(pod, target) + if err != nil { + return fmt.Errorf("netadmin profile: %s", err) + } + + allowNetadminCapability(pod, containerName) + + switch style { + case node: + useHostNamespaces(pod) + setPrivileged(pod, containerName) + + case podCopy, 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) { @@ -239,6 +266,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.BoolPtr(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 { @@ -274,13 +315,28 @@ func allowProcessTracing(p *corev1.Pod, containerName string) { if c.Name != containerName { return true } - if c.SecurityContext == nil { - c.SecurityContext = &corev1.SecurityContext{} - } - if c.SecurityContext.Capabilities == nil { - c.SecurityContext.Capabilities = &corev1.Capabilities{} - } - c.SecurityContext.Capabilities.Add = append(c.SecurityContext.Capabilities.Add, "SYS_PTRACE") + addCapability(c, "SYS_PTRACE") return false }) } + +// allowNetadminCapability grants NET_ADMIN capability to the container. +func allowNetadminCapability(p *corev1.Pod, containerName string) { + podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { + if c.Name != containerName { + return true + } + addCapability(c, "NET_ADMIN") + return false + }) +} + +func addCapability(c *corev1.Container, capability corev1.Capability) { + if c.SecurityContext == nil { + c.SecurityContext = &corev1.SecurityContext{} + } + if c.SecurityContext.Capabilities == nil { + c.SecurityContext.Capabilities = &corev1.Capabilities{} + } + c.SecurityContext.Capabilities.Add = append(c.SecurityContext.Capabilities.Add, capability) +} diff --git a/pkg/cmd/debug/profiles_test.go b/pkg/cmd/debug/profiles_test.go index cdaaebbd..1e94a7a9 100644 --- a/pkg/cmd/debug/profiles_test.go +++ b/pkg/cmd/debug/profiles_test.go @@ -17,6 +17,7 @@ limitations under the License. package debug import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -430,3 +431,140 @@ func TestRestrictedProfile(t *testing.T) { }) } } + +func TestNetAdminProfile(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("netadmin 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{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }, + }, + }}, + }, + }, + { + 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{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }, + }, + }, + }, + }, + { + 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, + Containers: []corev1.Container{ + { + Name: "dbg", + Image: "dbgimage", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.BoolPtr(true), + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := (&netadminProfile{}).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) + } + }) + } +}