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
Context
Section titled “Context”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:
- 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.housewith 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. - 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-connectto obtain its own token for theengramandkubernetesidentity 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).
Decision
Section titled “Decision”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.
Rationale
Section titled “Rationale”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-connectresolved via the split-horizon LAN record →403 Forbiddenfrom Traefik (thedeny-client-registrationMiddleware; the response is Traefik’s, not Keycloak’s). - Internet, through the Cloudflare tunnel. The same
GETwith the Host header sent to the zone’s public Cloudflare address104.21.94.30→404 from Keycloak: a Keycloak-shaped body, no Cloudflare Access redirect, no WAF challenge. The tunnel ingress intf/cloudflare/tunnel.tfroutesid.fzymgc.housedirectly tokeycloak-service.keycloak.svc.cluster.local:8443, bypassing Traefik entirely, and the Cloudflare Access application intf/cloudflare/access.tfcovers only/admin,/admin/*,/realms/masterand/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.
Alternatives Considered
Section titled “Alternatives Considered”- 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 in05-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.
Consequences
Section titled “Consequences”- Positive: a standards-conformant OAuth MCP client on the LAN can discover, register and
authenticate against the estate’s identity routes on
llm.fzymgc.housewith 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_absentintools/litellm-verifyasserts 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=falsethe source host is never consulted, by design (the source IP Keycloak sees is not stable; see the Job’s header).
References
Section titled “References”- 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(theid.fzymgc.houseingress 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