Merge pull request #119200 from eiffel-fl/francis/sysadmin-debug-profile
kubectl debug: add sysadmin profile Kubernetes-commit: 4163ce5017268b0ae25df327f0a210032ef1cc80
This commit is contained in:
		
						commit
						b73518af09
					
				
							
								
								
									
										10
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										10
									
								
								go.mod
								
								
								
								
							|  | @ -30,10 +30,10 @@ require ( | |||
| 	github.com/stretchr/testify v1.8.4 | ||||
| 	golang.org/x/sys v0.15.0 | ||||
| 	gopkg.in/yaml.v2 v2.4.0 | ||||
| 	k8s.io/api v0.0.0-20240118211853-d5724e467262 | ||||
| 	k8s.io/api v0.0.0-20240124211858-f3648a53522e | ||||
| 	k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523 | ||||
| 	k8s.io/cli-runtime v0.0.0-20240118214801-ad54ff319bf2 | ||||
| 	k8s.io/client-go v0.0.0-20240122172058-657d7be98b25 | ||||
| 	k8s.io/client-go v0.0.0-20240124011219-8092c71d3605 | ||||
| 	k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48 | ||||
| 	k8s.io/component-helpers v0.0.0-20240118212950-9a5801419916 | ||||
| 	k8s.io/klog/v2 v2.120.1 | ||||
|  | @ -96,11 +96,11 @@ require ( | |||
| ) | ||||
| 
 | ||||
| replace ( | ||||
| 	k8s.io/api => k8s.io/api v0.0.0-20240118211853-d5724e467262 | ||||
| 	k8s.io/api => k8s.io/api v0.0.0-20240124211858-f3648a53522e | ||||
| 	k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523 | ||||
| 	k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20240118214801-ad54ff319bf2 | ||||
| 	k8s.io/client-go => k8s.io/client-go v0.0.0-20240122172058-657d7be98b25 | ||||
| 	k8s.io/code-generator => k8s.io/code-generator v0.0.0-20240118211431-5ad9f43b6468 | ||||
| 	k8s.io/client-go => k8s.io/client-go v0.0.0-20240124011219-8092c71d3605 | ||||
| 	k8s.io/code-generator => k8s.io/code-generator v0.0.0-20240123225209-c781f8765cf8 | ||||
| 	k8s.io/component-base => k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48 | ||||
| 	k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20240118212950-9a5801419916 | ||||
| 	k8s.io/metrics => k8s.io/metrics v0.0.0-20240118214633-5b4611d6f391 | ||||
|  |  | |||
							
								
								
									
										8
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										8
									
								
								go.sum
								
								
								
								
							|  | @ -280,14 +280,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| k8s.io/api v0.0.0-20240118211853-d5724e467262 h1:iP5kk4e+c89xvFLRWdUb7BAYzBV9A//0vBHc9Ob/NUM= | ||||
| k8s.io/api v0.0.0-20240118211853-d5724e467262/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA= | ||||
| k8s.io/api v0.0.0-20240124211858-f3648a53522e h1:Lv52wennNKzlcDrBtANztawHC8xaTllHm51WUKIP0Ew= | ||||
| k8s.io/api v0.0.0-20240124211858-f3648a53522e/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA= | ||||
| k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523 h1:1iJCbQAZv58v4zxd0ECIIMnyYlFsPWa2hmjqGEsv/5g= | ||||
| k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523/go.mod h1:Oh3ZrffM1/I8O/43oAA+aoOYgSregIXHxcWJB9ZRfQ8= | ||||
| k8s.io/cli-runtime v0.0.0-20240118214801-ad54ff319bf2 h1:KFiGqjF1kq6YDXgsWmsms2bu10g1M17P9HRO2lGYry4= | ||||
| k8s.io/cli-runtime v0.0.0-20240118214801-ad54ff319bf2/go.mod h1:zmPMirb3vXLcTGRaL9Pw5SifCP8EY2asTp+PTgnwSYI= | ||||
| k8s.io/client-go v0.0.0-20240122172058-657d7be98b25 h1:iBiouUazhDUHxrqDywgcbmARvao6UwVu+nSbIrIRh4k= | ||||
| k8s.io/client-go v0.0.0-20240122172058-657d7be98b25/go.mod h1:WuuT9L6+pj4rHmL2pb22xnOdtSvjiEcpB18g9Fuk0js= | ||||
| k8s.io/client-go v0.0.0-20240124011219-8092c71d3605 h1:Dw3Ctw+SS3YmJTpaYP2nhIs4XagL4ctjKY0pHxN4RT8= | ||||
| k8s.io/client-go v0.0.0-20240124011219-8092c71d3605/go.mod h1:WuuT9L6+pj4rHmL2pb22xnOdtSvjiEcpB18g9Fuk0js= | ||||
| k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48 h1:3HvTUZ0ry5c0P15P+glBxBj+eh8Uv2ijNvjEORH+oOQ= | ||||
| k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48/go.mod h1:ANnr9YwsqK1XgjzXj9fGHEMDOp0QddDkKgQLLBPZ7Kg= | ||||
| k8s.io/component-helpers v0.0.0-20240118212950-9a5801419916 h1:Ptl0rZGRIrjdZuSqSO8Dwok1SWA9bqAi/Vcc3HYA/Ks= | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 <nil> 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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue