Skip to main content
This page covers the operational side of running Claude apps gateway: registering an OAuth client in your identity provider (IdP), deploying the gateway as a container, and running it day-to-day. For every option in the gateway.yaml file the gateway reads at boot, see the Configuration reference. A production deployment follows four steps in order, and the sections below match them. The first two are where you make choices; the second two are reference material to consult once it’s running.
  1. Set up your identity provider: register the OAuth client and check the per-IdP notes for Okta, Entra, and Google
  2. Deploy the gateway: build a pinned container image and run it on Kubernetes, Cloud Run, or your own platform. This section also covers cost, bypass, multiple-gateway, and serverless decisions
  3. Set up operations: logs, health probes, outage behavior, secret rotation, and upgrades. Reference for when you’re setting up monitoring and runbooks
  4. Review the security posture: what data flows where, the threat model, and compliance answers. Reference for a security review
If a sign-in or boot fails along the way, go straight to Troubleshooting, which is keyed on the error you see.
Deploy on your private network. Claude Code only connects to a gateway whose address is private. This is a security guard, because a trusted gateway can push settings that run commands on developer machines. Put the gateway you deploy behind an internal load balancer or VPN and give it a hostname that resolves to private IPs only.

Identity provider setup

Register a confidential OAuth/OpenID Connect (OIDC) web application with a single redirect URI, https://<gateway>/oauth/callback, and assign it to the users or groups who should have gateway access. Any OIDC-compliant IdP works: Okta, Microsoft Entra ID, Google Workspace, Keycloak, Dex, PingFederate, and others. The IdP must meet three requirements:
  • Serves /.well-known/openid-configuration, over HTTPS in production; the gateway accepts an http:// issuer, and a loopback issuer additionally requires CLAUDE_GATEWAY_ALLOW_LOOPBACK=1
  • Supports the authorization-code flow. PKCE (Proof Key for Code Exchange) is on by default; disable it with oidc.use_pkce: false for IdPs that don’t support it
  • Returns email and optionally groups in the id_token, or serves them from the userinfo endpoint with oidc.userinfo_fallback: true
For private PKI, set oidc.ca_cert_pem. A few providers handle email and group claims differently:
  • Okta: the org authorization server at https://example.okta.com returns a thin id_token that omits email and groups, so set oidc.userinfo_fallback: true whenever you use it as issuer. A custom authorization server such as https://example.okta.com/oauth2/default that includes email and optionally groups in the id_token emits them directly and needs no fallback. Okta emits groups only when the groups scope is requested in oidc.scopes and the app’s groups claim filter allows it; userinfo_fallback can’t fill a claim the IdP wasn’t asked for.
  • Microsoft Entra ID: issuer = https://login.microsoftonline.com/<tenant-id>/v2.0. Entra emits group Object IDs rather than names, so use the GUIDs in managed.policies.match.groups, or use App Roles for human-readable names. If your tenant emits roles under roles instead of groups, set oidc.groups_claim: roles.
  • Google Workspace: issuer = https://accounts.google.com. Google’s id_token doesn’t carry groups. To use group-based allowed_groups or managed.policies with Google as the IdP, configure oidc.google_groups, which looks up each user’s groups through the Admin SDK Directory API using a service account with domain-wide delegation. Without it, use oidc.allowed_email_domains for membership gating and managed.policies.match.email_domain for policy assignment. Google also ignores the standard offline_access scope. For refresh tokens, set oidc.scopes: [openid, profile, email] and oidc.extra_auth_params: { access_type: offline, prompt: consent }.
Refresh tokens let the gateway renew a developer’s session silently, without sending the developer back to the browser. They also drive deprovisioning, because when the IdP disables a user, the next refresh fails and the session ends within ttl_hours. The gateway requests offline_access by default to get a refresh token. If your IdP requires explicit consent for offline access, configure the OAuth client to allow it.If your IdP can’t issue refresh tokens at all, the gateway still works, but there is no silent renewal, so developers re-run the browser login when their session expires. To keep that from happening every hour, raise session.ttl_hours to 8 or 12. The tradeoff is deprovisioning latency, because without refresh tokens a disabled user keeps access until the longer TTL elapses.

Deployment

The gateway is a single stateless Linux binary that coordinates through Postgres, so deploy it the way you deploy any other stateless service in your environment. Keep it inside your network, where your developers and IdP can reach it over HTTPS, and treat it like any service holding a production credential. A few decisions shape the deployment beyond where it runs:
  • Cost: no separate license or per-seat fee. The gateway is part of the claude binary, so you pay for inference through your existing commitment, plus the compute it runs on.
  • Bypass: the gateway doesn’t enforce that the only route to a model goes through it. A developer with their own credential can still call the provider directly, so closing that path is a network policy decision, for example blocking egress to api.anthropic.com except from the gateway. Blocking that egress also breaks the WebFetch domain safety check, which calls api.anthropic.com from each developer’s machine. Set skipWebFetchPreflight: true in the managed policy to disable it.
  • Multiple gateways: each is a separate deployment with its own config, and the CLI stores trust and credentials per gateway hostname, so teams can use different gateways without conflict. To serve multiple OIDC issuers, run separate instances.
  • Serverless: Cloud Run works if you set min-instances: 1 to avoid cold OIDC discovery. Lambda and Cloud Functions don’t work, because the gateway is a long-running HTTP server.
Every production topology here puts an L7 proxy, such as an Ingress, Cloud Run’s front end, or an ALB, in front of plain-HTTP replicas. Set listen.trusted_proxies to the proxy’s source ranges so the gateway reads client IPs from X-Forwarded-For. The gateway honors the header only when the TCP peer is trusted. The Google Cloud and AWS worked examples have concrete values per topology. Without trusted proxies, every request appears to come from the proxy’s IP, which collapses per-IP rate limits into one shared bucket and records the proxy’s IP in audit events. Give the proxy any idle timeout longer than the gateway’s keepalive interval, which depends on the upstream:
  • On every upstream except provider: anthropic, the gateway writes an SSE ping once a stream has been silent for about 15 seconds.
  • On provider: anthropic, the gateway passes the response through unchanged, including the Anthropic API’s own pings.
A default such as the ALB’s 60 seconds is enough to keep a quiet stream open. The AWS worked example raises it to an hour anyway, and its troubleshooting row covers gateways older than v2.1.229, which sent nothing during quiet periods on the upstreams that now get pings.

Container image

Build your own image around the native claude binary from the standard Claude Code release:
  1. Download the Linux build for your image architecture from a pinned release; see Install a specific version for the download URL.
  2. Verify it against the release’s GPG-signed manifest.json as described in Binary integrity and code signing.
  3. Copy it into the build context.
Mirror the release into your internal registry if your builds can’t reach the release host, and pin the version your fleet runs. Beyond the binary, the image needs:
  • A glibc-based image: the glibc build’s only dynamic dependencies are glibc libraries. Musl-based images need the linux-x64-musl or linux-arm64-musl build plus additional packages; see Alpine Linux setup.
  • A writable state directory: the gateway runs as any user, but minimal images have no writable home. Set CLAUDE_CONFIG_DIR to a writable path such as /tmp/.claude.
  • The container command: claude gateway --config /etc/claude/gateway.yaml, with the config file mounted read-only and secrets supplied as environment variables; the gateway listens on listen.port, default 8080.

Kubernetes

Run the gateway as a Deployment, like any stateless service:
  • Mount the config from a ConfigMap and secrets from a Secret; reference secrets in the YAML via ${file:/path/to/secret} or as environment variables
  • Terminate TLS at the Ingress and set listen.public_url to the Ingress hostname
  • Point the readiness probe at GET /readyz and the liveness probe at GET /healthz
For a complete worked example on AWS, covering ECS Fargate or EKS, Amazon RDS, and AWS Secrets Manager, see Deploy on AWS. Prefer the platform’s workload identity over static keys; the upstreams reference has per-platform setup details. For a cross-cloud pairing, such as an Amazon Bedrock upstream on GKE, set explicit credentials in the upstream’s auth block instead.

Cloud Run

Configure the service as follows:
  • Leave listen.port at its default of 8080, which matches Cloud Run’s default PORT, or set port: ${PORT}
  • Set public_url to the externally reachable origin. For production this is normally an internal load balancer’s hostname, because /login rejects public addresses and the *.run.app URL resolves to one, so the Cloud Run URL alone works only for a curl or browser smoke test. The exception is a network where *.run.app resolves privately through Private Service Connect and a Cloud DNS private zone; in that topology the Cloud Run URL is a valid public_url. The Google Cloud worked example covers both.
  • Mount the config as a secret volume
  • Set min-instances: 1 to avoid a cold OIDC discovery on first request
For a complete worked example on Google Cloud, covering Cloud Run or GKE, Cloud SQL, and Secret Manager, see Deploy on Google Cloud.

Push the gateway URL to developer machines

Once the gateway is serving, push forceLoginMethod, forceLoginGatewayUrl, and parentSettingsBehavior: "merge" to each developer’s machine through managed settings, via MDM or by writing the per-OS managed-settings.json directly. Without this, /login shows the standard account picker with no gateway option. See Client-side managed settings for the file paths and the Claude Desktop bootstrapUrl equivalent.

Operations

Once the gateway is serving traffic, day-to-day operation is reading its logs, probing its health, and rotating its secrets on your schedule. The subsections cover each, plus what Postgres holds and how upgrades and rollbacks behave.

Logs

The gateway writes two streams to stderr, both JSON-friendly:
  • Audit events: single-line JSON per security-relevant event. Pipe stderr to your log aggregator. The events emitted include config.load, session.mint, session.refresh, device.authorize, device.verify, device.callback, auth.denied, access.denied, inference, managed.serve, desktop_bootstrap.serve, desktop_bootstrap.denied, spend.blocked, admin.denied, admin.limit.upsert, and admin.limit.delete. Fields vary by event:
    • Successful mint and refresh events carry sub, email, client_ip, and the result
    • auth.denied and access.denied carry the reason and client IP, plus the request path for auth.denied, since no user identity exists at those denials
    • inference records which upstream served the request and the response status
    • desktop_bootstrap.denied records a rejected Claude Desktop bootstrap fetch with the reason (not_configured, policy_not_opted_in, or no_policy_matched) and the user’s identity
    • admin.denied records a rejected admin-API auth attempt with the client IP, method, path, and a reason, without the presented key material: invalid_key when an x-api-key was presented but matched no configured key, bearer_rejected when only an Authorization header was presented and it didn’t verify as a gateway session in admin.admin_groups, or no_credentials when neither header was presented
  • Operational logs: human-readable [gateway]-prefixed lines for boot, warnings, and upstream errors. The CLAUDE_GATEWAY_LOG_LEVEL environment variable controls verbosity and accepts debug, info, warn, or error, with info as the default. At debug, each sign-in and refresh also logs the names, not the values, of the claims in the id_token, plus the names of the userinfo claims when userinfo_fallback supplied any, so you can diagnose email_claim and groups_claim settings without logging PII. It doesn’t affect audit events, which are always emitted.