Deploy Claude apps gateway on Google Cloud changedclaude-apps-gateway-on-gcp
Nearest release: v2.1.284, published 17 hours after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 28 Sep 2026 00:07 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.
Upstream edited
Recorded here
Lines+240added
Lines−246removed
From line
2
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits6to this page, all time
The whole hunk
from line 2, old and new numbered
/
from line 2
22
33> A worked example of running Claude apps gateway on Google Cloud: Cloud Run or GKE, Cloud SQL for PostgreSQL, Secret Manager, and service-account auth to Google Cloud's Agent Platform.
44
5<Note>
6 This page walks through one way to run Claude apps gateway on Google Cloud. The configuration is a working example for customer-managed infrastructure rather than a supported production deployment; use it to see how the pieces fit together before adapting it to your own environment. For the platform-agnostic requirements, see the [deployment guide](/docs/en/claude-apps-gateway-deploy).
7</Note>
5> [!NOTE]
6>
7> This page walks through one way to run Claude apps gateway on Google Cloud. The configuration is a working example for customer-managed infrastructure rather than a supported production deployment; use it to see how the pieces fit together before adapting it to your own environment. For the platform-agnostic requirements, see the [deployment guide](/docs/en/claude-apps-gateway-deploy).
88
99This example provisions Claude apps gateway on Google Cloud with Google Cloud's Agent Platform as the model upstream, using either Cloud Run or GKE for compute. Google Workspace is the example identity provider (IdP), but any OpenID Connect (OIDC) compliant IdP works; only the `oidc` block changes. See [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup) for per-IdP details.
1010
1111## What you'll build
1212
13<Frame>
14 <img src="https://mintcdn.com/claude-code/-uq-4JE0W_JO5Er5/images/claude-gateway-gcp-architecture.svg?fit=max&auto=format&n=-uq-4JE0W_JO5Er5&q=85&s=cb705151c69128ac0da235852d5600ab" alt="Diagram of Claude apps gateway on Google Cloud: Claude Code clients connect over HTTPS to the gateway (Cloud Run or GKE), which runs inside a VPC alongside a private-IP Cloud SQL database for session state. The gateway signs users in via OIDC against Google Workspace, reads config and secrets from Secret Manager, forwards model requests to Google Cloud's Agent Platform, and pulls its image from Artifact Registry at deploy." width="760" height="400" data-path="images/claude-gateway-gcp-architecture.svg" />
15</Frame>
13
1614
1715The deployment consists of:
1816
from line 42
4442
4543The steps below provision the full deployment with `gcloud` commands.
4644
47<Steps>
48 <Step title="Enable APIs">
49 Enable the service APIs the walkthrough uses:
45**1. Enable APIs**
5046
51 ```bash theme={null}
52 gcloud services enable \
53 aiplatform.googleapis.com \
54 artifactregistry.googleapis.com \
55 sqladmin.googleapis.com \
56 secretmanager.googleapis.com \
57 iamcredentials.googleapis.com \
58 iam.googleapis.com \
59 compute.googleapis.com \
60 servicenetworking.googleapis.com \
61 run.googleapis.com \
62 container.googleapis.com
63 ```
47Enable the service APIs the walkthrough uses:
6448
65 The APIs you need depend on the deployment path:
49```bash theme={null}
50gcloud services enable \
51 aiplatform.googleapis.com \
52 artifactregistry.googleapis.com \
53 sqladmin.googleapis.com \
54 secretmanager.googleapis.com \
55 iamcredentials.googleapis.com \
56 iam.googleapis.com \
57 compute.googleapis.com \
58 servicenetworking.googleapis.com \
59 run.googleapis.com \
60 container.googleapis.com
61```
6662
67 * `compute` and `servicenetworking`: needed for the private-IP Cloud SQL path
68 * `run`: Cloud Run only
69 * `container`: GKE only
70 </Step>
63The APIs you need depend on the deployment path:
7164
72 <Step title="Create the service account and grant IAM">
73 The gateway runs as a dedicated service account with permission to call Google Cloud's Agent Platform. It reaches Cloud SQL over the VPC with a password user, so no Cloud SQL IAM role is required:
65* `compute` and `servicenetworking`: needed for the private-IP Cloud SQL path
66* `run`: Cloud Run only
67* `container`: GKE only
7468
75 ```bash theme={null}
76 gcloud iam service-accounts create claude-gateway --display-name="Claude apps gateway"
77 SA="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"
69**2. Create the service account and grant IAM**
7870
79 gcloud projects add-iam-policy-binding "$PROJECT_ID" \
80 --member="serviceAccount:${SA}" --role="roles/aiplatform.user" --condition=None
81 ```
71The gateway runs as a dedicated service account with permission to call Google Cloud's Agent Platform. It reaches Cloud SQL over the VPC with a password user, so no Cloud SQL IAM role is required:
8272
83 Then enable the Claude models for the project in Model Garden; models publish to specific regions, so check each model card.
84 </Step>
73```bash theme={null}
74gcloud iam service-accounts create claude-gateway --display-name="Claude apps gateway"
75SA="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"
8576
86 <Step title="Build and push the image to Artifact Registry">
87 Build the image per the [container image requirements](/docs/en/claude-apps-gateway-deploy#container-image), using the `linux-x64` glibc binary, and push it:
77gcloud projects add-iam-policy-binding "$PROJECT_ID" \
78 --member="serviceAccount:${SA}" --role="roles/aiplatform.user" --condition=None
79```
8880
89 ```bash theme={null}
90 gcloud artifacts repositories create claude-gateway \
91 --repository-format=docker --location="$REGION"
92 gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
81Then enable the Claude models for the project in Model Garden; models publish to specific regions, so check each model card.
9382
94 # Cloud Run requires linux/amd64. --provenance=false avoids a buildx OCI
95 # image index that Cloud Run rejects.
96 docker build --platform=linux/amd64 --provenance=false \
97 -t "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" .
98 docker push "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>"
99 ```
100 </Step>
83**3. Build and push the image to Artifact Registry**
10184
102 <Step title="Provision Cloud SQL for PostgreSQL">
103 Create the instance on a VPC via Private Services Access so it has no public IP; this also satisfies projects where `constraints/sql.restrictPublicIp` is enforced:
85Build the image per the [container image requirements](/docs/en/claude-apps-gateway-deploy#container-image), using the `linux-x64` glibc binary, and push it:
10486
105 ```bash theme={null}
106 VPC=cc-gateway-vpc
107 gcloud compute networks create "$VPC" --subnet-mode=custom
108 gcloud compute networks subnets create cc-gateway-subnet \
109 --network="$VPC" --region="$REGION" --range=10.0.0.0/24
87```bash theme={null}
88gcloud artifacts repositories create claude-gateway \
89 --repository-format=docker --location="$REGION"
90gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
11091
111 # Private Services Access: one-time per VPC
112 gcloud compute addresses create "google-managed-services-${VPC}" \
113 --global --purpose=VPC_PEERING --prefix-length=16 --network="$VPC"
114 gcloud services vpc-peerings connect \
115 --service=servicenetworking.googleapis.com \
116 --ranges="google-managed-services-${VPC}" --network="$VPC"
92# Cloud Run requires linux/amd64. --provenance=false avoids a buildx OCI
93# image index that Cloud Run rejects.
94docker build --platform=linux/amd64 --provenance=false \
95 -t "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" .
96docker push "${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>"
97```
11798
118 gcloud sql instances create claude-gateway-db \
119 --database-version=POSTGRES_16 --tier=db-g1-small --region="$REGION" \
120 --network="projects/${PROJECT_ID}/global/networks/${VPC}" --no-assign-ip
121 gcloud sql databases create claude_gateway --instance=claude-gateway-db
122 PGPASS="$(openssl rand -hex 24)"
123 gcloud sql users create gateway --instance=claude-gateway-db --password="$PGPASS"
99**4. Provision Cloud SQL for PostgreSQL**
124100
125 PRIVATE_IP="$(gcloud sql instances describe claude-gateway-db \
126 --format='value(ipAddresses[0].ipAddress)')"
127 GATEWAY_POSTGRES_URL="postgres://gateway:${PGPASS}@${PRIVATE_IP}:5432/claude_gateway?sslmode=require"
128 ```
101Create the instance on a VPC via Private Services Access so it has no public IP; this also satisfies projects where `constraints/sql.restrictPublicIp` is enforced:
129102
130 The Cloud Run or GKE runtime must be on, or routed into, this VPC.
131 </Step>
103```bash theme={null}
104VPC=cc-gateway-vpc
105gcloud compute networks create "$VPC" --subnet-mode=custom
106gcloud compute networks subnets create cc-gateway-subnet \
107 --network="$VPC" --region="$REGION" --range=10.0.0.0/24
132108
133 <Step title="Write gateway.yaml">
134 The `upstreams` block points at Google Cloud's Agent Platform with `auth: {}`, so the gateway authenticates via Application Default Credentials from the runtime service account. See the [configuration reference](/docs/en/claude-apps-gateway-config) for every field.
109# Private Services Access: one-time per VPC
110gcloud compute addresses create "google-managed-services-${VPC}" \
111 --global --purpose=VPC_PEERING --prefix-length=16 --network="$VPC"
112gcloud services vpc-peerings connect \
113 --service=servicenetworking.googleapis.com \
114 --ranges="google-managed-services-${VPC}" --network="$VPC"
135115
136 Two `listen` fields describe what fronts the gateway:
116gcloud sql instances create claude-gateway-db \
117 --database-version=POSTGRES_16 --tier=db-g1-small --region="$REGION" \
118 --network="projects/${PROJECT_ID}/global/networks/${VPC}" --no-assign-ip
119gcloud sql databases create claude_gateway --instance=claude-gateway-db
120PGPASS="$(openssl rand -hex 24)"
121gcloud sql users create gateway --instance=claude-gateway-db --password="$PGPASS"
137122
138 * `public_url`: the external `https://` origin, required for any non-loopback bind; see the [`listen` reference](/docs/en/claude-apps-gateway-config#listen). The gateway builds the IdP `redirect_uri` and its discovery document only from this value, never from `X-Forwarded-*` headers.
139 * `trusted_proxies`: the front end's source ranges. The gateway honors `X-Forwarded-For` only when the TCP peer is in this list, then walks the chain past trusted hops, so per-IP sign-in rate limits and audit events record developer IPs instead of the load balancer's.
123PRIVATE_IP="$(gcloud sql instances describe claude-gateway-db \
124 --format='value(ipAddresses[0].ipAddress)')"
125GATEWAY_POSTGRES_URL="postgres://gateway:${PGPASS}@${PRIVATE_IP}:5432/claude_gateway?sslmode=require"
126```
140127
141 Set `trusted_proxies` to match your front end. An external GKE Ingress of class `gce` isn't listed: it provisions a public forwarding-rule address, which the `/login` [private-network check](/docs/en/claude-apps-gateway#prerequisites) rejects.
128The Cloud Run or GKE runtime must be on, or routed into, this VPC.
142129
143 | Front end | `trusted_proxies` |
144 | - | - |
145 | Cloud Run reached directly, no load balancer | `[169.254.0.0/16]` |
146 | Internal Application Load Balancer in front of Cloud Run | `169.254.0.0/16` plus your proxy-only subnet's CIDR |
147 | GKE internal Ingress, class `gce-internal` | Your proxy-only subnet's CIDR |
130**5. Write gateway.yaml**
148131
149 The example below uses the internal-load-balancer-in-front-of-Cloud-Run values.
132The `upstreams` block points at Google Cloud's Agent Platform with `auth: {}`, so the gateway authenticates via Application Default Credentials from the runtime service account. See the [configuration reference](/docs/en/claude-apps-gateway-config) for every field.
150133
151 ```yaml gateway.yaml theme={null}
152 listen:
153 host: 0.0.0.0
154 port: 8080
155 public_url: https://claude-gateway.internal.example.com
156 trusted_proxies: [169.254.0.0/16, <your-proxy-only-subnet-cidr>]
134Two `listen` fields describe what fronts the gateway:
157135
158 oidc:
159 issuer: https://accounts.google.com
160 client_id: <your-oauth-client-id>
161 client_secret: ${OIDC_CLIENT_SECRET} # GKE: ${file:/secrets/oidc-client-secret}
162 allowed_email_domains: [example.com]
163 # Google ignores offline_access; these yield refresh tokens:
164 scopes: [openid, profile, email]
165 extra_auth_params: { access_type: offline, prompt: consent }
136* `public_url`: the external `https://` origin, required for any non-loopback bind; see the [`listen` reference](/docs/en/claude-apps-gateway-config#listen). The gateway builds the IdP `redirect_uri` and its discovery document only from this value, never from `X-Forwarded-*` headers.
137* `trusted_proxies`: the front end's source ranges. The gateway honors `X-Forwarded-For` only when the TCP peer is in this list, then walks the chain past trusted hops, so per-IP sign-in rate limits and audit events record developer IPs instead of the load balancer's.
166138
167 session:
168 jwt_secret: ${GATEWAY_JWT_SECRET} # GKE: ${file:/secrets/jwt-secret}
139Set `trusted_proxies` to match your front end. An external GKE Ingress of class `gce` isn't listed: it provisions a public forwarding-rule address, which the `/login` [private-network check](/docs/en/claude-apps-gateway#prerequisites) rejects.
169140
170 store:
171 postgres_url: ${GATEWAY_POSTGRES_URL} # GKE: ${file:/secrets/postgres-url}
172 # readiness_grace_seconds: 300 # keep passing the readiness probe
173 # through a Cloud SQL failover
141| Front end | `trusted_proxies` |
142| - | - |
143| Cloud Run reached directly, no load balancer | `[169.254.0.0/16]` |
144| Internal Application Load Balancer in front of Cloud Run | `169.254.0.0/16` plus your proxy-only subnet's CIDR |
145| GKE internal Ingress, class `gce-internal` | Your proxy-only subnet's CIDR |
174146
175 upstreams:
176 - provider: vertex
177 region: <your-region> # must match $REGION
178 project_id: <your-project>
179 auth: {} # ADC via the runtime service account
180 ```
147The example below uses the internal-load-balancer-in-front-of-Cloud-Run values.
181148
182 <Note>
183 Google id\_tokens carry no `groups` claim. To use group-based policies in [`managed.policies`](/docs/en/claude-apps-gateway-config#managed) with Google Workspace as the IdP, configure [`oidc.google_groups`](/docs/en/claude-apps-gateway-config#oidc), which looks up each user's groups through the Admin SDK Directory API using a service account with domain-wide delegation. Without it, match on `email_domain` instead.
184 </Note>
185 </Step>
149```yaml gateway.yaml theme={null}
150listen:
151 host: 0.0.0.0
152 port: 8080
153 public_url: https://claude-gateway.internal.example.com
154 trusted_proxies: [169.254.0.0/16, <your-proxy-only-subnet-cidr>]
186155
187 <Step title="Store secrets in Secret Manager">
188 Create four secrets and grant `roles/secretmanager.secretAccessor` to the `claude-gateway` service account:
156oidc:
157 issuer: https://accounts.google.com
158 client_id: <your-oauth-client-id>
159 client_secret: ${OIDC_CLIENT_SECRET} # GKE: ${file:/secrets/oidc-client-secret}
160 allowed_email_domains: [example.com]
161 # Google ignores offline_access; these yield refresh tokens:
162 scopes: [openid, profile, email]
163 extra_auth_params: { access_type: offline, prompt: consent }
189164
190 | Secret | Source |
191 | - | - |
192 | `gateway-jwt-secret` | `openssl rand -base64 32` |
193 | `gateway-oidc-client-secret` | Google Cloud Console → OAuth client |
194 | `gateway-postgres-url` | `$GATEWAY_POSTGRES_URL` from the Cloud SQL step |
195 | `gateway-config` | the full `gateway.yaml` from the previous step |
165session:
166 jwt_secret: ${GATEWAY_JWT_SECRET} # GKE: ${file:/secrets/jwt-secret}
196167
197 How the secrets reach the container differs by track:
168store:
169 postgres_url: ${GATEWAY_POSTGRES_URL} # GKE: ${file:/secrets/postgres-url}
170 # readiness_grace_seconds: 300 # keep passing the readiness probe
171 # through a Cloud SQL failover
198172
199 * On GKE they mount as files via the Secret Manager CSI driver, and `gateway.yaml` references `${file:/secrets/...}`.
200 * On Cloud Run, which can't mount multiple secrets into one directory, `gateway.yaml` mounts as a file and the other three inject as environment variables, so `gateway.yaml` references `${GATEWAY_JWT_SECRET}`, `${OIDC_CLIENT_SECRET}`, and `${GATEWAY_POSTGRES_URL}` instead.
201 </Step>
173upstreams:
174 - provider: vertex
175 region: <your-region> # must match $REGION
176 project_id: <your-project>
177 auth: {} # ADC via the runtime service account
178```
202179
203 <Step title="Deploy">
204 <Tabs>
205 <Tab title="Cloud Run">
206 The command below deploys for production behind an internal load balancer.
180> [!NOTE]
181>
182> Google id\_tokens carry no `groups` claim. To use group-based policies in [`managed.policies`](/docs/en/claude-apps-gateway-config#managed) with Google Workspace as the IdP, configure [`oidc.google_groups`](/docs/en/claude-apps-gateway-config#oidc), which looks up each user's groups through the Admin SDK Directory API using a service account with domain-wide delegation. Without it, match on `email_domain` instead.
207183
208 ```bash theme={null}
209 gcloud run deploy claude-gateway \
210 --image="${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" \
211 --region="$REGION" \
212 --service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
213 --min-instances=1 \
214 --max-instances=8 \
215 --timeout=3600 \
216 --ingress=internal \
217 --network="$VPC" --subnet=cc-gateway-subnet --vpc-egress=private-ranges-only \
218 --set-secrets=/etc/claude/gateway.yaml=gateway-config:latest,GATEWAY_JWT_SECRET=gateway-jwt-secret:latest,OIDC_CLIENT_SECRET=gateway-oidc-client-secret:latest,GATEWAY_POSTGRES_URL=gateway-postgres-url:latest \
219 --no-invoker-iam-check
220 ```
184**6. Store secrets in Secret Manager**
221185
222 Direct VPC egress, via `--network`, `--subnet`, and `--vpc-egress=private-ranges-only`, lets the service reach the Cloud SQL private IP directly. Each instance holds up to [`store.max_connections`](/docs/en/claude-apps-gateway-config#store) Postgres connections, five by default, so keep maximum instances × `store.max_connections` below your Cloud SQL tier's connection limit; the [reference assets](#terraform-reference) cap instances at 8 for the `db-g1-small` tier for this reason. Public egress to Google Cloud's Agent Platform endpoints and `accounts.google.com` goes directly to the internet rather than through the VPC, so no Cloud NAT is needed.
186Create four secrets and grant `roles/secretmanager.secretAccessor` to the `claude-gateway` service account:
223187
224 The invoker IAM check must be open or disabled. The gateway runs its own OIDC and its clients carry no GCP token, so Cloud Run's invoker check has to admit unauthenticated requests. The gateway's OIDC sign-in authenticates the request once it reaches the container, with `allowed_email_domains` gating which domains may sign in.
188| Secret | Source |
189| - | - |
190| `gateway-jwt-secret` | `openssl rand -base64 32` |
191| `gateway-oidc-client-secret` | Google Cloud Console → OAuth client |
192| `gateway-postgres-url` | `$GATEWAY_POSTGRES_URL` from the Cloud SQL step |
193| `gateway-config` | the full `gateway.yaml` from the previous step |
225194
226 Two flags admit unauthenticated requests:
195How the secrets reach the container differs by track:
227196
228 * `--no-invoker-iam-check`: disables the check with no `allUsers` binding to manage, and works under Domain Restricted Sharing
229 * `--allow-unauthenticated`: grants `allUsers` the `run.invoker` role; use it if your organization doesn't allow `--no-invoker-iam-check`
197* On GKE they mount as files via the Secret Manager CSI driver, and `gateway.yaml` references `${file:/secrets/...}`.
198* On Cloud Run, which can't mount multiple secrets into one directory, `gateway.yaml` mounts as a file and the other three inject as environment variables, so `gateway.yaml` references `${GATEWAY_JWT_SECRET}`, `${OIDC_CLIENT_SECRET}`, and `${GATEWAY_POSTGRES_URL}` instead.
230199
231 Ingress restriction via `--ingress` is a separate, independent layer from the invoker check; keep it set to limit the service to your corporate network.
200**7. Deploy**
232201
233 By default the Cloud Run `*.run.app` URL resolves to a public address, which the `/login` [private-network check](/docs/en/claude-apps-gateway#prerequisites) rejects. Two topologies give developers a privately resolvable hostname, and Cloud Run provisions neither for you:
202**Cloud Run**
234203
235 * **Internal Application Load Balancer**, the topology this page's `gateway.yaml` assumes: provision an internal Application Load Balancer in front of the service with an internal DNS name and certificate, and set `listen.public_url` to that hostname. The `internal` ingress setting already admits traffic from internal Application Load Balancers; `internal-and-cloud-load-balancing` additionally admits external Application Load Balancers, whose public addresses the `/login` private-network check rejects, so no topology on this page needs it.
236 * **Internal-only ingress with no load balancer**: keep the deploy command as is and leave `listen.public_url` as the `*.run.app` URL, the default in the [reference assets](#terraform-reference) below. For `*.run.app` to resolve privately, your network team must already operate a Private Service Connect endpoint for Google APIs, a Cloud DNS private zone resolving `*.run.app` to it, and on-premises routing to that endpoint.
204The command below deploys for production behind an internal load balancer.
237205
238 Google's [private networking guide for Cloud Run](https://cloud.google.com/run/docs/securing/private-networking) covers the infrastructure both options need. Verify sign-in once the gateway is serving on a private hostname; until then, confirm the container booted from its logs in Cloud Run.
206```bash theme={null}
207gcloud run deploy claude-gateway \
208 --image="${REGION}-docker.pkg.dev/${PROJECT_ID}/claude-gateway/gateway:<version>" \
209 --region="$REGION" \
210 --service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
211 --min-instances=1 \
212 --max-instances=8 \
213 --timeout=3600 \
214 --ingress=internal \
215 --network="$VPC" --subnet=cc-gateway-subnet --vpc-egress=private-ranges-only \
216 --set-secrets=/etc/claude/gateway.yaml=gateway-config:latest,GATEWAY_JWT_SECRET=gateway-jwt-secret:latest,OIDC_CLIENT_SECRET=gateway-oidc-client-secret:latest,GATEWAY_POSTGRES_URL=gateway-postgres-url:latest \
217 --no-invoker-iam-check
218```
239219
240 Update the OAuth client's authorized redirect URI to `<public_url>/oauth/callback` before the first sign-in. Redeploy after changing `public_url`, because the gateway builds its public origin only from that setting and ignores `X-Forwarded-Host` and `X-Forwarded-Proto`. `X-Forwarded-For` is honored for client IPs only when `listen.trusted_proxies` is set.
241 </Tab>
220Direct VPC egress, via `--network`, `--subnet`, and `--vpc-egress=private-ranges-only`, lets the service reach the Cloud SQL private IP directly. Each instance holds up to [`store.max_connections`](/docs/en/claude-apps-gateway-config#store) Postgres connections, five by default, so keep maximum instances × `store.max_connections` below your Cloud SQL tier's connection limit; the [reference assets](#terraform-reference) cap instances at 8 for the `db-g1-small` tier for this reason. Public egress to Google Cloud's Agent Platform endpoints and `accounts.google.com` goes directly to the internet rather than through the VPC, so no Cloud NAT is needed.
242221
243 <Tab title="GKE">
244 The cluster must be on the `$VPC` created in the Cloud SQL step so pods can reach the database's private IP; VPC peering alone doesn't work, because Cloud SQL private IP is itself a peered network and peering is non-transitive. To create a new cluster on that VPC, pass `--network="$VPC" --subnetwork=cc-gateway-subnet` to `gcloud container clusters create`.
222The invoker IAM check must be open or disabled. The gateway runs its own OIDC and its clients carry no GCP token, so Cloud Run's invoker check has to admit unauthenticated requests. The gateway's OIDC sign-in authenticates the request once it reaches the container, with `allowed_email_domains` gating which domains may sign in.
245223
246 Enable Workload Identity on the cluster and its node pools, then bind the Google service account to the Kubernetes service account so pods inherit its credentials:
224Two flags admit unauthenticated requests:
247225
248 ```bash theme={null}
249 gcloud container clusters update <cluster> --region="$REGION" \
250 --workload-pool="${PROJECT_ID}.svc.id.goog"
251 # On a Standard cluster, existing node pools also need GKE_METADATA;
252 # Autopilot enables this by default.
253 gcloud container node-pools update <pool> --cluster=<cluster> \
254 --region="$REGION" --workload-metadata=GKE_METADATA
226* `--no-invoker-iam-check`: disables the check with no `allUsers` binding to manage, and works under Domain Restricted Sharing
227* `--allow-unauthenticated`: grants `allUsers` the `run.invoker` role; use it if your organization doesn't allow `--no-invoker-iam-check`
255228
256 kubectl create namespace claude-gateway
257 kubectl create serviceaccount gateway -n claude-gateway
229Ingress restriction via `--ingress` is a separate, independent layer from the invoker check; keep it set to limit the service to your corporate network.
258230
259 gcloud iam service-accounts add-iam-policy-binding \
260 "claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
261 --role roles/iam.workloadIdentityUser \
262 --member "serviceAccount:${PROJECT_ID}.svc.id.goog[claude-gateway/gateway]"
231By default the Cloud Run `*.run.app` URL resolves to a public address, which the `/login` [private-network check](/docs/en/claude-apps-gateway#prerequisites) rejects. Two topologies give developers a privately resolvable hostname, and Cloud Run provisions neither for you:
263232
264 kubectl annotate serviceaccount gateway -n claude-gateway \
265 iam.gke.io/gcp-service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"
266 ```
233* **Internal Application Load Balancer**, the topology this page's `gateway.yaml` assumes: provision an internal Application Load Balancer in front of the service with an internal DNS name and certificate, and set `listen.public_url` to that hostname. The `internal` ingress setting already admits traffic from internal Application Load Balancers; `internal-and-cloud-load-balancing` additionally admits external Application Load Balancers, whose public addresses the `/login` private-network check rejects, so no topology on this page needs it.
234* **Internal-only ingress with no load balancer**: keep the deploy command as is and leave `listen.public_url` as the `*.run.app` URL, the default in the [reference assets](#terraform-reference) below. For `*.run.app` to resolve privately, your network team must already operate a Private Service Connect endpoint for Google APIs, a Cloud DNS private zone resolving `*.run.app` to it, and on-premises routing to that endpoint.
267235
268 Deploy the gateway as a standard Deployment plus a Service and an internal Ingress, class `gce-internal`, as described in [Kubernetes deployment](/docs/en/claude-apps-gateway-deploy#kubernetes), with:
236Google's [private networking guide for Cloud Run](https://cloud.google.com/run/docs/securing/private-networking) covers the infrastructure both options need. Verify sign-in once the gateway is serving on a private hostname; until then, confirm the container booted from its logs in Cloud Run.
269237
270 * `serviceAccountName: gateway`
271 * the Secret Manager CSI driver mounting secrets at `/secrets`
272 * the readiness probe pointed at `GET /readyz`
238Update the OAuth client's authorized redirect URI to `<public_url>/oauth/callback` before the first sign-in. Redeploy after changing `public_url`, because the gateway builds its public origin only from that setting and ignores `X-Forwarded-Host` and `X-Forwarded-Proto`. `X-Forwarded-For` is honored for client IPs only when `listen.trusted_proxies` is set.
273239
274 Attach a BackendConfig with a raised `timeoutSec` to the gateway Service: the load balancer backend service behind GKE Ingress defaults to a 30-second timeout, which cuts off long streaming responses.
240**GKE**
275241
276 Don't apply an egress NetworkPolicy that blocks `169.254.169.254` on a Workload Identity cluster; the pod must reach the metadata server for credentials. The gateway's built-in [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) is the defense there.
242The cluster must be on the `$VPC` created in the Cloud SQL step so pods can reach the database's private IP; VPC peering alone doesn't work, because Cloud SQL private IP is itself a peered network and peering is non-transitive. To create a new cluster on that VPC, pass `--network="$VPC" --subnetwork=cc-gateway-subnet` to `gcloud container clusters create`.
277243
278 The gateway logs a boot warning that the metadata endpoint is reachable and suggests applying an egress NetworkPolicy. Under Workload Identity that warning is expected, because the pod needs the endpoint.
279 </Tab>
280 </Tabs>
281 </Step>
244Enable Workload Identity on the cluster and its node pools, then bind the Google service account to the Kubernetes service account so pods inherit its credentials:
282245
283 <Step title="Push the gateway URL to developer machines">
284 The gateway is now running, but developers can't reach it from `/login` until the gateway URL is on their machines. Deploy the full [managed settings snippet](/docs/en/claude-apps-gateway#set-the-gateway-url), with `forceLoginMethod`, `forceLoginGatewayUrl`, and the `parentSettingsBehavior: "merge"` opt-in, to each device via MDM. There is no gateway option in the login picker for a developer to select manually.
285 </Step>
286</Steps>
246```bash theme={null}
247gcloud container clusters update <cluster> --region="$REGION" \
248 --workload-pool="${PROJECT_ID}.svc.id.goog"
249# On a Standard cluster, existing node pools also need GKE_METADATA;
250# Autopilot enables this by default.
251gcloud container node-pools update <pool> --cluster=<cluster> \
252 --region="$REGION" --workload-metadata=GKE_METADATA
253
254kubectl create namespace claude-gateway
255kubectl create serviceaccount gateway -n claude-gateway
256
257gcloud iam service-accounts add-iam-policy-binding \
258 "claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com" \
259 --role roles/iam.workloadIdentityUser \
260 --member "serviceAccount:${PROJECT_ID}.svc.id.goog[claude-gateway/gateway]"
261
262kubectl annotate serviceaccount gateway -n claude-gateway \
263 iam.gke.io/gcp-service-account="claude-gateway@${PROJECT_ID}.iam.gserviceaccount.com"
264```
265
266Deploy the gateway as a standard Deployment plus a Service and an internal Ingress, class `gce-internal`, as described in [Kubernetes deployment](/docs/en/claude-apps-gateway-deploy#kubernetes), with:
267
268* `serviceAccountName: gateway`
269* the Secret Manager CSI driver mounting secrets at `/secrets`
270* the readiness probe pointed at `GET /readyz`
271
272Attach a BackendConfig with a raised `timeoutSec` to the gateway Service: the load balancer backend service behind GKE Ingress defaults to a 30-second timeout, which cuts off long streaming responses.
273
274Don't apply an egress NetworkPolicy that blocks `169.254.169.254` on a Workload Identity cluster; the pod must reach the metadata server for credentials. The gateway's built-in [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) is the defense there.
275
276The gateway logs a boot warning that the metadata endpoint is reachable and suggests applying an egress NetworkPolicy. Under Workload Identity that warning is expected, because the pod needs the endpoint.
277
278**8. Push the gateway URL to developer machines**
279
280The gateway is now running, but developers can't reach it from `/login` until the gateway URL is on their machines. Deploy the full [managed settings snippet](/docs/en/claude-apps-gateway#set-the-gateway-url), with `forceLoginMethod`, `forceLoginGatewayUrl`, and the `parentSettingsBehavior: "merge"` opt-in, to each device via MDM. There is no gateway option in the login picker for a developer to select manually.
287281
288282## Terraform reference
289283
No line in this hunk matches that.