1
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 package windows
42
43 import (
44 "context"
45 "fmt"
46 "os"
47 "regexp"
48 "strings"
49 "time"
50
51 "github.com/onsi/ginkgo/v2"
52 "github.com/onsi/gomega"
53 appsv1 "k8s.io/api/apps/v1"
54 v1 "k8s.io/api/core/v1"
55 rbacv1 "k8s.io/api/rbac/v1"
56 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
57 "k8s.io/apimachinery/pkg/util/uuid"
58 clientset "k8s.io/client-go/kubernetes"
59 "k8s.io/kubernetes/test/e2e/feature"
60 "k8s.io/kubernetes/test/e2e/framework"
61 e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
62 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
63 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
64 imageutils "k8s.io/kubernetes/test/utils/image"
65 admissionapi "k8s.io/pod-security-admission/api"
66 )
67
68 const (
69
70
71 gmsaFullNodeLabel = "agentpool=windowsgmsa"
72
73
74
75 gmsaCrdManifestPath = `C:\gmsa\gmsa-cred-spec-gmsa-e2e.yml`
76
77
78
79 gmsaCustomResourceName = "gmsa-e2e"
80
81
82 gmsaWebhookDeployScriptURL = "https://raw.githubusercontent.com/kubernetes-sigs/windows-gmsa/master/admission-webhook/deploy/deploy-gmsa-webhook.sh"
83
84
85 expectedQueryOutput = "The command completed successfully"
86
87
88 gmsaDomain = "k8sgmsa.lan"
89
90
91 gmsaSharedFolder = "write_test"
92 )
93
94 var _ = sigDescribe(feature.Windows, "GMSA Full", framework.WithSerial(), framework.WithSlow(), skipUnlessWindows(func() {
95 f := framework.NewDefaultFramework("gmsa-full-test-windows")
96 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
97
98 ginkgo.Describe("GMSA support", func() {
99 ginkgo.It("works end to end", func(ctx context.Context) {
100 defer ginkgo.GinkgoRecover()
101
102 ginkgo.By("finding the worker node that fulfills this test's assumptions")
103 nodes := findPreconfiguredGmsaNodes(ctx, f.ClientSet)
104 if len(nodes) != 1 {
105 e2eskipper.Skipf("Expected to find exactly one node with the %q label, found %d", gmsaFullNodeLabel, len(nodes))
106 }
107 node := nodes[0]
108
109 ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node")
110 crdManifestContents := retrieveCRDManifestFileContents(ctx, f, node)
111
112 ginkgo.By("deploying the GMSA webhook")
113 err := deployGmsaWebhook(ctx, f)
114 if err != nil {
115 framework.Failf(err.Error())
116 }
117
118 ginkgo.By("creating the GMSA custom resource")
119 err = createGmsaCustomResource(f.Namespace.Name, crdManifestContents)
120 if err != nil {
121 framework.Failf(err.Error())
122 }
123
124 ginkgo.By("creating an RBAC role to grant use access to that GMSA resource")
125 rbacRoleName, err := createRBACRoleForGmsa(ctx, f)
126 if err != nil {
127 framework.Failf(err.Error())
128 }
129
130 ginkgo.By("creating a service account")
131 serviceAccountName := createServiceAccount(ctx, f)
132
133 ginkgo.By("binding the RBAC role to the service account")
134 bindRBACRoleToServiceAccount(ctx, f, serviceAccountName, rbacRoleName)
135
136 ginkgo.By("creating a pod using the GMSA cred spec")
137 podName := createPodWithGmsa(ctx, f, serviceAccountName)
138
139
140
141
142 ginkgo.By("checking that nltest /QUERY returns successfully")
143 var output string
144 gomega.Eventually(ctx, func() bool {
145 output, err = runKubectlExecInNamespace(f.Namespace.Name, podName, "nltest", "/QUERY")
146 if err != nil {
147 framework.Logf("unable to run command in container via exec: %s", err)
148 return false
149 }
150
151 if !isValidOutput(output) {
152
153
154 output, err = runKubectlExecInNamespace(f.Namespace.Name, podName, "nltest", fmt.Sprintf("/sc_reset:%s", gmsaDomain))
155 if err != nil {
156 framework.Logf("unable to run command in container via exec: %s", err)
157 return false
158 }
159 framework.Logf("failed to connect to domain; tried resetting the domain, output:\n%s", string(output))
160 return false
161 }
162 return true
163 }, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue())
164 })
165
166 ginkgo.It("can read and write file to remote SMB folder", func(ctx context.Context) {
167 defer ginkgo.GinkgoRecover()
168
169 ginkgo.By("finding the worker node that fulfills this test's assumptions")
170 nodes := findPreconfiguredGmsaNodes(ctx, f.ClientSet)
171 if len(nodes) != 1 {
172 e2eskipper.Skipf("Expected to find exactly one node with the %q label, found %d", gmsaFullNodeLabel, len(nodes))
173 }
174 node := nodes[0]
175
176 ginkgo.By("retrieving the contents of the GMSACredentialSpec custom resource manifest from the node")
177 crdManifestContents := retrieveCRDManifestFileContents(ctx, f, node)
178
179 ginkgo.By("deploying the GMSA webhook")
180 err := deployGmsaWebhook(ctx, f)
181 if err != nil {
182 framework.Failf(err.Error())
183 }
184
185 ginkgo.By("creating the GMSA custom resource")
186 err = createGmsaCustomResource(f.Namespace.Name, crdManifestContents)
187 if err != nil {
188 framework.Failf(err.Error())
189 }
190
191 ginkgo.By("creating an RBAC role to grant use access to that GMSA resource")
192 rbacRoleName, err := createRBACRoleForGmsa(ctx, f)
193 if err != nil {
194 framework.Failf(err.Error())
195 }
196
197 ginkgo.By("creating a service account")
198 serviceAccountName := createServiceAccount(ctx, f)
199
200 ginkgo.By("binding the RBAC role to the service account")
201 bindRBACRoleToServiceAccount(ctx, f, serviceAccountName, rbacRoleName)
202
203 ginkgo.By("creating a pod using the GMSA cred spec")
204 podName := createPodWithGmsa(ctx, f, serviceAccountName)
205
206 ginkgo.By("getting the ip of GMSA domain")
207 gmsaDomainIP := getGmsaDomainIP(f, podName)
208
209 ginkgo.By("checking that file can be read and write from the remote folder successfully")
210 filePath := fmt.Sprintf("\\\\%s\\%s\\write-test-%s.txt", gmsaDomainIP, gmsaSharedFolder, string(uuid.NewUUID())[0:4])
211 gomega.Eventually(ctx, func() bool {
212
213 _, _ = runKubectlExecInNamespace(f.Namespace.Name, podName, "--", "powershell.exe", "-Command", "echo 'This is a test file.' > "+filePath)
214 output, err := runKubectlExecInNamespace(f.Namespace.Name, podName, "powershell.exe", "--", "cat", filePath)
215 if err != nil {
216 framework.Logf("unable to get file from AD server: %s", err)
217 return false
218 }
219 return strings.Contains(output, "This is a test file.")
220 }, 1*time.Minute, 1*time.Second).Should(gomega.BeTrue())
221
222 })
223 })
224 }))
225
226 func isValidOutput(output string) bool {
227 return strings.Contains(output, expectedQueryOutput) &&
228 !strings.Contains(output, "ERROR_NO_LOGON_SERVERS") &&
229 !strings.Contains(output, "RPC_S_SERVER_UNAVAILABLE")
230 }
231
232
233 func findPreconfiguredGmsaNodes(ctx context.Context, c clientset.Interface) []v1.Node {
234 nodeOpts := metav1.ListOptions{
235 LabelSelector: gmsaFullNodeLabel,
236 }
237 nodes, err := c.CoreV1().Nodes().List(ctx, nodeOpts)
238 if err != nil {
239 framework.Failf("Unable to list nodes: %v", err)
240 }
241 return nodes.Items
242 }
243
244
245
246
247
248
249 func retrieveCRDManifestFileContents(ctx context.Context, f *framework.Framework, node v1.Node) string {
250 podName := "retrieve-gmsa-crd-contents"
251
252 splitPath := strings.Split(gmsaCrdManifestPath, `\`)
253 dirPath := strings.Join(splitPath[:len(splitPath)-1], `\`)
254 volumeName := "retrieve-gmsa-crd-contents-volume"
255
256 pod := &v1.Pod{
257 ObjectMeta: metav1.ObjectMeta{
258 Name: podName,
259 Namespace: f.Namespace.Name,
260 },
261 Spec: v1.PodSpec{
262 NodeSelector: node.Labels,
263 Containers: []v1.Container{
264 {
265 Name: podName,
266 Image: imageutils.GetPauseImageName(),
267 VolumeMounts: []v1.VolumeMount{
268 {
269 Name: volumeName,
270 MountPath: dirPath,
271 },
272 },
273 },
274 },
275 Volumes: []v1.Volume{
276 {
277 Name: volumeName,
278 VolumeSource: v1.VolumeSource{
279 HostPath: &v1.HostPathVolumeSource{
280 Path: dirPath,
281 },
282 },
283 },
284 },
285 },
286 }
287 e2epod.NewPodClient(f).CreateSync(ctx, pod)
288
289 output, err := runKubectlExecInNamespace(f.Namespace.Name, podName, "cmd", "/S", "/C", fmt.Sprintf("type %s", gmsaCrdManifestPath))
290 if err != nil {
291 framework.Failf("failed to retrieve the contents of %q on node %q: %v", gmsaCrdManifestPath, node.Name, err)
292 }
293
294
295 return strings.ReplaceAll(output, "\r\n", "\n")
296 }
297
298
299
300
301 func deployGmsaWebhook(ctx context.Context, f *framework.Framework) error {
302 deployerName := "webhook-deployer"
303 deployerNamespace := f.Namespace.Name
304 webHookName := "gmsa-webhook"
305 webHookNamespace := deployerNamespace + "-webhook"
306
307
308 ginkgo.DeferCleanup(func() {
309 framework.Logf("Best effort clean up of the webhook:\n")
310 stdout, err := e2ekubectl.RunKubectl("", "delete", "CustomResourceDefinition", "gmsacredentialspecs.windows.k8s.io")
311 framework.Logf("stdout:%s\nerror:%s", stdout, err)
312
313 stdout, err = e2ekubectl.RunKubectl("", "delete", "CertificateSigningRequest", fmt.Sprintf("%s.%s", webHookName, webHookNamespace))
314 framework.Logf("stdout:%s\nerror:%s", stdout, err)
315
316 stdout, err = runKubectlExecInNamespace(deployerNamespace, deployerName, "--", "kubectl", "delete", "-f", "/manifests.yml")
317 framework.Logf("stdout:%s\nerror:%s", stdout, err)
318 })
319
320
321 s := createServiceAccount(ctx, f)
322 bindClusterRBACRoleToServiceAccount(ctx, f, s, "cluster-admin")
323
324 installSteps := []string{
325 "echo \"@community http://dl-cdn.alpinelinux.org/alpine/edge/community/\" >> /etc/apk/repositories",
326 "&& apk add kubectl@community gettext openssl",
327 "&& apk add --update coreutils",
328 fmt.Sprintf("&& curl %s > gmsa.sh", gmsaWebhookDeployScriptURL),
329 "&& chmod +x gmsa.sh",
330 fmt.Sprintf("&& ./gmsa.sh --file %s --name %s --namespace %s --certs-dir %s --tolerate-master", "/manifests.yml", webHookName, webHookNamespace, "certs"),
331 "&& /agnhost pause",
332 }
333 installCommand := strings.Join(installSteps, " ")
334
335 pod := &v1.Pod{
336 ObjectMeta: metav1.ObjectMeta{
337 Name: deployerName,
338 Namespace: deployerNamespace,
339 },
340 Spec: v1.PodSpec{
341 ServiceAccountName: s,
342 NodeSelector: map[string]string{
343 "kubernetes.io/os": "linux",
344 },
345 Containers: []v1.Container{
346 {
347 Name: deployerName,
348 Image: imageutils.GetE2EImage(imageutils.Agnhost),
349 Command: []string{"bash", "-c"},
350 Args: []string{installCommand},
351 },
352 },
353 Tolerations: []v1.Toleration{
354 {
355 Operator: v1.TolerationOpExists,
356 Effect: v1.TaintEffectNoSchedule,
357 },
358 },
359 },
360 }
361 e2epod.NewPodClient(f).CreateSync(ctx, pod)
362
363
364 err := waitForDeployment(func() (*appsv1.Deployment, error) {
365 return f.ClientSet.AppsV1().Deployments(webHookNamespace).Get(ctx, webHookName, metav1.GetOptions{})
366 }, 10*time.Second, f.Timeouts.PodStart)
367 if err == nil {
368 framework.Logf("GMSA webhook successfully deployed")
369 } else {
370 err = fmt.Errorf("GMSA webhook did not become ready: %w", err)
371 }
372
373
374 logs, _ := e2epod.GetPodLogs(ctx, f.ClientSet, deployerNamespace, deployerName, deployerName)
375 framework.Logf("GMSA deployment logs:\n%s", logs)
376
377 return err
378 }
379
380
381
382
383
384 func createGmsaCustomResource(ns string, crdManifestContents string) error {
385 tempFile, err := os.CreateTemp("", "")
386 if err != nil {
387 return fmt.Errorf("unable to create temp file: %w", err)
388 }
389 defer tempFile.Close()
390
391 ginkgo.DeferCleanup(func() {
392 e2ekubectl.RunKubectl(ns, "delete", "--filename", tempFile.Name())
393 os.Remove(tempFile.Name())
394 })
395
396 _, err = tempFile.WriteString(crdManifestContents)
397 if err != nil {
398 err = fmt.Errorf("unable to write GMSA contents to %q: %w", tempFile.Name(), err)
399 return err
400 }
401
402 output, err := e2ekubectl.RunKubectl(ns, "apply", "--filename", tempFile.Name())
403 if err != nil {
404 err = fmt.Errorf("unable to create custom resource, output:\n%s: %w", output, err)
405 }
406
407 return err
408 }
409
410
411
412
413 func createRBACRoleForGmsa(ctx context.Context, f *framework.Framework) (string, error) {
414 roleName := f.Namespace.Name + "-rbac-role"
415
416 role := &rbacv1.ClusterRole{
417 ObjectMeta: metav1.ObjectMeta{
418 Name: roleName,
419 },
420 Rules: []rbacv1.PolicyRule{
421 {
422 APIGroups: []string{"windows.k8s.io"},
423 Resources: []string{"gmsacredentialspecs"},
424 Verbs: []string{"use"},
425 ResourceNames: []string{gmsaCustomResourceName},
426 },
427 },
428 }
429
430 ginkgo.DeferCleanup(framework.IgnoreNotFound(f.ClientSet.RbacV1().ClusterRoles().Delete), roleName, metav1.DeleteOptions{})
431 _, err := f.ClientSet.RbacV1().ClusterRoles().Create(ctx, role, metav1.CreateOptions{})
432 if err != nil {
433 err = fmt.Errorf("unable to create RBAC cluster role %q: %w", roleName, err)
434 }
435
436 return roleName, err
437 }
438
439
440 func createServiceAccount(ctx context.Context, f *framework.Framework) string {
441 accountName := f.Namespace.Name + "-sa-" + string(uuid.NewUUID())
442 account := &v1.ServiceAccount{
443 ObjectMeta: metav1.ObjectMeta{
444 Name: accountName,
445 Namespace: f.Namespace.Name,
446 },
447 }
448 if _, err := f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(ctx, account, metav1.CreateOptions{}); err != nil {
449 framework.Failf("unable to create service account %q: %v", accountName, err)
450 }
451 return accountName
452 }
453
454
455 func bindRBACRoleToServiceAccount(ctx context.Context, f *framework.Framework, serviceAccountName, rbacRoleName string) {
456 binding := &rbacv1.RoleBinding{
457 ObjectMeta: metav1.ObjectMeta{
458 Name: f.Namespace.Name + "-rbac-binding",
459 Namespace: f.Namespace.Name,
460 },
461 Subjects: []rbacv1.Subject{
462 {
463 Kind: "ServiceAccount",
464 Name: serviceAccountName,
465 Namespace: f.Namespace.Name,
466 },
467 },
468 RoleRef: rbacv1.RoleRef{
469 APIGroup: "rbac.authorization.k8s.io",
470 Kind: "ClusterRole",
471 Name: rbacRoleName,
472 },
473 }
474 _, err := f.ClientSet.RbacV1().RoleBindings(f.Namespace.Name).Create(ctx, binding, metav1.CreateOptions{})
475 framework.ExpectNoError(err)
476 }
477
478 func bindClusterRBACRoleToServiceAccount(ctx context.Context, f *framework.Framework, serviceAccountName, rbacRoleName string) {
479 binding := &rbacv1.ClusterRoleBinding{
480 ObjectMeta: metav1.ObjectMeta{
481 Name: f.Namespace.Name + "-rbac-binding",
482 Namespace: f.Namespace.Name,
483 },
484 Subjects: []rbacv1.Subject{
485 {
486 Kind: "ServiceAccount",
487 Name: serviceAccountName,
488 Namespace: f.Namespace.Name,
489 },
490 },
491 RoleRef: rbacv1.RoleRef{
492 APIGroup: "rbac.authorization.k8s.io",
493 Kind: "ClusterRole",
494 Name: rbacRoleName,
495 },
496 }
497 _, err := f.ClientSet.RbacV1().ClusterRoleBindings().Create(ctx, binding, metav1.CreateOptions{})
498 framework.ExpectNoError(err)
499 }
500
501
502 func createPodWithGmsa(ctx context.Context, f *framework.Framework, serviceAccountName string) string {
503 podName := "pod-with-gmsa"
504 credSpecName := gmsaCustomResourceName
505
506 pod := &v1.Pod{
507 ObjectMeta: metav1.ObjectMeta{
508 Name: podName,
509 Namespace: f.Namespace.Name,
510 },
511 Spec: v1.PodSpec{
512 ServiceAccountName: serviceAccountName,
513 Containers: []v1.Container{
514 {
515 Name: podName,
516 Image: imageutils.GetE2EImage(imageutils.BusyBox),
517 Command: []string{
518 "powershell.exe",
519 "-Command",
520 "sleep -Seconds 600",
521 },
522 },
523 },
524 SecurityContext: &v1.PodSecurityContext{
525 WindowsOptions: &v1.WindowsSecurityContextOptions{
526 GMSACredentialSpecName: &credSpecName,
527 },
528 },
529 },
530 }
531 e2epod.NewPodClient(f).CreateSync(ctx, pod)
532
533 return podName
534 }
535
536 func runKubectlExecInNamespace(namespace string, args ...string) (string, error) {
537 namespaceOption := fmt.Sprintf("--namespace=%s", namespace)
538 return e2ekubectl.RunKubectl(namespace, append([]string{"exec", namespaceOption}, args...)...)
539 }
540
541 func getGmsaDomainIP(f *framework.Framework, podName string) string {
542 output, _ := runKubectlExecInNamespace(f.Namespace.Name, podName, "powershell.exe", "--", "nslookup", gmsaDomain)
543 re := regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`)
544 idx := strings.Index(output, gmsaDomain)
545
546 submatchall := re.FindAllString(output[idx:], -1)
547 if len(submatchall) < 1 {
548 framework.Logf("fail to get the ip of the gmsa domain")
549 return ""
550 }
551 return submatchall[0]
552 }
553
View as plain text