-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 95eaea6
Showing
7 changed files
with
797 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/config" | ||
"log" | ||
) | ||
|
||
func AWSClient() *aws.Config { | ||
cfg, err := config.LoadDefaultConfig(context.Background()) | ||
if err != nil { | ||
log.Fatalf("unable to load AWS configuration, %v", err) | ||
} | ||
return &cfg | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
module managed-k8s-auditing-toolkit | ||
|
||
go 1.19 | ||
|
||
require ( | ||
github.com/aws/aws-sdk-go-v2 v1.17.6 | ||
github.com/aws/aws-sdk-go-v2/config v1.18.16 | ||
github.com/aws/aws-sdk-go-v2/service/eks v1.27.6 | ||
github.com/aws/aws-sdk-go-v2/service/iam v1.19.5 | ||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 | ||
github.com/deckarep/golang-set/v2 v2.2.0 | ||
k8s.io/apimachinery v0.26.2 | ||
k8s.io/client-go v0.26.2 | ||
) | ||
|
||
require ( | ||
github.com/aws/aws-sdk-go-v2/credentials v1.13.16 // indirect | ||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.24 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect | ||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect | ||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect | ||
github.com/aws/smithy-go v1.13.5 // indirect | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect | ||
github.com/go-logr/logr v1.2.3 // indirect | ||
github.com/go-openapi/jsonpointer v0.19.5 // indirect | ||
github.com/go-openapi/jsonreference v0.20.0 // indirect | ||
github.com/go-openapi/swag v0.19.14 // indirect | ||
github.com/gogo/protobuf v1.3.2 // indirect | ||
github.com/golang/protobuf v1.5.2 // indirect | ||
github.com/google/gnostic v0.5.7-v3refs // indirect | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/google/gofuzz v1.1.0 // indirect | ||
github.com/imdario/mergo v0.3.6 // indirect | ||
github.com/jmespath/go-jmespath v0.4.0 // indirect | ||
github.com/josharian/intern v1.0.0 // indirect | ||
github.com/json-iterator/go v1.1.12 // indirect | ||
github.com/mailru/easyjson v0.7.6 // indirect | ||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||
github.com/modern-go/reflect2 v1.0.2 // indirect | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||
github.com/spf13/pflag v1.0.5 // indirect | ||
golang.org/x/net v0.7.0 // indirect | ||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect | ||
golang.org/x/sys v0.5.0 // indirect | ||
golang.org/x/term v0.5.0 // indirect | ||
golang.org/x/text v0.7.0 // indirect | ||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect | ||
google.golang.org/appengine v1.6.7 // indirect | ||
google.golang.org/protobuf v1.28.1 // indirect | ||
gopkg.in/inf.v0 v0.9.1 // indirect | ||
gopkg.in/yaml.v2 v2.4.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
k8s.io/api v0.26.2 // indirect | ||
k8s.io/klog/v2 v2.80.1 // indirect | ||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect | ||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect | ||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect | ||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect | ||
sigs.k8s.io/yaml v1.3.0 // indirect | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package main | ||
|
||
import ( | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/tools/clientcmd" | ||
"k8s.io/client-go/util/homedir" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
func K8sClient() *kubernetes.Clientset { | ||
config, err := clientcmd.BuildConfigFromFlags("", GetKubeConfigPath()) | ||
if err != nil { | ||
log.Fatalf("unable to build kube config: %v", err) | ||
} | ||
k8sClient, err := kubernetes.NewForConfig(config) | ||
if err != nil { | ||
log.Fatalf("unable to create kube client: %v", err) | ||
} | ||
return k8sClient | ||
} | ||
|
||
// unexported function with the main logic | ||
func GetKubeConfigPath() string { | ||
// if KUBECONFIG is set, use it | ||
if kubeConfigEnvPath := os.Getenv("KUBECONFIG"); kubeConfigEnvPath != "" { | ||
return kubeConfigEnvPath | ||
} | ||
|
||
// Otherwise, use $HOME/.kube/config if it exists | ||
if kubeConfigFilePath := filepath.Join(homedir.HomeDir(), ".kube/config"); FileExists(kubeConfigFilePath) { | ||
return kubeConfigFilePath | ||
} | ||
|
||
// Otherwise, return an empty string | ||
// This will cause `clientcmd.BuildConfigFromFlags` called in `GetClient` will try to use | ||
// in-cluster auth | ||
// c.f. https://pkg.go.dev/k8s.io/client-go/tools/clientcmd#BuildConfigFromFlags | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/eks" | ||
"github.com/aws/aws-sdk-go-v2/service/iam" | ||
"github.com/aws/aws-sdk-go-v2/service/iam/types" | ||
"github.com/aws/aws-sdk-go-v2/service/sts" | ||
mapset "github.com/deckarep/golang-set/v2" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"net/url" | ||
"strings" | ||
) | ||
|
||
func getClusterIssuer(awsClient *aws.Config, targetCluster string) string { | ||
println("Retrieving cluster OIDC issuer") | ||
eksClient := eks.NewFromConfig(*awsClient) | ||
clusterInfo, _ := eksClient.DescribeCluster(context.Background(), &eks.DescribeClusterInput{ | ||
Name: &targetCluster, | ||
}) | ||
//TODO: what if no identity? | ||
issuer := strings.Replace(*clusterInfo.Cluster.Identity.Oidc.Issuer, "https://", "", 1) | ||
return issuer | ||
} | ||
|
||
func findRoles(awsClient *aws.Config, issuer string) []types.Role { | ||
println("Listing roles in the AWS account") | ||
iamClient := iam.NewFromConfig(*awsClient) | ||
paginator := iam.NewListRolesPaginator(iamClient, &iam.ListRolesInput{}) | ||
identity, _ := sts.NewFromConfig(*awsClient).GetCallerIdentity(context.Background(), &sts.GetCallerIdentityInput{}) | ||
assumableRoles := make([]types.Role, 0) | ||
for paginator.HasMorePages() { | ||
roles, _ := paginator.NextPage(context.Background()) | ||
for _, role := range roles.Roles { | ||
if roleTrustsIssuer(role, *identity.Account, issuer) { | ||
assumableRoles = append(assumableRoles, role) | ||
} | ||
} | ||
} | ||
|
||
return assumableRoles | ||
} | ||
|
||
func roleTrustsIssuer(role types.Role, accountId string, issuer string) bool { | ||
var policy map[string]interface{} | ||
|
||
// url decode role.AssumeRolePolicyDocument | ||
rawPolicy, _ := url.PathUnescape(*role.AssumeRolePolicyDocument) | ||
err := json.Unmarshal([]byte(rawPolicy), &policy) | ||
if err != nil { | ||
panic(err) | ||
} | ||
for _, rawStatement := range policy["Statement"].([]interface{}) { | ||
statement := rawStatement.(map[string]interface{}) | ||
if statement["Effect"] != "Allow" { | ||
continue | ||
} | ||
// TODO: smart eval instead of string compare | ||
if statement["Principal"].(map[string]interface{})["Federated"] == "arn:aws:iam::"+accountId+":oidc-provider/"+issuer { | ||
// at this point we don't evaluate the conditions | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func main() { | ||
k8s := K8sClient() | ||
aws := AWSClient() | ||
//targetCluster := "datadog-pde-test-eks-cluster-us-east-1" | ||
targetCluster := "synthetics-eks" | ||
issuer := getClusterIssuer(aws, targetCluster) | ||
|
||
// All roles in AWS that at least one service account in the cluster is using | ||
clusterCandidateRolesArns := mapset.NewSet[string]() | ||
|
||
// All roles in AWS that have a trust relationship with the cluster's OIDC issuer | ||
candidateRoles := findRoles(aws, issuer) | ||
candidateRoleArns := mapset.NewSet[string]() | ||
candidatesRolesByARN := make(map[string]types.Role) | ||
for _, candidateRole := range candidateRoles { | ||
candidatesRolesByARN[*candidateRole.Arn] = candidateRole | ||
candidateRoleArns.Add(*candidateRole.Arn) | ||
} | ||
println("Found", candidateRoleArns.Cardinality(), "roles that can be assumed by the cluster's OIDC provider") | ||
|
||
// List service accounts in all namespaces | ||
println("Listing K8s service accounts") | ||
serviceAccounts, _ := k8s.CoreV1().ServiceAccounts("").List(context.Background(), metav1.ListOptions{}) | ||
serviceAccountsWithRoleConfiguration := make([]v1.ServiceAccount, 0) | ||
for _, serviceAccount := range serviceAccounts.Items { | ||
if annotations := serviceAccount.Annotations; annotations != nil { | ||
if roleArn, ok := annotations["eks.amazonaws.com/role-arn"]; ok { | ||
serviceAccountsWithRoleConfiguration = append(serviceAccountsWithRoleConfiguration, serviceAccount) | ||
clusterCandidateRolesArns.Add(roleArn) | ||
} | ||
} | ||
} | ||
|
||
// Find roles that are in both sets | ||
// These are our roles (1) that can be assumed by the cluster and (2) at least one SA is using | ||
commonRoles := clusterCandidateRolesArns.Intersect(candidateRoleArns).ToSlice() | ||
|
||
// Now for each role, we look at its 'Condition' in the trust policy and determine which service accounts can use it | ||
//assumableRolesByServiceAccount := make(map[string][]string) | ||
for _, roleArn := range commonRoles { | ||
for _, serviceAccount := range serviceAccountsWithRoleConfiguration { | ||
if serviceAccountCanAssumeRole(serviceAccount, candidatesRolesByARN[roleArn], issuer) { | ||
//assumableRolesByServiceAccount[serviceAccount.Namespace+"/"+serviceAccount.Name] = append(assumableRolesByServiceAccount[serviceAccount.Namespace+"/"+serviceAccount.Name], roleArn) | ||
println(roleArn + " can be assumed by " + serviceAccount.Namespace + "/" + serviceAccount.Name) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func serviceAccountCanAssumeRole(serviceAccount v1.ServiceAccount, role types.Role, issuer string) bool { | ||
//println("Checking if service account " + serviceAccount.Namespace + "/" + serviceAccount.Name + " can assume role " + *role.Arn) | ||
var policy map[string]interface{} | ||
|
||
// url decode role.AssumeRolePolicyDocument | ||
rawPolicy, _ := url.PathUnescape(*role.AssumeRolePolicyDocument) | ||
err := json.Unmarshal([]byte(rawPolicy), &policy) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
for _, rawStatement := range policy["Statement"].([]interface{}) { | ||
statement := rawStatement.(map[string]interface{}) | ||
if statement["Effect"] != "Allow" { | ||
continue | ||
} | ||
|
||
rawCondition, hasCondition := statement["Condition"] | ||
if !hasCondition { | ||
return true | ||
} | ||
|
||
condition := rawCondition.(map[string]interface{}) | ||
if stringEquals, ok := condition["StringEquals"]; ok { | ||
if stringEquals.(map[string]interface{})[issuer+":sub"] == "system:serviceaccount:"+serviceAccount.Namespace+":"+serviceAccount.Name { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package main | ||
|
||
import "os" | ||
|
||
func FileExists(path string) bool { | ||
_, err := os.Stat(path) | ||
if os.IsNotExist(err) { | ||
return false | ||
} else if err != nil { | ||
// In case of error, we assume the file doesn't exist to make the logic simpler | ||
return false | ||
} | ||
return true | ||
} |