diff --git a/go.mod b/go.mod index 8be5f3a..2beaae1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/shiftavenue/azure-clientid-syncer go 1.21.5 require ( - cloud.google.com/go/iam v1.1.6 + cloud.google.com/go/asset v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 @@ -26,8 +26,14 @@ require ( ) require ( + cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go/accesscontextmanager v1.8.4 // indirect cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + cloud.google.com/go/longrunning v0.5.4 // indirect + cloud.google.com/go/orgpolicy v1.12.0 // indirect + cloud.google.com/go/osconfig v1.12.4 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -35,6 +41,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.2.4 // indirect @@ -68,6 +75,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/go.sum b/go.sum index cd0483e..38ac04b 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,22 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= +cloud.google.com/go/accesscontextmanager v1.8.4/go.mod h1:ParU+WbMpD34s5JFEnGAnPBYAgUHozaTmDJU7aCU9+M= +cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg= +cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ= +cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= +cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= +cloud.google.com/go/osconfig v1.12.4/go.mod h1:B1qEwJ/jzqSRslvdOCI8Kdnp0gSng0xW4LOnIebQomA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= diff --git a/pkg/provider/const.go b/pkg/provider/const.go index d0dcd31..b49bd14 100644 --- a/pkg/provider/const.go +++ b/pkg/provider/const.go @@ -8,6 +8,8 @@ const ( // gcpRoleName represents the role associated with service accounts in GCP allowing them to use workload identity gcpRoleName = "roles/iam.workloadIdentityUser" + // gcpResourceAssetType represents the Asset Inventory resource type for GCP service accounts + gcpResourceAssetType = "iam.googleapis.com/ServiceAccount" // gcpServiceAccountAnnotation represents the GCP service account name to be used with the Kubernetes service account gcpServiceAccountAnnotation = "iam.gke.io/gcp-service-account" -) \ No newline at end of file +) diff --git a/pkg/provider/gcp.go b/pkg/provider/gcp.go index 59f6ed1..a860d32 100644 --- a/pkg/provider/gcp.go +++ b/pkg/provider/gcp.go @@ -2,14 +2,12 @@ package provider import ( "context" + "errors" "fmt" - "log" - "regexp" - "sync" + "strings" - admin "cloud.google.com/go/iam/admin/apiv1" - "cloud.google.com/go/iam/admin/apiv1/adminpb" - "cloud.google.com/go/iam/apiv1/iampb" + asset "cloud.google.com/go/asset/apiv1" + "cloud.google.com/go/asset/apiv1/assetpb" "github.com/go-logr/logr" "github.com/shiftavenue/azure-clientid-syncer/pkg/config" "google.golang.org/api/iterator" @@ -31,24 +29,25 @@ func NewGCPQueryProvider(serviceAccount *corev1.ServiceAccount, logger logr.Logg } func (g *gcpQueryProvider) Query() (*corev1.ServiceAccount, error) { - // Create a new IAM client + // Create a new Asset Inventory client ctx := context.Background() - iamClient, err := admin.NewIamClient(ctx) + assetClient, err := asset.NewClient(ctx) if err != nil { return nil, err } - // List all service accounts - req := &adminpb.ListServiceAccountsRequest{ - Name: fmt.Sprintf("projects/%s", g.config.GcpProjectId), + // Find GCP service account that can be impersonated by Kubernetes account using the GCP Asset Inventory + // the query looks for all resources on which the Kubernetes service account has the 'roles/iam.workloadIdentityUser' role assigned + req := &assetpb.SearchAllIamPoliciesRequest{ + Scope: fmt.Sprintf("projects/%s", g.config.GcpProjectId), + Query: fmt.Sprintf("policy:%s.svc.id.goog[%s/%s] roles:%s", g.config.GcpProjectId, g.serviceAccount.Namespace, g.serviceAccount.Name, gcpRoleName), } - it := iamClient.ListServiceAccounts(ctx, req) - - ch := make(chan *string, 1) - wg := sync.WaitGroup{} + it := assetClient.SearchAllIamPolicies(ctx, req) + // Iterate through all results and find service account + gcpServiceAccountMail := "" for { - sa, err := it.Next() + res, err := it.Next() if err == iterator.Done { break } @@ -56,58 +55,21 @@ func (g *gcpQueryProvider) Query() (*corev1.ServiceAccount, error) { return nil, err } - wg.Add(1) - - go func(ch chan *string, sa *adminpb.ServiceAccount) { - defer wg.Done() - // Get IAM policy for the service account - policyReq := &iampb.GetIamPolicyRequest{ - Resource: sa.Name, - } - policy, err := iamClient.GetIamPolicy(ctx, policyReq) - if err != nil { - log.Printf("failed to retrieve IAM policy: %v", err) - return - } - - // Iterate over policy bindings - for _, member := range policy.Members(gcpRoleName) { - fmt.Printf("Found member with role '%s': %s\n", gcpRoleName, member) - namespace, serviceAccountName := extractSubstrings(member) - if g.serviceAccount.Name == serviceAccountName && g.serviceAccount.Namespace == namespace { - fmt.Printf("Project ID: %s, Namespace: %s, Service Account: %s\n", g.config.GcpProjectId, namespace, serviceAccountName) - ch <- &sa.Name + // The service account resource is returned in the format: //iam.googleapis.com/projects//serviceAccounts/@.iam.gserviceaccount.com + // extract relevant account mail that can be used in the annotation + if res.AssetType == gcpResourceAssetType { + // Fail if two or more service accounts were found + if gcpServiceAccountMail != "" { + return nil, errors.New("multiple service accounts were found, cannot decide which one to use") + } else { + gcpServiceAccountMail = strings.Split(res.Resource, "/")[6] + if g.serviceAccount.Annotations == nil { + g.serviceAccount.Annotations = make(map[string]string) } + g.serviceAccount.Annotations[gcpServiceAccountAnnotation] = gcpServiceAccountMail } - }(ch, sa) - } - - go func() { - wg.Wait() - ch <- nil - close(ch) - }() - - v, ok := <-ch - if !ok || v == nil { - return nil, fmt.Errorf("failed to retrieve service account") - } - - if g.serviceAccount.Annotations == nil { - g.serviceAccount.Annotations = make(map[string]string) + } } - g.serviceAccount.Annotations[gcpServiceAccountAnnotation] = *v return g.serviceAccount, nil } - -func extractSubstrings(member string) (string, string) { - // Use a regular expression to extract the namespace and service account - re := regexp.MustCompile(`\[(.*?)\/(.*?)\]`) - match := re.FindStringSubmatch(member) - - namespace := match[1] - serviceAccount := match[2] - - return namespace, serviceAccount -} diff --git a/tests/provision-infrastructure.sh b/tests/provision-infrastructure.sh index 396ec45..37268b0 100755 --- a/tests/provision-infrastructure.sh +++ b/tests/provision-infrastructure.sh @@ -62,6 +62,7 @@ az keyvault wait --resource-group $RG --name $KV_NAME --created KV_ID="$(az keyvault show \ --name $KV_NAME \ + --resource-group $RG \ --query id \ -otsv)"