Claude apps gateway:部署与运维
向身份提供商注册网关、构建容器镜像、在 Kubernetes 或 Cloud Run 部署,以及日常运维:日志、健康探针、并发上游请求、故障行为、密钥轮换、Postgres、升级、安全模型与排障表。
本页讲运行 Claude apps gateway 的运维面:在身份提供商(IdP)里注册 OAuth 客户端、把网关作为容器部署,以及日常运行。网关在启动时读取的 gateway.yaml 的每个选项见配置参考。
生产部署按顺序分四步,下面的章节与之对应。前两步是你要做选择的地方,后两步是运行起来之后查阅的参考资料:
- 设置身份提供商:注册 OAuth 客户端,并查看 Okta、Entra 和 Google 各自的说明
- 部署网关:构建固定版本的容器镜像并在 Kubernetes、Cloud Run 或你自己的平台上运行;这一节还涵盖成本、绕过、多网关和无服务器方面的决定
- 设置运维:日志、健康探针、故障行为、密钥轮换和升级;在设置监控和运行手册时查阅
- 审查安全姿态:什么数据流向哪里、威胁模型和合规问题的答案;做安全评审时查阅
沿途如果登录或启动失败,直接看「排障」,它按你看到的错误组织。
在你的私有网络上部署。 Claude Code 只连接地址为私有的网关。这是一道安全防护,因为受信任的网关可以推送在开发者机器上运行命令的设置。把你部署的网关放在内部负载均衡器或 VPN 后面,并给它一个只解析到私有 IP 的主机名。如果你的内部网络用的是你的组织拥有的公网 IPv4 空间编号,见「允许在你拥有的公网地址空间上的网关」。
身份提供商设置
注册一个机密的 OAuth/OpenID Connect(OIDC)web 应用,只有一个重定向 URI https://<gateway>/oauth/callback,并把它分配给应有网关访问权的用户或组。任何兼容 OIDC 的 IdP 都可以:Okta、Microsoft Entra ID、Google Workspace、Keycloak、Dex、PingFederate 等。IdP 必须满足三个要求:
- 提供
/.well-known/openid-configuration,生产环境用 HTTPS;网关接受http://的 issuer,环回 issuer 另外要求CLAUDE_GATEWAY_ALLOW_LOOPBACK=1 - 支持授权码流程。PKCE(Proof Key for Code Exchange)默认开启,对不支持它的 IdP 用
oidc.use_pkce: false关闭 - 在 id_token 里返回
email(以及可选的groups),或在设了oidc.userinfo_fallback: true时从 userinfo 端点提供它们
对私有 PKI,设置 oidc.ca_cert_pem。少数提供商处理邮箱和组声明的方式不同:
- Okta:
https://example.okta.com上的组织授权服务器返回的 id_token 很薄,省略email和groups,所以用它作issuer时总要设oidc.userinfo_fallback: true。像https://example.okta.com/oauth2/default这样在 id_token 里包含email(以及可选groups)的自定义授权服务器直接发出它们,不需要回退。Okta 只在oidc.scopes里请求了groupsscope 且应用的组声明过滤器允许时才发出groups;userinfo_fallback无法补全 IdP 没被请求的声明。 - Microsoft Entra ID:
issuer=https://login.microsoftonline.com/<tenant-id>/v2.0。Entra 发出组的对象 ID 而不是名字,所以在managed.policies.match.groups里用 GUID,或用 App Roles 得到人类可读的名字。如果你的租户把角色放在roles而不是groups下发出,设oidc.groups_claim: roles。 - Google Workspace:
issuer=https://accounts.google.com。Google 的 id_token 不带组。要在以 Google 作 IdP 时使用基于组的allowed_groups或managed.policies,配置oidc.google_groups,它用带域范围委派的服务账号经 Admin SDK Directory API 查找每个用户的组;没有它时,用oidc.allowed_email_domains做成员把关,用managed.policies.match.email_domain做策略分配。Google 还忽略标准的offline_accessscope;要得到刷新令牌,设oidc.scopes: [openid, profile, email]和oidc.extra_auth_params: { access_type: offline, prompt: consent }。
注意:刷新令牌让网关能静默续期开发者的会话,不必把开发者送回浏览器。它们还驱动取消预配:IdP 禁用用户后,下一次刷新失败,会话在 ttl_hours 内结束。网关默认请求 offline_access 来获得刷新令牌;如果你的 IdP 要求对离线访问显式同意,要把 OAuth 客户端配置为允许它。如果你的 IdP 完全无法签发刷新令牌,网关仍能工作,但没有静默续期,开发者要在会话过期时重新运行浏览器登录;要避免这每小时发生,把 session.ttl_hours 调高到 8 或 12,代价是取消预配的延迟,因为没有刷新令牌时,被禁用的用户在更长的 TTL 过去之前仍保有访问。
部署
网关是一个通过 Postgres 协调的无状态 Linux 二进制,所以按你环境里部署其他无状态服务的方式部署它。把它放在你的网络内,让开发者和 IdP 能通过 HTTPS 到达,并把它当作任何持有生产凭据的服务来对待。除了运行在哪里,还有几个决定塑造部署:
- 成本:没有单独的许可证或按席位收费。网关是
claude二进制的一部分,所以你按现有承诺为推理付费,加上它运行所用的计算。 - 绕过:网关不强制通往模型的唯一路由要经过它。持有自己凭据的开发者仍然可以直接调用提供商,所以关闭这条路径是网络策略的决定,例如阻止除网关之外的、到
api.anthropic.com的出口。阻止该出口也会破坏 WebFetch 域名安全检查(它从每个开发者的机器调用api.anthropic.com);在托管策略里设skipWebFetchPreflight: true可禁用它。 - 多个网关:每个是单独的部署,有自己的配置,CLI 按网关主机名存储信任和凭据,所以团队能使用不同的网关而不冲突。要服务多个 OIDC issuer,就运行单独的实例。
- 无服务器:Cloud Run 可行,只要设
min-instances: 1以避免冷启动的 OIDC 发现;Lambda 和 Cloud Functions 不行,因为网关是长期运行的 HTTP 服务器。
这里每个生产拓扑都在纯 HTTP 副本前面放一个 L7 代理(如 Ingress、Cloud Run 的前端或 ALB)。把 listen.trusted_proxies 设为代理的源范围,让网关从 X-Forwarded-For 读取客户端 IP;网关只在 TCP 对端受信任时才采信该头。Google Cloud 和 AWS 的工作示例里有每种拓扑的具体值。没有受信任的代理时,每个请求看起来都来自代理的 IP,这会让每 IP 的速率限制塌缩成一个。不要重定向到网关设备授权和令牌端点的请求(例如在入口处做 HTTP 到 HTTPS 或主机名规范化的重写):Claude Code 对这些请求不跟随重定向,所以重定向它们的入口规则会破坏登录和令牌刷新。要给代理设置比网关的 keepalive 间隔更长的任何空闲超时,该间隔取决于上游:在 provider: anthropic 之外的每个上游上,流静默约 15 秒后网关写一个 SSE ping;在 provider: anthropic 上,网关原样透传响应,包括 Anthropic API 自己的 ping。ALB 的 60 秒这样的默认值足以让安静的流保持打开;AWS 工作示例仍把它调高到一小时,其排障行涵盖 v2.1.229 之前的网关(它们在现在已有 ping 的上游上的安静期间什么都不发)。
容器镜像
围绕标准 Claude Code 发布的原生 claude 二进制构建你自己的镜像:从固定版本的发布下载你镜像架构的 Linux 构建(下载 URL 见「安装特定版本」);对照发布的 GPG 签名 manifest.json 验证(见「二进制完整性与代码签名」);把它复制进构建上下文。如果你的构建无法访问发布主机,就把发布镜像到你的内部仓库,并固定你的机队运行的版本。除二进制外,镜像还需要:
- 基于 glibc 的镜像:glibc 构建唯一的动态依赖是 glibc 库;基于 musl 的镜像需要
linux-x64-musl或linux-arm64-musl构建加额外的软件包(见 Alpine Linux 设置)。 - 可写的状态目录:网关能以任何用户运行,但最小镜像没有可写的 home;把
CLAUDE_CONFIG_DIR设为/tmp/.claude这样的可写路径。 - 容器命令:
claude gateway --config /etc/claude/gateway.yaml,配置文件只读挂载,密钥作为环境变量提供;网关监听listen.port,默认8080。
Kubernetes
像任何无状态服务一样把网关作为 Deployment 运行:从 ConfigMap 挂载配置、从 Secret 挂载密钥,YAML 里通过 ${file:/path/to/secret} 或作为环境变量引用密钥;在 Ingress 终止 TLS 并把 listen.public_url 设为 Ingress 主机名;把就绪探针指向 GET /readyz,存活探针指向 GET /healthz。AWS 上的完整工作示例(涵盖 ECS Fargate 或 EKS、Amazon RDS 和 AWS Secrets Manager)见「在 AWS 上部署」。优先用平台的工作负载身份而不是静态密钥(每个平台的设置细节见 upstreams 参考);对跨云组合(如 GKE 上的 Amazon Bedrock 上游),改在上游的 auth 块里设显式凭据。
Cloud Run
按下面配置服务:listen.port 保持默认 8080(与 Cloud Run 默认的 PORT 一致),或设 port: ${PORT};把 public_url 设为外部可达的源——生产里这通常是内部负载均衡器的主机名,因为 /login 拒绝公网地址、而 *.run.app URL 解析到公网地址,所以单独的 Cloud Run URL 只适用于 curl 或浏览器冒烟测试;例外是 *.run.app 通过 Private Service Connect 和 Cloud DNS 私有区域私有解析的网络,在那种拓扑里 Cloud Run URL 是有效的 public_url(Google Cloud 工作示例涵盖两者);把配置挂载为密钥卷;设 min-instances: 1 以避免首次请求时冷启动的 OIDC 发现。Google Cloud 上的完整工作示例见「在 Google Cloud 上部署」。
把网关 URL 推送到开发者机器
网关开始服务之后,通过托管设置把 forceLoginMethod、forceLoginGatewayUrl 和 parentSettingsBehavior: "merge" 推送到每个开发者的机器,经 MDM 或直接写每个操作系统的 managed-settings.json。没有这一步,/login 显示的是没有网关选项的标准账号选择器。部署这些键之后,Claude Code 不再使用机器上遗留的 API key 或 claude.ai 登录,所以要把推送与你的登录说明一起规划(开发者看到的消息见「管理员策略要求 Cloud 网关登录」)。每种机制把策略存在哪里的文件路径,以及 Claude Desktop bootstrapUrl 的等价物,见「客户端托管设置」。
大规模推广
登录按客户端 IP 地址做速率限制,默认值适合小团队:每个地址每 10 分钟 30 次登录开始和 10 次代码提交。向数千开发者推广可能在第一天早上就达到这些限制,原因有两种:
- 网关看不穿你的负载均衡器。 没有
listen.trusted_proxies,每个开发者看起来都来自负载均衡器的地址并共享一个限制。要先设它,网关在第一次忽略X-Forwarded-For头时会记录警告。 - 许多开发者共享少数几个 NAT 或 VPN 出口地址。 即使
trusted_proxies正确,他们也共享这些地址的限制。要调高rate_limits来适应。
确定 max:用开发者数除以他们共享的出口地址数,估算其中多少人会在一个 window_seconds 周期(默认 10 分钟)内登录,再翻倍以涵盖重试和同时登录 Claude Code 与 Claude Desktop 的开发者。例如,10,000 个开发者在 4 个出口地址后面,在一小时里均匀登录:每个地址 2,500 个开发者,每 10 分钟约 420 个,翻倍并向上取整为 1,000。下面的例子把两个限制都设为 1,000:
rate_limits:
device_authorization: { max: 1000, window_seconds: 600 }
device_verify: { max: 1000, window_seconds: 600 }device_verify 是阻止有人猜别的开发者登录码的东西,所以只调高到你的估算需要的程度。即使在这些限制下,码也是取自 20 个字符的字母表的 8 个字符且 10 分钟后过期,所以猜测仍不可行(见「用户码暴力破解的抵抗」)。当你的 IdP 签发刷新令牌时,Claude Code 静默续期会话,所以推广之后可以把限制调回去;没有刷新令牌时,开发者每 session.ttl_hours 再次登录,两个限制也要按这个稳态速率定大小并保持调高。达到限制时,Claude Code v2.1.274 及以上显示 The gateway is limiting sign-in attempts right now;v2.1.274 及以上的网关在验证页面上显示 Too many attempts came from your network address 并给出要检查的设置,还会写一行点名要更改的设置的 sign-in refused 日志。
运维
网关开始处理流量之后,日常运维就是读它的日志、探测它的健康,以及按你的计划轮换它的密钥。下面各小节涵盖每一项,外加 Postgres 里存什么以及升级和回滚的行为。
日志
网关向 stderr 写两个流,都对 JSON 友好:
审计事件:每个与安全相关的事件一行 JSON;把 stderr 接到你的日志聚合器。发出的事件包括 config.load、session.mint、session.refresh、device.authorize、device.verify、device.callback、auth.denied、access.denied、access.public_client、inference、managed.serve、desktop_bootstrap.serve、desktop_bootstrap.denied、spend.blocked、admin.denied、admin.limit.upsert 和 admin.limit.delete。字段随事件而异:
- 成功的 mint 和 refresh 事件携带
sub、email、client_ip和结果 auth.denied和access.denied携带原因和客户端 IP,auth.denied还有请求路径,因为在这些拒绝处不存在用户身份。两个access.denied原因会改变事件携带的内容:xff_unparseable——事件还携带无法读取的X-Forwarded-For条目;client_ip_unknown——事件不带客户端 IP,因为设了access_control列表而连接没有对端地址access.public_client携带access_control.allow_cidrs为空时、每个进程第一个来自公网地址的请求的客户端 IP;网关照常服务该请求,事件表示网关可能能从公网到达(什么算公网以及推荐的允许列表见access_control参考)inference记录哪个上游服务了请求以及响应状态desktop_bootstrap.denied记录被拒绝的 Claude Desktop bootstrap 获取,带原因(not_configured、policy_not_opted_in或no_policy_matched)和用户身份admin.denied记录被拒绝的管理 API 认证尝试,带客户端 IP、方法、路径和原因,不含出示的密钥材料:出示了x-api-key但不匹配任何已配置密钥时是invalid_key;只出示了Authorization头且它没有验证为admin.admin_groups里的网关会话时是bearer_rejected;两个头都没出示时是no_credentials
运维日志:用于启动、警告和上游错误的、带 [gateway] 前缀的人类可读行。环境变量 CLAUDE_GATEWAY_LOG_LEVEL 控制详细程度,接受 debug、info、warn 或 error,默认 info。在 debug 下,每次登录和刷新还会记录 id_token 里声明的名字(不是值),以及 userinfo_fallback 提供了任何声明时的 userinfo 声明名字,使你能在不记录个人信息的情况下诊断 email_claim 和 groups_claim 设置;它不影响审计事件,审计事件总是发出。
健康
网关在 GET /healthz 提供存活探针,在 GET /readyz 提供就绪探针。/readyz 验证存储可达;如果你设了 store.readiness_grace_seconds,/readyz 在存储停止应答之后最多那么多秒内仍报告就绪。两个端点都不受 access_control.allow_cidrs 限制,所以探针在锁定的监听器上仍能工作。位于 /.well-known/oauth-authorization-server 的 OAuth 发现文档也只在配置加载、OIDC 发现、上游客户端构造和 Postgres 迁移全部成功之后才返回 200,所以它兼作端到端的启动检查。
并发上游请求
默认每个网关副本同时最多向上游发 256 个请求;流式响应在流结束前一直计入限制。副本达到限制时到达的请求在网关内等待空闲槽位,开发者看到的是启动缓慢或似乎挂起的响应。在 provider: anthropic 上游上,等待时间超过 timeouts.upstream_ttfb_ms 的请求会放弃该上游,没有后面的上游服务它时以 502 失败。启动日志里含 upstream requests: 的一行显示生效的限制;副本打开的请求多于限制时,它还会记录含 client requests are open 的警告,每分钟最多一次。要同时服务更多请求,有两个选择:增加副本;调高每个副本的限制——在网关容器上设环境变量 BUN_CONFIG_MAX_HTTP_REQUESTS 为 1 到 65535 的整数,然后重启容器。副本在约为限制除以请求平均保持打开秒数的请求速率下占满限制,例如请求平均保持打开 10 秒时,默认限制 256 的副本在约每秒 26 个请求时占满。如果你按 CPU 自动伸缩,达到限制的副本会排队请求而不触发扩容,所以要把目标设在副本记录 client requests are open 警告时所显示的 CPU 水平之下。
注意:每个打开的请求在流式传输和等待槽位期间都占用网关进程里的内存。即使把限制保持在 256,过载副本上的内存仍会增长,因为等待的请求保留着它们的请求体。要按峰值时打开的请求数给容器内存定大小,更改限制时要观察内存;内存耗尽的副本会被杀掉并丢掉它持有的每个流。
故障行为
Postgres 宕机时,网关本身继续为已登录的开发者服务,新登录失败。开发者是否真能继续工作取决于你的编排器如何处理就绪:
- 已有会话:bearer 令牌用 JWT 密钥本地验证,会话刷新不碰存储,网关进程仍能提供推理
- 新登录:失败直到 Postgres 恢复,因为设备流程和它的速率限制计数器存在 Postgres 里
- 支出限额强制:中断期间默认失败放行,所以推理仍然流动;如果你宁愿阻止也不愿不计量地运行,把它改成失败关闭
- 就绪:默认
/readyz在 Postgres 一不可达就报告未就绪,所以每个副本同时就绪检查失败;在流量只到达通过检查的副本的地方,所有流量(包括网关本来还能服务的推理)都会失败,直到 Postgres 恢复;/healthz上的存活探针始终通过
如果你的 IdP 宕机,已有会话在 ttl_hours 之前仍能工作,新登录失败;会话刷新得到重试的答复,IdP 恢复后成功。如果你的 IdP 有频繁的维护窗口,设更长的 ttl_hours。
就绪宽限期。 要让已登录的开发者在短暂的 Postgres 中断(如数据库故障转移)期间继续工作,把 store.readiness_grace_seconds 设为比故障转移耗时更长的值,例如 300。开启支出限额且为默认的失败放行行为时,经保持就绪的副本的请求在 Postgres 恢复前不被计量,所以要把该值保持在刚好涵盖你的故障转移的低值。如果你设了 enforcement.fail_closed_on_error: true,网关会以 429 spend limit unavailable 消息拒绝已登录开发者的推理,直到 Postgres 恢复,即使副本仍通过就绪检查。该设置需要网关服务器 Claude Code v2.1.282 及以上;更早的网关遇到该键会拒绝启动,所以要在加它之前升级每个副本(回滚见「升级」)。如果你改把就绪探针指向 /healthz,副本在中断期间也继续通过它,但 /healthz 从不报告未就绪,所以 Postgres 连接没有恢复的副本也继续通过。
JWT 密钥轮换
分阶段轮换签名密钥,使已有会话保持有效:1. 生成新密钥,前置到 session.jwt_secret 数组;2. 滚动部署(新令牌用新密钥签名,旧令牌仍能验证);3. 在 ttl_hours 加余量之后,移除旧密钥再滚动一次。轮换也是在会话过期之前强制它们退出的唯一办法:bearer 令牌对 JWT 密钥本地验证,所以没有按会话的撤销。直接替换密钥而不把旧的留在数组里,会让所有未过期的会话立即失效。对个别人员离职,在你的 IdP 里取消预配该用户;他们的会话在 ttl_hours 内结束。
Postgres
网关持有五张数据表加一张 _migrations 表,都由启动时的迁移创建:
| 表 | 内容 | 保留 |
|---|---|---|
kv | 设备授权(10 分钟 TTL)和速率限制计数器 | 每行的 TTL |
spend | 按主体的周期至今支出计数器,以分计 | admin.spend_retention_months,默认 13 |
spend_limits | 已配置的支出上限 | 直到经 API 删除 |
admin_audit | 管理 API 变更轨迹 | admin.audit_retention_days,默认 365 |
principal_emails | 每个主体最后见到的邮箱、显示名和 IdP 组;含个人身份信息 | 自上次活动起 admin.identity_retention_days,默认 90 |
每 30 秒的循环让超过 TTL 的 kv 行过期,每小时的清扫在支出表上强制保留窗口,所以没有东西会无限增长;没有配置支出限额时,只写 kv。网关在启动时和每次升级时应用自己的 schema 迁移,所以它的数据库角色需要创建和修改表的权限;把它指向专用于网关的数据库或 schema,以保持该授权狭窄。使用支出限额时,丢失数据库意味着丢失支出跟踪和上限,不只是开发者重新登录,所以要做定期备份。要立即抹去一个离职的开发者而不是等保留期,直接运行 DELETE FROM principal_emails WHERE principal = '<sub>';这移除了唯一持有其邮箱、名字和组的表;spend 和 admin_audit 行只引用假名化的 OIDC sub。
升级
副本是无状态的,所以滚动重启不丢失网关状态。网关在启动时运行 schema 迁移,这意味着部署新二进制就会让数据库自行迁移;并发的副本在 Postgres 咨询锁上串行,所以每个迁移只有一个应用它。当你的编排器用 SIGTERM 停止副本(如滚动重启或缩容)时,网关停止接受新连接,让已在进行的请求和流在退出前完成;它最多等 25 秒(称为排空窗口),然后关闭仍然打开的一切。SIGINT(如终端里的 Ctrl+C)启动同样的排空,排空期间的第二个信号会关闭打开的请求并立即退出(排空需要网关 v2.1.274 及以上)。长时间生成可能流式传输几分钟。在 Kubernetes 和 Amazon ECS 上,把这两者一起调高来给这些流更多时间:
- 排空窗口:在网关容器上把环境变量
CLAUDE_GATEWAY_DRAIN_TIMEOUT_MS设为毫秒数的正整数,如120000;网关忽略任何其他形式的值(如120s)并保持 25 秒的默认值 - 你的编排器的宽限期:Kubernetes 上的
terminationGracePeriodSeconds,或 Amazon ECS 上的stopTimeout
宽限期在两个平台上默认 30 秒。要让它至少比排空窗口长 5 秒,否则编排器会在排空完成前杀掉网关;在 Kubernetes 上还要加上任何 preStop hook 的时长,因为宽限期在 hook 运行之前就开始计时,而不是在网关收到 SIGTERM 时。你的平台也可能限制排空能运行多久:Amazon ECS on Fargate——stopTimeout 最多允许 120 秒;Cloud Run——在 SIGTERM 之后 10 秒停止实例,所以那里打开的流最多得到 10 秒,不论排空窗口是多少。排空窗口结束时仍有请求打开,网关会记录含 drain window over after 的警告,统计它切断的请求,并点名要调高的两个设置。迁移是只追加的,所以回滚到认识更少迁移的旧二进制是安全的,它忽略多出的行。回滚还会按旧二进制的 schema 重新验证 YAML,所以采用了新版本引入的键的配置会在旧版本上启动失败;回滚前先移除新键。因为你在自己的镜像里固定网关的版本,新 Claude Code 发布里的修复(包括安全修复)只有在你更新固定版本并重新部署时才到达你的部署;要把网关纳入你对其他持有生产凭据的服务使用的同一修补节奏。
安全
本节回答安全评审会问的问题:什么数据流经网关、流向哪里,设计防御哪些攻击,以及哪些答案属于合规问卷。
数据流
| 数据 | 路径 | 网关是否发给 Anthropic |
|---|---|---|
| 推理(提示、补全) | CLI → 网关 → 你的上游 | 仅当 Anthropic API 是已配置的上游时 |
| 遥测(OTLP 指标,加选择加入的日志和追踪) | CLI → 网关 → 你的收集器 | 从不 |
| 身份(邮箱、组、sub) | IdP → 网关 → CLI;CLI 把它打在 OTLP 导出上。如果开启 forward_user_identity,网关还把开发者的邮箱和 IdP subject 作为头发给你的代理 | 从不 |
| 托管设置 | 你的网关 YAML → CLI | 从不 |
| 审计日志 | 网关 stderr → 你的聚合器 | 从不 |
威胁模型摘要
网关位于你的网络边界之内,但单个开发者笔记本不被视为受信任。设计用三种方式应对:
- 开发者持有短期 JWT 而不是原始的上游密钥。CLI 到网关这一段用 RFC 8628 设备授权,网关与 IdP 的授权码交换在默认配置下运行 PKCE,所以被截获的 IdP 授权码没用。
- 设备验证页面按 RFC 8628 §5.1 强制同源 POST 和每 IP 速率限制(见「用户码暴力破解的抵抗」)。
- 网关对你的 IdP、你的 OTLP 收集器和
provider: anthropic上游的请求要经过服务端请求伪造(SSRF)防护:它解析 DNS,默认阻止链路本地、云元数据地址和环回,并把连接固定到解析出的 IP,所以这些受运营者影响的 URL 无法被重定向到云元数据端点;RFC 1918 私有范围被刻意允许,因为 IdP 和 OTLP 收集器常在私有 IP 上。对其他提供商,网关在加载配置时拒绝点名这些地址或元数据主机名的base_url,提供商的 SDK 随后不带 DNS 检查地连接。开启仅代理出口时,该地址检查移到你的正向代理:网关把主机名交给它,代理的允许列表必须拒绝这些目的地。只有当网关必须合法访问的东西位于环回上(如本地开发的 IdP 或localhost上的 sidecar OTLP 收集器)时,才在网关环境里设CLAUDE_GATEWAY_ALLOW_LOOPBACK=1:该变量会对每个运营者配置的 URL 放宽环回阻止,并跳过检查 pod 能否到达云元数据端点的启动警告,所以优先给收集器自己的内部地址。如果你添加自己的出口控制,网关在使用实例元数据凭据(如工作负载身份)时必须能到达元数据服务器。
两个威胁不在范围内,因为它们是你要保护的基础设施:被攻破的网关主机——该主机既持有上游凭据又向每个已连接的开发者分发托管设置,所以对网关配置的控制相当于对你的 MDM 的控制;CLI 对可执行 shell 的设置的批准对话框限制了静默更改,但不能取代主机安全;恶意的 OIDC 提供商——提供商签署网关信任的 id_token,所以它可以断言任何身份;审查并保护你的 IdP 是你的责任。
用户码暴力破解的抵抗
开发者在 /device 验证页面键入的 user_code 是取自 20 个字符的字母表的 8 个字符,即 20⁸ 或约 2.56×10¹⁰ 种组合,并在 10 分钟后过期。网关对设备授权端点应用每 IP 速率限制,可经 rate_limits 配置。如果许多开发者从单个共享的企业 NAT 地址登录,就调高限制(定大小见「大规模推广」)。这些限制只适用于登录流程,不适用于推理。
合规姿态
- 数据驻留:除非 Anthropic API 是已配置的上游,网关自己的数据平面不向 Anthropic 发送任何东西;是的话,你现有的数据处理协议适用于推理路径。遥测、审计、身份和设置只去往你配置的目的地。
- 宿主进程流量:宿主进程是 Claude Code CLI。
claude gateway遵循与 Amazon Bedrock 和 Google Cloud Agent Platform 部署相同的第三方规则,不向 Anthropic 发送任何东西。v2.1.227 之前,宿主进程会发送产品版本和平台这类启动遥测,在容器环境里设CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1可以关闭;那些版本还会在启动时向https://api.anthropic.com(或环境设了ANTHROPIC_BASE_URL时向它)的/api/hello发一个不带正文或凭据的HEAD请求,除非环境还设了HTTPS_PROXY这类代理变量或 mTLS 客户端证书;它们忽略响应,所以在出口防火墙阻止该请求不影响网关。 - 客户端分析:CLI 在登录到网关期间禁用自己的用量分析和错误报告。第一次登录之前,CLI 仍向 Anthropic 发送启动事件,包括在托管设置强制网关登录的机器上;要把这些也关掉,把
DISABLE_TELEMETRY放在强制网关登录的同一份客户端托管设置里下发。 - 错误报告:只要模型请求发往 Anthropic 第一方 API 之外的任何端点(如 Amazon Bedrock 或自定义
ANTHROPIC_BASE_URL),CLI 就关闭错误报告。 - 客户端机器:除非设了
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1和skipWebFetchPreflight: true,开发者的 CLI 仍会向 Anthropic 发送 WebFetch 主机名检查和版本检查(见数据使用)。 - 调查评分:登录到网关期间,CLI 与分析流一起禁用发往 Anthropic 的评分上传,所以不向 Anthropic 发送评分。
- 记录分享:对调查的记录分享提示选择"是",会在
~/.claude/feedback-bundles/下写一个本地文件,而不是上传到 Anthropic。 - 客户端更新:更新检查与网关流量是分开的。通过你自己的分发固定版本,并在笔记本不得获取发布时设
DISABLE_UPDATES;DISABLE_AUTOUPDATER只停止后台更新,claude update仍能用。 - TLS:生产里经 HTTPS 提供
public_url,可以由网关自己经listen.tls的监听器提供,也可以由纯 HTTP 副本前面终止 TLS 的入口提供,两种情况下都要设listen.public_url;网关不拒绝纯 HTTP。IdP 在生产里必须提供 HTTPS,Postgres 支持?sslmode=require;在你的入口设置Strict-Transport-Security。 - 漏洞披露:遵循「报告安全问题」。
排障
有问题和反馈时,用 Claude Code 支持,或在 Claude Code GitHub 仓库提 issue。报告问题时要包含:
- 网关问题:相关时间窗口的网关 stderr、已脱敏密钥的
gateway.yaml、网关版本(显示在/的落地页上,以及/managed/settings上的x-cc-gateway-version响应头里),以及最近改了什么 - 登录问题:开发者运行
claude --debug-file ./claude-debug.txt、重现,并把该文件与同一窗口的网关审计日志一起发来 - 推理问题:请求的模型、配置的上游,以及该请求的网关审计日志(它记录哪个上游服务了它以及响应状态)
网关的 stderr 包含审计事件流,审计日志记录开发者身份,调试文件记录来自开发者机器的 hook 和 MCP 服务器输出;发到公开 issue 之前要审查并脱敏。
| 症状 | 原因 | 修复 |
|---|---|---|
开发者的 /login 显示标准账号选择器而不是 Cloud gateway 屏幕 | 该机器的托管设置里没设 forceLoginMethod 或 forceLoginGatewayUrl | 把托管设置文件部署到设备;/login 从那里读取网关 URL |
开发者的请求以 Not signed in to the Cloud gateway — run /login. 失败 | 机器的托管设置设了 forceLoginMethod: "gateway" 或 forceLoginGatewayUrl,而会话没有网关登录;遗留的 claude.ai 登录不满足要求 | 让开发者运行 /login 并完成网关登录(另见「管理员策略要求 Cloud 网关登录」) |
| Claude Desktop 报告无法获取它的 bootstrap 配置 | /user/bootstrap 返回 404:匹配该用户的策略不带 desktop 键,或没有策略匹配;网关审计日志把每次拒绝记为 desktop_bootstrap.denied 并带原因 | 给匹配该用户的策略或 match: {} 基础层加 desktop 块,空的 desktop: {} 就够了(见 Claude Desktop 叠加层) |
启动显示 Gateway login is configured in managed settings, but this Claude Code build does not include Cloud gateway support. | 安装的 Claude Code 构建早于网关支持 | 让开发者把 Claude Code 更新到包含 Cloud 网关支持的发布 |
启动以 Administrator policy requires a Cloud gateway sign-in on this machine 退出 | 开发者的环境设了 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN,他们的设置配置了 apiKeyHelper,或较早的 Claude Console 登录的 API key 仍被保存 | 让开发者清除适用的每一项:取消设置该变量、移除 apiKeyHelper 条目,或运行 claude auth logout 移除保存的 key;用 CLAUDE_CODE_USE_* 选择云提供商的会话随后无需登录即可启动,其他每个会话让他们启动 claude 并用 /login 登录(另见「管理员策略要求 Cloud 网关登录」) |
在托管设置加载遇到 403 之后,启动或 /login 报告 Claude Code may not be enabled for your organization | 网关或它前面的东西对 /managed/settings 请求回答了 403;网关自己的设置路由从不回答 403,该状态来自 access_control IP 检查,或网关前面的代理或 WAF;审计日志把 IP 检查拒绝记为 access.denied 并带原因;开发者保持登录 | 查看失败时刻的审计日志里的 access.denied,修复 access_control 列表或前端,然后让开发者再次启动 claude |
CLI /login:The gateway is limiting sign-in attempts right now(旧版本为 Request failed with status code 429);/device 页面可能对没试过的开发者显示 Too many attempts | 达到了每 IP 登录速率限制:要么 listen.trusted_proxies 没覆盖负载均衡器,所以每个开发者共享它的地址,要么许多开发者共享 NAT 或 VPN 出口地址;带 result: rate_limited 的审计事件显示相同的一个或少数几个 client_ip | 先把 listen.trusted_proxies 设为负载均衡器的源范围,开发者仍共享地址时再调高 rate_limits(见「大规模推广」) |
CLI /login:Gateway hosts must be on your organization's private network; <host> resolves to the public (or unrecognized) address <ip> | 网关主机名解析到至少一个公网 IP 地址;Claude Code 检查每个解析出的地址并要求每个都是私有的;常见原因是双栈名字的一个地址族解析到公网地址,包括返回公网范围 AAAA 地址的 AWS 内部双栈负载均衡器 | 让网关名在开发者机器上只解析到私有地址;对双栈名,去掉公网范围的记录或提供单独的仅内部 DNS 名(见私有网络前提);如果该地址是你的组织拥有并在内部使用的公网空间,改为声明该地址块 |
CLI /login:Gateway login would go through proxy <proxy>, which is not on a private network | HTTPS_PROXY 或 HTTP_PROXY 适用于网关主机,且代理的主机名解析到公网地址;主机只解析到私有地址的代理是允许的,不触发此错误 | 在开发者机器上把网关主机加入 NO_PROXY 使连接直连,或用主机名解析到私有地址的代理;消息会点名要加的确切 NO_PROXY 条目 |
CLI /login:Claude Code only signs in to <host> from inside its declared network <block> (managed settings), and this machine is connecting from <ip>, outside it | 网关位于 gatewayInternalNetworks 里声明的地址块,而开发者机器从该块之外的地址到达它:VPN 地址池、容器或 WSL2 NAT 段,或不是你的网络 | 让开发者从你网络上的宿主操作系统运行 /login;如果显示的地址也是你组织自己的公网空间,把网关的条目换成同时涵盖两者的块(最大到 /8);第二个重叠的条目会被拒绝 |
CLI /login:Every address for gateway host <host> must be inside its declared network <block>, and it also resolves to <ip> | 网关的名字解析到 gatewayInternalNetworks 里声明的块之外的地址:第二个站点,或双栈名上的 IPv6 记录;在声明的块下,每条记录(包括私有和 IPv6 地址)都必须在那个 IPv4 块之内 | 在开发者机器上只为网关名发布块内的记录,或提供单独的仅内部名字 |
CLI /login:<host> is on the declared network <block>, which Claude Code checks over a direct connection, not through an HTTP proxy | HTTPS_PROXY 或 HTTP_PROXY 适用于位于声明块上的网关 | 在开发者机器上加消息点名的 NO_PROXY 条目 |
CLI /login:以 gatewayInternalNetworks in managed settings 开头的消息 | 该值违反了某条校验规则,消息会点名是哪条;在你修复之前,Claude Code 拒绝该机器上的每个新网关 /login(包括私有地址上的网关),已有的登录继续工作 | 在你部署的托管设置来源里更正消息点名的条目,然后重新运行 /login |
CLI /login:Could not resolve the configured HTTP proxy | HTTPS_PROXY 或 HTTP_PROXY 里的主机名无法从开发者机器解析,通常因为它没有连到企业网络 | 让开发者连接到你的网络或 VPN 后重试,或修复代理 URL |
CLI /login:Could not resolve gateway host <host> | 机器无法解析网关的内部 DNS 名,通常因为它不在企业网络上 | 让开发者连接到你的网络或 VPN,然后重试 /login |
启动因点名 store.postgres_url 的配置校验错误退出 | 没配置 Postgres;网关要求 Postgres | 设 store.postgres_url;本地开发用一次性容器:docker run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres |
启动退出:requires the native binary | 在 Node 而不是原生二进制下运行 | 用某种独立安装方法安装 Claude Code |
config.load 之后启动因 OIDC 发现错误退出 | oidc.issuer 不可达,或 TLS 链不受信任 | 检查 issuer 从 pod 可达并提供 /.well-known/openid-configuration;私有 PKI 设 ca_cert_pem;如果 pod 只经正向代理到达 IdP,设 oidc.use_proxy: true(v2.1.227 之前的版本,改为给 pod 一条到 IdP 每个端点的直连路由);如果 pod 还无法解析 IdP 的主机名,或代理拒绝对 IP 地址的 CONNECT,见「仅代理出口」(需要 v2.1.277 及以上) |
| 启动因 Postgres 权限错误退出 | 数据库角色在它的 schema 上缺少 DDL 权限 | 给该角色在网关 schema 上的 CREATE 权限,使它能在启动时创建和修改表 |
日志:could not connect to Postgres at boot, attempt 1 of 3 | 网关启动时数据库还不可达,例如冷实例的网络仍在启动 | 如果网关随后完成启动,无需操作;数据库不可达时,网关在退出前尝试三次连接、每次间隔两秒;如果它以 could not connect to Postgres 退出,检查 store.postgres_url 和到数据库的网络路径;如果尝试是超时而不是被拒绝,调高 store.connect_timeout_seconds 给每次尝试更长时间 |
/oauth/callback 显示 "Sign-in could not be completed" | 邮箱域被拒绝、id_token 校验失败,或 email_verified 明确为 false(网关总是拒绝它,没有覆盖办法) | 检查 allowed_email_domains,并检查 IdP 返回已验证的 email 声明;对 email_verified: false,修复 IdP 侧的验证;如果你的 IdP 在不同的声明名下发出邮箱,设 oidc.email_claim |
日志:token exchange failed request_id=<id>: id_token missing email claim | IdP 默认没有在 id_token 里包含 email;该拒绝只在设了 allowed_email_domains 时触发,没设时缺失的邮箱会铸造一个没有邮箱的会话 | 配置 IdP 在 id_token 里发出 email:Okta——把 email 加到自定义授权服务器的 ID 令牌声明里;Entra——把 email 作为可选声明加到应用注册上;PingFederate——启用发出 email 的 OpenID Connect Policy;如果 IdP 从 userinfo 端点提供 email 但不把它放进 id_token(如 Okta 组织授权服务器),设 oidc.userinfo_fallback: true |
日志:refresh failed request_id=<id>: invalid_token (…) (at userinfo_no_id_token, …),开发者每 session.ttl_hours 看到 Cloud gateway session expired | IdP 接受了刷新令牌但没有随之返回 id_token,所以网关向 IdP 的 userinfo 端点要用户的声明,IdP 在那里拒绝了刷新后的访问令牌;网关回答 temporarily_unavailable,所以 Claude Code 保留刷新令牌但无法续期会话(v2.1.260 之前的网关记录同一行但不带 (at …) 细节) | 设 oidc.scope_on_refresh: true(网关 v2.1.260 及以上可用),使刷新请求再次请求 openid——有些 IdP(如 Okta)只在被请求时才在刷新时返回 id_token;在 PingFederate 上,改为在 Applications > OAuth > OpenID Connect Policy Management 下启用 Return ID Token On Refresh Grant(该键不改变 PingFederate 的行为);对仍然省略它的其他 IdP,检查 userinfo 端点是否接受刷新签发的访问令牌;作为权宜之计,调高 session.ttl_hours(取消预配的权衡见 IdP 设置) |
每个 Amazon Bedrock 请求都返回 502;日志显示 Could not load credentials from any providers | 在 EC2 上,IMDSv2 默认的跳数限制 1 把容器内的实例元数据请求挡住了;启动和 /readyz 仍通过,因为 AWS SDK 在第一个请求而不是客户端构造时解析实例凭据 | 用 aws ec2 modify-instance-metadata-options --instance-id <id> --http-put-response-hop-limit 2 提高跳数限制,或在启动模板里设置;该更改适用于实例上的每个容器;在可用的地方优先用 ECS 任务角色(它们从 ECS 容器凭据端点读取凭据,完全避免该更改),或在专用的网关实例上应用该更改以限制暴露 |
峰值负载时,响应启动缓慢或似乎挂起,或在上游健康时以 502 all upstreams failed 失败 | 副本打开的请求多于它一次向上游发送的数量,所以多出的请求在网关内等待;在 provider: anthropic 上游上,等待超过 timeouts.upstream_ttfb_ms 的请求放弃该上游,没有后面的上游服务它时产生 502;日志显示含 client requests are open 的警告 | 增加副本,或调高每个副本的限制(见「并发上游请求」) |
| IdP 错误:unknown or unsupported scope | IdP 拒绝它不认识的 scope | 把 oidc.scopes 设为恰好是你的 IdP 接受的列表,必须包含 openid;默认是 openid profile email offline_access |
设置 oidc.scopes 之后会话不再静默续期 | 覆盖里丢掉了 offline_access | 如果你的 IdP 支持,把 offline_access 加回去;没有刷新令牌时,开发者每 session.ttl_hours 重新运行浏览器登录 |
| 浏览器显示 "This request came from another site and was blocked" | 跨站表单 POST,被作为 CSRF 防护阻止;对嵌入或被代理的页面是预期的 | 直接打开验证链接 |
| Chrome 以 "Refused to send form data … violates … Content Security Policy directive: form-action" 阻止 Approve 按钮,但同一页面在 Safari 或 Firefox 里正常 | Chrome 对整个重定向链强制 form-action;你的 IdP 继续重定向到没有加入允许列表的第二个主机 | 把重定向链里每个额外的源加入 oidc.form_action_origins;在 Approve 页面打开 Chrome DevTools → Console 查看哪个源被阻止 |
| 登录在 IdP 完成但回调失败,在 Chrome 里有 CSP 错误,或在 Safari 里是 "this sign-in link has expired" | IdP 经 response_mode=form_post 返回代码,它以跨源 POST 自动提交到 /oauth/callback;Chrome 在严格 CSP 下阻止它,Safari 允许提交但回调只读取查询字符串 | 确保你的 IdP 遵从 response_mode=query,网关会显式请求它,使回调是普通重定向 |
| 登录在本地能用,但在 ALB 后面失败 | public_url 仍然是本地或内部的 http:// 源,所以 IdP 得到错误的 redirect_uri | 把 listen.public_url 设为外部的 https:// 源,并在 IdP 注册 <public_url>/oauth/callback |
| 开发者反复看到信任提示 | TLS 证书按副本或按请求轮换 | 在入口使用稳定的证书,或只终止一次 TLS 并让副本在内部用纯 HTTP 运行 |
CLI /login:"Could not verify the gateway's TLS certificate" 或 SELF_SIGNED_CERT_IN_CHAIN | 网关的 TLS 链由不在 CLI 主机信任存储里的私有 CA 签发 | Claude Code 在原生二进制上以及 Node 22.15 及以上默认读取操作系统信任存储,CLAUDE_CODE_CERT_STORE 控制该行为;如果 CA 安装在操作系统信任存储里,要确保开发者使用当前的运行时;否则在启动前把 NODE_EXTRA_CA_CERTS 设为 CA 证书 PEM;首次连接的指纹提示仍然适用 |
CLI /login 完成浏览器登录,随后会话以 Cloud gateway sign-in was not completed 和 TLS 证书不匹配结束 | 登录后的第一个请求上,网关出示的证书与 Claude Code 固定的指纹不匹配,所以 Claude Code 没保留网关凭据;常见原因是同一个地址后面的副本提供不同的证书,或网络路径上有东西拦截 TLS | 为该主机名提供同一个证书(例如在入口只终止一次 TLS),然后让开发者再次运行 /login;如果该证书与固定的不同,Claude Code 会再次显示信任提示并警告证书已变化 |
CLI /login 以 The gateway's TLS certificate changed during sign-in: it no longer matches the one you trusted 停止 | 登录请求到达了证书与开发者在 /login 开始时接受的证书不匹配的服务器:同一地址后面的副本提供不同的证书、路径上的 TLS 拦截,或登录进行期间的证书轮换 | 为该主机名提供同一个证书,然后让开发者重新开始登录,并在信任提示处审查新证书 |
Cloud gateway sign-in was not completed 消息会点名网关主机名;当 Claude Code 同时有固定的指纹和出示的指纹时,消息还显示两者各自的前 16 个字符。如果 Claude Code 在网关登录之后报告 couldn't load your organization's managed settings,它会点名原因、就地重启并恢复对话;如果 Claude Code 无法重启(例如在后台会话里),它会结束会话并保留登录。
相关
- Claude apps gateway 概览:快速开始和开发者连接
- 配置参考:每个
gateway.yaml选项