iam_visibility_audit

command module
v0.0.0-...-1393586 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2021 License: Apache-2.0 Imports: 19 Imported by: 0

README

Auditing users for IAM bindings outside of primarily Cloud Organization

Sample script to help identify which users within a Cloud Organization who have IAM bindings in projects or organizations outside of a primary cloud org.

For example, Alice is a member of Cloud Org MyDomain. Alice has access to projects A,B,C within MyDomain.

Bob creates a project D in Cloud Org OtherDomain. Bob grants IAM permissions to Alice to have access to project D. If Alice accepts the invite from Bob to use project D, Alice now has access to a project outside of the primary cloud org domain.

In another situation, Alice may have created a project early on before the domain MyDomain was created. Later on, an org admin will have to identify and migrate projects into the new domain, MyDomain.

This script will help identify which projects and organizations a user in a domain may have been granted access to that resides in another organizations or directly granted to a project. This will not identify direct permissions on resources within a project (e.g. if Bob set a direct IAM grant to Alice on a GCS bucket that Bob owns).

Either way, please review Restricting project visibility for users in combination with VPC-SC.

This is not an officially supported Google product

Please note that Google Cloud Support will not identify owners of projects not associated with your OrganizationID.

How does it work?

This script will:

  1. Enumerate all projects within a given cloud org.
  2. Iterate all users within a Workspace domain tied to the cloud org.
  3. For each user, use user impersonation to identify which organizations they have access to.
  4. For each user, use user impersonation to identify which projects they have access to.
  5. If the user has access to any other organization that's not in the subject org in #1, print that external org.
  6. If the user has access to any other project thats not in the set in #1, print that external project.

In order to ensure all users of the domain are part of this audit, it is recommended that all unmanaged users of your domain are migrated into your Workspace or Cloud Identity account.

For more information about Workspace domain delegation and user impersonation, see Authentication Best Practices for Workspace APIs.

Usage

To configure this script, you need to have access to act as

Once the permissions and configuration below is done, the script will run with ReadOnly/Viewer permissions to run the discovery. These permissions are needed initially to configure the least-privilege IAM roles the script will ultimately use.

# select a project that will host the service account
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
export DOMAIN_ADMIN=`gcloud config get-value core/account`

# acquire the organization ID
gcloud organizations list
	DISPLAY_NAME  ID            DIRECTORY_CUSTOMER_ID
	myDomain      673208786099  D123zw3x8

export ORGANIZATION_ID=673208786099

export CX=`gcloud organizations describe $ORGANIZATION_ID --format="value(owner.directoryCustomerId)"`

# Enable the apis on the project
gcloud services enable cloudasset.googleapis.com \
  cloudresourcemanager.googleapis.com \
  admin.googleapis.com \
  iamcredentials.googleapis.com --project $PROJECT_ID

# create service account
gcloud iam service-accounts create dwd-sa

gcloud iam service-accounts describe dwd-sa@$PROJECT_ID.iam.gserviceaccount.com

# get the service account's clientID
export SA_CLIENT_ID=`gcloud iam service-accounts describe dwd-sa@$PROJECT_ID.iam.gserviceaccount.com --format="value(uniqueId)"`
echo $SA_CLIENT_ID

# enable impersonation instead of key-download and adapt the workspace AdminAPI to use the impersonated access token
gcloud iam service-accounts  add-iam-policy-binding \
  --role=roles/iam.serviceAccountTokenCreator \
  --member=user:$DOMAIN_ADMIN dwd-sa@$PROJECT_ID.iam.gserviceaccount.com

# Alternatively, you can download a key but this is _NOT_ recommended
# gcloud iam service-accounts keys create svc_account.json --iam-account=dwd-sa@$PROJECT_ID.iam.gserviceaccount.com

# Allow service account domain IAM privileges
#   http://cloud.google.com/asset-inventory/docs/access-control#required_permissions

gcloud organizations add-iam-policy-binding \
   --member=serviceAccount:dwd-sa@$PROJECT_ID.iam.gserviceaccount.com \
   --role=roles/cloudasset.viewer  $ORGANIZATION_ID

Cloud org Domain IAM Binding:

images/domain_iam.png

Navigate to the Google Workspace Console and enable Domain Wide Delegation

Enter the $SA_CLIENT_ID for the service account and these precise scopes

  • https://www.googleapis.com/auth/admin.directory.user.readonly
  • https://www.googleapis.com/auth/cloud-platform

Note, we really should only need * https://www.googleapis.com/auth/cloud-platform.read-only scope here but the Cloud ResourceManager v3 API calls projects.Query() method declares https://www.googleapis.com/auth/cloud-platform (which is a bug).

Domain Delegation Scope permission:

images/client_id.png

Acquire Application Default Credentials for $DOMAIN_ADMIN

gcloud auth application-default login

Now run the utility

# with service account impersonation (preferred)
go run main.go --impersonatedServiceAccount=dwd-sa@$PROJECT_ID.iam.gserviceaccount.com \
  --subject=$DOMAIN_ADMIN \
  --organization $ORGANIZATION_ID \
  -cx $CX --alsologtostderr=1 -v 10


# with service account Key (not recommended)
# go run main.go --serviceAccountFile=svc_account.json \
#   --subject=$DOMAIN_ADMIN \
#   --organization $ORGANIZATION_ID \
#   -cx $CX --alsologtostderr=1 -v 10

The output will show all the projects in an org and for each user see if he/she has access to a project/org outside the domain

I1003 09:59:32.852107  485974 main.go:213]              User [user10@mydomain.com] has external project visibility to [projects/919583951822](otherdomain-project-1)
I1003 09:59:33.214895  485974 main.go:213]              User [user4@mydomain.com] has external project visibility to [projects/1071284184433](yetotherdomain-project-1)
I1003 09:59:33.252026  485974 main.go:147]              User [user10@mydomain.com] has external organization visibility to [organizations/479774786222](otherdomain)

Output logs are also written to file through glog level logging package. By default the ERROR|INFO|etc level logs are under /tmp/ (eg /tmp/main.INFO). Adjust the log verbosity with -v flag.

Check /tmp/main.ERROR log for users that threw exceptions and were skipped. You can selectively run the script to evaluate individual users by setting the searchFilter variable. For example, to evaluate one user only:

	searchFilter := "email=alice@mydomain.com"
	allUsers, err := findDomainUsers(ctx, *cx, searchFilter, adminService)

Working with Quotas/Limits

Each API used by this script has different per-second rate limits

The restrictive quota is for the Cloud Resource Manager is one that is not listed on the documented page but is visible on the console:

https://console.cloud.google.com/apis/api/cloudresourcemanager.googleapis.com/metrics?project=$PROJECT_ID

The project.List() is actually limited to 4 requests/second and exceeding ith would lead to errors if the quota rate isn't managed over the progressive duration of the audit:

googleapi: Error 429: Quota exceeded for quota metric 'List projects V1' and limit 'List projects V1 per minute' 
    of service 'cloudresourcemanager.googleapis.com' for consumer 'project_number:'., rateLimitExceeded

There are several ways to work with this:

Sleep

sleep is applied per User thats evaluated

delay                      = flag.Int("delay", 1*1000, "delay in ms for each user iterated")

time.Sleep(time.Duration(*delay) * time.Millisecond)
Throttling

Limit all api calls for the resource managers globally using "golang.org/x/time/rate"

	maxRequestsPerSecond float64 = 4 // "golang.org/x/time/rate" limiter to throttle operations
	burst                int     = 1

which is enforced within the iteration to get the list of projects and organization per user:

func getProjects(ctx context.Context, limiter *rate.Limiter, filter string, fields string, crmService cloudresourcemanager.Service, u admin.User) ([]*cloudresourcemanager.Project, error) {
	glog.V(50).Infof("             Getting Projects for user %s", u.PrimaryEmail)

	projects := make([]*cloudresourcemanager.Project, 0)
	req := crmService.Projects.Search().Query(filter).Fields(googleapi.Field(fields)).PageSize(maxPageSize)
	err := req.Pages(ctx, func(page *cloudresourcemanager.SearchProjectsResponse) error {
		projects = append(projects, page.Projects...)
		if err := limiter.Wait(ctx); err != nil {
			glog.Errorf("Error in rate limiter for user %s %v", u.PrimaryEmail, err)
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	return projects, nil
}

It is recommended to not alter these defaults. They have been empirically tested to stay within the quota limits and benchmarked with

  • 1584 users within the org.
  • 4623 projects with in the org.
  • took ~25min runtime.

If you still see rate errors, reduce maxRequestsPerSecond and increase delay

Sharding

Another approach is to use the same script but initialize using N service account from N projects. Then for each users projects.list() call, select a random serviceAccount. To use this pattern, create follow the steps above as you would to initialize a new configuration:

  • Create a new project (e.g. $PROJECT_2_ID)
  • Enable apis
  • Create a service account (dwd-sa@$PROJECT_2_ID.iam.gserviceaccount.com)
  • Allow impersonation
  • Enable IAM role at organization level for service account
  • Enable domain delegation in Workspace Admin Console

Invoke main.go and specify the list of service accounts as comma-separated list. For example

go run main.go --impersonatedServiceAccount=dwd-sa@$PROJECT_ID.iam.gserviceaccount.com,dwd-sa@$PROJECT_2_ID.iam.gserviceaccount.com  \
  --subject=$DOMAIN_ADMIN \
  --organization $ORGANIZATION_ID \
  -cx $CX --alsologtostderr=1 -v 50
X-Goog-User-Project

To note for completeness, the standard way to redirect quota usage is bootstrap a client and specify the quota project using options.WithQuotaProject. That option enables X-Goog-User-Project system parameter to redirect quota consumption to another project.

For example usage would have been to initialize the Cloud Resource Manager client as such:

func getResourceManagerClient(...) {
	...
      cloudresourcemanager.NewService(ctx, option.WithTokenSource(ts),option.WithQuotaProject(targetQuotaProject))
}   ...

However, the complication in applying this pattern with this script would require each end users to have the serviceusage.services.use IAM permission (or role roles/serviceusage.serviceUsageConsumer) granted on the target project. Not specifying the quota project works here when used with domain delegation user impersonation as it automatically targets the quota consumption to the project that tied to the service account with domain delegation.

This technique is omitted from the script.

Documentation

Overview

Command iam_visibility_audit will enumerate all projects and organizations Workspace users may have access to outside of their primary organizationID.

see:

https://cloud.google.com/resource-manager/docs/access-control-org#restricting_visibility

Arguments:

impersonatedServiceAccount = flag.String("impersonatedServiceAccount", "", "Impersonated Service Accounts the script should run as")
organization               = flag.String("organization", "", "The organizationID that is the subject of this audit")
subject                    = flag.String("subject", "", "The admin user to for the organization that can use the Directory API to list users")
cx                         = flag.String("cx", "", "Workspace Customer ID number")
serviceAccountFile         = flag.String("serviceAccountFile", "", "Servie Account JSON files with IAM permissions to the org")

-v 10  adjust log verbosity level

Usage:

$ go run main.go --impersonatedServiceAccount=dwd-sa@$PROJECT_ID.iam.gserviceaccount.com \
  --subject=$DOMAIN_ADMIN \
  --organization $ORGANIZATION_ID \
  -cx $CX --alsologtostderr=1 -v 10

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL