firebase / firebase/firebase-tools
Web frameworks: hosting:channel:deploy creates broken preview channels when the SSR function lacks public invoker IAM
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.3k
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 84
Description
### [REQUIRED] Environment info
**firebase-tools:** 15.19.1
**Platform:** macOS
### [REQUIRED] Test case
A minimal Next.js app deployed through the Hosting web frameworks integration
(`"source"` + `"frameworksBackend"` in `firebase.json`), where the generated
SSR function's Cloud Run service is missing the public invoker IAM binding
(`allUsers` with `roles/run.invoker`).
Note: the app needs at least one page using `next/image` (or some other
reason for a backend, like a dynamic route). A fully static app never gets an
SSR function created, so there is nothing to reproduce against. Image
optimization is the easiest trigger and also what surfaces the failure on
otherwise prerendered pages.
We hit this state in production: the IAM policy on our `ssr` service
was completely empty (just an etag). We don't know exactly how it got that
way. The repro below sidesteps that question by
removing the binding manually, which puts the service in the same state.
```json
// firebase.json
{
"hosting": {
"source": ".",
"frameworksBackend": { "region": "us-central1" }
}
}
```
### [REQUIRED] Steps to reproduce
1. `npx create-next-app@latest repro` and add a page that renders a
statically imported image with `next/image`. This is required, otherwise
no SSR function is created and the deploy is fully static.
2. `firebase init hosting` (web frameworks), then deploy live with
`firebase deploy --only hosting`. Everything works.
3. Put the service in the broken state:
```bash
gcloud run services remove-iam-policy-binding ssr \
--member=allUsers --role=roles/run.invoker --region us-central1
```
4. Deploy a preview channel:
```bash
firebase hosting:channel:deploy preview --only
```
The command succeeds and prints the channel URL.
5. Open the channel URL and load a page with an image.
### [REQUIRED] Expected behavior
The channel deploy should check that the rewrite target is publicly
invokable, and either re-assert the invoker binding (live frameworks deploys
do this when they redeploy the function) or fail loudly. Even a one-line
warning ("the ssr function is not publicly invokable, dynamic routes on this
channel will return 403") would save a lot of debugging time.
### [REQUIRED] Actual behavior
The channel deploy reports success, but the channel is broken for all
dynamic content:
- Prerendered pages load fine, since they come from the Hosting CDN.
- Anything that has to invoke the SSR function, including `/_next/image`,
returns Google's generic 403 page ("Error: Forbidden. Your client does not
have permission to get URL ...") with nothing in the function logs. The
request is rejected by IAM before the container runs, so there is no
invocation to log. For example:
```
https://--preview-.web.app/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1.479aff49.jpg&w=1080&q=75
-> 403 Forbidden
https://--preview-.web.app/about
-> 200 (prerendered, served from CDN)
```
- Meanwhile the live channel looks healthy the whole time, because the
popular `_next/image` URLs are already in the Hosting CDN cache. That makes
this look like a preview-channel-specific problem and makes it very hard to
search for.
What finally surfaced it:
```bash
$ gcloud run services get-iam-policy ssr --region us-central1
etag: ACAB
```
That's an empty policy, no allUsers binding at all. Restoring it fixed every
channel immediately, no redeploy needed:
```bash
gcloud run services add-iam-policy-binding ssr \
--member=allUsers --role=roles/run.invoker --region us-central1
```
Running the channel deploy with `--debug` shows no IAM-related warning or
error anywhere, which is the core of the issue. Happy to attach the full
debug logs if that helps.
Contributor guide
Assessment
This issue has not been assessed yet.