Chapter 14 · IAM, Admin/Server SDKs, Service Accounts, Authentication, and Trust Boundaries

Admin / Server SDKs Bypass Security Rules: Service Account Least Privilege and Application-Layer Authorization

Contain the power of Admin/server SDKs with least-privilege workload identities and explicit AtlasMart application authorization instead of relying on client Security Rules.

Intermediate → Advanced160–190 minutesAdmin bypass · least privilege · app authzFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0 · Rules tests 5.0.2Last reviewed: September 2026

1. The dangerous misconception: “Admin means trusted, therefore safe”

Execution and safety note

Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.

Admin/server libraries are trusted by Firestore in the narrow sense that Security Rules are not evaluated. That is not evidence that every caller of AtlasMart’s backend deserves every database action. If the orders API can update any order and the API simply forwards req.body.path and req.body.data, a single application authorization bug converts the service account’s IAM scope into an attacker’s write scope.

Chapter 14 reproducibility baseline · reviewed 16 September 2026

AtlasMart continues the same mandatory environment used in Chapters 01–13: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database, Firestore emulator 127.0.0.1:8080, Authentication emulator 127.0.0.1:9099, Emulator UI 127.0.0.1:4000, Firebase CLI 15.30.0, Firebase JavaScript SDK 12.19.0, Firebase Admin Node.js SDK 14.4.0 carrying @google-cloud/firestore 9.1.0, @firebase/rules-unit-testing 5.0.2, and Node.js 22+. Mandatory work remains local/no-cost. Standard Native Core is the canonical lab; Enterprise Native Core/Pipeline and MongoDB compatibility differences are called out explicitly.

Trust boundary that must not be blurred

A Firebase Authentication ID token represents an end user. A Google Cloud access token or attached service account represents a workload. Firestore mobile/web SDK calls are authorized by Security Rules using request.auth; Admin/server libraries bypass those Rules and are authorized with IAM. If a backend receives a user ID token, verifying that token establishes user identity only—the backend must still make its own application authorization decision before using its more powerful workload identity. App Check, when enabled, is an additional app/device attestation signal and does not replace either user authorization or IAM.

Learning outcomes

01

Explain precisely why Admin/server Firestore calls bypass Security Rules and where IAM takes over.

02

Choose a workload identity and predefined Firestore IAM role from required operations instead of granting Owner/Editor by convenience.

03

Implement a privileged AtlasMart refund operation that verifies end-user identity and applies tenant/role/invariant checks before the Admin write.

04

Keep the local emulator proof distinct from production IAM proof.

05

Model blast radius when one service identity is reused across unrelated services.

2. Least privilege has two dimensions

Cloud least privilege limits what the workload principal can do to Google Cloud resources. Application least privilege limits what a verified user may ask that workload to do. Firestore IAM roles operate at resource/API permission scope; they do not encode AtlasMart’s customerId, tenant refund policy, order state machine, or fraud review.

Control Question Example
IAM May this workload read/write Firestore at all? Orders API gets a Firestore data role; reporting worker may be read-only
Application authorization May this actor perform this business action on this resource? Support role + same tenant + PAID order + amount within policy
Security Rules May a mobile/web client perform this direct Firestore request? Customer can read own order, cannot set REFUNDED
Network/runtime policy Where can workload credentials be used from? Attached identity/WIF, egress controls, service boundary
Audit Can we reconstruct who requested and which workload executed? Correlation ID + actor UID + workload/service + resource + outcome

3. Build the privileged refund endpoint as a narrow capability

backend/admin.js · emulator-safe initialization
// Never ship this module to a browser bundle.process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";process.env.FIREBASE_AUTH_EMULATOR_HOST = "127.0.0.1:9099";process.env.GCLOUD_PROJECT = "demo-atlasmart-firestore";import { initializeApp } from "firebase-admin/app";import { getAuth } from "firebase-admin/auth";import { getFirestore } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });export const adminAuth = getAuth();export const adminDb = getFirestore();
backend/authorize.js · identity then application authorization
export async function verifiedActor(adminAuth, authorizationHeader) {  const prefix = "Bearer ";  if (!authorizationHeader?.startsWith(prefix)) {    throw Object.assign(new Error("UNAUTHENTICATED"), { status: 401 });  }  const idToken = authorizationHeader.slice(prefix.length);  const decoded = await adminAuth.verifyIdToken(idToken);  return {    uid: decoded.uid,    tenantId: decoded.tenantId,    role: decoded.role,    authTime: decoded.auth_time  };}export function authorizeRefund(actor, order) {  if (actor.tenantId !== order.tenantId) {    throw Object.assign(new Error("CROSS_TENANT"), { status: 403 });  }  if (!["support", "admin"].includes(actor.role)) {    throw Object.assign(new Error("ROLE_REQUIRED"), { status: 403 });  }  if (order.status !== "PAID") {    throw Object.assign(new Error("ORDER_NOT_REFUNDABLE"), { status: 409 });  }}
backend/refund.js
import { adminAuth, adminDb } from "./admin.js";import { verifiedActor, authorizeRefund } from "./authorize.js";import { auditAuthorization } from "./audit.js";export async function refundOrder({ authorization, orderId, correlationId }) {  const actor = await verifiedActor(adminAuth, authorization);  const ref = adminDb.doc(`orders/${orderId}`);  const snap = await ref.get();  if (!snap.exists) throw Object.assign(new Error("NOT_FOUND"), { status: 404 });  const order = snap.data();  try {    authorizeRefund(actor, order);    auditAuthorization({      correlationId, actor, action: "order.refund",      resource: ref.path, outcome: "allow", reason: "policy_passed"    });  } catch (error) {    auditAuthorization({      correlationId, actor, action: "order.refund",      resource: ref.path, outcome: "deny", reason: error.message    });    throw error;  }  // Chapter 9/10 invariants still apply: use transaction/precondition/idempotency  // if concurrent state or external payment side effects are involved.  await ref.update({ status: "REFUNDED" });  return { orderId, status: "REFUNDED" };}
backend/audit.js · log decisions, never credentials
export function auditAuthorization({ correlationId, actor, action, resource, outcome, reason }) {  console.log(JSON.stringify({    event: "authorization_decision",    correlationId,    actorUid: actor?.uid ?? null,    actorTenantId: actor?.tenantId ?? null,    actorRole: actor?.role ?? null,    action,    resource,    outcome,    reason,    // Deliberately omitted: ID token, refresh token, Authorization header,    // service-account private keys, ADC credential material.    at: new Date().toISOString()  }));}

This endpoint exposes one business operation rather than a generic Firestore proxy. The service account may technically write many documents, but the code narrows what users can ask it to do. That separation also makes authorization tests readable.

4. Deliberate failure: a generic Admin proxy defeats your Rules design

do-not-build-this.js
// DANGEROUS: verified identity is not enough.export async function updateAnything(req) {  const actor = await verifiedActor(adminAuth, req.headers.authorization);  console.log("authenticated", actor.uid);  await adminDb.doc(req.body.path).update(req.body.patch);}

An ordinary customer could request orders/o-9001 with {"status":"REFUNDED"}. Security Rules cannot save this code because the Admin write bypasses them. The safe repair is to expose purpose-specific commands such as refundOrder, derive target paths from validated identifiers, and authorize against authoritative server data.

5. IAM roles are capabilities, not business roles

IAM choice Appropriate use Why not more?
roles/datastore.viewer Read-only Firestore workloads No data mutation permission
roles/datastore.user Application workloads that need normal entity/document read/write Avoid Owner/Editor; still broader than per-document business policy
Index/admin-specific roles Deployment/operations jobs that manage indexes or administration Do not give runtime APIs schema/admin permissions they never use
Owner/Editor Rare administration/bootstrap contexts Primitive roles create a much larger blast radius and are poor runtime defaults
optional isolated-cloud IAM sketch · not part of mandatory lab
PROJECT_ID="YOUR_ISOLATED_PROJECT"SA="atlasmart-orders-api@${PROJECT_ID}.iam.gserviceaccount.com"# Create one workload identity for one service responsibility.gcloud iam service-accounts create atlasmart-orders-api   --project="$PROJECT_ID"   --display-name="AtlasMart orders API"# roles/datastore.user permits Firestore entity read/write operations.# It is still broader than per-customer authorization; application code must enforce that.gcloud projects add-iam-policy-binding "$PROJECT_ID"   --member="serviceAccount:${SA}"   --role="roles/datastore.user"# Inspect exactly what the principal has before deployment.gcloud projects get-iam-policy "$PROJECT_ID"   --flatten="bindings[].members"   --filter="bindings.members:serviceAccount:${SA}"   --format="table(bindings.role)"
Cloud exercise is optional

The mandatory emulator lab cannot verify IAM because the Firestore emulator does not enforce Google Cloud IAM. Run IAM commands only in an isolated billed project you control. Record the workload principal, role binding, database, region and cleanup commands; do not present emulator success as IAM evidence.

6. One workload identity per responsibility limits blast radius

Suppose AtlasMart deploys an orders API, a reporting job and an index-deployment job. Reusing one highly privileged service account means compromise of the reporting job can mutate orders and manage indexes. Separating identities permits a read-only reporting principal and keeps operational roles away from request-serving workloads. The design objective is not “many service accounts”; it is independent revocation and minimum permissions aligned with service responsibilities.

Service User-facing? Suggested capability shape Audit value
orders-api Yes Data read/write + strict app authz Correlate actor UID with workload principal
reporting-worker No Read-only if possible Unexpected writes become impossible by IAM
index-deployer No Index administration only Schema change attributable to CI/deployer identity
local developer No User ADC or impersonation; emulator when possible Avoid persistent downloaded runtime keys

7. Observability: preserve two actors

A backend log should preserve the end-user actor and the workload executor as separate concepts. Cloud Audit Logs can identify the Google Cloud principal for supported production actions; AtlasMart application logs must add the verified user UID/tenant/role and the business reason. Never put raw credentials in either.

structured authorization record
{  "event": "authorization_decision",  "correlationId": "corr-7f8c",  "actorUid": "u-support-1",  "actorTenantId": "t-atlas",  "actorRole": "support",  "workload": "atlasmart-orders-api",  "action": "order.refund",  "resource": "orders/o-9001",  "outcome": "allow",  "reason": "policy_passed"}

8. Verification matrix

Case Expected app decision Expected Rules relevance Expected production IAM relevance
Customer directly updates status Denied Yes: Rules deny None; no backend call
Customer calls refund endpoint Denied ROLE_REQUIRED No Rules on Admin write because write never occurs Workload IAM is irrelevant to the user denial
Support from wrong tenant Denied CROSS_TENANT No IAM could still allow workload; app must deny
Authorized support same tenant Allowed if order invariant passes No on backend write Workload needs data write permission
Workload role removed App policy may allow, Firestore call fails No Yes: IAM denies

Production judgment and bridge to Lesson 3

IAM limits what a service can technically do; application authorization limits what a caller can cause it to do. Neither replaces the other. Keep service identities narrow, avoid primitive roles, and design privileged APIs as business capabilities rather than generic database tunnels. Lesson 3 now examines the user-side role signal—custom claims—and why token refresh and revocation make them unsuitable as an instant, rapidly changing authorization database.

Knowledge check

  1. Why can a customer exploit a generic Admin proxy even if Firestore Rules are perfect?
  2. What is the difference between roles/datastore.user and an AtlasMart support role?
  3. Why is one service account for every backend a blast-radius problem?
  4. Can the emulator prove a production IAM binding?
  5. What should privileged APIs expose instead of arbitrary document paths?
Review the answers

1. Because server/Admin Firestore requests bypass Security Rules; the proxy must enforce application authorization itself.

2. The IAM role gives the workload Firestore API capability; the support role is an application policy attribute for an end user.

3. Compromise of any one service inherits all permissions granted to that shared identity.

4. No. Emulator tests can prove application logic and Rules behavior, not Google Cloud IAM enforcement.

5. Purpose-specific business operations with validated identifiers, authoritative reads and explicit authorization checks.

Summary

The Admin SDK is powerful because it bypasses client Rules. That power is safe only when the workload principal is least-privileged and every user-driven privileged operation has an independent application authorization decision.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.