Cloud Run service accounts are your secret weapon for granting granular access to other Google Cloud services, but most people grant them way too much power by default.

Let’s say you have a Cloud Run service that needs to read data from a Cloud Storage bucket. Here’s how you’d typically set it up:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: my-data-reader
spec:
  template:
    spec:
      serviceAccountName: my-data-reader@my-project.iam.gserviceaccount.com

And then, on your my-data-reader@my-project.iam.gserviceaccount.com service account, you’d grant it the roles/storage.objectViewer role on the specific bucket. This works! Your Cloud Run service can now read objects. But what if this service account also needs to write to a Pub/Sub topic? You’d add roles/pubsub.publisher to it. What if it needs to query a BigQuery dataset? Add roles/bigquery.dataViewer.

Pretty soon, your my-data-reader service account has a laundry list of permissions, potentially including roles/owner or roles/editor on the entire project, just because it’s convenient.

Here’s the core of the problem: Cloud Run services, by default, are launched with the Compute Engine default service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com). This account often has broad permissions, like roles/editor, to make development easier. If you don’t explicitly specify a service account for your Cloud Run service, it inherits these potentially excessive permissions.

The solution is to create a dedicated service account for each Cloud Run service and grant it only the permissions it absolutely needs.

Consider a Cloud Run service that needs to send messages to a Pub/Sub topic.

First, create a dedicated service account:

gcloud iam service-accounts create pubsub-publisher \
  --display-name "Pub/Sub Publisher Service Account" \
  --project=my-project

This creates pubsub-publisher@my-project.iam.gserviceaccount.com.

Next, deploy your Cloud Run service, explicitly assigning this new service account:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: my-pubsub-sender
spec:
  template:
    spec:
      serviceAccountName: pubsub-publisher@my-project.iam.gserviceaccount.com

Now, grant only the necessary permission to this specific service account on the target Pub/Sub topic:

gcloud pubsub topics add-iam-policy-binding my-topic \
  --member="serviceAccount:pubsub-publisher@my-project.iam.gserviceaccount.com" \
  --role="roles/pubsub.publisher" \
  --project=my-project

This ensures your my-pubsub-sender service can only publish messages to my-topic and nothing else. It can’t read from Cloud Storage, query BigQuery, or even publish to other Pub/Sub topics.

The surprising thing about this pattern is how much it shifts your security posture from a "deny by default, allow specific exceptions" model to an "allow by default, deny everything else" model at the service account level. You’re not just granting permissions; you’re constructing an identity for your application.

When your Cloud Run service starts, it authenticates to Google Cloud using the credentials of its assigned service account. This identity is then used to make API calls to other Google Cloud services. If the service account doesn’t have the required IAM role for a specific action on a specific resource, the API call will be denied.

The actual mechanism is that the Cloud Run runtime environment injects short-lived access tokens associated with the configured service account. When your application code uses a Google Cloud client library (like the Pub/Sub client), it automatically picks up these credentials and uses them to sign requests to the Google Cloud API. The API then checks IAM policies to see if the identity (the service account) has permission to perform the requested operation.

The one thing most people don’t realize is that even though you might grant a broad role like roles/storage.admin on a bucket to a service account, if that service account is also used by another Cloud Run service that only needs to read objects, the second service will still only be able to read objects if you’ve applied a more granular IAM policy at the resource level (the bucket itself). The principle of least privilege applies at every layer: the service account’s roles, and the roles granted on the target resources.

If you forget to assign a service account to your Cloud Run service, it will default to the Compute Engine default service account, which often has broad permissions.

The next concept to explore is how to manage these service accounts and their permissions at scale using tools like Terraform or Pulumi.

Want structured learning?

Take the full Cloud-run course →