Skip to content

Using Firestore Admin API via Cloud Run Service does not bypass security rules #2467

@lewisc-triptease

Description

@lewisc-triptease

Environment details

  • OS: Linux / Alpine
  • Node.js version: 22.0.0
  • npm version:
  • @google-cloud/firestore version:

Steps to reproduce

Firebase Admin SDK Not Bypassing Security Rules on Cloud Run

Issue Summary

Firebase Admin SDK with ServiceAccountCredential successfully bypasses Firestore security rules when running locally, but fails with PERMISSION_DENIED when running on Google Cloud Run with identical code and credentials.

Environment

  • Local (Working): Bun 1.2 on macOS
  • Cloud Run (Failing): Node.js 22 on Alpine Linux (also tested with Bun)
  • Firebase Admin SDK: v13.6.0
  • Firestore Database: Named database integrations-hub
  • Security Rules: allow read, write: if false;

Minimal Reproducible Code

firestore-init.ts

import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

export async function initializeFirestore() {
  const serviceAccountKey = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
  const serviceAccount = JSON.parse(serviceAccountKey);
  
  const app = initializeApp({
    credential: cert(serviceAccount),
    projectId: 'my-project'
  });
  
  const db = getFirestore(app, 'integrations-hub');
  
  // Verify credential type
  const credential = (app as any).options?.credential;
  console.log('Credential Type:', credential?.constructor?.name); // ServiceAccountCredential
  console.log('Client Email:', serviceAccount.client_email); // firebase-adminsdk-fbsvc@...
  
  // Verify access token generation
  const token = await credential.getAccessToken();
  console.log('Access Token Generated:', !!token.access_token); // true
  
  return db;
}

export async function testRead() {
  const db = await initializeFirestore();
  
  // This should bypass security rules according to Firebase docs
  const snapshot = await db.collection('integrations').get();
  console.log('Documents:', snapshot.size);
}

Dockerfile (Cloud Run)

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

CMD ["node", "build/index.js"]

Cloud Run Environment

env:
  - name: FIREBASE_SERVICE_ACCOUNT_KEY
    valueFrom:
      secretKeyRef:
        name: integrations-hub-firebase-key
        key: latest

Service Account Details

{
  "type": "service_account",
  "project_id": "my-project",
  "client_email": "firebase-adminsdk-fbsvc@my-project.iam.gserviceaccount.com"
}

IAM Roles:

  • roles/datastore.owner
  • roles/firebase.sdkAdminServiceAgent
  • roles/iam.serviceAccountTokenCreator

Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Actual Behavior

Local Execution (✅ Works)

Credential Type: ServiceAccountCredential
Access Token Generated: true
Documents: 180  <-- SUCCESS

Cloud Run Execution (❌ Fails)

Credential Type: ServiceAccountCredential
Client Email: firebase-adminsdk-fbsvc@my-project.iam.gserviceaccount.com
Access Token Generated: true
Access Token Length: 1024
Token Expires In: 3597s
hasDbCredential: true
hasSettings: true

Error: 7 PERMISSION_DENIED: Missing or insufficient permissions.
    at callErrorFromStatus (/app/node_modules/google-gax/node_modules/@grpc/grpc-js/build/src/call.js:32:23)
    at onReceiveStatus (/app/node_modules/google-gax/node_modules/@grpc/grpc-js/build/src/client.js:359:53)

Evidence

Cloud Run Logs (timestamp: 2026-01-09 13:44:04):

{
  "message": "Successfully generated access token",
  "hasAccessToken": true,
  "tokenLength": 1024,
  "expiresIn": 3597
}
{
  "message": "Firestore DB object inspection",
  "hasDbCredential": true,
  "dbClientType": "Firestore",
  "projectIdFromDb": "my-project",
  "databaseIdFromDb": "integrations-hub"
}
{
  "message": "Fetching integrations from Firestore",
  "credentialType": "ServiceAccountCredential",
  "hasCredential": true,
  "dbClientAuth": {
    "hasSettings": true,
    "hasCredentials": true,
    "projectId": "my-project",
    "clientEmail": "firebase-adminsdk-fbsvc@my-project.iam.gserviceaccount.com"
  }
}
// Immediately followed by PERMISSION_DENIED

Expected Behavior

According to Firebase documentation:

"The server client libraries automatically bypass Cloud Firestore Security Rules, and access data using privileged Admin SDK privileges"

The Admin SDK should bypass security rules on Cloud Run just as it does locally.

Workaround Tested

  • ✅ Opening security rules to if true → Cloud Run works
  • ✅ Using the exact same service account key locally → works with if false
  • ❌ Both Node.js and Bun runtimes on Cloud Run → still fails
  • ❌ Using @google-cloud/firestore directly instead of firebase-admin/firestore → still fails
  • ✅ Verified IAM permissions are correct

Questions

  1. Why does the Admin SDK bypass rules locally but not on Cloud Run?
  2. Is there a Cloud Run-specific configuration needed for Admin SDK privilege escalation?
  3. Is this related to the named database (integrations-hub) vs default database?

Additional Context

  • The (default) database exhibits the same behavior
  • Previously deleted and recreated the (default) database (may have corrupted Firebase project config?)
  • Cloud Run service account has all necessary IAM roles
  • Network: No VPC, HTTP/2 disabled, no service mesh

Making sure to follow these steps will guarantee the quickest resolution possible.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    api: firestoreIssues related to the googleapis/nodejs-firestore API.priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions