修复 Firebase 部署 "Permission Denied"(403)

firebase deploy 报 403 或 "caller does not have permission"?沿着身份链——账号、项目、IAM 角色、CI 认证——逐环排查,快速搞定。

你在本地或 CI 上跑 firebase deploy,结果卡在下面这几种报错之一:

Error: HTTP Error: 403, The caller does not have permission
Error: Request to https://firebase.googleapis.com/... had HTTP Error: 403

或者更直白的:

Error: Failed to authenticate, have you run firebase login?

最快的修法: 九成情况是你登错了 Google 账号。跑 firebase login:list,确认真正拥有该项目的那个 email,用 firebase login:use you@company.com 切过去(或者在 deploy 命令上加 --account you@company.com),然后重新部署。如果账号没错还是 403,那就需要 admin 给你授 roles/firebasehosting.admin(部署 Functions 还要加 roles/cloudfunctions.admin + roles/iam.serviceAccountUser)。

问题几乎从来不在 firebase deploy 命令本身,而在它背后这串身份链:你的本地 Google 账号 -> 这个账号当前指向哪个项目 -> 该账号在 Google Cloud 上的 IAM 角色。任一环断了,就是 permission denied。按顺序往下逐环检查,几分钟就能找到断的那一环。

先对号入座

动手改之前,先把你看到的确切错误文本对到具体原因。

你看到的错误文本最可能的原因跳到
Failed to authenticate, have you run firebase login?没登录,或 token 过期Step 2
403 ... caller does not have permission(本地)账号错了,或缺 IAM 角色Step 2-3
只有 CI 报 403,本地正常service account 凭据失效,或还在用已废弃的 --tokenStep 5
Failed to get Firebase project <id> ... check permissions.firebaserc 里项目 ID 写错,或没访问权限Step 4
API ... has not been used ... or it is disabled必要的 API 没启用Step 6
principalType is not allowed by the organization policyorg policy 禁止外部 / 个人账号下方原因 6

常见原因

按命中率从高到低排。

1. 登的是个人账号,但项目在公司账号下

最高频的一种。你 firebase login 时随手用了 personal@gmail.com,但要部署的项目在 you@company.com 的 Google Cloud organization 下,个人账号在那儿根本没有任何权限。

如何判断: firebase login:list 会列出所有已授权账号并标出当前激活的那个。把这个 email 跟 Firebase console 里项目的拥有者对一下。

2. 账号没有 Firebase Hosting Admin 角色

账号能”看见”项目,但没有部署权限。这在公司新人身上很典型:admin 加了 Viewer,却忘了加部署角色。

如何判断: 打开 Google Cloud IAM,选中项目,找到你的 email,看角色列表。如果只有 Viewer 或 Browser,问题就在这。

3. .firebaserc 里项目 ID 写错

{
  "projects": {
    "default": "my-project-prod"
  }
}

如果真实项目 ID 是 my-project-prod-x9q,CLI 就会去部署一个不存在、或你没权限的项目——两种情况都表现为权限错或 “failed to get project”。项目 ID 是全局唯一的,Google 创建时常会自动追加一段随机后缀,所以好记的名字和真实 ID 往往对不上。

如何判断: firebase projects:list 会显示真实的 Project ID 一列,拿它对照 .firebaserc

4. CI 用了过期、被删或权限不足的凭据

CI 通过 GOOGLE_APPLICATION_CREDENTIALS(service account key)或老的 FIREBASE_TOKEN 认证。Key 被删、被 rotate、角色被撤都会失败;又或者你还在用 firebase login:ci,而它现在已经废弃(见 Step 5)。

如何判断: CI 日志报 401/403,但本地部署同一个项目一切正常。

5. 必要的 Google Cloud API 没启用

部署 Hosting 需要 firebasehosting.googleapis.com。部署 Functions 需要的就多了,因为现在 Cloud Functions 默认是 2nd-gen、跑在 Cloud Run 上——这意味着 run.googleapis.comeventarc.googleapis.compubsub.googleapis.comcloudbuild.googleapis.comartifactregistry.googleapis.com 都得开着。CLI 在交互模式下会提示你启用,但在 --non-interactive 的 CI 里没人去点确认。

如何判断: 错误里含 API ... has not been used in project ... before or it is disabled,或 Permission denied to enable service [artifactregistry.googleapis.com]

6. org policy 阻止个人账号

某些 Google Cloud organization 启用了 Domain Restricted Sharing 策略(“External members not allowed”)。gmail.com 账号即使被 invite,绑定也会被拒,所以永远无法部署。

如何判断: 错误里含 principalType is not allowed by the organization policy。这个只能由 admin 那边修:要么换用 org 域名内的账号,要么让 org admin 在 Organization Policies 里加例外。

最短修复路径

Step 1:自检身份链

# 1. 有哪些已授权账号,当前激活的是哪个
firebase login:list

# 2. 当前账号实际能访问哪些项目
firebase projects:list

# 3. 当前目录 .firebaserc 里的项目别名
cat .firebaserc

# 4. 当前的部署目标
firebase use   # 打印当前项目

从上到下交叉核对:激活的账号 -> 那个项目出现在 projects:list 里 -> .firebaserc 里的 ID 在这个列表里 -> firebase use 指向的是你真正想部署的那个。第一处对不上的地方,就是元凶。

Step 2:登对账号

如果你只有一个账号、只是它过期了:

firebase logout
firebase login        # 在浏览器里选正确的账号
firebase login:list   # 确认激活的 email

如果你要在多个 Google 账号间切换,别反复 logout / login。每个账号授权一次,然后按目录或按命令切:

firebase login:add                 # 给 CLI 添加第二个账号
firebase login:use you@company.com # 把"当前项目目录"的默认账号设成它

要给单条命令临时覆盖账号,用 --account(该账号必须已经 add 过):

firebase deploy --account you@company.com --project my-prod

Step 3:让 admin 授 IAM 角色

截至 2026 年 6 月,部署 Hosting + Functions 至少需要这几个预定义角色:

- Firebase Hosting Admin   (roles/firebasehosting.admin)   # 部署 Hosting
- Cloud Functions Admin    (roles/cloudfunctions.admin)    # 部署 Functions
- Service Account User     (roles/iam.serviceAccountUser)  # 部署 Functions 必需
- Firebase Admin           (roles/firebase.admin)          # 简单粗暴的全能角色

两个最容易被忽略的点:

  • Functions 需要这一对角色,不只是 cloudfunctions.admin。少了 roles/iam.serviceAccountUser,部署会失败,因为 CLI 没法”扮演”运行时 service account。只加 cloudfunctions.admin 是最常见的”我明明加了角色还是失败”的情形。
  • 自定义角色管不了 Firebase Hosting 的访问控制——Google 只认预定义的 roles/firebasehosting.admin / roles/firebasehosting.viewer,所以别费劲为 Hosting 去搭自定义角色。

admin 在 IAM -> 选中项目 -> Grant Access -> 填你的 email -> 加上述角色 -> Save 即可。IAM 改动一两分钟内生效;如果刚授完还是 403,等一会儿再试一次。

Step 4:修 .firebaserc

firebase use --add   # 交互式选项目,并给它起个别名

或者手动改文件,用 firebase projects:list 里的真实 ID:

{
  "projects": {
    "default": "my-project-prod-x9q",
    "staging": "my-project-staging-a3p"
  }
}

然后切到正确的环境再部署:

firebase use staging
firebase deploy --only hosting

Step 5:CI 用 service account 认证(别用个人 token)

绝不要在 CI 里用个人账号跑 firebase login。这里有两种正确做法。

方案 A(推荐):Workload Identity Federation——无密钥。 没有长期 key 文件,也就没有泄露和轮换的负担。配一个信任你 CI 提供方的 workload identity pool,把它绑到一个拥有 Step 3 角色的 service account 上,然后在 GitHub Actions 里:

permissions:
  contents: read
  id-token: write   # 无密钥 OIDC 必需
steps:
  - uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: projects/123/locations/global/workloadIdentityPools/gh/providers/gh
      service_account: deployer@my-project-prod.iam.gserviceaccount.com
  - run: firebase deploy --project my-project-prod --non-interactive

方案 B:service account key 文件。 配起来更简单,但你得自己保管一个长期 secret。

env:
  GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
steps:
  - run: |
      echo "$GOOGLE_APPLICATION_CREDENTIALS" > "$RUNNER_TEMP/sa.json"
      export GOOGLE_APPLICATION_CREDENTIALS="$RUNNER_TEMP/sa.json"
      firebase deploy --project my-project-prod --non-interactive

别再用 firebase login:ci / --token / FIREBASE_TOKEN 了——这种认证方式已经废弃,会在 firebase-tools 的某个未来大版本里移除,Google 官方文档现在也直接让你改用 service account。如果老流水线里还有它,迁到方案 A 或 B。

还有一个隐蔽的 CI 坑:firebase init hosting:github 会帮你配好一个 service account,但它只带 Hosting 权限。如果同一个 workflow 还要部署 Functions、Firestore rules 或 Storage rules,你得自己给这个 service account 补上 Step 3 里的额外角色,否则它会在那些目标上 403,而 Hosting 却部署成功。

Step 6:启用必要的 API

gcloud services enable \
  firebasehosting.googleapis.com \
  cloudfunctions.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
  run.googleapis.com \
  eventarc.googleapis.com \
  pubsub.googleapis.com \
  --project=my-project-prod

runeventarcpubsub 只在部署 Functions(2nd-gen)时才需要,但提前开着不花钱,还能省一次来回。你也可以去 API Library 里逐个打开。注意:启用 API 这个动作本身需要 roles/serviceusage.serviceUsageAdmin(或 Editor/Owner)——如果 gcloud services enable 报 403,缺的是这个权限,而不是部署角色。

怎么确认修好了

先跑一次 dry run,快速失败、不碰生产:

firebase deploy --only hosting --dry-run

dry run 干净通过,说明认证、项目映射、角色都对得上。然后再做真正的部署。如果你需要完整的请求 / 响应链路、想看清到底是哪个 API 调用返回了 403,加 --debug

firebase deploy --debug

输出里最后一行 403 会点名是哪个 API、哪个资源拒了你,直接对应到你缺的那个角色或 API。

预防建议

  • 把所需的 IAM 角色写进新人入职 wiki,让大家第一次就配齐。
  • 个人项目和工作项目用不同的 Google 账号,再用浏览器 profile 隔离,这样 firebase login 不会抓错账号。
  • .firebaserc 提交到 git,每个环境一个别名,让团队共享同一份事实来源。
  • CI 里优先用 Workload Identity Federation;非得用 key 的话,定期 rotate。
  • firebase use --add 和别名,别手动改 .firebaserc——更可重现。
  • 真正上生产前,先 firebase deploy --only hosting --dry-run

常见问题

为什么本地能跑,CI 上却 403? 身份不一样。本地是用你的 Google 账号部署,CI 是用 service account(或 OIDC 联合身份)部署。那个 service account 缺了 Step 3 里的某个角色,或者它的 key / 凭据过期了。该查的是 CI 的认证步骤和 service account 的 IAM 角色,不是你自己的账号。

我加了 roles/cloudfunctions.admin,Functions 还是部署不了,为什么? 部署 Functions 还需要运行时 service account 上的 roles/iam.serviceAccountUser,这样部署方才被允许”扮演”它。加上这个角色再试。这一对角色的要求几乎绊倒所有人。

怎样不 logout 就切换当前 Firebase 账号?firebase login:add 把第二个账号授权一次,然后用 firebase login:use <email> 给当前项目目录设默认账号,或者在单条命令上加 --account <email>

FIREBASE_TOKEN / firebase login:ci 现在还能用吗? 暂时还能用,但已经被官方废弃,计划在 firebase-tools 的某个未来大版本里移除。把 CI 迁到 service account(key 文件),或者更好——Workload Identity Federation。

我的项目名跟 .firebaserc 里的 ID 不一样,这就是 bug 吗? 很可能是。显示名不等于项目 ID。项目 ID 是全局唯一的,通常带一段随机后缀(my-app-prod-x9q)。跑 firebase projects:list,把确切的 ID 抄进 .firebaserc

错误里提到了 organization policy,我自己能修吗? 不能。principalType is not allowed by the organization policy 是 org 级别强制的。要么换用 org 域名内的账号,要么让 org admin 在 Organization Policies 里加例外。

相关阅读

标签: #后端 #排查 #排查 #Firebase