chore: drop SkipDryRunOnMissingResource from the oauth2 middlewares - #1489
Merged
venkatamutyala merged 1 commit intoSep 5, 2026
Conversation
The traefik.io CRDs are no longer rendered by this Application - they come from the platform-crds layer-0 bundle, applied by captain_utils before the platform chart is installed, as the comment at the top of this file already says. So there is no window in which the Middleware type is unregistered when this syncs, and nothing for the option to skip a dry run over. Removed from all eight middlewares. Two comments went with it, one of which was never accurate: # ensure it happens after CRDs are registered argocd.argoproj.io/sync-wave: "1" A sync-wave orders resources within this Application's own sync; it cannot wait on CRDs applied out-of-band by captain_utils. Whatever protection existed came from SkipDryRunOnMissingResource, not from the wave. What the wave actually does is keep the four leaf middlewares ahead of the four chains that reference them, so the comment now says that. The sync-waves themselves stay. Traefik registers no admission webhook for these CRs, so Kubernetes will happily accept a chain naming a middleware that does not exist - and Traefik then drops every router using that chain, which is a 404 on the affected host until it resolves. Wave 2 on the chains is what keeps that window closed on a first install. Rendered output differs by exactly the twelve lines removed or reworded; all eight Middlewares still render and both waves are intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrpnuCduw2KivngVRQmnG7
venkatamutyala
merged commit Sep 5, 2026
661a6f8
into
feat/cli-sso-through-oauth2-proxy
2 checks passed
venkatamutyala
added a commit
that referenced
this pull request
Sep 5, 2026
…1485) * feat: let the argocd and bao CLIs authenticate through oauth2-proxy Neither CLI can reach its backend on a GlueOps cluster today. Every request is answered with a login redirect or a bare 401: ArgoCD / -> 302 oauth2/start ArgoCD /api/v1/session (POST) -> 401 ArgoCD /version.VersionService/Version -> 302 oauth2/start Vault /v1/sys/seal-status -> 302 oauth2/start The backends are healthy - `argocd version --grpc-web` returns the server version over a port-forward and only the client version through the ingress. Three independent things are in the way. 1. Dex had no client an operator could mint a token from. Add "toolbox", public so no client secret reaches developer machines. It is used with the device flow, which needs no redirect URI, so nothing is registered per developer and it works over SSH, in a container, or when the browser is on another machine. Loopback callbacks are deliberately NOT added to the argocd and vault clients: a localhost redirect only works when browser and CLI share a host, which is the case this is meant to solve. 2. oauth2-proxy only accepted a session cookie, so a CLI holding a valid Dex id_token was still redirected. skip_jwt_bearer_tokens plus oidc_extra_audiences lets that token satisfy the edge. Traefik already forwards the caller's Authorization header to the auth server, so nothing else is needed. 3. forwardauth stripped the caller's own bearer token. oauth2-forwardauth lists "authorization" in authResponseHeaders, and Traefik applies that list as an unconditional req.Header.Del(name) followed by a re-add only when the auth server returned the header - which oauth2-proxy never does, since set_authorization_header is off. The entry can therefore only ever strip the token before the backend sees it, which is fatal for a backend that authenticates the caller itself. Add oauth2-forwardauth-bearer without that entry, plus oauth2-api (401 rather than a redirect, for pure API/gRPC routes) and oauth2-with-redirect-bearer (for a host serving a UI and an API on the same routes, as ArgoCD does with gRPC-web at root paths). Nothing is exempted from authentication: every request still passes the same forwardauth, it just gains a second way to prove identity. Nor does this widen who gets in - email_domains=["*"] with no allowed_groups already authorizes anyone Dex admits, and Dex only admits members of the configured GitHub orgs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM * feat(vault): route OpenBao's API without the errors-redirect plugin The vault Ingress uses oauth2-with-redirect, whose errors-redirect plugin rewrites any 401-403 response into a 302 to the login page. That is right for the browser UI and wrong for an API: OpenBao answers "missing client token" and "permission denied" with 403, and the plugin turns both into a login redirect. A CLI then receives an HTML page where it expected JSON, and cannot distinguish an expired edge token from simply not having logged in to OpenBao - which sends anyone debugging it in entirely the wrong direction. Add a second Ingress for /v1 using oauth2-no-redirect, which authenticates identically - same forwardauth, nothing exempted - but returns status codes instead of redirects. It also keeps stripping the caller's Authorization header, which OpenBao does not need: it reads X-Vault-Token, and that takes precedence over a bearer anyway. The explicit priority is required rather than decorative: Traefik derives a router priority from rule length when none is set, and the chart's "/" rule outranks a lower explicit value. Note this goes in the existing extraObjects list rather than a new key. A second extraObjects: silently wins over the first in YAML, which drops every object already there - including the vault-init-controller Application. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM * fix: accept only the toolbox audience at the edge oidc_extra_audiences also listed argocd and vault, but no flow presents a token for either audience at the edge: the argocd CLI sends the toolbox token as ARGOCD_AUTH_TOKEN and bao sends the same one. Listing them was not free. With skip_jwt_bearer_tokens the edge accepts any validly-signed token whose aud is in the set, so every ArgoCD browser-SSO id_token and every Vault OIDC login token -- credentials that today unlock exactly one service -- also opened the entire edge for every host behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM * fix: fail the render when dex.github.orgs is empty (#1488) dex.github.orgs was optional: {{- if .Values.dex.github.orgs }} orgs: ... {{- end }} loadAllGroups: true but it is the only thing restricting who Dex admits. Dex v2.44.0 decides group handling in connector/github/github.go getGroups: switch { case len(c.orgs) > 0: return c.groupsForOrgs(...) case c.org != "": return c.teamsForOrg(...) case groupScope && c.loadAllGroups: return c.userGroups(...) } groupsForOrgs is the only branch that can deny anyone - it is the sole source of "github: user %q not in required orgs or teams" (github.go:375). With the list empty that branch is unreachable and, because this chart sets loadAllGroups, control lands on userGroups, which returns the caller's groups and cannot return an error. Every GitHub account then authenticates. Nothing downstream catches it: oauth2-proxy runs email_domains=["*"] with no allowed_groups, so anyone Dex admits reaches every host behind the edge - including the five that have no auth of their own (goldilocks, cluster-info, and the three Traefik dashboards, whose /api/rawdata lists every router, service and TLS config). OpenBao stays gated by the roles' bound_claims, and ArgoCD by its rendered RBAC policy, so this is the edge rather than all three - but the cluster would report Synced/Healthy throughout, with nothing saying it was open. The default in values.yaml carries two orgs, so this is a footgun rather than a live hole: it needs a tenant to override the list to empty or null. Rendering now fails instead. `fail` guarded by `not`, rather than `required`: Helm's `required` rejects only nil and the empty string, so an empty list passes straight through it (`required "x" .Values.orgs` with `orgs: []` renders `a: []`). `not` is falsy for nil, empty list and empty string alike. Verified: default values render unchanged; a single-org list renders; `orgs: []` and `orgs: null` both abort with the message. Claude-Session: https://claude.ai/code/session_01TrpnuCduw2KivngVRQmnG7 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: drop SkipDryRunOnMissingResource from the oauth2 middlewares (#1489) The traefik.io CRDs are no longer rendered by this Application - they come from the platform-crds layer-0 bundle, applied by captain_utils before the platform chart is installed, as the comment at the top of this file already says. So there is no window in which the Middleware type is unregistered when this syncs, and nothing for the option to skip a dry run over. Removed from all eight middlewares. Two comments went with it, one of which was never accurate: # ensure it happens after CRDs are registered argocd.argoproj.io/sync-wave: "1" A sync-wave orders resources within this Application's own sync; it cannot wait on CRDs applied out-of-band by captain_utils. Whatever protection existed came from SkipDryRunOnMissingResource, not from the wave. What the wave actually does is keep the four leaf middlewares ahead of the four chains that reference them, so the comment now says that. The sync-waves themselves stay. Traefik registers no admission webhook for these CRs, so Kubernetes will happily accept a chain naming a middleware that does not exist - and Traefik then drops every router using that chain, which is a 404 on the affected host until it resolves. Wave 2 on the chains is what keeps that window closed on a first install. Rendered output differs by exactly the twelve lines removed or reworded; all eight Middlewares still render and both waves are intact. Claude-Session: https://claude.ai/code/session_01TrpnuCduw2KivngVRQmnG7 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Into #1485, not
main. RemovesSkipDryRunOnMissingResource=truefrom all 8 oauth2 middlewares intemplates/traefik-crds.yaml.Why it's dead
The option only matters when the resource type isn't registered yet. The
traefik.ioCRDs are no longer rendered by this Application — they come from the platform-crds layer-0 bundle, applied bycaptain_utilsbefore the platform chart installs. This file's own header already says so:So there's no window where the
Middlewaretype is unregistered when this syncs.Applied to all eight rather than just the three #1485 adds — those copied the existing convention, and changing only the new ones would leave the file inconsistent for no reason.
One comment was never accurate
A sync-wave orders resources within this Application's own sync. It cannot wait on CRDs applied out-of-band by
captain_utils, so it never provided that guarantee —SkipDryRunOnMissingResourcedid. Corrected to say what the wave actually does:The sync-waves stay
Worth being explicit, since it'd be easy to assume they were CRD-related too and remove them in the same pass. They aren't, and they're load-bearing:
ssl-redirect,forwardauth,errors-redirect,forwardauth-bearerwith-redirect,no-redirect,api,with-redirect-bearerTraefik registers no admission webhook for these CRs, so Kubernetes will accept a chain naming a middleware that doesn't exist — and Traefik then drops every router using that chain, which is a 404 on the affected host until it resolves. That's the same mechanism as the ArgoCD ordering hazard in #63. Wave 2 keeps the window closed on a first install.
Verification
Rendered the whole chart before and after; the diff is exactly the intended twelve lines and nothing else:
kind: Middlewareobjects still present ✅SkipDryRunOnMissingResourceremaining in the repo ✅🤖 Generated with Claude Code
https://claude.ai/code/session_01TrpnuCduw2KivngVRQmnG7