Bedrock via Kong
This is the topology Halos was designed for: Kong, then your auth gateway, then an orchestrator, then Bedrock.
Because Claude Code does not fetch server-managed settings on this path, configuration must be delivered by file, MDM or environment. You can equally run halo-proxy in place of the plugin; see stack-agnostic.
1. Describe the identity and the gateway
Section titled “1. Describe the identity and the gateway”Identity is verified from the caller’s OIDC JWT, so Kong may sit before your auth gateway. In halos.yaml:
identity: issuer: https://acme.okta.com/oauth2/default audience: halos-gatewayThen the gateway document:
apiVersion: halos.dev/v1kind: Gatewayname: acmebaseURL: https://ai.acme.exampleprotocols: {claude-code: anthropic-messages}auth: helperCommand: acme-sso-token --audience halos-gateway ttlSeconds: 900models: sonnet: {upstream: orchestrator, model: us.anthropic.claude-sonnet-4-5-v1:0}upstreams: orchestrator: {url: https://orchestrator.internal.acme.example, kind: orchestrator}auth.helperCommand becomes Claude Code’s apiKeyHelper: it must print a token whose aud matches identity.audience. auth.identityHeader is only needed for trusted_header mode.
2. What the Claude Code adapter renders
Section titled “2. What the Claude Code adapter renders”Run halo render --policy-dir examples/acme-corp --ring ring1-canary --os linux --out /tmp/rendered and read /tmp/rendered/etc/claude-code/managed-settings.json. It carries:
env.ANTHROPIC_BASE_URLpointing at the gateway,env.ANTHROPIC_CUSTOM_HEADERSfor release stamping (informational; the gateway overwritesx-halo-*),requiredMinimumVersion/requiredMaximumVersionpinning the ring’s CLI,availableModelsandenforceAvailableModelsfrommodels,allowManagedHooksOnly,allowManagedMcpServersOnly,permissions.disableBypassPermissionsModefrom the profile,- OTEL env with
OTEL_RESOURCE_ATTRIBUTEScarryinghalo.ring,halo.releaseandhalo.harness.
3. Compile the policy and generate Kong config
Section titled “3. Compile the policy and generate Kong config”halo gateway compile --policy-dir examples/acme-corp -o policy.json # the snapshot halo-kong loadshalo gateway deck --policy-dir examples/acme-corp \ --policy-path /etc/kong/policy.json \ --halo-shadow-url http://halo-shadow:8090 -o kong.yml # decK declarative configdeck gateway sync kong.ymlThe generated config uses exact regex routes (so /v1/messages/batches cannot bypass the model allowlist), https-only routes, and a Kong vault reference for the shadow token. Run Kong with KONG_NGINX_HTTP_CLIENT_BODY_BUFFER_SIZE=32m and KONG_NGINX_MAIN_ENV=HALO_SHADOW_TOKEN. deploy/compose/ has a working Dockerfile and kong.yml.
4. Verify
Section titled “4. Verify”Use a real token from your IdP:
curl -s https://ai.acme.example/v1/messages -H "Authorization: Bearer $TOKEN" \ -H 'content-type: application/json' -H 'anthropic-version: 2023-06-01' \ -d '{"model":"sonnet","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'Check: the request succeeds; asking for an alias not in models returns 400 invalid_request_error; and a forged x-halo-ring does not change the ring your orchestrator sees. The compose demo reproduces all three against mocks.
5. Kill switch (optional)
Section titled “5. Kill switch (optional)”Give halo-kong the signed kill list from halo-server and a rollback verdict or an admin click takes effect in seconds instead of after a PR merge. halo gateway deck --killswitch-url https://<halo-server>/api/v1/gateway/killswitch generates killswitch_url, killswitch_token and killswitch_pubkey (the last two as the vault references below); killswitch_allow_insecure_in_cluster is never generated, add it by hand if you need it.
| Field | Meaning |
|---|---|
killswitch_url |
https://<halo-server>/api/v1/gateway/killswitch. Empty disables the kill switch |
killswitch_token |
The gateway bearer token (halo-server --gateway-token-file). A literal or a Kong env-vault reference such as {vault://env/halo-killswitch-token} |
killswitch_pubkey |
ed25519 public key PEM matching halo-server --killswitch-key-file (killswitch.pub from halo keys generate --name killswitch). A literal or {vault://env/halo-killswitch-pubkey} |
killswitch_allow_insecure_in_cluster |
Default false. killswitch_url must be https:// unless it targets loopback or this is true, because the bearer token travels on every poll. Set it only for a trusted in-cluster URL |
plugins: - name: halo-kong config: policy_path: /etc/kong/policy.json killswitch_url: https://halo.acme.example/api/v1/gateway/killswitch killswitch_token: "{vault://env/halo-killswitch-token}" killswitch_pubkey: "{vault://env/halo-killswitch-pubkey}"Vault references resolve from the plugin server’s environment, so export HALO_KILLSWITCH_TOKEN and HALO_KILLSWITCH_PUBKEY to Kong and list them in KONG_NGINX_MAIN_ENV alongside HALO_SHADOW_TOKEN. The plugin polls every 10 seconds. Killed experiments are treated as not running (control routing, no shadow, no x-halo-experiment or x-halo-variant). On a failed fetch it keeps the last list; until its first successful fetch nothing is killed. An invalid config (bad key, missing token, cleartext URL) does not block traffic, but it is loud: the plugin logs halo-kong: killswitch config invalid; kill switch not (re)configured, killed experiments may keep running at ERROR at most once a minute and adds x-halo-killswitch: misconfigured to the upstream request (client copies are cleared first). Alert on that header; while it is present, kills are not being enforced.
Check: kill an experiment (POST /api/v1/experiments/<name>/kill as an admin), wait one poll interval, and send a request as a user in that experiment: the upstream sees no x-halo-experiment header. Semantics, failure policy and the wire format: experiments, API reference. The same settings for halo-proxy are in its README.
6. Posture and version gates (optional)
Section titled “6. Posture and version gates (optional)”Rings with posture or versionGate set to warn or enforce (default warn) are checked on every model call (gateway gates). The version gate needs no config. For the posture gate, point the plugin at halo-server:
| Field | Meaning |
|---|---|
posture_url |
https://<halo-server>/api/v1/gateway/posture. Empty disables the posture gate |
posture_token |
The gateway bearer token, a literal or {vault://env/halo-gateway-token} |
posture_allow_insecure_in_cluster |
Default false; as killswitch_allow_insecure_in_cluster |
Verdicts are cached per user for 1 minute and kept for 15 minutes while halo-server is unreachable (a transport error or 5xx); after that the posture is unknown and the request passes. A lookup halo-server refuses (401 for a wrong token, 404 when posture is not configured on the server, any other non-200 below 500) is a failed verdict, not unknown: enforce rings answer 403, warn rings pass flagged. Enforce answers 403 in the CLI’s wire format. Warn-mode failures and unknown posture pass, are logged at NOTICE, and tag the upstream request x-halo-gate (for example version=warn,posture=unknown; client copies are always cleared, whether or not the plugin saw them). An invalid posture config (for example posture_token naming an unset env var) never turns the gate off: it is logged at ERROR on every request, enforce rings refuse every caller with 403, and warn rings pass tagged posture=unknown.
7. Metrics and limits
Section titled “7. Metrics and limits”Set HALO_KONG_METRICS_LISTEN (for example 127.0.0.1:9542) in the plugin server’s environment and list it in KONG_NGINX_MAIN_ENV to serve Prometheus text on GET /metrics (unauthenticated: bind it to loopback or a pod-local address). Unset disables it. Counters, process-wide across routes:
| Metric | Labels |
|---|---|
halo_kong_directory_resolutions_total |
outcome: ok, stale (cached entry served through an outage), failopen, unavailable (503), not_provisioned (403), invalid_config_failopen, invalid_config_refused, revocations_unavailable (a revocation-list fetch failed; the fail mode then decides) |
halo_kong_posture_total |
ring, outcome: pass, fail (warn, let through), deny (enforce, 403), unknown |
halo_kong_header_cap_rejections_total |
none |
halo_kong_agent_denials_total |
none: delegated agent calls refused by the agent policy |
Kong hands the plugin at most 1000 request header lines and truncates silently, so a request that reaches 1000 is answered 431 before identity is derived: a duplicate identity header past the cap would otherwise go unseen.