Skip to content

Commit 8da3988

Browse files
authored
Merge pull request #2 from DataDog/aws-policy-evaluation
Implement stronger policy evaluation logic
2 parents 694e456 + 283eb7a commit 8da3988

26 files changed

+1775
-431
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 7 deletions
This file was deleted.

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ $ mkat eks test-imds-access
119119
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
120120
```
121121
122-
## How does MKAT compare to other tools?
122+
## FAQ
123+
124+
### How does MKAT compare to other tools?
123125
124126
| **Tool** | **Description** |
125127
|:---:|:---:|
@@ -132,10 +134,18 @@ $ mkat eks test-imds-access
132134
| [kubeletmein](https://github.com/4ARMED/kubeletmein) | kubeletmein _is_ specific to managed K8s environments. It's an utility to generate a kubeconfig file using the node's IAM credentials, to then use it in a compromised pod. |
133135
| [hardeneks](https://github.com/aws-samples/hardeneks) | hardeneks _is_ specific to managed K8s environments, but only for EKS. It identifies issues and lack of best practices inside of the cluster, and does not focus on cluster to cloud pivots. |
134136
137+
### What permissions does MKAT need to run?
138+
139+
See [this page](./permissions.md) for a detailed list of the permissions MKAT needs to run.
140+
135141
## Roadmap
136142
137143
We currently plan to:
138144
* Add a feature to identify EKS pods that are exposed through an AWS load balancer, through the [aws-load-balancer-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller)
139145
* Add support for GCP GKE
140146
* Allow scanning for additional types of cloud credentials
141147
* Enhance the IAM role trust policy evaluation logic to take into account additional edge cases
148+
149+
## Acknowledgements
150+
151+
Thank you to Rami McCarthi and Mikail Tunç for their early testing and actionable feedback on MKAT!

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import (
99
"github.com/spf13/cobra"
1010
)
1111

12+
var skipEksHostnameCheck bool
13+
1214
func BuildEksSubcommand() *cobra.Command {
1315
eksCommand := &cobra.Command{
1416
Use: "eks",
1517
Short: "Commands to audit your EKS cluster",
1618
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
1719
figure.NewFigure("mkat", "", true).Print()
1820
println()
19-
if !utils.IsEKS() {
21+
if !skipEksHostnameCheck && !utils.IsEKS() {
2022
return errors.New("you do not seem to be connected to an EKS cluster. Connect to an EKS cluster and try again")
2123
}
2224
clusterName := utils.GetEKSClusterName()
@@ -27,6 +29,7 @@ func BuildEksSubcommand() *cobra.Command {
2729
},
2830
}
2931

32+
eksCommand.PersistentFlags().BoolVarP(&skipEksHostnameCheck, "skip-eks-hostname-check", "", false, "Don't check that the hostname of your current API server ends with .eks.amazonaws.com")
3033
eksCommand.AddCommand(buildEksRoleRelationshipsCommand())
3134
eksCommand.AddCommand(buildEksFindSecretsCommand())
3235
eksCommand.AddCommand(buildTestImdsAccessCommand())

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

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/awalterschulze/gographviz"
1111
"github.com/aws/aws-sdk-go-v2/aws/arn"
1212
"github.com/datadog/managed-kubernetes-auditing-toolkit/internal/utils"
13-
"github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks"
1413
"github.com/datadog/managed-kubernetes-auditing-toolkit/pkg/managed-kubernetes-auditing-toolkit/eks/role_relationships"
1514
"github.com/jedib0t/go-pretty/v6/table"
1615
"github.com/jedib0t/go-pretty/v6/text"
@@ -22,6 +21,7 @@ import (
2221
// Command-line arguments
2322
var outputFormat string
2423
var outputFile string
24+
var eksClusterName string
2525

2626
// Output formats
2727
const (
@@ -50,53 +50,62 @@ func buildEksRoleRelationshipsCommand() *cobra.Command {
5050
RunE: func(cmd *cobra.Command, args []string) error {
5151
cluster := utils.GetEKSClusterName()
5252
if cluster == "" {
53-
return errors.New("unable to determine your current EKS cluster name")
53+
// If we cannot determine the EKS cluster name automatically, give the user a chance to specify it on the CLI
54+
cluster = eksClusterName
55+
}
56+
if cluster == "" {
57+
return errors.New("unable to determine your current EKS cluster name. Try specifying it explicitely with the --eks-cluster-name flag")
5458
}
5559
return doFindRoleRelationshipsCommand(cluster)
5660
},
5761
}
5862

5963
eksRoleRelationshipsCommand.Flags().StringVarP(&outputFormat, "output-format", "f", DefaultOutputFormat, "Output format. Supported formats: "+strings.Join(availableOutputFormats, ", "))
6064
eksRoleRelationshipsCommand.Flags().StringVarP(&outputFile, "output-file", "o", "", "Output file. If not specified, output will be printed to stdout.")
65+
eksRoleRelationshipsCommand.Flags().StringVarP(&eksClusterName, "eks-cluster-name", "", "", "When the EKS cluster name cannot be automatically detected from your KubeConfig, specify this argument to pass the EKS cluster name of your current kubectl context")
66+
6167
return eksRoleRelationshipsCommand
6268
}
6369

6470
// Actual logic implementing the "find-role-relationships" command
6571
func doFindRoleRelationshipsCommand(targetCluster string) error {
66-
resolver := role_relationships.EKSClusterRolesResolver{K8sClient: utils.K8sClient(), AwsClient: utils.AWSClient()}
67-
cluster, err := resolver.ResolveClusterRoles(targetCluster)
72+
resolver := role_relationships.EKSCluster{
73+
K8sClient: utils.K8sClient(),
74+
AwsClient: utils.AWSClient(),
75+
Name: targetCluster,
76+
}
77+
err := resolver.AnalyzeRoleRelationships()
6878
if err != nil {
6979
log.Fatalf("unable to analyze cluster role relationships: %v", err)
7080
}
7181

72-
output, err := getOutput(cluster)
82+
output, err := getOutput(&resolver)
7383
if err != nil {
7484
return err
7585
}
7686
if outputFile != "" {
7787
log.Println("Writing " + strings.ToUpper(outputFormat) + " output to " + outputFile)
7888
return os.WriteFile(outputFile, []byte(output), 0644)
79-
} else {
80-
print(output)
8189
}
8290

91+
print(output)
8392
return nil
8493
}
8594

86-
func getOutput(cluster *eks.EKSCluster) (string, error) {
95+
func getOutput(resolver *role_relationships.EKSCluster) (string, error) {
8796
switch outputFormat {
8897
case TextOutputFormat:
89-
return getTextOutput(cluster)
98+
return getTextOutput(resolver)
9099
case DotOutputFormat:
91-
return getDotOutput(cluster)
100+
return getDotOutput(resolver)
92101
case CsvOutputFormat:
93-
return getCsvOutput(cluster)
102+
return getCsvOutput(resolver)
94103
default:
95104
return "", fmt.Errorf("unsupported output format %s", outputFormat)
96105
}
97106
}
98107

99-
func getTextOutput(cluster *eks.EKSCluster) (string, error) {
108+
func getTextOutput(resolver *role_relationships.EKSCluster) (string, error) {
100109
t := table.NewWriter()
101110
if term.IsTerminal(0) {
102111
width, _, err := term.GetSize(0)
@@ -111,7 +120,7 @@ func getTextOutput(cluster *eks.EKSCluster) (string, error) {
111120
})
112121
t.AppendHeader(table.Row{"Namespace", "Service Account", "Pod", "Assumable Role ARN"})
113122
var found = false
114-
for namespace, pods := range cluster.PodsByNamespace {
123+
for namespace, pods := range resolver.PodsByNamespace {
115124
for _, pod := range pods {
116125
if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 {
117126
continue
@@ -125,20 +134,19 @@ func getTextOutput(cluster *eks.EKSCluster) (string, error) {
125134
}
126135
if !found {
127136
return "No service accounts found that can assume AWS roles", nil
128-
} else {
129-
return t.Render(), nil
130137
}
138+
return t.Render(), nil
131139
}
132140

133141
type Vertex struct {
134-
Id int
142+
ID int
135143
Label string
136144
}
137145

138-
func (v *Vertex) ID() int {
139-
return v.Id
146+
func (v *Vertex) GetID() int {
147+
return v.ID
140148
}
141-
func getDotOutput(cluster *eks.EKSCluster) (string, error) {
149+
func getDotOutput(resolver *role_relationships.EKSCluster) (string, error) {
142150
graphAst, _ := gographviz.ParseString(`digraph G { }`)
143151
graphViz := gographviz.NewGraph()
144152
gographviz.Analyse(graphAst, graphViz)
@@ -150,7 +158,7 @@ func getDotOutput(cluster *eks.EKSCluster) (string, error) {
150158
graphViz.AddAttr("G", "overlap", "false")
151159
graphViz.AddAttr("G", "newrank", "true")
152160

153-
for namespace, pods := range cluster.PodsByNamespace {
161+
for namespace, pods := range resolver.PodsByNamespace {
154162
subgraph := fmt.Sprintf(` "cluster_%s" `, namespace)
155163
graphViz.AddSubGraph("G", subgraph, map[string]string{
156164
"rank": "same",
@@ -192,60 +200,12 @@ func getDotOutput(cluster *eks.EKSCluster) (string, error) {
192200
}
193201

194202
return graphViz.String(), nil
195-
/*g := graph.New(graph.StringHash, graph.Directed(), graph.Acyclic())
196-
197-
for namespace, pods := range cluster.PodsByNamespace {
198-
for _, pod := range pods {
199-
if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 {
200-
continue
201-
}
202-
podLabel := fmt.Sprintf("Pod %s/%s", namespace, pod.Name)
203-
204-
g.AddVertex(podLabel,
205-
graph.VertexAttribute("shape", "box"),
206-
graph.VertexAttribute("rank", "same"),
207-
)
208-
}
209-
}
210-
211-
for namespace, pods := range cluster.PodsByNamespace {
212-
for _, pod := range pods {
213-
if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 {
214-
continue
215-
}
216-
podLabel := fmt.Sprintf("Pod %s/%s", namespace, pod.Name)
217-
for _, role := range pod.ServiceAccount.AssumableRoles {
218-
parsedArn, _ := arn.Parse(role.Arn)
219-
roleLabel := fmt.Sprintf("IAM Role %s", parsedArn.Resource)
220-
221-
g.AddVertex(
222-
roleLabel,
223-
graph.VertexAttribute("style", "filled"),
224-
graph.VertexAttribute("shape", "box"),
225-
graph.VertexAttribute("fillcolor", "#BFEFFF"),
226-
graph.VertexAttribute("rank", "max"),
227-
)
228-
229-
g.AddEdge(
230-
podLabel, roleLabel,
231-
//graph.EdgeAttribute("label", "can assume"),
232-
)
233-
}
234-
}
235-
}
236-
237-
sb := new(strings.Builder)
238-
if err := draw.DOT(g, sb); err != nil {
239-
return "", err
240-
}
241-
242-
return sb.String(), nil*/
243203
}
244204

245-
func getCsvOutput(cluster *eks.EKSCluster) (string, error) {
205+
func getCsvOutput(resolver *role_relationships.EKSCluster) (string, error) {
246206
sb := new(strings.Builder)
247207
sb.WriteString("namespace,pod,service_account,role_arn")
248-
for namespace, pods := range cluster.PodsByNamespace {
208+
for namespace, pods := range resolver.PodsByNamespace {
249209
for _, pod := range pods {
250210
if pod.ServiceAccount == nil || len(pod.ServiceAccount.AssumableRoles) == 0 {
251211
continue

go.mod

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,38 +33,56 @@ require (
3333
github.com/aws/smithy-go v1.13.5 // indirect
3434
github.com/davecgh/go-spew v1.1.1 // indirect
3535
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
36+
github.com/emirpasic/gods v1.12.0 // indirect
3637
github.com/go-logr/logr v1.2.3 // indirect
3738
github.com/go-openapi/jsonpointer v0.19.5 // indirect
3839
github.com/go-openapi/jsonreference v0.20.0 // indirect
3940
github.com/go-openapi/swag v0.19.14 // indirect
4041
github.com/gogo/protobuf v1.3.2 // indirect
42+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4143
github.com/golang/protobuf v1.5.2 // indirect
4244
github.com/google/gnostic v0.5.7-v3refs // indirect
4345
github.com/google/go-cmp v0.5.9 // indirect
46+
github.com/google/go-licenses v1.6.0 // indirect
4447
github.com/google/gofuzz v1.1.0 // indirect
48+
github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect
4549
github.com/imdario/mergo v0.3.6 // indirect
4650
github.com/inconshreveable/mousetrap v1.0.1 // indirect
51+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
4752
github.com/jmespath/go-jmespath v0.4.0 // indirect
4853
github.com/josharian/intern v1.0.0 // indirect
4954
github.com/json-iterator/go v1.1.12 // indirect
55+
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
5056
github.com/mailru/easyjson v0.7.6 // indirect
5157
github.com/mattn/go-colorable v0.1.13 // indirect
5258
github.com/mattn/go-isatty v0.0.17 // indirect
5359
github.com/mattn/go-runewidth v0.0.13 // indirect
60+
github.com/mitchellh/go-homedir v1.1.0 // indirect
5461
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5562
github.com/modern-go/reflect2 v1.0.2 // indirect
5663
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
64+
github.com/otiai10/copy v1.6.0 // indirect
5765
github.com/pmezard/go-difflib v1.0.0 // indirect
5866
github.com/rivo/uniseg v0.2.0 // indirect
67+
github.com/sergi/go-diff v1.2.0 // indirect
5968
github.com/spf13/pflag v1.0.5 // indirect
69+
github.com/src-d/gcfg v1.4.0 // indirect
70+
github.com/xanzy/ssh-agent v0.2.1 // indirect
71+
go.opencensus.io v0.23.0 // indirect
72+
golang.org/x/crypto v0.1.0 // indirect
73+
golang.org/x/mod v0.7.0 // indirect
6074
golang.org/x/net v0.7.0 // indirect
6175
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
6276
golang.org/x/sys v0.6.0 // indirect
6377
golang.org/x/text v0.7.0 // indirect
6478
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
79+
golang.org/x/tools v0.5.0 // indirect
6580
google.golang.org/appengine v1.6.7 // indirect
6681
google.golang.org/protobuf v1.28.1 // indirect
6782
gopkg.in/inf.v0 v0.9.1 // indirect
83+
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
84+
gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
85+
gopkg.in/warnings.v0 v0.1.2 // indirect
6886
gopkg.in/yaml.v2 v2.4.0 // indirect
6987
gopkg.in/yaml.v3 v3.0.1 // indirect
7088
k8s.io/klog/v2 v2.80.1 // indirect

0 commit comments

Comments
 (0)