Practical advice on specifying more granular permissions with Google Cloud IAM

Katie McLaughlin - May 14 '20 - - Dev Community

In our last post, we discussed some ways you could go about working out the minimum viable permissions you needed to deploy your application, keeping in mind the principle of least privilege.

In this post, we'll show a practical example of these concepts using a sample automation, working through how to determine what services are used and what roles and permissions are required.


We'll use a four-step Cloud Build automation as an example:

cloudbuild.yaml

steps:
    - id: 'build'
      name: 'gcr.io/cloud-builders/docker'
      args: ['build', '-t', 'gcr.io/${PROJECT_ID}/myservice', '.']

    - id: 'push'
      name: 'gcr.io/cloud-builders/docker'
      args: ['push', 'gcr.io/${PROJECT_ID}/myservice']

    - id: 'migrate'
      name: "gcr.io/google-appengine/exec-wrapper"
      args: ["-i", "gcr.io/${PROJECT_ID}/myservice",
           "-s", "${PROJECT_ID}:us-central1:psql",
           "--", "python", "manage.py", "migrate"]

    - id: 'deploy'
      name: 'gcr.io/cloud-builders/gcloud'
      args: ["run", "deploy", "service",
            "--platform", "managed",
            "--region", "us-central1",
            "--image", "gcr.io/${PROJECT_ID}/myservice"]

When run with gcloud builds submit, this configuration will tell Cloud Build perform four actions:

  • step 0: build a container image
  • step 1: push that container image to the Google Container Repository (gcr.io)
  • step 2: run a migrate action against a Cloud SQL database, and
  • step 3: deploy a Cloud Run service.

This setup does assume that the Cloud SQL instance and Cloud Run service already exist. This may seem like a throwaway line, but it's important to note. In this example, Cloud Build isn't creating the instance, so not only should the instance already exist, but Cloud Build doesn't need rights to create an instance. Same with the Cloud Run service; only permission to run deploy needs to be granted. By default, Cloud Build only gets the "Cloud Build Service Account" role, not anything related to Cloud Run (or other products).

Another thing to note: While I may describe "Cloud Build" doing something, it's actually the service account entity that Cloud Build uses that is performing the operations. But having to use the term "The Cloud Build Service Account Entity" is too long, so I'll just be using "Cloud Build".

If we attempt to run this build with just the base role for Cloud Build, it will not succeed, failing on step 2:

$ gcloud builds submit
...
Step #2 - "migrate": django.db.utils.OperationalError:
          could not connect to server: No such file or directory

So what permissions does Cloud Build need to execute this configuration? First, let's see what it has already, then work out what more it needs.

The best place is to start with the configuration of the main product we're dealing with. Checking the Cloud Build settings in the Cloud Console, it details a few things: The service account email is (PROJECTNUM)@cloudbuild.gserviceaccount.com, and useful documentation about what permissions it does and does not have.

For our purposes, these points stick out:

  • Cloud Build has permission to:
    • push and pull images from Container Registry (that's step 0 push covered ✅)
    • save build logs in Cloud Logging (also useful to be able to see the results of the build)
  • Cloud Build does not have permission to do other things like:
    • deploying to App Engine
    • manage Compute Engine
    • accessing Cloud Storage buckets

There are also details about how to grant these permissions. For our purposes it suggests that for Cloud Run it should get the Cloud Run Admin role. There's also docs in Cloud Build backing this up. But what does the Cloud Run Admin role do?

Checking the list of roles for our project, we can search for Cloud Run and look at the permissions Cloud Run Admin (roles/run.admin) lists:

  • resourcemanager.projects.get
  • resourcemanager.projects.list
  • run.configurations.get
  • run.configurations.list
  • run.locations.list
  • run.revisions.delete
  • run.revisions.get
  • run.revisions.list
  • run.routes.get
  • run.routes.invoke
  • run.routes.list
  • run.services.create
  • run.services.delete
  • run.services.get
  • run.services.getIamPolicy
  • run.services.list
  • run.services.setIamPolicy
  • run.services.update

That's quite a few permissions including some we don't immediately recognise, for example: resourcemanager. It's best to leave those permissions alone. But, from what we do recognise (those run.services permissions), some of those look like they grant too much for our circumstances. For instance, we probably don't want Cloud Build to have the ability to delete services (run.services.delete).

Consider the step we failed on, Step #3 migrate. Performing this action requires access to the database, so we need to connect to Cloud SQL. Looking through the Cloud SQL roles, there's an entry for Cloud SQL Client (roles/cloudsql.client):

  • cloudsql.instances.connect
  • cloudsql.instances.get

This seems to fit our purposes well! It allows connecting to the instance, but it also allows to get details on this instance, which is probably also useful when connecting.

So we've determined that we need the Cloud SQL Client role, and a subset of permissions form the Cloud Run Admin role. We could just assign these roles to Cloud Build, or we can create a custom role containing just the permissions we need. Starting with all the permissions from these two roles, we can check if our build succeeds, then remove extra permissions, testing each time, until we have the minimum set of permissions we are comfortable with.

To start, let's create a new role, copying from Cloud Run Admin, copying in the two Cloud SQL Client permissions, then assign that role to Cloud Build.

$ PROJECT_ID=$(gcloud config get-value project)
$ gcloud iam roles copy --source roles/run.admin \
    --dest-project $PROJECT_ID \
    --destination MinimumDeploy

description: "Full control over all Cloud Run resources."
etag: BwWlG0pkePM=
includedPermissions:
includedPermissions:
- resourcemanager.projects.get
- run.configurations.get
...
name: projects/PROJECT/roles/MinimumDeploy
stage: ALPHA
title: "Cloud Run Admin"

$ gcloud iam roles update MinimumDeploy --project $PROJECT_ID \
    --add-permissions cloudsql.instances.connect,cloudsql.instances.get

Let's also clean up that title and description while we're here.

$ gcloud iam roles describe MinimumDeploy

ERROR: (gcloud.iam.roles.describe) Missing required argument [--organization or --project]:
Should specify the project or organization name for custom roles.

This error message is an important thing to note: this is a custom role, so we need to specify our project in all of this, since it's unique to our project.

$ gcloud iam roles update MinimumDeploy --project $PROJECT_ID \
     --title "Minimum Deploy" \
     --description "Minimum permissions for Cloud Build custom service deployment"

description: "Minimum permissions for Cloud Build custom service deployment"
etag: BwWlG4-qSY0=
includedPermissions:
- resourcemanager.projects.get
- run.configurations.get
...
name: projects/PROJECT_ID/roles/MinimumDeploy
stage: ALPHA
title: "Minimum Deploy"

Let's assign this new role to Cloud Build, and try our build again.

# Generate the value of the Cloud Build service account email
export PROJECTNUM=$(gcloud projects describe ${PROJECT_ID} --format 'value(projectNumber)')
export CLOUDBUILD=${PROJECTNUM}@cloudbuild.gserviceaccount.com

# Get the fully qualified custom role name
MINIMUMDEPLOY=$(gcloud iam roles describe MinimumDeploy \
    --project $PROJECT_ID --format 'value(name)')

# Bind the role as a member of Cloud Build
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member serviceAccount:$CLOUDBUILD \
    --role $MINIMUMDEPLOY
$ gcloud builds submit

...
Finished Step #2 - "migrate"
Starting Step #3 - "deploy "
Step #3 - "deploy ": Already have image (with digest): gcr.io/cloud-builders/gcloud
Step #3 - "deploy ": ERROR: (gcloud.run.deploy) PERMISSION_DENIED:
  Permission 'iam.serviceaccounts.actAs' denied on service account serivce@PROJECT_ID.iam.gserviceaccount.com 
  (or it may not exist).
Finished Step #3 - "deploy"

The migrate step succeeded! But it failed on the final deploy step.

But that's okay, because thanks to the build logs we have the exact issue: we are lacking a "iam.serviceaccounts.actAs" permission. Let's go back to the documentation and see if we missed something.

Ah! "Grant the IAM Service Account User role to the Cloud Build service account on the Cloud Run runtime service account"

This is important. The service that is being deployed isn't owned by Cloud Build. We need to grant Cloud Build to do things on behalf of the service account that owns our Cloud Run service.

So what's that account? We can check by describing the service, and searching for the service account:

$ gcloud run services describe myservice --format "json" | grep serviceAccount

        "serviceAccountName": "myservice@PROJECT_ID.iam.gserviceaccount.com",

Ah, good thing we checked. This deployment isn't using the default compute service account, but a custom service account! So let's add Cloud Build as a user of that service account:

$ SERVICEACCT=myservice@PROJECT_ID.iam.gserviceaccount.com
$ gcloud iam service-accounts add-iam-policy-binding $SERVICEACCT \
    --member "serviceAccount:${CLOUDBUILD}" \
    --role "roles/iam.serviceAccountUser"

And try our build again:

$ gcloud builds submit

...
Step #3 - "deploy": Service [myservice] revision [myservice-00002-miw] has been deployed
and is serving 100 percent of traffic at https://myservice-<hash>-uc.a.run.app
Finished Step #3 - "deploy"

Success! We have deployed our service!

Now that we know the superset of permissions we need, we can go about removing the permissions we do not want, namely those delete permissions:

gcloud iam roles update MinimumDeploy --project $PROJECT_ID \
    --remove-permissions run.services.delete
gcloud iam roles update MinimumDeploy --project $PROJECT_ID \
    --remove-permissions run.revisions.delete

And trying the build again:

$ gcloud build submit
Step #3 - "deploy ": Service [myservice] revision [myservice-00003-waj] has been deployed
and is serving 100 percent of traffic at https://serivce-<hash>-uc.a.run.app
Finished Step #3 - "deploy"

We are still able to successfully deploy, but Cloud Build has reduced permissions.


By starting with the documented recommendations, we were able to get our deployment working, then go about reducing the permissions that the service account had for our specific use case. This same methodology can be used outside of the specific services used in this example, but when working through this process for your own deployment, ensure that you read, understand, and follow both the documentation and error messages throughout the process.

Cheat sheet: gcloud commands

Learn more

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .