@@ -19,6 +19,8 @@ package node
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "os/exec"
23
+ "strconv"
22
24
"strings"
23
25
"time"
24
26
@@ -43,6 +45,10 @@ import (
43
45
var (
44
46
// non-root UID used in tests.
45
47
nonRootTestUserID = int64 (1000 )
48
+
49
+ // kubelet user used for userns mapping.
50
+ kubeletUserForUsernsMapping = "kubelet"
51
+ getsubuidsBinary = "getsubids"
46
52
)
47
53
48
54
var _ = SIGDescribe ("Security Context" , func () {
@@ -112,6 +118,74 @@ var _ = SIGDescribe("Security Context", func() {
112
118
}
113
119
})
114
120
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
+
115
189
f .It ("must not create the user namespace if set to true [LinuxOnly]" , feature .UserNamespacesSupport , func (ctx context.Context ) {
116
190
// with hostUsers=true the pod must use the host user namespace
117
191
pod := makePod (true )
@@ -683,3 +757,57 @@ func waitForFailure(ctx context.Context, f *framework.Framework, name string, ti
683
757
},
684
758
)).To (gomega .Succeed (), "wait for pod %q to fail" , name )
685
759
}
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