Skip to content

Commit c984f0f

Browse files
committed
tests/e2e: Add tests for userns kubelet mappings
Signed-off-by: Rodrigo Campos <[email protected]>
1 parent fd34707 commit c984f0f

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

test/e2e/common/node/security_context.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package node
1919
import (
2020
"context"
2121
"fmt"
22+
"os/exec"
23+
"strconv"
2224
"strings"
2325
"time"
2426

@@ -43,6 +45,10 @@ import (
4345
var (
4446
// non-root UID used in tests.
4547
nonRootTestUserID = int64(1000)
48+
49+
// kubelet user used for userns mapping.
50+
kubeletUserForUsernsMapping = "kubelet"
51+
getsubuidsBinary = "getsubids"
4652
)
4753

4854
var _ = SIGDescribe("Security Context", func() {
@@ -112,6 +118,74 @@ var _ = SIGDescribe("Security Context", func() {
112118
}
113119
})
114120

121+
f.It("must create the user namespace in the configured hostUID/hostGID range [LinuxOnly]", feature.UserNamespacesSupport, func(ctx context.Context) {
122+
// We need to check with the binary "getsubuids" the mappings for the kubelet.
123+
// If something is not present, we skip the test as the node wasn't configured to run this test.
124+
id, length, err := kubeletUsernsMappings(getsubuidsBinary)
125+
if err != nil {
126+
e2eskipper.Skipf("node is not setup for userns with kubelet mappings: %v", err)
127+
}
128+
129+
for i := 0; i < 4; i++ {
130+
// makePod(false) creates the pod with user namespace
131+
podClient := e2epod.PodClientNS(f, f.Namespace.Name)
132+
createdPod := podClient.Create(ctx, makePod(false))
133+
ginkgo.DeferCleanup(func(ctx context.Context) {
134+
ginkgo.By("delete the pods")
135+
podClient.DeleteSync(ctx, createdPod.Name, metav1.DeleteOptions{}, f.Timeouts.PodDelete)
136+
})
137+
getLogs := func(pod *v1.Pod) (string, error) {
138+
err := e2epod.WaitForPodSuccessInNamespaceTimeout(ctx, f.ClientSet, createdPod.Name, f.Namespace.Name, f.Timeouts.PodStart)
139+
if err != nil {
140+
return "", err
141+
}
142+
podStatus, err := podClient.Get(ctx, pod.Name, metav1.GetOptions{})
143+
if err != nil {
144+
return "", err
145+
}
146+
return e2epod.GetPodLogs(ctx, f.ClientSet, f.Namespace.Name, podStatus.Name, containerName)
147+
}
148+
149+
logs, err := getLogs(createdPod)
150+
framework.ExpectNoError(err)
151+
152+
// The hostUID is the second field in the /proc/self/uid_map file.
153+
hostMap := strings.Fields(logs)
154+
if len(hostMap) != 3 {
155+
framework.Failf("can't detect hostUID for container, is the format of /proc/self/uid_map correct?")
156+
}
157+
158+
tmp, err := strconv.ParseUint(hostMap[1], 10, 32)
159+
if err != nil {
160+
framework.Failf("can't convert hostUID to int: %v", err)
161+
}
162+
hostUID := uint32(tmp)
163+
164+
// Here we check the pod got a userns mapping within the range
165+
// configured for the kubelet.
166+
// To make sure the pod mapping doesn't fall within range by chance,
167+
// we do the following:
168+
// * The configured kubelet range as small as possible (enough to
169+
// fit 110 pods, the default of the kubelet) to minimize the chance
170+
// of this range being used "by chance" in the node configuration.
171+
// * We also run this in a loop, so it is less likely to get lucky
172+
// several times in a row.
173+
//
174+
// There are 65536 ranges possible and we configured the kubelet to
175+
// use 110 of them. The chances of this test passing by chance 4
176+
// times in a row and the kubelet not using only the configured
177+
// range are:
178+
//
179+
// (110/65536) ^ 4 = 4.73e-12. IOW, less than 1 in a trillion.
180+
//
181+
// Furthermore, the unit tests would also need to be buggy and not
182+
// detect the bug. We expect to catch off-by-one errors there.
183+
if hostUID < id || hostUID > id+length {
184+
framework.Failf("user namespace created outside of the configured range. Expected range: %v-%v, got: %v", id, id+length, hostUID)
185+
}
186+
}
187+
})
188+
115189
f.It("must not create the user namespace if set to true [LinuxOnly]", feature.UserNamespacesSupport, func(ctx context.Context) {
116190
// with hostUsers=true the pod must use the host user namespace
117191
pod := makePod(true)
@@ -683,3 +757,57 @@ func waitForFailure(ctx context.Context, f *framework.Framework, name string, ti
683757
},
684758
)).To(gomega.Succeed(), "wait for pod %q to fail", name)
685759
}
760+
761+
// parseGetSubIdsOutput parses the output from the `getsubids` tool, which is used to query subordinate user or group ID ranges for
762+
// a given user or group. getsubids produces a line for each mapping configured.
763+
// Here we expect that there is a single mapping, and the same values are used for the subordinate user and group ID ranges.
764+
// The output is something like:
765+
// $ getsubids kubelet
766+
// 0: kubelet 65536 2147483648
767+
// $ getsubids -g kubelet
768+
// 0: kubelet 65536 2147483648
769+
// XXX: this is a c&p from pkg/kubelet/kubelet_pods.go. It is simpler to c&p than to try to reuse it.
770+
func parseGetSubIdsOutput(input string) (uint32, uint32, error) {
771+
lines := strings.Split(strings.Trim(input, "\n"), "\n")
772+
if len(lines) != 1 {
773+
return 0, 0, fmt.Errorf("error parsing line %q: it must contain only one line", input)
774+
}
775+
776+
parts := strings.Fields(lines[0])
777+
if len(parts) != 4 {
778+
return 0, 0, fmt.Errorf("invalid line %q", input)
779+
}
780+
781+
// Parsing the numbers
782+
num1, err := strconv.ParseUint(parts[2], 10, 32)
783+
if err != nil {
784+
return 0, 0, fmt.Errorf("error parsing line %q: %w", input, err)
785+
}
786+
787+
num2, err := strconv.ParseUint(parts[3], 10, 32)
788+
if err != nil {
789+
return 0, 0, fmt.Errorf("error parsing line %q: %w", input, err)
790+
}
791+
792+
return uint32(num1), uint32(num2), nil
793+
}
794+
795+
func kubeletUsernsMappings(subuidBinary string) (uint32, uint32, error) {
796+
cmd, err := exec.LookPath(getsubuidsBinary)
797+
if err != nil {
798+
return 0, 0, fmt.Errorf("getsubids binary not found in PATH")
799+
}
800+
outUids, err := exec.Command(cmd, kubeletUserForUsernsMapping).Output()
801+
if err != nil {
802+
return 0, 0, fmt.Errorf("no additional uids for user %q: %w", kubeletUserForUsernsMapping, err)
803+
}
804+
outGids, err := exec.Command(cmd, "-g", kubeletUserForUsernsMapping).Output()
805+
if err != nil {
806+
return 0, 0, fmt.Errorf("no additional gids for user %q", kubeletUserForUsernsMapping)
807+
}
808+
if string(outUids) != string(outGids) {
809+
return 0, 0, fmt.Errorf("mismatched subuids and subgids for user %q", kubeletUserForUsernsMapping)
810+
}
811+
812+
return parseGetSubIdsOutput(string(outUids))
813+
}

0 commit comments

Comments
 (0)