Skip to content

Commit 37f9c32

Browse files
authored
Merge pull request #9 from DataDog/enhance-imds-testing
Enhance IMDS access detection to support cases where IMDSv2 is enforced (closes #8)
2 parents 0548301 + 0ad48dc commit 37f9c32

File tree

3 files changed

+143
-37
lines changed

3 files changed

+143
-37
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ $ mkat eks find-secrets
104104
### Test if pods can access the AWS Instance Metadata Service (IMDS)
105105
106106
Pods accessing the EKS nodes Instance Metadata Service is a [common and dangerous attack vector](https://blog.christophetd.fr/privilege-escalation-in-aws-elastic-kubernetes-service-eks-by-compromising-the-instance-role-of-worker-nodes/)
107-
that can be used to escalate privileges. MKAT can test if pods can access the IMDS. It tests it by creating a temporary pod that tries to access the IMDS, and then deletes it.
107+
that can be used to escalate privileges. MKAT can test if pods can access the IMDS, both through IMDSv1 and IMDSv2.
108+
109+
It tests this by creating two temporary pods (one for IMDSv1, one for IMDSv2) that try to access the IMDS, and are then deleted.
108110
109111
```bash
110112
$ mkat eks test-imds-access
@@ -114,9 +116,10 @@ $ mkat eks test-imds-access
114116
| | | | | | | < | (_| | | |_
115117
|_| |_| |_| |_|\_\ \__,_| \__|
116118

117-
2023/04/12 00:35:10 Connected to EKS cluster mkat-cluster
118-
2023/04/12 00:35:10 Testing if IMDS is accessible to pods by creating a pod that attempts to access it
119-
2023/04/12 00:35:15 IMDS is accessible and allows any pod to retrieve credentials for the AWS role eksctl-mkat-cluster-nodegroup-ng-NodeInstanceRole-AXWUFF35602Z
119+
2023/07/11 21:56:19 Connected to EKS cluster mkat-cluster
120+
2023/07/11 21:56:19 Testing if IMDSv1 and IMDSv2 are accessible from pods by creating a pod that attempts to access it
121+
2023/07/11 21:56:23 IMDSv2 is accessible: any pod can retrieve credentials for the AWS role eksctl-mkat-cluster-nodegroup-ng-NodeInstanceRole-AXWUFF35602Z
122+
2023/07/11 21:56:23 IMDSv1 is not accessible to pods in your cluster: able to establish a network connection to the IMDS, but no credentials were returned
120123
```
121124
122125
## FAQ

cmd/managed-kubernetes-auditing-toolkit/eks/imds.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,82 @@ package eks
22

33
import (
44
"log"
5+
"sync"
56

67
"github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils"
78
"github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks/imds"
89
"github.com/fatih/color"
910
"github.com/spf13/cobra"
1011
)
1112

13+
var successColor = color.New(color.BgBlack, color.FgGreen, color.Bold)
14+
var warningColor = color.New(color.BgRed, color.FgWhite, color.Bold)
15+
1216
func buildTestImdsAccessCommand() *cobra.Command {
1317
eksFindSecretsCommand := &cobra.Command{
1418
Use: "test-imds-access",
1519
Example: "mkat eks test-imds-access",
1620
Short: "Test if your EKS cluster allows pod access to the IMDS",
1721
Long: "test-imds-access will check if your EKS cluster allows pods to access the IMDS by running a pod and executing a curl command hitting the IMDS",
1822
DisableFlagsInUseLine: true,
19-
RunE: func(cmd *cobra.Command, args []string) error {
20-
return doTestImdsAccessCommand()
23+
Run: func(cmd *cobra.Command, args []string) {
24+
doTestImdsAccessCommand()
2125
},
2226
}
2327

2428
return eksFindSecretsCommand
2529
}
2630

27-
func doTestImdsAccessCommand() error {
31+
func doTestImdsAccessCommand() {
2832
tester := imds.ImdsTester{K8sClient: utils.K8sClient(), Namespace: "default"}
29-
result, err := tester.TestImdsAccessible()
33+
log.Println("Testing if IMDSv1 and IMDSv2 are accessible from pods by creating a pod that attempts to access it")
34+
35+
// We run the test for IMDSv1 and IMDSv2 in parallel
36+
var wg sync.WaitGroup
37+
wg.Add(2)
38+
go doTestImdsAccess(IMDSv1, &tester, &wg)
39+
go doTestImdsAccess(IMDSv2, &tester, &wg)
40+
wg.Wait()
41+
}
42+
43+
type ImdsVersion string
44+
45+
const (
46+
IMDSv1 ImdsVersion = "IMDSv1"
47+
IMDSv2 ImdsVersion = "IMDSv2"
48+
)
49+
50+
func doTestImdsAccess(imdsVersion ImdsVersion, tester *imds.ImdsTester, wg *sync.WaitGroup) {
51+
var result *imds.ImdsTestResult
52+
var err error
53+
54+
defer wg.Done()
55+
56+
switch imdsVersion {
57+
case IMDSv1:
58+
result, err = tester.TestImdsV1Accessible()
59+
case IMDSv2:
60+
result, err = tester.TestImdsV2Accessible()
61+
default:
62+
panic("invalid IMDS version")
63+
}
64+
3065
if err != nil {
31-
return err
66+
log.Printf("Unable to determine if %s is accessible in your cluster: %s\n", imdsVersion, err.Error())
67+
return
3268
}
69+
3370
if result.IsImdsAccessible {
34-
warningColor := color.New(color.BgRed, color.FgWhite, color.Bold)
35-
log.Println(warningColor.Sprint("IMDS is accessible") + " and allows any pod to retrieve credentials for the AWS role " + result.NodeRoleName)
71+
log.Printf("%s: %s\n", warningColor.Sprintf("%s is accessible", imdsVersion), result.ResultDescription)
3672
} else {
37-
log.Println("IMDS is not accessible in your cluster")
73+
description := ""
74+
if result.ResultDescription != "" {
75+
description = ": " + result.ResultDescription
76+
}
77+
log.Printf("%s %s%s\n",
78+
successColor.Sprintf("%s is not accessible", imdsVersion),
79+
"to pods in your cluster",
80+
description,
81+
)
3882
}
39-
return nil
4083
}

pkg/managed-kubernetes-auditing-toolkit/eks/imds/imds_tester.go

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"k8s.io/apimachinery/pkg/util/wait"
1111
"k8s.io/client-go/kubernetes"
1212
typedv1 "k8s.io/client-go/kubernetes/typed/core/v1"
13-
"log"
1413
"os"
1514
"os/signal"
1615
"strings"
@@ -24,51 +23,110 @@ type ImdsTester struct {
2423
}
2524

2625
type ImdsTestResult struct {
27-
IsImdsAccessible bool
28-
NodeRoleName string
26+
IsImdsAccessible bool
27+
ResultDescription string
2928
}
3029

31-
const ImdsTesterPodName = "mkat-imds-tester"
30+
const ImdsTesterV1PodName = "mkat-imds-tester"
31+
const ImdsTesterV2PodName = "mkat-imds-v2-tester"
3232

33-
func (m *ImdsTester) TestImdsAccessible() (*ImdsTestResult, error) {
33+
func (m *ImdsTester) TestImdsV1Accessible() (*ImdsTestResult, error) {
34+
commandToRun := []string{
35+
"sh",
36+
"-c",
37+
"(curl --silent --show-error --connect-timeout 2 169.254.169.254/latest/meta-data/iam/security-credentials/ || true)",
38+
}
39+
podLogs, err := m.runCommandInPodAndGetLogs(ImdsTesterV1PodName, commandToRun)
40+
if err != nil {
41+
return nil, fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err)
42+
}
43+
44+
// Case 1: no network connection (e.g. NetworkPolicy in place)
45+
if strings.Contains(podLogs, "Failed to connect") {
46+
return &ImdsTestResult{
47+
IsImdsAccessible: false,
48+
ResultDescription: "unable to establish a network connection to the IMDS",
49+
}, nil
50+
}
51+
52+
// Case 2: IMDSv2 enforced, IMDSv1 is accessible at the network level but returns a 401 error
53+
if strings.TrimSpace(podLogs) == "" {
54+
return &ImdsTestResult{
55+
IsImdsAccessible: false,
56+
ResultDescription: "able to establish a network connection to the IMDS, but no credentials were returned",
57+
}, nil
58+
}
59+
60+
// Case 3: IMDSv1 is accessible and returns credentials
61+
return &ImdsTestResult{
62+
IsImdsAccessible: true,
63+
ResultDescription: fmt.Sprintf("any pod can retrieve credentials for the AWS role %s", podLogs),
64+
}, nil
65+
}
66+
67+
func (m *ImdsTester) TestImdsV2Accessible() (*ImdsTestResult, error) {
68+
commandToRun := []string{
69+
"sh",
70+
"-c",
71+
// We use "--max-time" because when the IMDS max-response-hop is set to 1, the TCP connection succeeds initially but hangs indefinitely when calling /latest/api/token
72+
`TOKEN=$(curl --show-error --max-time 2 --silent -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
73+
(curl --silent --show-error --max-time 2 -H "X-aws-ec2-metadata-token: $TOKEN" 169.254.169.254/latest/meta-data/iam/security-credentials/ || true)`,
74+
}
75+
podLogs, err := m.runCommandInPodAndGetLogs(ImdsTesterV2PodName, commandToRun)
76+
if err != nil {
77+
return nil, fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err)
78+
}
79+
80+
// Case 1: no network connection (e.g. NetworkPolicy in place)
81+
if strings.Contains(podLogs, "Failed to connect") || strings.Contains(podLogs, "timed out") {
82+
return &ImdsTestResult{
83+
IsImdsAccessible: false,
84+
ResultDescription: "unable to establish a network connection to the IMDS",
85+
}, nil
86+
}
87+
88+
// Case 3: IMDSv2 is accessible and returns credentials
89+
return &ImdsTestResult{
90+
IsImdsAccessible: true,
91+
ResultDescription: fmt.Sprintf("any pod can retrieve credentials for the AWS role %s", podLogs),
92+
}, nil
93+
}
94+
95+
func (m *ImdsTester) runCommandInPodAndGetLogs(podName string, command []string) (string, error) {
96+
podsClient := m.K8sClient.CoreV1().Pods(m.Namespace)
3497
podDefinition := &v1.Pod{
35-
ObjectMeta: metav1.ObjectMeta{Name: ImdsTesterPodName, Namespace: m.Namespace},
98+
ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: m.Namespace},
3699
Spec: v1.PodSpec{
37100
Containers: []v1.Container{{
38-
Name: ImdsTesterPodName,
101+
Name: podName,
39102
Image: "curlimages/curl:8.00.1",
40-
Command: []string{"sh", "-c", "(curl --silent --show-error --connect-timeout 2 169.254.169.254/latest/meta-data/iam/security-credentials/ || true)"},
103+
Command: command,
41104
}},
42105
RestartPolicy: v1.RestartPolicyNever, // don't restart the pod once the command has been executed
43106
},
44107
}
45-
podsClient := m.K8sClient.CoreV1().Pods(m.Namespace)
46-
47-
log.Println("Testing if IMDS is accessible to pods by creating a pod that attempts to access it")
48108
_, err := podsClient.Create(context.Background(), podDefinition, metav1.CreateOptions{})
49109
if err != nil {
50-
return nil, fmt.Errorf("unable to create IMDS tester pod: %v", err)
110+
return "", fmt.Errorf("unable to create IMDS tester pod: %v", err)
51111
}
52112
m.handleCtrlC()
53-
defer removePod(podsClient, ImdsTesterPodName)
113+
defer removePod(podsClient, podName)
54114

55115
err = wait.PollImmediate(1*time.Second, 120*time.Second, func() (bool, error) {
56-
return podHasSuccessfullyCompleted(podsClient, ImdsTesterPodName)
116+
return podHasSuccessfullyCompleted(podsClient, podName)
57117
})
58118

59119
if err != nil {
60-
return nil, fmt.Errorf("unable to wait for IMDS tester pod to complete: %v", err)
120+
return "", fmt.Errorf("unable to wait for IMDS tester pod to complete: %v", err)
61121
}
62122

63123
// Retrieve command output
64-
podLogs, err := getPodLogs(podsClient, ImdsTesterPodName)
124+
podLogs, err := getPodLogs(podsClient, podName)
65125
if err != nil {
66-
return nil, fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err)
126+
return "", fmt.Errorf("unable to retrieve logs from IMDS tester pod: %v", err)
67127
}
68-
if strings.Contains(podLogs, "Failed to connect") {
69-
return &ImdsTestResult{IsImdsAccessible: false}, nil
70-
}
71-
return &ImdsTestResult{IsImdsAccessible: true, NodeRoleName: podLogs}, nil
128+
129+
return podLogs, nil
72130
}
73131

74132
func (m *ImdsTester) handleCtrlC() {
@@ -77,8 +135,10 @@ func (m *ImdsTester) handleCtrlC() {
77135
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
78136
go func() {
79137
<-c
80-
println("Received SIGINT, cleaning up IMDS tester pod")
81-
removePod(m.K8sClient.CoreV1().Pods(m.Namespace), ImdsTesterPodName)
138+
println("Received SIGINT, cleaning up IMDS tester pods")
139+
podsClient := m.K8sClient.CoreV1().Pods(m.Namespace)
140+
removePod(podsClient, ImdsTesterV1PodName)
141+
removePod(podsClient, ImdsTesterV2PodName)
82142
os.Exit(1)
83143
}()
84144
}
@@ -99,7 +159,7 @@ func podHasSuccessfullyCompleted(podsClient typedv1.PodInterface, podName string
99159
}
100160

101161
func getPodLogs(podsClient typedv1.PodInterface, podName string) (string, error) {
102-
podLogsRequest := podsClient.GetLogs(ImdsTesterPodName, &v1.PodLogOptions{})
162+
podLogsRequest := podsClient.GetLogs(podName, &v1.PodLogOptions{})
103163
podLogs, err := podLogsRequest.Stream(context.Background())
104164
if err != nil {
105165
return "", fmt.Errorf("unable to get logs for pod %s: %v", podName, err)

0 commit comments

Comments
 (0)