1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Deploy Claude apps gateway on AWS
6
7> A worked example of running Claude apps gateway on AWS: ECS Fargate or EKS, Amazon RDS for PostgreSQL, AWS Secrets Manager, and IAM-role auth to Amazon Bedrock.
8
9<Note>
10 This page walks through one way to run Claude apps gateway on AWS. 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).
11</Note>
12
13This example provisions Claude apps gateway on AWS with Amazon Bedrock as the model upstream, using either [Amazon ECS](https://aws.amazon.com/ecs/) on [AWS Fargate](https://aws.amazon.com/fargate/) or [Amazon EKS](https://aws.amazon.com/eks/) for compute. [Okta](https://www.okta.com/) is the example identity provider (IdP), but any OpenID Connect (OIDC) compliant IdP works; see [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup) for per-IdP details.
14
15<Note>
16 Bedrock isn't the only Claude upstream on AWS. The gateway also supports Claude Platform on AWS, the Anthropic-operated Claude API with AWS authentication and AWS Marketplace billing, in place of Bedrock or alongside it. Its upstream entry, credentials, and IAM permissions differ from this page's Bedrock-scoped ones; the [Claude Platform on AWS upstream reference](/docs/en/claude-apps-gateway-config#claude-platform-on-aws) covers what changes, and the rest of this page applies unchanged.
17</Note>
18
19## Architecture
20
21<Frame caption="The example architecture, with Amazon Bedrock as the model upstream. A Claude Platform on AWS upstream occupies the same position.">
22 <img src="https://mintcdn.com/claude-code/PHweeRmDUYEKff49/images/claude-gateway-aws-architecture.svg?fit=max&auto=format&n=PHweeRmDUYEKff49&q=85&s=8599cc34aa28522cde208ee831439bb4" alt="Diagram of Claude apps gateway on AWS: Claude Code clients connect over HTTPS to an internal Application Load Balancer fronting the gateway (ECS Fargate or EKS), which runs in private subnets alongside an Amazon RDS for PostgreSQL instance for session state. The gateway signs users in via OIDC against the corporate IdP, reads secrets from AWS Secrets Manager, forwards model requests to Amazon Bedrock using its IAM role, and pulls its image from Amazon ECR at deploy." width="820" height="430" data-path="images/claude-gateway-aws-architecture.svg" />
23</Frame>
24
25The gateway runs as a private HTTPS endpoint on your network that developers sign in to through your IdP. Their Claude Code sessions reach Claude models on Amazon Bedrock through the gateway's IAM role, so no model credentials land on developer machines. The reference configuration provisions:
26
27* **Amazon ECS on AWS Fargate** service or **Amazon EKS** Deployment running the gateway container
28* **Amazon ECR** repository for the gateway image
29* **Amazon RDS for PostgreSQL** instance in private subnets, not publicly accessible, for the gateway's [store](/docs/en/claude-apps-gateway-config#store)
30* **AWS Secrets Manager** secrets for the JWT signing key, the OIDC client secret, and the Postgres URL
31* **IAM role** with `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`, attached as the ECS task role or bound via IAM Roles for Service Accounts (IRSA) on EKS
32* **Internal Application Load Balancer** for HTTPS
33
34## Prerequisites
35
36The walkthrough creates the gateway's own resources, but it builds on network and identity infrastructure you already have. Before you start, you need:
37
38* An AWS account with permission to create the [resources above](#architecture)
39* The [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and [authenticated](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html), and [Docker](https://docs.docker.com/get-started/get-docker/) installed locally
40* A [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html) with at least two [private subnets](https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html) in different Availability Zones, with outbound internet access through a [NAT gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html); the internal load balancer needs subnets in two AZs, and the gateway needs egress to Bedrock and your IdP
41* An Okta OIDC web application with redirect URI `https://<gateway-host>/oauth/callback`; see [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup)
42* A TLS hostname for the gateway, typically an internal DNS name in a [Route 53 private hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-private.html) pointing at the load balancer, with an [ACM certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs.html) for that name, imported or issued by [AWS Private CA](https://docs.aws.amazon.com/privateca/latest/userguide/PcaWelcome.html)
43
44### Set your environment variables
45
46Every command on this page reads four values from your shell: `AWS_REGION`, `ACCOUNT_ID`, `VPC_ID`, and `PRIVATE_SUBNETS`.
47
48Pick a US region where Bedrock serves the Claude models you need. The walkthrough relies on the gateway's built-in model catalog, which resolves to `us.anthropic.*` inference profiles, and the IAM policy grants those ARNs. In a non-US region, add a [`models:` block](/docs/en/claude-apps-gateway-config#models) with that geo's inference-profile IDs and change the IAM policy's ARN prefix to match.
49
50If you don't have the VPC ID at hand, list your VPCs with `aws ec2 describe-vpcs`, then list that VPC's subnets to find two private ones in different Availability Zones:
51
52```bash theme={null}
53aws ec2 describe-subnets --filters "Name=vpc-id,Values=<your-vpc-id>" \
54 --query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,CIDR:CidrBlock}' --output table
55```
56
57Export all four before continuing:
58
59```bash theme={null}
60export AWS_REGION=us-east-1 # a US region where Bedrock serves the Claude models you need
61export ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
62export VPC_ID=<your-vpc-id>
63export PRIVATE_SUBNETS="<subnet-id-a> <subnet-id-b>"
64```
65
66## Deploy the gateway
67
68The steps below provision the full deployment with `aws` commands.
69
70<Steps>
71 <Step title="Create the security groups">
72 Three security groups chain the traffic path: your corporate network reaches the load balancer on 443, the load balancer reaches the gateway on 8080, and the gateway reaches Postgres on 5432. Nothing else is reachable. How you attach them depends on the compute track:
73
74 * On ECS Fargate, the deploy step attaches `$ALB_SG` to the load balancer and `$GW_SG` to the service.
75 * On EKS, the AWS Load Balancer Controller creates its own frontend security group for the ALB, so `$ALB_SG` and `$GW_SG` go unused: the deploy step's `inbound-cidrs` annotation restricts the listener to your corporate network, and the database security group admits the cluster's security group instead.
76
77 ```bash theme={null}
78 ALB_SG="$(aws ec2 create-security-group --group-name claude-gateway-alb \
79 --description "Claude gateway ALB" --vpc-id "$VPC_ID" \
80 --query GroupId --output text)"
81 GW_SG="$(aws ec2 create-security-group --group-name claude-gateway-svc \
82 --description "Claude gateway service" --vpc-id "$VPC_ID" \
83 --query GroupId --output text)"
84 DB_SG="$(aws ec2 create-security-group --group-name claude-gateway-db \
85 --description "Claude gateway Postgres" --vpc-id "$VPC_ID" \
86 --query GroupId --output text)"
87
88 aws ec2 authorize-security-group-ingress --group-id "$ALB_SG" \
89 --protocol tcp --port 443 --cidr <your-corporate-cidr>
90 aws ec2 authorize-security-group-ingress --group-id "$GW_SG" \
91 --protocol tcp --port 8080 --source-group "$ALB_SG"
92 aws ec2 authorize-security-group-ingress --group-id "$DB_SG" \
93 --protocol tcp --port 5432 --source-group "$GW_SG"
94 ```
95 </Step>
96
97 <Step title="Create the IAM roles and submit the use case form">
98 The gateway runs with a dedicated task role whose only permission is invoking Claude models on Bedrock. Per the [Bedrock upstream reference](/docs/en/claude-apps-gateway-config#amazon-bedrock), the policy must cover both the cross-region inference-profile ARNs and the underlying foundation-model ARNs:
99
100 ```bash theme={null}
101 cat > bedrock-invoke.json <<EOF
102 {
103 "Version": "2012-10-17",
104 "Statement": [{
105 "Effect": "Allow",
106 "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
107 "Resource": [
108 "arn:aws:bedrock:${AWS_REGION}:${ACCOUNT_ID}:inference-profile/us.anthropic.*",
109 "arn:aws:bedrock:*::foundation-model/anthropic.*"
110 ]
111 }]
112 }
113 EOF
114 cat > ecs-trust.json <<'EOF'
115 {
116 "Version": "2012-10-17",
117 "Statement": [{
118 "Effect": "Allow",
119 "Principal": { "Service": "ecs-tasks.amazonaws.com" },
120 "Action": "sts:AssumeRole"
121 }]
122 }
123 EOF
124
125 aws iam create-role --role-name claude-gateway-task \
126 --assume-role-policy-document file://ecs-trust.json
127 aws iam put-role-policy --role-name claude-gateway-task \
128 --policy-name bedrock-invoke --policy-document file://bedrock-invoke.json
129 ```
130
131 ECS also needs an execution role, which the ECS agent itself uses to pull the image from ECR and inject the Secrets Manager values created later. It is separate from the task role the gateway's AWS SDK uses at runtime:
132
133 ```bash theme={null}
134 aws iam create-role --role-name claude-gateway-execution \
135 --assume-role-policy-document file://ecs-trust.json
136 aws iam attach-role-policy --role-name claude-gateway-execution \
137 --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
138 cat > secrets-read.json <<EOF
139 {
140 "Version": "2012-10-17",
141 "Statement": [{
142 "Effect": "Allow",
143 "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
144 "Resource": [
145 "arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-jwt-secret-??????",
146 "arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-oidc-client-secret-??????",
147 "arn:aws:secretsmanager:${AWS_REGION}:${ACCOUNT_ID}:secret:gateway-postgres-url-??????"
148 ]
149 }]
150 }
151 EOF
152 aws iam put-role-policy --role-name claude-gateway-execution \
153 --policy-name read-gateway-secrets --policy-document file://secrets-read.json
154 ```
155
156 The policy names one ARN per secret rather than a bare `gateway-*` wildcard, which in a shared account would also match unrelated secrets; the trailing `-??????` matches exactly the random six-character suffix Secrets Manager appends to every secret's ARN. A trailing `-*` would be a plain prefix glob and would also match longer names such as `gateway-postgres-url-prod`.
157
158 The IAM policy grants the gateway permission to call Bedrock, and Bedrock enables model access by default in commercial regions. The remaining account-level gate is Anthropic's one-time use case form: if no one in your account has submitted it, open the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/), select an Anthropic model from the Model catalog, and complete the form. Access is granted immediately after submission; see [Claude Code on Amazon Bedrock](/docs/en/amazon-bedrock#1-submit-use-case-details) for the AWS Organizations form and the IAM permissions the submitter needs.
159
160 The EKS track reuses both policy documents on an IRSA role instead of the two ECS roles; see the deploy step.
161 </Step>
162
163 <Step title="Provision Amazon RDS for PostgreSQL">
164 The instance runs in the private subnets with no public address and storage encryption on. The engine version is pinned to Postgres 16, which satisfies the gateway's supported floor of PostgreSQL 14 and guarantees the parameter-group family below matches the instance.
165
166 First, create the subnet group that places the database in the private subnets, and a parameter group with `rds.force_ssl=1` so the server rejects plaintext connections. The engine version is pinned once because the parameter group's family must match the engine major version the instance runs:
167
168 ```bash theme={null}
169 aws rds create-db-subnet-group --db-subnet-group-name claude-gateway-db \
170 --db-subnet-group-description "Claude gateway" --subnet-ids $PRIVATE_SUBNETS
171
172 PG_VERSION=16
173 PG_FAMILY="postgres${PG_VERSION}"
174 aws rds create-db-parameter-group --db-parameter-group-name claude-gateway-db \
175 --db-parameter-group-family "$PG_FAMILY" \
176 --description "Claude gateway - require TLS on every connection"
177 aws rds modify-db-parameter-group --db-parameter-group-name claude-gateway-db \
178 --parameters "ParameterName=rds.force_ssl,ParameterValue=1,ApplyMethod=immediate"
179 ```
180
181 Then create the instance with a generated master password:
182
183 ```bash theme={null}
184 PGPASS="$(openssl rand -hex 24)"
185 aws rds create-db-instance --db-instance-identifier claude-gateway-db \
186 --engine postgres --engine-version "$PG_VERSION" \
187 --db-instance-class db.t4g.micro \
188 --allocated-storage 20 --db-name claude_gateway \
189 --master-username gateway --master-user-password "$PGPASS" \
190 --db-subnet-group-name claude-gateway-db \
191 --db-parameter-group-name claude-gateway-db \
192 --vpc-security-group-ids "$DB_SG" \
193 --no-publicly-accessible --storage-encrypted
194 ```
195
196 The literal `--master-user-password` argument is visible in the process table and in audit/EDR logs while the command runs, the same exposure the secrets step's note covers. On a shared or monitored host, pass the password via `--cli-input-json` from a `0600` file instead, the way the bundle's `setup.sh` does.
197
198 Wait for the instance to come up, which can take several minutes, then read its private endpoint and assemble the connection string the gateway will use:
199
200 ```bash theme={null}
201 aws rds wait db-instance-available --db-instance-identifier claude-gateway-db
202 DB_HOST="$(aws rds describe-db-instances --db-instance-identifier claude-gateway-db \
203 --query 'DBInstances[0].Endpoint.Address' --output text)"
204 GATEWAY_POSTGRES_URL="postgres://gateway:${PGPASS}@${DB_HOST}:5432/claude_gateway?sslmode=verify-full"
205 ```
206
207 `sslmode=verify-full` makes the gateway verify the RDS server certificate's chain and hostname, not only encrypt. The trust anchor is the [AWS RDS certificate bundle](https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem), which the image build step below copies to `/etc/claude/rds-global-bundle.pem` and trusts via `NODE_EXTRA_CA_CERTS`. Don't append a libpq-style `sslrootcert=` parameter to the URL: the gateway's driver reads only `sslmode` from the query string and would forward `sslrootcert` to Postgres as a startup parameter, which the server rejects.
208
209 The ECS service or EKS pods must run in this VPC so they can reach the instance's private endpoint, and the `claude-gateway-db` security group only admits the gateway's security group.
210 </Step>
211
212 <Step title="Write gateway.yaml">
213 The `upstreams` block points at Bedrock with `auth: {}`, so the gateway authenticates via the AWS default credential chain from the task role on ECS or the IRSA role on EKS. See the [configuration reference](/docs/en/claude-apps-gateway-config) for every field.
214
215 Two `listen` fields depend on what fronts the gateway:
216
217 * `public_url`: required behind a load balancer. The gateway builds the IdP `redirect_uri` and its discovery document only from this value, never from `X-Forwarded-*` headers.
218 * `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.
219
220 On both tracks the front end is an internal ALB, whether created directly or by the AWS Load Balancer Controller, and an ALB's nodes take addresses from the subnets it is attached to, so set `trusted_proxies` to those subnets' CIDRs. This trusts every host in those subnets as a proxy. Keep the ALB's ingress source, your corporate CIDR, from overlapping them, and don't share the subnets with untrusted workloads that could spoof client IPs via `X-Forwarded-For`.
221
222 ```yaml gateway.yaml theme={null}
223 listen:
224 host: 0.0.0.0
225 port: 8080
226 public_url: https://claude-gateway.internal.example.com
227 trusted_proxies: [<your-alb-subnet-cidrs>]
228
229 oidc:
230 issuer: https://example.okta.com
231 client_id: 0oa1example2
232 client_secret: ${OIDC_CLIENT_SECRET} # EKS: ${file:/secrets/oidc-client-secret}
233 allowed_email_domains: [example.com]
234 # The Okta org authorization server returns a thin id_token that omits
235 # email and groups; the gateway fills them from /userinfo.
236 userinfo_fallback: true
237 # Okta emits groups only when the `groups` scope is requested and the
238 # app's groups claim filter allows them.
239 scopes: [openid, profile, email, offline_access, groups]
240
241 session:
242 jwt_secret: ${GATEWAY_JWT_SECRET} # EKS: ${file:/secrets/jwt-secret}
243 ttl_hours: 8 # bounds deprovision latency; lower
244 # toward 1 for tighter revocation
245
246 store:
247 postgres_url: ${GATEWAY_POSTGRES_URL} # EKS: ${file:/secrets/postgres-url}
248
249 upstreams:
250 - provider: bedrock
251 region: <your-region> # match $AWS_REGION so the IAM
252 # policy's ARNs cover it
253 auth: {} # AWS default credential chain:
254 # ECS task role, or IRSA on EKS
255 ```
256
257 <Note>
258 Only the `oidc` block is Okta-specific. To use Microsoft Entra ID instead, set `issuer` to `https://login.microsoftonline.com/<tenant-id>/v2.0`, drop `userinfo_fallback` and the `groups` scope, and note that Entra emits group Object IDs rather than names, so [`managed.policies`](/docs/en/claude-apps-gateway-config#managed) must match on the GUIDs, or on App Roles with `oidc.groups_claim: roles`. See [Identity provider setup](/docs/en/claude-apps-gateway-deploy#identity-provider-setup).
259 </Note>
260 </Step>
261
262 <Step title="Store secrets in AWS Secrets Manager">
263 Create three secrets; the execution role from the IAM step can already read them:
264
265 ```bash theme={null}
266 aws secretsmanager create-secret --name gateway-jwt-secret \
267 --secret-string "$(openssl rand -base64 32)"
268 aws secretsmanager create-secret --name gateway-oidc-client-secret \
269 --secret-string '<your-okta-client-secret>'
270 aws secretsmanager create-secret --name gateway-postgres-url \
271 --secret-string "$GATEWAY_POSTGRES_URL"
272 ```
273
274 Note the ARN each call prints; the ECS task definition references secrets by ARN.
275
276 <Note>
277 Literal `--secret-string` arguments are visible in the process table and in audit/EDR logs while each command runs. On a shared or monitored host, put the value in a `0600` file and pass `--secret-string file://<path>` instead. The bundle's `setup.sh` keeps secret values off process argv the same way, passing `0600` temporary files to `--cli-input-json`.
278 </Note>
279
280 Unlike the secrets, `gateway.yaml` itself contains no secret values, because every credential resolves at boot through [`${VAR}` or `${file:...}` expansion](/docs/en/claude-apps-gateway-config#secret-expansion). How everything reaches the container differs by track:
281
282 * On ECS, the next step's build copies `gateway.yaml` into the image at `/etc/claude/gateway.yaml`, and the task definition injects the three secrets as environment variables via its `secrets` field, so the YAML references `${GATEWAY_JWT_SECRET}`, `${OIDC_CLIENT_SECRET}`, and `${GATEWAY_POSTGRES_URL}`.
283 * On EKS, mount `gateway.yaml` from a ConfigMap and the secrets as files at `/secrets`, referenced as `${file:/secrets/...}`. Source the Kubernetes Secrets from Secrets Manager with External Secrets Operator or the Secrets Store CSI driver's AWS provider, or create them directly with `kubectl`.
284 </Step>
285
286 <Step title="Build and push the image to Amazon ECR">
287 Build the image per the [container image requirements](/docs/en/claude-apps-gateway-deploy#container-image), placing the `linux-x64` glibc binary at `./claude` in the build context. Write your own Dockerfile per those requirements or start from the bundle's [`Dockerfile`](https://github.com/anthropics/claude-code/blob/main/examples/gateway/aws/Dockerfile), which copies the filled-in `gateway.yaml` from the previous steps into the image at `/etc/claude/gateway.yaml`. On ECS that embedded copy is how the configuration reaches the container, which is why the build comes after the file is written. The EKS track instead mounts `gateway.yaml` from a ConfigMap at deploy, so the embedded copy is unused there.
288
289 The image also carries the AWS RDS certificate bundle as the trust anchor for the connection string's `sslmode=verify-full`, so download it into the build context first. AWS rotates the bundle (new regional CAs get appended), so download it per build rather than pinning a checksum or committing it:
290
291 ```bash theme={null}
292 curl -fL --proto '=https' -o rds-global-bundle.pem \
293 https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
294 ```
295
296 The container image requirements don't cover the bundle, so if you write your own Dockerfile, add the two lines that copy and trust it; the bundle's `Dockerfile` already includes both:
297
298 ```dockerfile theme={null}
299 COPY rds-global-bundle.pem /etc/claude/rds-global-bundle.pem
300 ENV NODE_EXTRA_CA_CERTS=/etc/claude/rds-global-bundle.pem
301 ```
302
303 Create the ECR repository and sign Docker in to it. Immutable tags mean the `<version>` tag the deploy step pins cannot later be silently re-pointed at a different image:
304
305 ```bash theme={null}
306 aws ecr create-repository --repository-name claude-gateway \
307 --image-tag-mutability IMMUTABLE \
308 --image-scanning-configuration scanOnPush=true
309 aws ecr get-login-password --region "$AWS_REGION" \
310 | docker login --username AWS --password-stdin \
311 "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
312 ```
313
314 Build and push the image. The task definition below runs `linux/amd64`, so the platform must match here; for Fargate on ARM64 (Graviton), build `linux/arm64` with the `linux-arm64` binary and set `cpuArchitecture` to `ARM64` instead:
315
316 ```bash theme={null}
317 docker build --platform=linux/amd64 \
318 -t "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/claude-gateway:<version>" .
319 docker push "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/claude-gateway:<version>"
320 ```
321 </Step>
322
323 <Step title="Deploy">
324 <Tabs>
325 <Tab title="ECS Fargate">
326 Create the cluster and a log group for the gateway's stderr, which carries both its audit events and operational logs. Retention is a separate call, and without one CloudWatch keeps the logs forever; align the 90 days with your audit retention policy:
327
328 ```bash theme={null}
329 aws ecs create-cluster --cluster-name claude-gateway
330 aws logs create-log-group --log-group-name /ecs/claude-gateway
331 aws logs put-retention-policy --log-group-name /ecs/claude-gateway \
332 --retention-in-days 90
333 ```
334
335 Write the task definition. The task role carries the Bedrock permission and the execution role injects the secrets; use the secret ARNs from the Secrets Manager step:
336
337 ```json claude-gateway-task.json theme={null}
338 {
339 "family": "claude-gateway",
340 "networkMode": "awsvpc",
341 "requiresCompatibilities": ["FARGATE"],
342 "cpu": "1024",
343 "memory": "2048",
344 "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
345 "executionRoleArn": "arn:aws:iam::<account-id>:role/claude-gateway-execution",
346 "taskRoleArn": "arn:aws:iam::<account-id>:role/claude-gateway-task",
347 "containerDefinitions": [
348 {
349 "name": "gateway",
350 "image": "<account-id>.dkr.ecr.<region>.amazonaws.com/claude-gateway:<version>",
351 "portMappings": [{ "containerPort": 8080 }],
352 "secrets": [
353 { "name": "GATEWAY_JWT_SECRET", "valueFrom": "<gateway-jwt-secret ARN>" },
354 { "name": "OIDC_CLIENT_SECRET", "valueFrom": "<gateway-oidc-client-secret ARN>" },
355 { "name": "GATEWAY_POSTGRES_URL", "valueFrom": "<gateway-postgres-url ARN>" }
356 ],
357 "logConfiguration": {
358 "logDriver": "awslogs",
359 "options": {
360 "awslogs-group": "/ecs/claude-gateway",
361 "awslogs-region": "<region>",
362 "awslogs-stream-prefix": "gateway"
363 }
364 }
365 }
366 ]
367 }
368 ```
369
370 Register it:
371
372 ```bash theme={null}
373 aws ecs register-task-definition --cli-input-json file://claude-gateway-task.json
374 ```
375
376 Put an internal ALB in front with a target group that health-checks the gateway. `--ip-address-type ipv4` matters: an internal dual-stack ALB publishes public-range AAAA records, which the `/login` private-network check rejects:
377
378 ```bash theme={null}
379 ALB_ARN="$(aws elbv2 create-load-balancer --name claude-gateway \
380 --scheme internal --type application --ip-address-type ipv4 \
381 --subnets $PRIVATE_SUBNETS --security-groups "$ALB_SG" \
382 --query 'LoadBalancers[0].LoadBalancerArn' --output text)"
383
384 TG_ARN="$(aws elbv2 create-target-group --name claude-gateway \
385 --protocol HTTP --port 8080 --vpc-id "$VPC_ID" --target-type ip \
386 --health-check-path /readyz \
387 --query 'TargetGroups[0].TargetGroupArn' --output text)"
388 ```
389
390 Add the HTTPS listener and raise the idle timeout. `--ssl-policy` pins a modern TLS floor, since omitting it falls back to the legacy `ELBSecurityPolicy-2016-08` default, which still accepts TLS 1.0/1.1. The idle timeout matters for streaming: the ALB closes a connection after 60 seconds with no data by default, which cuts off streams during quiet periods, such as long prompt processing before the first token:
391
392 ```bash theme={null}
393 aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" \
394 --protocol HTTPS --port 443 \
395 --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
396 --certificates CertificateArn=<your-acm-certificate-arn> \
397 --default-actions Type=forward,TargetGroupArn="$TG_ARN"
398
399 aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" \
400 --attributes Key=idle_timeout.timeout_seconds,Value=3600
401 ```
402
403 Create the service. The deployment circuit breaker rolls a deployment whose tasks keep failing, from a bad image or an unbootable config, back to the last steady state instead of relaunching failing tasks forever:
404
405 ```bash theme={null}
406 aws ecs create-service --cluster claude-gateway --service-name claude-gateway \
407 --task-definition claude-gateway --desired-count 1 --launch-type FARGATE \
408 --deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true}" \
409 --health-check-grace-period-seconds 60 \
410 --network-configuration "awsvpcConfiguration={subnets=[$(echo $PRIVATE_SUBNETS | tr ' ' ',')],securityGroups=[$GW_SG],assignPublicIp=DISABLED}" \
411 --load-balancers "targetGroupArn=$TG_ARN,containerName=gateway,containerPort=8080"
412 ```
413
414 The 60-second grace period gives a cold task time to pull the image, connect to the store, and answer its first health check before ECS starts counting failures against the deployment. The target group's health check on `GET /readyz` verifies the store is reachable, so a task that can't reach Postgres never enters rotation; see [Outage behavior](/docs/en/claude-apps-gateway-deploy#outage-behavior) for the tradeoff and the `/healthz` alternative.
415
416 The tasks run in private subnets with no public IP, so all egress (to Bedrock, your IdP, Secrets Manager, ECR, and CloudWatch Logs) goes through the NAT gateway. To keep Bedrock traffic off the public path, create a `bedrock-runtime` interface VPC endpoint and point the upstream's `base_url` at it, as shown in the [Bedrock upstream reference](/docs/en/claude-apps-gateway-config#amazon-bedrock); the IdP still needs internet egress.
417
418 Finish by giving developers a privately resolvable hostname: in a Route 53 private hosted zone, alias the gateway's internal DNS name to the ALB, and set `listen.public_url` to that hostname. The ALB's own `*.elb.amazonaws.com` name resolves to private addresses on an internal ALB, but it can't carry your ACM certificate, so use your own name.
419
420 Update the OAuth client's authorized redirect URI to `<public_url>/oauth/callback` before the first sign-in. After changing `public_url`, rebuild and push the image under a new tag, register a new task definition revision, and redeploy. On ECS the setting lives in the image's embedded `gateway.yaml`, and the gateway builds its public origin only from that setting, ignoring `X-Forwarded-Host` and `X-Forwarded-Proto`. `X-Forwarded-For` is honored for client IPs only when `listen.trusted_proxies` is set.
421 </Tab>
422
423 <Tab title="EKS">
424 This track needs `kubectl` and `eksctl` installed locally, and an existing EKS cluster with an IAM OIDC provider and the AWS Load Balancer Controller installed. The cluster must be on `$VPC_ID` so pods can reach the RDS private endpoint, and the `claude-gateway-db` security group must admit the cluster's pod or node security group in place of `$GW_SG`.
425
426 On EKS the gateway gets its Bedrock credentials through IRSA rather than the ECS roles. The `ecs-tasks.amazonaws.com` trust policy from the IAM step does not apply here; IRSA needs a role whose trust policy federates on the cluster's OIDC provider, scoped to `system:serviceaccount:claude-gateway:gateway`. `eksctl create iamserviceaccount` creates that role, attaches the policies, and annotates the Kubernetes service account with the role ARN in one step. Turn the two policy documents from the IAM step into managed policies it can attach:
427
428 ```bash theme={null}
429 BEDROCK_POLICY_ARN="$(aws iam create-policy --policy-name claude-gateway-bedrock-invoke \
430 --policy-document file://bedrock-invoke.json --query Policy.Arn --output text)"
431 SECRETS_POLICY_ARN="$(aws iam create-policy --policy-name claude-gateway-secrets-read \
432 --policy-document file://secrets-read.json --query Policy.Arn --output text)"
433
434 kubectl create namespace claude-gateway
435 eksctl create iamserviceaccount --cluster <your-cluster> --region "$AWS_REGION" \
436 --namespace claude-gateway --name gateway --role-name claude-gateway \
437 --attach-policy-arn "$BEDROCK_POLICY_ARN" \
438 --attach-policy-arn "$SECRETS_POLICY_ARN" \
439 --approve
440 ```
441
442 The secrets policy is needed only when the pods read Secrets Manager themselves, as the Secrets Store CSI driver's AWS provider does using the mounting pod's service account; drop it if you create the Kubernetes Secrets another way. The provider needs both of the policy's actions: it calls `DescribeSecret` when it reconciles rotated secrets, so a `GetSecretValue`-only grant mounts on the first deploy but stops picking up rotations.
443
444 Deploy the gateway as a standard Deployment plus a Service and an Ingress, as described in [Kubernetes deployment](/docs/en/claude-apps-gateway-deploy#kubernetes), with:
445
446 * `serviceAccountName: gateway`
447 * `gateway.yaml` mounted from a ConfigMap and the secrets mounted at `/secrets`
448 * the readiness probe pointed at `GET /readyz`
449
450 For the front end, an Ingress managed by the AWS Load Balancer Controller provisions the internal ALB. Annotate it with:
451
452 * `alb.ingress.kubernetes.io/scheme: internal` and `alb.ingress.kubernetes.io/target-type: ip`
453 * `alb.ingress.kubernetes.io/ip-address-type: ipv4`, so no public-range AAAA records are published for the `/login` [private-network check](/docs/en/claude-apps-gateway#prerequisites) to reject
454 * `alb.ingress.kubernetes.io/inbound-cidrs: <your-corporate-cidr>`, so the controller-managed frontend security group admits only your corporate network in place of its `0.0.0.0/0` default
455 * `alb.ingress.kubernetes.io/certificate-arn` with the ACM certificate
456 * `alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06`, so the listener doesn't fall back to the legacy default policy that accepts TLS 1.0 and 1.1
457 * `alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=3600`, so a 60-second data gap in a stream doesn't close the connection
458
459 With IRSA, the AWS SDK reads a projected service-account token and exchanges it with AWS STS, so the pod never needs the EC2 instance metadata service; an egress NetworkPolicy may block `169.254.169.254` for gateway pods. The node hop-limit problem in [Troubleshooting](#troubleshooting) below applies only to clusters that skip IRSA and rely on node instance roles.
460 </Tab>
461 </Tabs>
462 </Step>
463
464 <Step title="Push the gateway URL to developer machines">
465 The gateway is now running, but developers can't reach it from `/login` until the gateway URL is on their machines. Set `forceLoginMethod` and `forceLoginGatewayUrl` in the [managed settings file](/docs/en/claude-apps-gateway#set-the-gateway-url) you deploy to each device via MDM. There is no gateway option in the login picker for a developer to select manually.
466 </Step>
467</Steps>
468
469## Terraform reference
470
471The companion bundle at [`examples/gateway/aws`](https://github.com/anthropics/claude-code/tree/main/examples/gateway/aws) packages this page as code:
472
473* **`setup.sh`** scripts the provisioning walkthrough above with the same `aws` commands, on the ECS Fargate track. It is idempotent: existing resources are detected and skipped, so re-running it is safe, and any default can be overridden via environment variable. You still create the Okta OIDC client secret and the ACM certificate yourself: a run without them skips the ECS/ALB deploy, names the missing inputs, and prints the `create-secret` command; create both and re-run. The Bedrock use case form and the Route 53 alias print as next steps rather than running automatically, and the client MDM push stays a manual step from this page.
474* **`gateway.yaml.example`** is the configuration template from the gateway.yaml step, with the optional keys included commented out. Copy it to `gateway.yaml` and replace every `REPLACE_ME` before building.
475* **`Dockerfile`** builds the runtime image from the prebuilt `linux-x64` binary and copies in your filled-in `gateway.yaml` at `/etc/claude/gateway.yaml`, plus the AWS RDS certificate bundle that anchors the store's `sslmode=verify-full`. `setup.sh` downloads the bundle only when it isn't already in the build context; delete the file and rebuild under a new tag to pick up an AWS CA rotation. The config file holds no secret values, since every credential resolves at boot through `${VAR}` expansion. A config edit therefore means a rebuild under a new tag; `setup.sh` automates this by tagging images with a hash of the file.
476* **`terraform/`** provisions the same ECS Fargate scope declaratively: the security groups, IAM roles, ECR repository, RDS instance, Secrets Manager secrets, and the ECS service behind the internal ALB. The VPC and private subnets stay prerequisites, passed in as variables. Terraform creates the ECR repository but doesn't build the image, and the service definition references the image, so the apply is two passes: a targeted apply for the repository, then the build and push, then the full apply. The bundle's `terraform/README.md` covers the variables, remote state, and teardown.
477
478Like this page, the bundle is a working example for customer-managed infrastructure rather than a supported production deployment; review and adapt it to your own environment before relying on it.
479
480## Troubleshooting
481
482For gateway boot and login errors, see the platform-agnostic [troubleshooting table](/docs/en/claude-apps-gateway-deploy#troubleshooting). The entries below are specific to AWS.
483
484| Symptom | Cause | Fix |
485| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
486| CLI `/login`: `Gateway hosts must be on your organization's private network; <host> resolves to the public (or unrecognized) address <ip>` | The gateway name resolves to at least one public address. A dual-stack internal ALB publishes public-range AAAA records, and the [private-network check](/docs/en/claude-apps-gateway#prerequisites) requires every resolved address to be private | Create the ALB with `--ip-address-type ipv4`, or serve a separate internal-only DNS name with no public AAAA record |
487| Every Bedrock request returns 502; log shows `Could not load credentials from any providers` | The task runs on the ECS EC2 launch type without a task role, or the pod runs on an EKS node without IRSA, so credentials come from instance metadata, which IMDSv2's default hop limit of 1 stops inside a container. Neither track on this page is affected: Fargate task roles and IRSA don't use instance metadata | Prefer task roles and IRSA. Where instance credentials are unavoidable, raise the hop limit with `aws ec2 modify-instance-metadata-options --instance-id <id> --http-put-response-hop-limit 2`; the [platform-agnostic table](/docs/en/claude-apps-gateway-deploy#troubleshooting) covers the tradeoffs |
488| Bedrock requests return `403 AccessDeniedException` | The account hasn't submitted Anthropic's one-time use case form, the automatic AWS Marketplace subscription that starts on the account's first invoke hasn't finished yet, or the task role's policy is missing the inference-profile or foundation-model ARNs | Submit the use case form from the Bedrock console's Model catalog; if it was just submitted or this is the account's first invoke, retry after a few minutes. Grant `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` on both ARN families. |
489| Bedrock returns a `ValidationException` saying on-demand throughput isn't supported | A custom `models:` entry maps to a bare foundation-model ID that the region serves only through inference profiles | Map the model to its cross-region inference profile ID (`us.anthropic.*`) instead; the built-in catalog already does this |
490| ECS task stops with `ResourceInitializationError` before the gateway logs anything | The execution role can't read the Secrets Manager secrets, or the private subnets have no path to Secrets Manager or ECR | Grant `secretsmanager:GetSecretValue` on the three `gateway-` secrets' ARNs to the execution role, and provide egress via the NAT gateway, or, without one, interface endpoints for Secrets Manager, ECR, and CloudWatch Logs, which the `awslogs` driver needs at the same stage, plus an S3 gateway endpoint |
491| Gateway boot exits with a Postgres connection-timeout error | The database security group doesn't admit the gateway's security group on 5432, or the service runs outside the database's VPC; the store stops waiting after 5 seconds | Allow 5432 from the gateway's security group on the database's, and run the service in the same VPC as the DB subnet group |
492| Gateway boot exits with a Postgres TLS certificate verification error | The connection string sets `sslmode=verify-full` but the image doesn't trust the RDS CA bundle: the bundle wasn't copied into the image, or `NODE_EXTRA_CA_CERTS` doesn't point at it | Add the build step's two Dockerfile lines that copy the bundle and set `NODE_EXTRA_CA_CERTS`, then rebuild, push under a new tag, and redeploy |
493| Streaming responses drop mid-stream after a quiet period | The ALB idle timeout closes the connection after 60 seconds with no data by default. A stream that is actively emitting tokens isn't affected; one that goes quiet, during long prompt processing before the first token or extended thinking with no streamed output, is cut at the gap | Set the `idle_timeout.timeout_seconds` attribute to `3600`, via `modify-load-balancer-attributes` or the `load-balancer-attributes` Ingress annotation on EKS |
494
495## Telemetry
496
497The gateway gives you per-developer usage metrics without any per-machine OTEL configuration. Claude Code emits OpenTelemetry (OTLP) metrics, logs, and opt-in traces; [Monitoring usage](/docs/en/monitoring-usage) covers everything the CLI reports. On gateway sessions the CLI stamps each export with the authenticated IdP identity attributes `user.id`, `user.email`, and `user.groups`, so usage rolls up per developer with no `OTEL_RESOURCE_ATTRIBUTES` plumbing.
498
499The gateway itself is an authenticated OTLP relay. Set [`telemetry.forward_to`](/docs/en/claude-apps-gateway-config#telemetry) together with `listen.public_url`, and it pushes the OTEL exporter settings to every connected client and forwards their OTLP traffic verbatim to each destination you list. Each destination opts into metrics, logs, and traces independently, and the default is metrics only; see the [`telemetry` reference](/docs/en/claude-apps-gateway-config#telemetry) for the per-signal fields and their sensitivity tradeoffs. The gateway doesn't buffer, aggregate, or store telemetry, so where the data lands is entirely the collector's exporter configuration.
500
501Client telemetry is off by default; configuring `telemetry.forward_to` is what turns it on for connected developers, and each interactive client shows a one-time security approval dialog for the pushed settings, as described in the [configuration reference](/docs/en/claude-apps-gateway-config#telemetry). On AWS, each signal maps to a destination as follows.
502
503### Client metrics, logs, and traces
504
505Point `telemetry.forward_to` at an OpenTelemetry collector, such as the [AWS Distro for OpenTelemetry (ADOT) collector](https://aws-otel.github.io/), and export from there to Amazon CloudWatch, Amazon Managed Service for Prometheus, or any OTLP backend.
506
507Run the collector as its own internal service reachable over `https://`: the gateway accepts plaintext `http://` only for loopback URLs, and even then its [SSRF guard](/docs/en/claude-apps-gateway-deploy#threat-model-summary) blocks loopback connections at send time by default. A sidecar collector on `http://localhost:4318` passes config validation but receives no traffic, with exports failing as `ECONNREFUSED_SSRF` in the gateway logs, unless `CLAUDE_GATEWAY_ALLOW_LOOPBACK=1` is set in the gateway's environment. That variable relaxes the loopback block for every operator-configured URL, not only telemetry, so prefer the internal-service pattern and reserve the sidecar-plus-flag setup for tasks whose network is otherwise locked down.
508
509### Gateway logs
510
511On ECS Fargate, no extra setup: the `awslogs` driver delivers the gateway's stderr, which carries its audit events and operational logs, to the `/ecs/claude-gateway` log group created above. On EKS, pod logs don't reach CloudWatch by default, so the audit trail is lost until you install log collection: the Amazon CloudWatch Observability add-on with container log capture enabled, or a Fluent Bit DaemonSet. On either track, query the logs with CloudWatch Logs Insights and drive alarms from metric filters.
512
513### Container metrics
514
515Enable Container Insights on the cluster with `aws ecs update-cluster-settings --cluster claude-gateway --settings name=containerInsights,value=enabled` for per-task CPU, memory, and network. On EKS, install the Amazon CloudWatch Observability add-on.
516
517### Spend
518
519Telemetry shows usage after the fact; [spend limits](/docs/en/claude-apps-gateway-spend-limits) are the gateway's live per-developer view and enforcement on top of the shared upstream credential.
520
521## Next steps
522
523* [Configuration reference](/docs/en/claude-apps-gateway-config): every `gateway.yaml` option, including `managed.policies` and `telemetry`
524* [Deployment and operations](/docs/en/claude-apps-gateway-deploy): IdP setup, health checks, JWT secret rotation, upgrades, and the security model
525* [Claude apps gateway overview](/docs/en/claude-apps-gateway): quickstart and connecting developers
526* [AWS samples for Claude apps gateway](https://github.com/aws-samples/anthropic-on-aws/tree/main/claude-apps-gateway): AWS-maintained deployment samples covering a range of customer environments