Cloud Security Wire
AWS Azure GCP RSS
GCP Misconfiguration high

GCP Cloud Functions Security: Unauthenticated HTTP Triggers and Service Account Privilege Escalation

Google Cloud Functions is one of the most common sources of cloud security misconfigurations: HTTP trigger functions deployed without authentication, overprivileged service accounts attached at the project level, and secrets exposed in environment variables. This guide covers each risk with gcloud CLI detection and remediation.

By Cloud Security Wire · ·
#GCP#Cloud Functions#serverless#misconfiguration#IAM#service-accounts#privilege-escalation#unauthenticated-access#google-cloud#secret-manager#CSPM
High Severity

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.developer or 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

ControlCommand
No unauthenticated HTTP triggers in productiongcloud 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 variablesgcloud functions describe FUNC --format="value(environmentVariables)"
Secret Manager bindings instead of env var secretsCheck --set-secrets flag in deployment config
VPC egress PRIVATE_RANGES_ONLY where applicablegcloud functions describe FUNC --format="value(vpcConnectorEgressSettings)"
Function source code stored in Artifact Registry, not inlineReview deployment source
Audit trail: Cloud Audit Logs enabled for Cloud Functions APIgcloud 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.

← All Analysis Subscribe via RSS