When you deploy a Cloud Function, it runs using a service account, and by default, that service account has broad permissions.

Let’s see a Cloud Function in action, and then we’ll break down how to lock down its permissions.

Imagine you have a Cloud Function that reads from a Cloud Storage bucket and writes to a Firestore database. Here’s the main.py for that function:

import functions_framework
from google.cloud import storage
from google.cloud import firestore

@functions_framework.http
def process_data(request):
    """HTTP Cloud Function to process data from GCS and write to Firestore."""
    bucket_name = "my-input-bucket-12345"
    blob_name = "data/input.txt"
    collection_name = "processed_data"
    document_id = "output_record"

    storage_client = storage.Client()
    firestore_client = firestore.Client()

    try:
        bucket = storage_client.get_bucket(bucket_name)
        blob = bucket.blob(blob_name)
        data = blob.download_as_text()

        # Simulate some processing
        processed_content = f"Processed: {data.upper()}"

        doc_ref = firestore_client.collection(collection_name).document(document_id)
        doc_ref.set({"content": processed_content})

        return f"Successfully processed {blob_name} and wrote to Firestore.", 200
    except Exception as e:
        return f"An error occurred: {str(e)}", 500

And here’s the requirements.txt:

functions-framework
google-cloud-storage
google-cloud-firestore

When you deploy this function using gcloud functions deploy process_data --runtime python311 --trigger-http --entry-point process_data, it will be assigned the default service account, which is PROJECT_ID@appspot.gserviceaccount.com. This default service account is a member of the Editor role, meaning it has permissions to create, modify, and delete most resources in your project. This is convenient for development but a major security risk in production.

The problem this solves is the principle of least privilege: granting only the necessary permissions for a service to perform its function. For our Cloud Function, this means it should only have permissions to read from my-input-bucket-12345 and write to the processed_data collection in Firestore. It should not have permission to, say, create new Cloud Functions, delete other buckets, or manage IAM policies.

To achieve this, we first need to create a new, dedicated service account for our function. We’ll give it a descriptive name like my-function-processor-sa.

gcloud iam service-accounts create my-function-processor-sa \
  --display-name "Service account for data processing function"

This command creates the service account. Now, we need to grant it the specific roles it requires. For reading from a Cloud Storage bucket, the roles/storage.objectViewer role is sufficient. For writing to Firestore, the roles/firestore.dataWriter role is appropriate.

# Grant read access to the specific bucket
gcloud storage buckets add-iam-policy-binding gs://my-input-bucket-12345 \
  --member="serviceAccount:my-function-processor-sa@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

# Grant write access to the Firestore collection (implicitly grants access to the whole database)
gcloud firestore databases add-iam-policy-binding \
  --member="serviceAccount:my-function-processor-sa@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/firestore.dataWriter"

Important Note: Firestore IAM roles are applied at the database level, not per collection. So, roles/firestore.dataWriter grants write access to all collections within the Firestore database. For finer-grained control on Firestore, you would typically implement security rules within Firestore itself, which are evaluated at runtime. The IAM role here ensures the service account can interact with Firestore.

Finally, when deploying the Cloud Function, we specify our newly created service account using the --service-account flag:

gcloud functions deploy process_data \
  --runtime python311 \
  --trigger-http \
  --entry-point process_data \
  --service-account="my-function-processor-sa@PROJECT_ID.iam.gserviceaccount.com" \
  --region us-central1 # Replace with your desired region

Now, the my-function-processor-sa service account has only the permissions necessary to read from my-input-bucket-12345 and write to Firestore. If this function were to be compromised, the attacker would have significantly limited ability to cause damage across your project.

The most surprising thing about service account permissions is that while IAM roles grant broad capabilities, the actual runtime permissions are a combination of IAM and any resource-specific access controls, like Firestore Security Rules or Cloud Storage ACLs, which can further restrict what a service account can do even if its IAM role allows it.

The next concept you’ll likely encounter is managing secrets for your Cloud Functions, such as API keys or database credentials, which also requires careful consideration of least privilege.

Want structured learning?

Take the full Cloud-functions course →