This check flags GCP Cloud Functions running as the default Compute Engine service account, which grants the broadly privileged Editor role. Assign each function a dedicated service account scoped to only the permissions it needs.
When you deploy a Cloud Function without specifying a runtime service account, GCP quietly attaches the default Compute Engine service account. It works, your function runs, and nothing breaks. That is exactly the problem. The default account carries the project-level Editor role, so your tiny image-resize function now has the keys to almost everything in the project.
This Lensix check, functions_defaultserviceaccount, looks at each Cloud Function in your GCP projects and reports any that are still tied to the default Compute Engine service account.
What this check detects
The check inspects the serviceAccountEmail field on each Cloud Function. If that value matches the project's default Compute Engine service account, the function is flagged.
The default account follows a predictable pattern:
[email protected]
For 2nd gen Cloud Functions (built on Cloud Run), the same logic applies to the underlying Cloud Run service identity. If you never set --service-account at deploy time, you inherited the default.
Note: The default Compute Engine service account is created automatically when you enable the Compute Engine API. By default it is granted roles/editor on the project, which covers read and write access to nearly every resource type.
Why it matters
The runtime service account defines what your function can do when it calls other Google Cloud APIs. If that identity is over-privileged, every line of code in the function inherits those privileges, and so does anyone who manages to run code inside it.
The blast radius problem
Say you have a function that reads a single Cloud Storage bucket and writes a row to BigQuery. With the default account, that same function can also:
- Read and delete objects in every bucket in the project
- Spin up or destroy Compute Engine instances
- Modify firewall rules and VPC settings
- Read secrets from Secret Manager that other services depend on
- Alter IAM bindings in some configurations
None of that is needed for the job, but it is all available the moment something goes wrong.
How attackers use it
Cloud Functions are a common target because they often process untrusted input: webhook payloads, uploaded files, HTTP requests from the public internet. A dependency with a known RCE vulnerability, an SSRF bug, or a deserialization flaw can give an attacker code execution inside the function runtime.
Once inside, the first move is almost always to grab credentials from the metadata server:
curl -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
If the function runs as the default Editor account, that token is a project-wide Editor token. A single compromised function becomes a full project compromise. If it had a tightly scoped identity instead, the attacker walks away with the ability to read one bucket, which is annoying but survivable.
Warning: The Editor role does not include the ability to manage IAM policy directly, but it can still create resources, exfiltrate data, rack up costs through crypto mining, and disrupt production services. Treat any Editor-level compromise as critical.
How to fix it
The fix is to create a dedicated service account for the function, grant it only the roles it actually needs, and redeploy the function with that identity.
Step 1: Create a dedicated service account
gcloud iam service-accounts create image-resizer-fn \
--display-name="Image Resizer Function" \
--project=my-project
Step 2: Grant least-privilege roles
Figure out what the function genuinely does and grant only those permissions. For the image-resize example, that might be object read on one bucket and write on another:
# Read access to the source bucket
gsutil iam ch \
serviceAccount:[email protected]:roles/storage.objectViewer \
gs://source-uploads
# Write access to the destination bucket
gsutil iam ch \
serviceAccount:[email protected]:roles/storage.objectCreator \
gs://resized-images
Note: Prefer resource-level bindings (on the bucket, dataset, or topic) over project-level role grants. A project-level roles/storage.objectViewer grant lets the function read every bucket, which recreates a smaller version of the original problem.
Step 3: Redeploy the function with the new identity
For a 2nd gen function:
gcloud functions deploy resize-image \
--gen2 \
--runtime=nodejs20 \
--region=us-central1 \
--trigger-http \
--service-account=image-resizer-fn@my-project.iam.gserviceaccount.com \
--source=. \
--entry-point=resizeImage
For a 1st gen function the flag is the same:
gcloud functions deploy resize-image \
--runtime=nodejs20 \
--region=us-central1 \
--trigger-http \
--service-account=image-resizer-fn@my-project.iam.gserviceaccount.com \
--source=.
Warning: Changing the runtime service account takes effect on the next invocation after deploy completes. If the new account is missing a permission the function relied on, you will see 403 PERMISSION_DENIED errors at runtime, not at deploy time. Test in a non-production environment first and watch the logs.
Step 4: Verify the change
gcloud functions describe resize-image \
--gen2 \
--region=us-central1 \
--format="value(serviceConfig.serviceAccountEmail)"
The output should be your new dedicated account, not the [email protected] address.
Optionally lock down the default account
Once no workloads depend on the default Compute Engine account, you can remove its Editor binding so it stops being a liability:
Danger: Removing the Editor role from the default service account will break any function, VM, or other workload still using it. Audit every resource first. Run this only after you have confirmed nothing depends on it.
gcloud projects remove-iam-policy-binding my-project \
--member="serviceAccount:[email protected]" \
--role="roles/editor"
How to define this in IaC
If you manage functions through Terraform, set the service account explicitly so the default is never used. Here is a 2nd gen example:
resource "google_service_account" "resizer" {
account_id = "image-resizer-fn"
display_name = "Image Resizer Function"
}
resource "google_storage_bucket_iam_member" "read_source" {
bucket = google_storage_bucket.source.name
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.resizer.email}"
}
resource "google_cloudfunctions2_function" "resize" {
name = "resize-image"
location = "us-central1"
build_config {
runtime = "nodejs20"
entry_point = "resizeImage"
source {
storage_source {
bucket = google_storage_bucket.source_code.name
object = google_storage_bucket_object.source.name
}
}
}
service_config {
service_account_email = google_service_account.resizer.email
}
}
Tip: In Terraform, always set service_config.service_account_email explicitly. If you leave it out, the provider falls back to the default Compute Engine account and the drift will not be obvious in your plan output.
How to prevent it from happening again
Manual remediation fixes today's functions. Policy and automation keep them fixed.
Disable automatic Editor grants org-wide
GCP has an organization policy that stops new default service accounts from automatically receiving the Editor role. Turn it on so future projects start clean:
gcloud resource-manager org-policies enable-enforce \
iam.automaticIamGrantsForDefaultServiceAccounts \
--organization=YOUR_ORG_ID
Gate deployments in CI/CD
Add a check to your pipeline that rejects any function deploy missing an explicit service account. A simple guard for gcloud-based deploys:
if ! grep -q -- "--service-account" deploy.sh; then
echo "ERROR: Cloud Function deploy is missing --service-account"
exit 1
fi
Enforce with policy-as-code
For Terraform pipelines, write a Conftest or OPA policy that fails the plan when a function has no service account set:
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_cloudfunctions2_function"
not resource.change.after.service_config[_].service_account_email
msg := sprintf("Function '%s' must set an explicit service account", [resource.address])
}
Tip: Run the Lensix functions_defaultserviceaccount check on a schedule rather than only at deploy time. Functions get created out-of-band through the console or quick scripts, and a recurring scan catches what slips past the pipeline.
Best practices
- One identity per function. Sharing a service account across functions means a compromise in one spreads to the permissions of all. Dedicated accounts keep blast radius small and make audit logs readable.
- Bind roles at the resource, not the project. Grant access on the specific bucket, topic, or dataset the function touches. Project-level grants almost always over-provision.
- Start from zero permissions and add what fails. Deploy with no roles, run the function, read the
PERMISSION_DENIEDerrors in Cloud Logging, and grant exactly those. This produces the tightest possible policy. - Review with the IAM Recommender. GCP analyzes actual usage and suggests role downgrades. It will flag unused permissions on your function accounts over time.
- Never store keys for these accounts. Cloud Functions get credentials from the metadata server automatically. There is no reason to generate and ship a JSON key.
- Audit the default account regularly. Even if your functions are clean, VMs and other services may still depend on the default account. Know what uses it before you try to lock it down.
Assigning a scoped service account costs you a few extra lines at deploy time. In exchange, a vulnerability in one function stays contained to that function instead of becoming a project-wide incident. That trade is always worth making.

