Skip to content

Bedrock via Kong

This is the topology Halos was designed for: Kong, then your auth gateway, then an orchestrator, then Bedrock.

Claude Code calls Kong with halo-kong, then your auth gateway, then an orchestrator that signs with SigV4 for Amazon Bedrock. Claude Code calls Kong with halo-kong, then your auth gateway, then an orchestrator that signs with SigV4 for Amazon 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.

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-gateway

Then the gateway document:

apiVersion: halos.dev/v1
kind: Gateway
name: acme
baseURL: https://ai.acme.example
protocols: {claude-code: anthropic-messages}
auth:
helperCommand: acme-sso-token --audience halos-gateway
ttlSeconds: 900
models:
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.

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_URL pointing at the gateway,
  • env.ANTHROPIC_CUSTOM_HEADERS for release stamping (informational; the gateway overwrites x-halo-*),
  • requiredMinimumVersion / requiredMaximumVersion pinning the ring’s CLI,
  • availableModels and enforceAvailableModels from models,
  • allowManagedHooksOnly, allowManagedMcpServersOnly, permissions.disableBypassPermissionsMode from the profile,
  • OTEL env with OTEL_RESOURCE_ATTRIBUTES carrying halo.ring, halo.release and halo.harness.

3. Compile the policy and generate Kong config

Section titled “3. Compile the policy and generate Kong config”
Terminal window
halo gateway compile --policy-dir examples/acme-corp -o policy.json # the snapshot halo-kong loads
halo 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 config
deck gateway sync kong.yml

The 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.

Use a real token from your IdP:

Terminal window
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.

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.

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.

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.