Skip to content

Commit

Permalink
initiail commit
Browse files Browse the repository at this point in the history
  • Loading branch information
christophetd committed Mar 14, 2023
0 parents commit 95eaea6
Show file tree
Hide file tree
Showing 7 changed files with 797 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
16 changes: 16 additions & 0 deletions aws.go
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
}
64 changes: 64 additions & 0 deletions go.mod
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
)
512 changes: 512 additions & 0 deletions go.sum

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions kubernetes.go
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 ""
}
149 changes: 149 additions & 0 deletions main.go
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
}
14 changes: 14 additions & 0 deletions utils.go
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
}

0 comments on commit 95eaea6

Please sign in to comment.