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.
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:
- The request arrives at Google’s managed load balancer for the service.
Cloud Run checks the service’s IAM policy. If
allUsershasroles/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.If the token is valid and the caller is authorised, the request is routed to a container instance.
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.
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.
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.
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.invokerroles/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.
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.com3. 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:latest4. 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=allSetting —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.
| Type | Who can call it | Auth required | Common use case | Main risk |
|---|---|---|---|---|
| Public | Anyone with the URL | No | Website, open API, external webhook | Exposed to internet with no audit trail of callers |
| Authenticated | Identities with roles/run.invoker | Yes (OIDC token) | Internal API, microservice | Misconfigured bindings grant too broad access |
| Internal ingress + authenticated | GCP-internal traffic only + IAM | Yes (both controls) | Secure backend service | Ingress 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.
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.
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 SQLroles/secretmanager.secretAccessor- read specific secretsroles/storage.objectViewer- read from Cloud Storageroles/storage.objectCreator- write to Cloud Storageroles/pubsub.publisher- publish to a Pub/Sub topicroles/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.
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.
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
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-unauthenticatedand grantroles/run.invokerto the specific service accounts that call it. Add—ingress=internalfor 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. Grantroles/cloudsql.clientto the service account.Service that reads from Secret Manager. Grant
roles/secretmanager.secretAccessoron each specific secret, not at the project level. Use—set-secretsto inject at deploy time.Microservices calling each other inside GCP. Give each service its own dedicated service account. Grant
roles/run.invokeron 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.developerscoped to the service or region. See CI/CD pipelines for Cloud Run for the full setup.
Common beginner mistakes
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.
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-secretsto pull from Secret Manager, where access is controlled, audited, and versioned.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.Confusing ingress restrictions with IAM authentication.
—ingress=internalrestricts 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-unauthenticatedand—ingress=internaltogether for internal-only services.Granting broad project-level roles to service accounts. Instead of
roles/editorat the project level, bind the minimum role to the specific resource. Grantroles/secretmanager.secretAccessoron the secret itself, not on the whole project. See principle of least privilege.Making everything public during testing and forgetting to lock it down. It is easy to use
—allow-unauthenticatedwhile iterating quickly and forget to remove it before production. Treat every deployed service, including test services, as potentially reachable until you confirm otherwise.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 Run | Cloud Functions | |
|---|---|---|
| Invocation control | IAM (roles/run.invoker) | IAM (roles/cloudfunctions.invoker) |
| Default access | Requires —allow-unauthenticated to go public | Same behaviour |
| Service account | Any account you specify | Any account you specify |
| Secret injection | —set-secrets (Secret Manager) | Direct Secret Manager integration |
| Ingress control | —ingress=all / internal / internal-and-cloud-load-balancing | Similar settings |
| VPC egress | VPC Access connector or Direct VPC Egress | VPC 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.
Summary
- Cloud Run security has four parts: invocation control, runtime identity, secret handling, and network exposure. Apply all four correctly.
- Use
—no-allow-unauthenticatedfor all internal services and grantroles/run.invokerto specific callers only. - Never use the Compute Engine default service account. Create a dedicated one with only the roles the service needs.
- Pass sensitive values via
—set-secretsfrom Secret Manager. Plain—set-env-varsare visible in the Cloud Console. - Service-to-service calls use short-lived OIDC tokens from the metadata server. No API keys or key files needed.
- Use a Serverless VPC Access connector or Direct VPC Egress to reach private VPC resources like Cloud SQL and Memorystore.
—ingress=internalrestricts network origin but does not replace IAM authentication. Use both together for internal-only services.
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.