Skip to content

Keycloak DCR deny removed; the loopback Trusted-Hosts policy is the sole DCR control — measured: the Traefik deny 403'd LAN clients only, while the Cloudflare tunnel already delivered anonymous DCR straight to Keycloak

Date: 2026-09-08 Status: Accepted — supersedes hl-ll5p; re-adopts hl-amad’s control model Deciders: Sean Brandt

ADR hl-ll5p (2026-07-02) re-added a Traefik network-level deny on Keycloak’s dynamic client registration path — the priority-1000 rule Host(id.fzymgc.house) && PathPrefix(/realms/fzymgc/clients-registrations) with the deny-client-registration Middleware (ipAllowList 127.0.0.1/32) in argocd/app-configs/keycloak/ingress-route.yaml. It could do so because agentgateway’s own DCR proxy call had been routed cluster-internally (CoreDNS rewrite + ICA1 trust in the gateway pod), so the deny no longer 403’d the gateway’s hairpinned registration. The stated intent was “cluster-internal-only DCR”: only agentgateway’s in-cluster proxy would register clients, and no LAN caller would reach the endpoint.

Two things changed by the litellm-return milestone’s Phase 5:

  1. agentgateway is being retired, and with it the in-cluster DCR proxy the deny existed to privilege. The client-facing OAuth contract moves to llm.fzymgc.house with Keycloak as the sole issuer (Phase 5 D-66): LiteLLM relays each upstream MCP server’s own RFC 9728 protected-resource document, and that document names Keycloak’s realm as the authorization server. The MCP client then performs RFC 7591 dynamic client registration directly against Keycloak — from wherever the client runs.
  2. The population Phase 5 needs is exactly the one the deny blocked. Claude Code CLI on the LAN must register itself against https://id.fzymgc.house/realms/fzymgc/clients-registrations/openid-connect to obtain its own token for the engram and kubernetes identity routes without any credential handed over out of band (04-09 Ruling 1). With the deny in place that registration is refused before it reaches Keycloak.

The decision was taken on measurement, not argument. Both probes were run read-only on 2026-09-08 (recorded in full in engram hhv2dxr4ja, with the earlier 6vazrjz58k and 9jx6sh5ej0 from 2026-07-02 that first noted the tunnel path).

Delete the priority-1000 clients-registrations IngressRoute rule and the deny-client-registration Middleware from argocd/app-configs/keycloak/ingress-route.yaml. Keycloak’s loopback Trusted-Hosts client-registration policy — configured by argocd/app-configs/keycloak/dcr-trusted-hosts-job.yaml as client-uris-must-match=true, host-sending-registration-request-must-match=false, trusted-hosts=[localhost,127.0.0.1] — is the sole anonymous-DCR control, on both the LAN path and the tunnel path.

The Job is not modified. The bare Host(id.fzymgc.house) priority-1 route and the admin-console admin-allowlist Middleware stay. hl-ll5p is superseded; the control model is the one hl-amad (2026-06-29) ran on, re-adopted for a different reason.

The deny was LAN-only, and the LAN was never the only path. Two probes, same endpoint, same day:

  • LAN, through Traefik. GET https://id.fzymgc.house/realms/fzymgc/clients-registrations/openid-connect resolved via the split-horizon LAN record → 403 Forbidden from Traefik (the deny-client-registration Middleware; the response is Traefik’s, not Keycloak’s).
  • Internet, through the Cloudflare tunnel. The same GET with the Host header sent to the zone’s public Cloudflare address 104.21.94.30404 from Keycloak: a Keycloak-shaped body, no Cloudflare Access redirect, no WAF challenge. The tunnel ingress in tf/cloudflare/tunnel.tf routes id.fzymgc.house directly to keycloak-service.keycloak.svc.cluster.local:8443, bypassing Traefik entirely, and the Cloudflare Access application in tf/cloudflare/access.tf covers only /admin, /admin/*, /realms/master and /realms/master/* — the DCR path is outside it.

So anonymous DCR from the public internet already reached Keycloak, governed only by the Trusted-Hosts policy, for as long as the tunnel route has existed. The Traefik deny never delivered “cluster-internal-only DCR”; it delivered “LAN-blocked, internet-open” — the inverse of any threat model a homelab would choose. Removing it adds no exposure: the policy that has been the effective control on the open path all along becomes the stated control on both.

The policy is an adequate control for this threat model. client-uris-must-match=true with loopback trusted-hosts confines every anonymously registered client to http://localhost:* / http://127.0.0.1:* redirect URIs — an attacker who registers a client gains a client_id that can only ever redirect to their own machine, against a realm whose users authenticate with their own credentials. The realm also already ships the static public PKCE client mcp-public with the same loopback redirects (tf/keycloak/mcp_public.tf), so a registered client obtains nothing a public client did not already offer.

The minimal change was chosen deliberately. Closing the tunnel path as well was offered and declined for this phase; see Alternatives.

  • Remove the Traefik deny and leave the tunnel path as it is (chosen): the smallest change that unblocks LAN DCR; exposure is unchanged because the tunnel path was already open; the Keycloak policy is a single control model stated once for both paths.
  • Also close the tunnel path with a Cloudflare Access or WAF rule on /realms/fzymgc/clients-registrations: would make the DCR endpoint LAN-only for the first time. Declined by the operator as part of this phase — it is a hardening change with its own decision to make (which clients need DCR from off-LAN, and whether Access can gate a non-browser flow), and it is deferred rather than rejected. Recorded in 05-CONTEXT.md § Deferred Ideas.
  • Rebuild agentgateway’s in-cluster DCR proxy behind LiteLLM: rejected — it is precisely the synthesis workaround Phase 5 retires (D-66), and it would keep the LAN blocked for a client population that no longer has a proxy to go through.
  • Keep the deny and hand tokens to clients out of band: rejected — this is the acquisition step 04-09 Ruling 1 declined to institutionalise, and at scale it ends in a shared long-lived token.
  • Positive: a standards-conformant OAuth MCP client on the LAN can discover, register and authenticate against the estate’s identity routes on llm.fzymgc.house with nothing handed over out of band (Phase 5 D-66, D-67, D-68 make the discovery chain reachable; this record makes the registration step admissible).
  • Positive: one control model, stated once — the Keycloak policy governs every path to the DCR endpoint, and test_keycloak_dcr_deny_absent in tools/litellm-verify asserts both the removal and that the Job still applies the three policy settings.
  • Neutral: registered clients remain confined to loopback redirect URIs, exactly as under hl-amad.
  • Neutral: the engram store rule proposal dnvmg3a68t (“keep the Traefik DCR deny”) was declined on 2026-09-08 — do not re-propose it.
  • Negative: the DCR endpoint is reachable from the internet through the tunnel, as it has been since the tunnel route landed — now recorded rather than assumed closed. Hardening that path is the deferred follow-up above.
  • Negative: hl-ll5p’s own deferred follow-up — re-enabling Keycloak-side Trusted-Hosts host-match — stays deferred; with host-sending-registration-request-must-match=false the source host is never consulted, by design (the source IP Keycloak sees is not stable; see the Job’s header).
  • Supersedes: hl-ll5p — Restore cluster-internal-only Keycloak DCR (A1+trust)
  • Amends (re-adopts): hl-amad — Remove Traefik DCR deny; loopback Trusted-Hosts as sole DCR control
  • Original policy: hl-xktj (Keycloak Trusted-Hosts client-registration policy)
  • Phase 5 decision D-69, with D-66 (Keycloak the sole issuer), D-67 (the per-server protected-resource allow on llm.fzymgc.house) and D-68 (the upstreams publish their own documents) — .planning/workstreams/litellm-return/phases/05-migration-cleanup/05-CONTEXT.md
  • 04-09 Ruling 1 and Finding — .planning/workstreams/litellm-return/phases/04-mcp-gateway/04-09-DECISION.md
  • engram records 6vazrjz58k, 9jx6sh5ej0 (2026-07-02: the tunnel path first noted), hhv2dxr4ja (2026-09-08: the two probes), dnvmg3a68t (rule declined)
  • tf/cloudflare/tunnel.tf (the id.fzymgc.house ingress rule), tf/cloudflare/access.tf (keycloak_admin — the only Access application on this host)
  • argocd/app-configs/keycloak/dcr-trusted-hosts-job.yaml — the sole control