Understanding Firebase Functions Security
Firebase Functions are built on top of Google Cloud Functions, which means they inherit the full security capabilities of Google Cloud Platform. When we talk about securing Firebase Functions, two critical layers demand your attention: IAM (Identity and Access Management) Policies and Network Security controls. IAM governs who can invoke, deploy, or manage your functions, while network security governs how and from where traffic reaches them.
Many developers deploy Firebase Functions with default settings, leaving them exposed to the public internet and accessible to anyone with the URL. In production environments, this is a serious vulnerability. A properly secured function stack combines granular IAM bindings with network-level ingress and egress restrictions to create defense in depth.
What Are IAM Policies for Firebase Functions?
IAM policies in Google Cloud are declarative statements that bind an identity (user, service account, or group) to a role on a specific resource. For Firebase Functions, the resource is the function itself, and the relevant roles determine what actions that identity can perform.
The four core Cloud Functions IAM roles you need to know:
- Cloud Functions Admin (
roles/cloudfunctions.admin) — Full control: create, update, delete, and invoke functions. - Cloud Functions Developer (
roles/cloudfunctions.developer) — Create, update, delete functions but cannot invoke them or set IAM policies. - Cloud Functions Viewer (
roles/cloudfunctions.viewer) — Read-only access: list and view function metadata. - Cloud Functions Invoker (
roles/cloudfunctions.invoker) — Only invoke (call) an existing function, nothing else.
Additionally, the Service Account attached to a function determines what that function itself can do inside Google Cloud—access Firestore, call other APIs, read from Storage buckets, and so on. This is distinct from who can invoke the function.
Why IAM Policies Matter
Without explicit IAM controls, you risk:
- Unauthorized invocation — Anyone who discovers the HTTPS endpoint can call your function, potentially incurring costs and triggering side effects.
- Privilege escalation — A compromised function running with a broad default service account can access every resource in your project.
- Accidental deletion or modification — Team members with overly broad roles can break production deployments.
Locking down IAM ensures each identity has exactly the permissions it needs—no more, no less. This is the principle of least privilege.
How to Apply IAM Policies to Firebase Functions
You can grant the cloudfunctions.invoker role to specific users or service accounts, restricting who can trigger your function. Here is a complete walkthrough using the gcloud CLI.
Step 1: Deploy a Function with a Custom Service Account
First, create a dedicated service account for your function. This ensures the function itself operates with minimal permissions.
# Create a new service account
gcloud iam service-accounts create my-function-sa \
--display-name="Service Account for My Function" \
--project=your-project-id
# Grant only the permissions the function needs (e.g., Firestore read/write)
gcloud projects add-iam-policy-binding your-project-id \
--member="serviceAccount:my-function-sa@your-project-id.iam.gserviceaccount.com" \
--role="roles/datastore.user"
Now deploy a Firebase function using that service account:
// index.ts — a simple callable function
import * as functions from 'firebase-functions/v2';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const getDocument = functions.https.onCall(async (request) => {
const { docId } = request.data;
const snapshot = await admin.firestore().collection('items').doc(docId).get();
if (!snapshot.exists) {
throw new functions.https.HttpsError('not-found', 'Document not found');
}
return snapshot.data();
});
Deploy with the custom service account:
# Deploy using Firebase CLI, specifying the service account
firebase deploy --only functions \
--service-account="my-function-sa@your-project-id.iam.gserviceaccount.com"
Step 2: Restrict Who Can Invoke the Function
By default, HTTPS functions deployed with Firebase are publicly accessible. To lock it down, remove the allUsers grant and add specific invokers:
# First, find the fully qualified function resource name
FUNCTION_NAME="getDocument"
FUNCTION_RESOURCE="projects/your-project-id/locations/us-central1/functions/${FUNCTION_NAME}"
# Remove public access (allUsers)
gcloud functions remove-iam-policy-binding ${FUNCTION_NAME} \
--region=us-central1 \
--member="allUsers" \
--role="roles/cloudfunctions.invoker"
# Grant invoker role to a specific user
gcloud functions add-iam-policy-binding ${FUNCTION_NAME} \
--region=us-central1 \
--member="user:developer@example.com" \
--role="roles/cloudfunctions.invoker"
# Grant invoker role to a specific service account (for machine-to-machine calls)
gcloud functions add-iam-policy-binding ${FUNCTION_NAME} \
--region=us-central1 \
--member="serviceAccount:client-app-sa@your-project-id.iam.gserviceaccount.com" \
--role="roles/cloudfunctions.invoker"
Step 3: Invoke the Function Authenticated from a Client
Once public access is removed, callers must present a valid credential. From a Firebase app, you can use the authenticated user token. From a backend service, you use a service account key or Application Default Credentials:
// Node.js client invoking the function with a service account
import { GoogleAuth } from 'google-auth-library';
import axios from 'axios';
async function callFunction() {
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform']
});
const client = await auth.getClient();
const token = await client.getAccessToken();
const response = await axios.post(
'https://us-central1-your-project-id.cloudfunctions.net/getDocument',
{ docId: 'item-123' },
{
headers: {
Authorization: `Bearer ${token.token}`,
'Content-Type': 'application/json'
}
}
);
console.log(response.data);
}
callFunction().catch(console.error);
Network Security for Firebase Functions
IAM controls who can call your function. Network security controls where traffic originates and what your function can reach. This is especially important for functions that handle sensitive data or sit behind internal microservices.
Ingress Controls: Restricting Inbound Traffic
Cloud Functions V2 (which Firebase Functions v2 uses under the hood) supports two ingress settings:
- Allow all traffic — Accepts requests from the public internet and from Google Cloud internal networks.
- Allow internal traffic only — Only requests from other Google Cloud resources within the same project or VPC are accepted. Public internet calls are blocked entirely.
Setting a function to internal-only is a powerful way to ensure it's only callable from other Cloud services you control, such as Cloud Run, App Engine, or another Cloud Function.
// Deploying a function with internal-only ingress
// firebase.json configuration snippet for v2 functions
{
"functions": {
"source": ".",
"ingressSettings": {
"getDocument": "ALLOW_INTERNAL_ONLY"
}
}
}
You can also set this at deploy time via the Firebase CLI or by using gcloud directly:
# Deploy with internal-only ingress using gcloud
gcloud functions deploy getDocument \
--runtime=nodejs20 \
--trigger-http \
--ingress-settings=internal-only \
--service-account="my-function-sa@your-project-id.iam.gserviceaccount.com" \
--project=your-project-id
When ingress is internal-only, calling the function from your laptop or an external server will fail with a 403. Internal callers must still satisfy IAM requirements.
Egress Controls: VPC Connector and Outbound Traffic
By default, a Cloud Function's outbound traffic uses the public internet. If your function needs to reach private resources—like a Cloud SQL instance with a private IP, an internal API on Compute Engine, or a third-party service that requires a static IP—you need a VPC Connector.
A VPC Connector routes your function's egress traffic through your Virtual Private Cloud, where you can apply firewall rules, NAT configurations, and private DNS.
# Step 1: Create a VPC network and subnet (if you don't have one)
gcloud compute networks create my-private-network \
--subnet-mode=custom \
--project=your-project-id
gcloud compute networks subnets create my-subnet \
--network=my-private-network \
--region=us-central1 \
--range=10.0.0.0/24 \
--project=your-project-id
# Step 2: Create a Serverless VPC Connector
gcloud compute networks vpc-access connectors create my-connector \
--network=my-private-network \
--region=us-central1 \
--range=10.8.0.0/28 \
--project=your-project-id
# Step 3: Deploy the function with the connector, routing all egress through VPC
gcloud functions deploy getDocument \
--runtime=nodejs20 \
--trigger-http \
--vpc-connector=projects/your-project-id/locations/us-central1/connectors/my-connector \
--egress-settings=all-traffic \
--ingress-settings=internal-only \
--service-account="my-function-sa@your-project-id.iam.gserviceaccount.com"
The --egress-settings flag accepts two values:
private-ranges-only— Only traffic destined for RFC 1918 private IPs or your VPC ranges goes through the connector. Public internet traffic exits directly.all-traffic— All outbound traffic goes through the VPC connector. This is necessary when you need a static outbound IP for allowlisting with external services.
Combining IAM and Network Security for Defense in Depth
The most secure posture layers both controls. Consider a function that processes payment data:
- IAM: Only a specific backend service account can invoke it.
- Ingress: Internal-only, blocking any request from the public internet.
- Egress: Routes through a VPC connector with strict firewall rules, allowing only connections to an internal payment processor and a Cloud SQL private IP.
- Service Account: The function's own identity has exactly one role—access to the specific Firestore collection it needs.
Here is how you would deploy such a function programmatically using the Cloud Functions API with full security parameters:
// Using @google-cloud/functions framework to deploy with full security
import { FunctionsClient } from '@google-cloud/functions';
import { protos } from '@google-cloud/functions';
const client = new FunctionsClient();
const projectPath = `projects/your-project-id/locations/us-central1`;
async function deploySecureFunction() {
const functionConfig: protos.google.cloud.functions.v2.ICreateFunctionRequest = {
parent: projectPath,
functionId: 'processPayment',
function: {
name: `${projectPath}/functions/processPayment`,
buildConfig: {
runtime: 'nodejs20',
entryPoint: 'processPayment',
source: {
storageSource: {
bucket: 'your-deployment-bucket',
object: 'function-source.zip'
}
},
serviceAccount: 'projects/your-project-id/serviceAccounts/payment-fn-sa@your-project-id.iam.gserviceaccount.com'
},
serviceConfig: {
ingressSettings: 'INTERNAL_ONLY',
vpcConnector: 'projects/your-project-id/locations/us-central1/connectors/my-connector',
egressSettings: 'ALL_TRAFFIC',
// Only the backend service account can invoke
securityLevel: 'SECURE_ALWAYS',
// Environment variables for internal endpoints
environmentVariables: {
PAYMENT_API: 'http://10.0.0.50:8080',
DB_HOST: 'cloud-sql-private-ip'
}
}
}
};
const [operation] = await client.createFunction(functionConfig);
console.log('Deployment started:', operation.name);
// Wait for deployment to complete
const [deployedFunction] = await operation.promise();
console.log('Function deployed:', deployedFunction.name);
}
deploySecureFunction().catch(console.error);
Best Practices for Firebase Functions Security
Here is a distilled checklist of security practices you should adopt for every production Firebase Functions deployment:
- Apply least privilege to service accounts. Create one service account per function or per logical group of functions. Never use the default App Engine or Compute Engine service accounts.
- Remove public invoker grants on sensitive functions. After deployment, immediately revoke
allUsersand grantcloudfunctions.invokeronly to specific identities. - Use internal-only ingress for backend functions. Any function that isn't called directly from a browser or mobile app should be shielded from the public internet.
- Route egress through a VPC connector when accessing private resources. Avoid exposing private IPs to the open internet. Use
all-trafficegress when you need a predictable outbound IP for allowlists. - Audit IAM bindings regularly. Use
gcloud functions get-iam-policyon critical functions and store the output in version control for change tracking. - Combine Firebase Authentication with function-level IAM. For callable functions, validate the user's identity inside the function logic, but also restrict invocation at the cloud level for an extra security layer.
- Monitor invocation logs. Set up log-based metrics and alerts in Cloud Monitoring to detect unusual invocation patterns, such as spikes from unexpected IP ranges.
- Rotate service account keys and avoid embedding them in code. Use Application Default Credentials and workload identity wherever possible instead of static key files.
Auditing and Monitoring Your Security Posture
Regularly check your function's IAM bindings. The following script lists all functions in a region and their current invoker grants:
#!/bin/bash
# audit-function-iam.sh — list invoker bindings for all functions in a region
PROJECT="your-project-id"
REGION="us-central1"
echo "Auditing IAM bindings for Cloud Functions in ${REGION}..."
for FUNC in $(gcloud functions list --project=${PROJECT} --format="value(name)" --filter="REGION:${REGION}"); do
echo "---"
echo "Function: ${FUNC}"
gcloud functions get-iam-policy ${FUNC} --project=${PROJECT} --region=${REGION} \
--format="table[box](bindings.members, bindings.role)"
done
For network security auditing, verify your function's ingress and egress settings with:
# Check ingress and egress settings for a specific function
gcloud functions describe getDocument \
--project=your-project-id \
--region=us-central1 \
--format="json(serviceConfig.ingressSettings, serviceConfig.egressSettings, serviceConfig.vpcConnector)"
Conclusion
Securing Firebase Functions demands a deliberate combination of IAM policies and network controls. IAM defines the perimeter around who can interact with your functions, while network ingress and egress settings define how traffic flows. Together, they form a robust defense-in-depth architecture that protects your serverless workloads from unauthorized access, data exfiltration, and lateral movement within your cloud environment. Start by auditing your current deployments, remove public access where it isn't needed, assign minimal service account permissions, and route sensitive traffic through your VPC. These steps transform your Firebase Functions from publicly exposed endpoints into tightly controlled, production-grade cloud resources.