This issue has been assessed as high severity. Review affected configurations immediately.
Cloud Functions is the GCP equivalent of AWS Lambda: deploy a function, set a trigger, pay for invocations. Like Lambda, the deployment simplicity creates predictable security problems. HTTP trigger functions that should be internal are made publicly reachable by default in many deployment toolchains. The default compute service account — attached to functions unless otherwise specified — often has roles/editor at the project level, turning a function compromise into a full project takeover. Secrets end up in environment variables because Secret Manager is an extra step teams skip under deadline pressure.
These aren’t edge cases. They’re the default failure modes visible in GCP security reviews.
Unauthenticated HTTP Triggers
Cloud Functions HTTP triggers can be deployed with --allow-unauthenticated, which grants the allUsers principal the roles/cloudfunctions.invoker role. Any internet user can call the function with no credentials.
The problem is that many deployment pipelines default to this flag for convenience during development and the setting follows the function into production. The 2nd Gen Cloud Functions (powered by Cloud Run) carry this same misconfiguration pattern from Cloud Run deployments.
Enumerate unauthenticated functions in your project:
# List all Cloud Functions and their authentication status (1st gen)
gcloud functions list --format="table(name,status,httpsTrigger.url,httpsTrigger.securityLevel)"
# Check IAM bindings for a specific function
gcloud functions get-iam-policy FUNCTION_NAME
# Find functions with allUsers invoker binding (1st gen)
for func in $(gcloud functions list --format="value(name)"); do
policy=$(gcloud functions get-iam-policy $func --format=json 2>/dev/null)
if echo "$policy" | grep -q "allUsers"; then
echo "UNAUTHENTICATED: $func"
fi
done
# For 2nd gen (Cloud Run-based)
gcloud functions list --gen2 --format="table(name,state,serviceConfig.uri)"
for func in $(gcloud functions list --gen2 --format="value(name)"); do
policy=$(gcloud functions get-iam-policy $func --gen2 --format=json 2>/dev/null)
if echo "$policy" | grep -q "allUsers"; then
echo "UNAUTHENTICATED (gen2): $func"
fi
done
Remediate: require authentication
# Remove allUsers from 1st gen function
gcloud functions remove-iam-policy-binding FUNCTION_NAME \
--member="allUsers" \
--role="roles/cloudfunctions.invoker"
# For 2nd gen
gcloud functions remove-iam-policy-binding FUNCTION_NAME \
--gen2 \
--member="allUsers" \
--role="roles/cloudfunctions.invoker"
For legitimate cross-service invocations, use a dedicated service account with roles/cloudfunctions.invoker bound to only the calling service’s service account identity — not allUsers or allAuthenticatedUsers.
Default Compute Service Account and Project-Level Editor
Cloud Functions without an explicit --service-account flag run as the default compute service account: PROJECT_NUMBER-compute@developer.gserviceaccount.com. This account is automatically granted roles/editor at the project level when Compute Engine API is enabled, which covers nearly all GCP projects with any infrastructure.
roles/editor allows the function to:
- Read and write to any Cloud Storage bucket in the project
- Invoke any other Cloud Function, Cloud Run service, or App Engine app
- Create and delete resources (VMs, buckets, queues)
- Access Secret Manager secrets the account has access to
- Read all Pub/Sub messages
A compromised function running as the default compute service account compromises the entire project.
Enumerate functions running as the default compute service account:
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
DEFAULT_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# Check 1st gen functions
gcloud functions list --format=json | \
python3 -c "
import json, sys
funcs = json.load(sys.stdin)
for f in funcs:
sa = f.get('serviceAccountEmail', '')
if sa == '${DEFAULT_SA}' or sa == '':
print(f'DEFAULT SA: {f[\"name\"]}')
"
# Verify the default compute SA has editor role
gcloud projects get-iam-policy $(gcloud config get-value project) \
--format=json | \
python3 -c "
import json, sys
policy = json.load(sys.stdin)
for binding in policy.get('bindings', []):
if 'roles/editor' in binding['role']:
print(f'Role: {binding[\"role\"]}')
for member in binding.get('members', []):
print(f' {member}')
"
Remediate: dedicated least-privilege service accounts
# Create a dedicated service account for each function
gcloud iam service-accounts create my-function-sa \
--display-name="Cloud Function: my-function"
# Grant only what the function needs (example: read from one bucket)
gcloud storage buckets add-iam-policy-binding gs://my-data-bucket \
--member="serviceAccount:my-function-sa@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# Deploy function with the dedicated SA
gcloud functions deploy my-function \
--service-account=my-function-sa@PROJECT_ID.iam.gserviceaccount.com \
--runtime=python312 \
--trigger-http \
--no-allow-unauthenticated
# Remove editor from the default compute SA (do this carefully — audit impact first)
gcloud projects remove-iam-policy-binding PROJECT_ID \
--member="serviceAccount:${DEFAULT_SA}" \
--role="roles/editor"
Secrets in Environment Variables
The easiest way to get a secret into a Cloud Function is to paste it as an environment variable in the --set-env-vars flag or the GCP Console. It’s also one of the easiest ways to expose it.
Environment variables on Cloud Functions are stored in plaintext in the function configuration and are visible to:
- Anyone with
roles/cloudfunctions.developeror higher on the function - Anyone who can call
gcloud functions describe FUNCTION_NAME - Any code running inside the function (including code injected via dependency confusion or supply chain attacks)
- Cloud Logging if the function accidentally logs the environment
Find functions with secrets in environment variables:
# List environment variables for all functions (1st gen)
for func in $(gcloud functions list --format="value(name)"); do
echo "=== $func ==="
gcloud functions describe $func --format="value(environmentVariables)"
done
# Look for common secret patterns
for func in $(gcloud functions list --format="value(name)"); do
env_vars=$(gcloud functions describe $func --format=json | python3 -c "
import json, sys
cfg = json.load(sys.stdin)
env = cfg.get('environmentVariables', {})
for k, v in env.items():
print(f'{k}={v}')
")
if echo "$env_vars" | grep -iE "(key|secret|token|password|api|credential|auth)" > /dev/null; then
echo "POTENTIAL SECRET IN ENV VARS: $func"
echo "$env_vars" | grep -iE "(key|secret|token|password|api|credential|auth)"
fi
done
Remediate: use Secret Manager
# Store the secret
echo -n "my-api-key-value" | gcloud secrets create MY_API_KEY \
--data-file=- \
--replication-policy="automatic"
# Grant the function's service account access to the secret
gcloud secrets add-iam-policy-binding MY_API_KEY \
--member="serviceAccount:my-function-sa@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
# Reference secret in deployment (replaces env var with Secret Manager binding)
gcloud functions deploy my-function \
--service-account=my-function-sa@PROJECT_ID.iam.gserviceaccount.com \
--set-secrets="MY_API_KEY=MY_API_KEY:latest" \
--runtime=python312 \
--trigger-http \
--no-allow-unauthenticated
In the function code, the secret is then available as os.environ["MY_API_KEY"] but the underlying value is fetched from Secret Manager at invocation time rather than stored in the function config.
Privilege Escalation via Cloud Functions
An attacker with cloudfunctions.functions.create or cloudfunctions.functions.update permission can deploy or update a function to run arbitrary code as the function’s service account. If that service account has elevated permissions, this is a privilege escalation path.
The key IAM permissions for this vector:
cloudfunctions.functions.create # Deploy new functions
cloudfunctions.functions.update # Update existing functions (code + config)
cloudfunctions.functions.call # Invoke HTTP-triggered functions
iam.serviceAccounts.actAs # Required to bind a SA to a function
If an identity has these permissions without also having the target service account’s permissions checked, they can escalate to that service account.
Audit who can deploy or update functions:
# Find all identities with function create/update permissions
gcloud projects get-iam-policy PROJECT_ID --format=json | python3 -c "
import json, sys
policy = json.load(sys.stdin)
privileged_roles = [
'roles/cloudfunctions.developer',
'roles/cloudfunctions.admin',
'roles/editor',
'roles/owner'
]
for binding in policy.get('bindings', []):
if binding['role'] in privileged_roles:
print(f'Role: {binding[\"role\"]}')
for member in binding['members']:
print(f' {member}')
"
VPC Connector and Network Exposure
By default, Cloud Functions can access the internet (outbound) and cannot access VPC-internal resources. Adding a VPC connector grants access to internal resources. Misconfigured connector routing can expose the function to internal network segments it shouldn’t reach.
# Check VPC connector configuration on functions
gcloud functions list --format=json | python3 -c "
import json, sys
funcs = json.load(sys.stdin)
for f in funcs:
connector = f.get('vpcConnector', 'none')
egress = f.get('vpcConnectorEgressSettings', 'none')
print(f'{f[\"name\"]}: connector={connector}, egress={egress}')
"
ALL_TRAFFIC egress routes all function traffic through the VPC connector, giving the function full access to any VPC-internal resources the connector subnet can reach. PRIVATE_RANGES_ONLY limits the connector to RFC1918 traffic only. Review all functions with ALL_TRAFFIC egress settings.
Hardening Checklist
| Control | Command |
|---|---|
| No unauthenticated HTTP triggers in production | gcloud functions get-iam-policy FUNC — no allUsers |
| Dedicated service accounts (not default compute SA) | gcloud functions describe FUNC --format="value(serviceAccountEmail)" |
| No secrets in environment variables | gcloud functions describe FUNC --format="value(environmentVariables)" |
| Secret Manager bindings instead of env var secrets | Check --set-secrets flag in deployment config |
VPC egress PRIVATE_RANGES_ONLY where applicable | gcloud functions describe FUNC --format="value(vpcConnectorEgressSettings)" |
| Function source code stored in Artifact Registry, not inline | Review deployment source |
| Audit trail: Cloud Audit Logs enabled for Cloud Functions API | gcloud logging sinks list |
The patterns above — unauthenticated triggers, default service accounts with excessive permissions, secrets in env vars — appear in nearly every GCP security review that includes serverless workloads. None require sophisticated exploitation. They require only that the default settings are never changed.