Cloud Run Security Model Explained for Beginners

Cloud Run handles the infrastructure, but security is still your responsibility. The model is straightforward once you see the four things it controls: who can call your service, what your service can access, how sensitive values reach your app, and how the service is exposed on the network.

Most Cloud Run security incidents trace back to three mistakes: using the default service account (which has project-wide Editor permissions), storing credentials in plain environment variables that show up in the Cloud Console, and leaving internal services open to the public internet. This page explains each part of the model clearly and gives you the safe defaults to follow.

If you are new to Cloud Run, start with the Cloud Run overview first, then come back here for the security layer.

Simple explanation

Think of Cloud Run security as four separate controls, each answering one question.

  • Inbound access (invocation). Can anyone on the internet call this service, or does the caller need to prove their identity first? This is controlled by an IAM policy on the service itself. Publicly callable services skip identity checks. Private services require a valid Google-signed identity token before the request reaches your container.

  • Runtime identity (service account). Every Cloud Run service runs as a service account — its identity inside GCP. When your service calls Cloud SQL, reads from Cloud Storage, or accesses Secret Manager, it authenticates as this account. If the account has too many permissions, a vulnerability in your code becomes a much bigger incident.

  • Secrets and sensitive config. Passwords, API keys, and database connection strings should not live in your environment variables or your container image. They should come from Secret Manager at startup, injected securely and without appearing in config logs or the Cloud Console.

  • Network controls (ingress and egress). Ingress controls where requests can arrive from: the public internet, internal GCP traffic only, or a specific load balancer. Egress controls where outbound traffic goes. Reaching private VPC resources like Cloud SQL on a private IP requires a Serverless VPC Access connector or Direct VPC Egress.

Analogy

Think of your Cloud Run service as a building. The front door (invocation) controls who can enter. Once inside, your badge (service account) determines which rooms you can access. Sensitive documents (secrets) are kept in a locked safe (Secret Manager), not left on a desk where anyone walking in can read them. And the building itself sits either on a public street (default) or inside a private compound (internal ingress plus VPC).

How the Cloud Run security model works

When a request hits a Cloud Run service, here is the sequence:

  1. The request arrives at Google’s managed load balancer for the service.
  2. Cloud Run checks the service’s IAM policy. If allUsers has roles/run.invoker, the request goes straight through (public service). If not, the caller must present a valid Google OIDC ID token. A missing or invalid token returns 403.

  3. If the token is valid and the caller is authorised, the request is routed to a container instance.

  4. The container runs as the attached service account. Any GCP API calls made by your application code authenticate as this account. Its IAM bindings determine what it can read, write, or invoke.

  5. If the service accesses secrets, it reads them from Secret Manager at instance startup. The secret is injected as an environment variable or file, not embedded in the service definition or visible in the console.

  6. Outbound traffic follows egress rules. Without a VPC connector, Cloud Run cannot reach private IP resources. With a connector and —vpc-egress=private-ranges-only, RFC 1918 traffic routes through the VPC.

Important

Each of these steps is independent. A public service can still use a tightly scoped service account and read secrets from Secret Manager. A private service can still use an over-permissioned account. The controls do not enforce each other. You need to apply all four correctly.

The 4 main parts of Cloud Run security

1. Who can invoke the service

Cloud Run services are either public or authenticated. Public services accept requests from anyone with the URL. Authenticated services require the caller to send a valid Google OIDC ID token. Cloud Run validates this before the request reaches your container.

The risk of getting this wrong: leaving an internal service public means any attacker who discovers the URL can call it without authentication. This is the most common Cloud Run exposure.

# Public service — no authentication required
gcloud run deploy my-public-api \
  --image=IMAGE \
  --region=us-central1 \
  --allow-unauthenticated

# Private service — callers must present a valid OIDC token
gcloud run deploy my-private-api \
  --image=IMAGE \
  --region=us-central1 \
  --no-allow-unauthenticated

# Grant a service account permission to invoke the private service
gcloud run services add-iam-policy-binding my-private-api \
  --region=us-central1 \
  --member=serviceAccount:caller-sa@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/run.invoker
Note

roles/run.invoker can be bound to individual service accounts, a Google group, or allUsers (public). Bind it to specific service accounts for every internal service. See IAM roles explained for the role types involved.

2. What the service can access

The service account attached to a Cloud Run service is its runtime identity. It determines what your application can do inside GCP: read Cloud Storage, query Firestore, publish to Pub/Sub, call other Cloud Run services.

Risk

The Compute Engine default service account carries the project Editor role. A single vulnerability in your app gives an attacker read and write access to Cloud Storage, Cloud SQL, Secret Manager, and most other GCP services across the whole project. Never use the default service account for Cloud Run workloads.

The principle of least privilege says give each service only the permissions it actually needs for its job. One service connecting to Cloud SQL should have exactly roles/cloudsql.client, nothing more.

# Create a dedicated service account for this specific service
gcloud iam service-accounts create my-api-sa \
  --display-name="Service account for my-api"

# Grant only the roles this service actually needs
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member=serviceAccount:my-api-sa@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/cloudsql.client

# Deploy using the dedicated account
gcloud run deploy my-api \
  --image=IMAGE \
  --region=us-central1 \
  --service-account=my-api-sa@PROJECT_ID.iam.gserviceaccount.com

3. How secrets are passed safely

Plain environment variables set with —set-env-vars are stored in the Cloud Run service definition. Anyone with read access to the service in the Cloud Console can see them. Do not use them for passwords, tokens, or API keys.

The safe approach is Secret Manager with —set-secrets. The value is pulled at instance startup and never appears in the service definition or console output.

# Create the secret
echo -n "my-database-password" | \
  gcloud secrets create db-password --data-file=-

# Grant the Cloud Run service account access to this specific secret
gcloud secrets add-iam-policy-binding db-password \
  --member=serviceAccount:my-api-sa@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/secretmanager.secretAccessor

# Inject the secret as an environment variable at deploy time
gcloud run deploy my-api \
  --image=IMAGE \
  --region=us-central1 \
  --service-account=my-api-sa@PROJECT_ID.iam.gserviceaccount.com \
  --set-secrets=DB_PASSWORD=db-password:latest

# Or mount it as a file (better for certificates and JSON key files)
gcloud run deploy my-api \
  --image=IMAGE \
  --region=us-central1 \
  --set-secrets=/secrets/credentials=my-credential:latest

4. How network exposure is controlled

Invocation IAM controls who can authenticate to call your service. Ingress settings control where requests can arrive from at the network level. These are two separate controls and both matter.

By default, Cloud Run is reachable from the public internet. You can restrict it to internal GCP traffic using —ingress=internal, which limits requests to traffic from within GCP, even if those callers have a valid identity token.

# Restrict to internal GCP traffic only
gcloud run services update my-private-api \
  --region=us-central1 \
  --ingress=internal

# Restore public internet access (default)
gcloud run services update my-public-api \
  --region=us-central1 \
  --ingress=all
Warning

Setting —ingress=internal does not replace IAM authentication. A service can have internal ingress and still allow unauthenticated callers from within GCP. Use both —no-allow-unauthenticated and —ingress=internal together for true internal-only services.

Public vs private Cloud Run services

The most important decision for any Cloud Run service is whether it should be publicly callable or restricted to authenticated identities. Here is what each pattern means.

TypeWho can call itAuth requiredCommon use caseMain risk
PublicAnyone with the URLNoWebsite, open API, external webhookExposed to internet with no audit trail of callers
AuthenticatedIdentities with roles/run.invokerYes (OIDC token)Internal API, microserviceMisconfigured bindings grant too broad access
Internal ingress + authenticatedGCP-internal traffic only + IAMYes (both controls)Secure backend serviceIngress alone is not enough without IAM auth

Use public access only when you genuinely want anyone to be able to call the service: a public-facing website, an open API, or an external webhook from a provider that cannot send identity tokens. Every internal service should use —no-allow-unauthenticated.

When deploying for the first time, read Deploying Your First Cloud Run Service for the full deployment flow including how to set these flags correctly from the start.

Service accounts in Cloud Run

A service account is a non-human identity that a GCP resource uses to authenticate to Google APIs. Every Cloud Run service runs as exactly one service account. That account’s IAM permissions determine what the service can do.

Analogy

Think of a service account like a contractor’s access badge. A good contractor gets a badge that only opens the rooms they need for the job. You would not hand every contractor a master key to the whole building. Cloud Run’s default service account is that master key. Create a specific badge for each service instead.

If you do not specify a service account at deploy time, Cloud Run uses the Compute Engine default service account. This account typically carries the project Editor role, broad enough to read and write Cloud Storage, query databases, access secrets, and call most GCP APIs across the entire project. That is a serious blast radius if something goes wrong with your app.

The right pattern is one dedicated service account per Cloud Run service, with only the roles that specific service needs. If a service only reads from one Cloud Storage bucket, its service account should only have roles/storage.objectViewer on that bucket, not at the project level.

Safe default

Create a dedicated service account before you deploy. Name it after the service (e.g., my-api-sa). Grant only the specific roles it needs on the specific resources it needs. Pass it to Cloud Run with —service-account. Never use the Compute Engine default service account for application workloads.

Common narrowly scoped roles for Cloud Run service accounts:

  • roles/cloudsql.client - connect to Cloud SQL
  • roles/secretmanager.secretAccessor - read specific secrets
  • roles/storage.objectViewer - read from Cloud Storage
  • roles/storage.objectCreator - write to Cloud Storage
  • roles/pubsub.publisher - publish to a Pub/Sub topic
  • roles/run.invoker - call another Cloud Run service

Cloud Run uses the attached service account directly, with no JSON key files needed. Understanding why service account keys are dangerous explains why this is the safer pattern compared to downloading and storing credentials.

Secrets in Cloud Run

Secrets are values that should not appear in logs, config files, or the Cloud Console: database passwords, API keys, OAuth tokens, TLS certificates. Cloud Run gives you two ways to pass these to a container. —set-env-vars is the wrong one. —set-secrets is the correct one.

Do not do this

Environment variables set with —set-env-vars are stored unencrypted in the Cloud Run service definition and visible to anyone who can describe the service in the Cloud Console or via gcloud run services describe. Never use plain env vars for passwords, tokens, or credentials.

With —set-secrets, the value is pulled from Secret Manager at instance startup. It never appears in the service definition. Access is controlled by IAM. Only the service account you grant roles/secretmanager.secretAccessor to can read it. Every access is logged in Cloud Audit Logs.

Two injection methods are available:

  • Environment variable injection. The secret value is available as an env var inside the container. Use this for string values like database passwords and API keys.

  • File mount. The secret is written to a file path inside the container. Use this for certificates, private keys, and JSON files that your app reads from disk.

For the secret version, :latest always picks up the most recent version, which is convenient for automated rotation workflows. In production, consider pinning to a specific version number (e.g., db-password:3) and updating deliberately after testing, so a rotation mid-deployment does not cause unexpected behaviour across running instances.

If you manage secrets as part of a deployment pipeline, read Secrets in CI/CD Pipelines for how to handle this safely in Cloud Build and GitHub Actions.

Service-to-service authentication

When one Cloud Run service needs to call another private Cloud Run service, it uses an OIDC ID token: a short-lived, Google-signed token that proves the caller’s identity. The token is fetched from the instance metadata server at runtime, so no API keys or stored credentials are involved.

An OIDC token here is a signed JSON Web Token (JWT) issued by Google. It contains the caller’s identity (the service account email) and the audience (the URL of the target service). Cloud Run validates the token’s signature and checks that the caller has roles/run.invoker on the target service before passing the request through.

API keys are the wrong pattern for service-to-service calls. They are static, shareable, and have no identity attached to them. OIDC tokens expire after one hour and are tied to a specific identity and audience.

# Calling a private Cloud Run service from another Cloud Run service
import google.auth.transport.requests
import google.oauth2.id_token
import requests

def call_private_service(target_url: str, payload: dict) -> dict:
    auth_req = google.auth.transport.requests.Request()
    token = google.oauth2.id_token.fetch_id_token(auth_req, target_url)

    response = requests.post(
        target_url,
        json=payload,
        headers={"Authorization": f"Bearer {token}"}
    )
    response.raise_for_status()
    return response.json()

The calling service account needs roles/run.invoker on the target service. Tokens are valid for one hour and refreshed automatically by the google-auth library.

Tip

The audience for the OIDC token must be the exact base URL of the target service, not a path. Use https://my-service-hash-uc.a.run.app as the audience, not https://my-service-hash-uc.a.run.app/api/v1/endpoint. The token fetch will succeed but Cloud Run will reject it with a 403.

Ingress, VPC access, and private resources

Two separate network security concerns often get mixed up in Cloud Run:

  • Ingress controls where incoming requests can arrive from. It is about who can reach the service’s URL at the network level.

  • Egress / VPC access controls where the service’s outbound traffic goes. It is about what private resources the service can reach.

Cloud Run services live outside your VPC network by default. They can call public internet endpoints but cannot reach private VPC resources like Cloud SQL on a private IP, Memorystore, or internal load balancers. To enable that, you need a Serverless VPC Access connector or Direct VPC Egress.

# Create a Serverless VPC Access connector
gcloud compute networks vpc-access connectors create my-connector \
  --region=us-central1 \
  --subnet=my-subnet \
  --subnet-project=PROJECT_ID \
  --min-instances=2 \
  --max-instances=10

# Deploy Cloud Run with the connector — only private IP traffic goes through VPC
gcloud run deploy my-api \
  --image=IMAGE \
  --region=us-central1 \
  --vpc-connector=my-connector \
  --vpc-egress=private-ranges-only

# Restrict incoming requests to internal GCP traffic only
gcloud run services update my-api \
  --region=us-central1 \
  --ingress=internal

—vpc-egress=private-ranges-only routes only RFC 1918 traffic through the VPC connector. Public internet traffic goes directly from Cloud Run’s managed infrastructure. Use —vpc-egress=all-traffic if you need all outbound requests to exit through a fixed IP address for firewall rules on an external service.

When to apply each security pattern

Quick reference

Every Cloud Run service should have a dedicated service account and use Secret Manager for credentials. Everything else below builds on that base.

  • Public website or open API. Use —allow-unauthenticated. Still use a dedicated service account with minimal permissions. Use Secret Manager for any credentials the app needs.

  • Internal API called by other services. Use —no-allow-unauthenticated and grant roles/run.invoker to the specific service accounts that call it. Add —ingress=internal for an extra network-level restriction.

  • Service that connects to Cloud SQL or Memorystore. Add a Serverless VPC Access connector with —vpc-egress=private-ranges-only. Grant roles/cloudsql.client to the service account.

  • Service that reads from Secret Manager. Grant roles/secretmanager.secretAccessor on each specific secret, not at the project level. Use —set-secrets to inject at deploy time.

  • Microservices calling each other inside GCP. Give each service its own dedicated service account. Grant roles/run.invoker on the target to the caller’s service account. Use OIDC tokens, not API keys.

  • Service deployed via a CI/CD pipeline. The Cloud Build or GitHub Actions service account needs roles/run.developer scoped to the service or region. See CI/CD pipelines for Cloud Run for the full setup.

Common beginner mistakes

  1. Using the Compute Engine default service account. It has project Editor permissions by default. Any vulnerability in the app gives an attacker those permissions across the whole project. Always create a dedicated service account per service with only the roles it actually needs.

  2. Passing secrets with —set-env-vars. Plain environment variables are stored in the service definition and visible in the Cloud Console to anyone with describe access. Use —set-secrets to pull from Secret Manager, where access is controlled, audited, and versioned.

  3. Leaving internal services publicly invocable. If a service has no legitimate external callers, deploy it with —no-allow-unauthenticated. Public exposure adds attack surface with no benefit for services that are never meant to be called from outside.

  4. Confusing ingress restrictions with IAM authentication. —ingress=internal restricts the network origin of requests but does not authenticate callers. A service can have internal ingress and still allow unauthenticated requests from within GCP. Use both —no-allow-unauthenticated and —ingress=internal together for internal-only services.

  5. Granting broad project-level roles to service accounts. Instead of roles/editor at the project level, bind the minimum role to the specific resource. Grant roles/secretmanager.secretAccessor on the secret itself, not on the whole project. See principle of least privilege.

  6. Making everything public during testing and forgetting to lock it down. It is easy to use —allow-unauthenticated while iterating quickly and forget to remove it before production. Treat every deployed service, including test services, as potentially reachable until you confirm otherwise.

  7. Using stored service account key files for service-to-service auth. Cloud Run services can call each other using short-lived OIDC tokens from the metadata server. There is no reason to create a JSON key file. Key files are harder to rotate, easier to leak, and unnecessary. Read why service account keys are dangerous for the full picture.

Cloud Run vs Cloud Functions: security comparison

Both services use the same underlying IAM and service account model, so many patterns are identical. The key differences are in operational flexibility and how secrets are integrated.

Cloud RunCloud Functions
Invocation controlIAM (roles/run.invoker)IAM (roles/cloudfunctions.invoker)
Default accessRequires —allow-unauthenticated to go publicSame behaviour
Service accountAny account you specifyAny account you specify
Secret injection—set-secrets (Secret Manager)Direct Secret Manager integration
Ingress control—ingress=all / internal / internal-and-cloud-load-balancingSimilar settings
VPC egressVPC Access connector or Direct VPC EgressVPC Access connector

The security model is not the differentiator. They are nearly identical on IAM, secrets, and network controls. The choice comes down to workload fit. See Cloud Functions overview and Choosing between Cloud Run, GKE, and VMs for the trade-offs.

Frequently asked questions

What is the Cloud Run security model?

The Cloud Run security model has four parts: invocation control (who can call the service), runtime identity (which service account the container runs as), secret handling (how sensitive values reach the app), and network exposure (whether the service is reachable from the public internet, only internally, or only via VPC). Getting these four right covers the vast majority of Cloud Run security risks.

Is Cloud Run public by default?

Yes. When you deploy a Cloud Run service without specifying access settings, it allows unauthenticated requests, meaning anyone with the URL can call it. For services that should only be reachable by other internal services or specific identities, always deploy with --no-allow-unauthenticated and explicitly grant roles/run.invoker to the service accounts that need access.

Should I use the default Compute Engine service account for Cloud Run?

No. The Compute Engine default service account has the project Editor role by default, which gives your Cloud Run service read and write access to almost every resource in the project. If your service is compromised, an attacker inherits those permissions. Always create a dedicated service account with only the roles the specific service needs (for example, roles/cloudsql.client if it only connects to Cloud SQL).

How do private Cloud Run services authenticate incoming requests?

When a Cloud Run service has --no-allow-unauthenticated set, every incoming request must include a valid Google-signed OIDC ID token in the Authorization: Bearer header. Cloud Run validates the token before passing the request to your container. The caller must be a Google identity (service account, user, or group) with the roles/run.invoker role bound on the target service.

What is the safest way to pass secrets to Cloud Run?

Use Secret Manager with --set-secrets at deploy time. Store the secret in Secret Manager, grant the Cloud Run service account roles/secretmanager.secretAccessor on that specific secret, then inject it with --set-secrets=MY_SECRET=secret-name:latest. For certificates or JSON key files, use a file mount. Never use --set-env-vars for passwords, API keys, or database credentials. Plain env vars are stored in the service definition and visible in the Cloud Console.

Last verified: 22 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.