feat: dockerised toolbox with the platform CLIs preauthenticated - #1
Merged
Merged
Conversation
Everything on a GlueOps cluster sits behind oauth2-proxy, which expects a browser
session cookie. CLIs do not have one, so out of the box every argocd and bao
request is answered with a login redirect - and developers have no kubectl to
work around it. Ship the CLIs in a container that handles the edge, so they can
be used normally.
Authentication uses the OIDC device flow against Dex. That is what makes this
work from a container: there is no loopback listener and no redirect URI, so the
browser can live on the host - a localhost:8085 callback could not. Dex issues a
refresh token alongside, so the browser step happens once rather than daily.
ArgoCD accepts the same token directly (the platform sets the toolbox audience in
allowedAudiences), so one token satisfies both the edge and ArgoCD. The wrapper
sets ARGOCD_AUTH_TOKEN per call, so a long-lived shell never goes stale.
OpenBao cannot work that way: its own credential travels in X-Vault-Token while
the edge wants an Authorization bearer, and the -header flag that would supply it
must sit after the subcommand and before any positional argument -
bao kv get -header="..." secret/foo ok
bao kv get secret/foo -header="..." rejected
bao -header="..." kv get secret/foo rejected
- and since `bao kv list secret` is indistinguishable from a subcommand plus a
path, no wrapper can place it correctly in general. So a small loopback proxy
adds the header and forwards upstream, with BAO_ADDR pointed at it. bao then
needs no flags and scripts work unmodified. It binds 127.0.0.1 only, since it
attaches the caller's credential to whatever it forwards.
Built for linux/amd64 and linux/arm64 so Apple Silicon is native. TARGETARCH is
left without a default deliberately: a default would silently produce an arm64
image full of amd64 binaries when built natively on a Mac.
No VOLUME directive, and the cache directory is created owned by the runtime
user. A VOLUME would create a fresh anonymous volume per `docker run`, so the
token cache would never survive a restart, and an unowned mount point makes the
first cache write fail outright.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
ARGOCD_AUTH_TOKEN does not work through oauth2-proxy. The CLI sends that
credential in a `Token:` header, and oauth2-proxy only reads `Authorization` - so
the edge saw no bearer at all, bounced the request to a login page, and the CLI
reported `rpc error: code = Unknown desc = unexpected EOF`, which says nothing
about what went wrong.
Confirmed by dumping what the client sends:
ARGOCD_AUTH_TOKEN=x -> Token: x
-H "Authorization: ..." -> Authorization: Bearer x
-H is a global flag, so it can precede the subcommand and the wrapper stays
trivial. Verified against the live cluster: oauth2-proxy now logs a bearer
verification attempt for argocd-client requests, where previously it logged none.
Also stop reporting every upstream 3xx as an edge rejection. OpenBao's own 403 was
being rewritten into a login redirect by the platform's errors-redirect plugin,
and this message attributed that to a bad token - which sent the diagnosis in
entirely the wrong direction. Only a redirect to the oauth2 login endpoint is an
auth failure now; anything else is reported as what it is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
Two follow-on failures from the previous fix, both found against the live cluster.
argocd needs the token in TWO headers, because each side reads only its own:
ARGOCD_AUTH_TOKEN -> "Token: <jwt>" ArgoCD reads this
-H "Authorization" -> "Authorization: Bearer" oauth2-proxy reads this
The previous commit sent only the second, which got past the edge but left
Token: empty, so ArgoCD answered "Unauthenticated: no session information". The
one before sent only the first, which the edge ignored. Send both - it is the
same Dex token, and ArgoCD accepts it via allowedAudiences.
`bao login -method=jwt` also does not exist: the OpenBao CLI registers no jwt
auth method, only oidc, which is the browser redirect flow - so it fails with
"Unknown auth method: jwt". Post to the login endpoint directly instead, and do
it as part of toolbox-login so a single login leaves both CLIs usable rather than
requiring a second, differently-shaped one. Roles are tried most-privileged first;
the role's bound_claims decide which one a given user actually gets, so a
rejection there is expected rather than an error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
The platform now mounts the jwt auth method separately from the web UI's oidc mount, so its roles no longer need the "-jwt" suffix that only existed to avoid a name collision on a shared mount. Operators write role=reader and get the reader policy. TOOLBOX_BAO_AUTH_PATH (default "jwt") for clusters that mount it elsewhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
`docker exec` bypasses the ENTRYPOINT, so BAO_ADDR, ARGOCD_SERVER and ARGOCD_OPTS were unset in any shell started that way. bao then fell back to its own default address instead of the local proxy - silently, since that default happens to be the same port on https rather than http. Move the computation into a sourceable /etc/toolbox-env.sh, read by the entrypoint and by shells started later (profile.d for login shells, .bashrc for interactive ones), so `docker exec -it toolbox bash` behaves like the session the entrypoint set up. Found while running the flow end to end as a developer would. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
Matches the convention used by the other GlueOps repositories: release-please driven by conventional commits, authenticated with the release-please GitHub App rather than GITHUB_TOKEN so the release tag triggers the image build. The manifest starts at 0.0.0 and bump-patch-for-minor-pre-major is set, so feat commits bump the patch while below 1.0.0 and the first release lands on v0.0.1 rather than v0.1.0. No Release-As override is needed, so the behaviour does not depend on how the first PR happens to be merged. Also add a .dockerignore: without it the build context carried .git and the docs, which are not needed to build the image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
Someone can now point an agent at this repo and ask a question about their cluster, and the agent has what it needs to get in. Scoped to authentication only. An agent already knows argocd and bao; what it cannot infer is how to get access, and the parts it gets wrong without being told: - `docker run -it` is useless to it. No TTY means nothing to type into and no way to read the device URL back out. Run detached, then docker exec. - toolbox-login blocks until a human approves. It has to run in the background, and the URL has to be relayed to the person rather than polled silently - they cannot approve something they have not been shown. - `docker exec` bypasses the ENTRYPOINT, so commands need a login shell to pick up /etc/toolbox-env.sh. Without it argocd and bao are misconfigured. Also states the obvious-to-a-human rules worth making explicit: do not print the token, default to the read-only role, do not mutate anything unasked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiwgsqcQ4JikhFYj4NHEjM
venkatamutyala
force-pushed
the
feat/dockerised-toolbox
branch
from
August 30, 2026 15:56
5794ef9 to
ed89f1f
Compare
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.
Developers can't use the platform CLIs today. Everything on a GlueOps cluster sits behind oauth2-proxy, which expects a browser session cookie; a CLI has none, so every request is answered with a login redirect — and they have no
kubectlto work around it.This ships the CLIs in a container that deals with the edge, so they behave normally:
One browser approval. No
argocd login, nobao login, no flags, no kubectl.How
Device flow, not loopback.
toolbox-loginuses the OIDC device flow against Dex — no loopback listener and no redirect URI, so the browser can live on the host. Alocalhost:8085callback could not work from a container. Dex returns a refresh token too, so the browser step happens once, not daily.One token, three verifiers. The Dex
id_tokenis checked independently by oauth2-proxy (oidc_extra_audiences), ArgoCD (allowedAudiences) and OpenBao (jwt roles'bound_audiences). None of them coordinate — they each verify the same signature against Dex's JWKS.Each side wants it in a different header, which is most of what made this fiddly:
ARGOCD_AUTH_TOKENToken:-H "Authorization: …"Authorization: BearerbaoX-Vault-TokenThe
argocdwrapper sends the first two. Forbaothere is no workable wrapper —-headermust sit after the subcommand and before any positional, andbao kv list secretis indistinguishable from a subcommand plus a path:So a loopback proxy adds the header and forwards upstream, with
BAO_ADDRpointed at it.baoneeds no flags and scripts work unmodified. It binds127.0.0.1only — it attaches the caller's credential to whatever it forwards.toolbox-loginalso exchanges the Dex token for an OpenBao token (posting to the login endpoint directly, since the OpenBao CLI registers nojwtmethod), so both CLIs are usable after one login.Restricting access
TOOLBOX_BAO_ROLES=readerpins the session to read-only. Verified:bao kv put→403 permission denied, nothing written.Platforms
linux/amd64andlinux/arm64, so Apple Silicon is native.TARGETARCHhas no default on purpose — a default would silently produce an arm64 image full of amd64 binaries when built natively on a Mac. CI builds both and smoke-tests the native one.Releases
release-please, matching the other GlueOps repos. The manifest starts at
0.0.0withbump-patch-for-minor-pre-major, so the first release is v0.0.1 rather than v0.1.0, without depending on how this PR is merged.Testing
Run end to end against a live cluster, twice, as a developer would — fresh image, fresh container, empty volume, real browser approval:
Four bugs were found by actually running it, not by review:
ARGOCD_AUTH_TOKENalone never reaches the edge — ArgoCD sends it inToken:, which oauth2-proxy doesn't read.-Halone gets past the edge but leavesToken:empty, so ArgoCD answers "no session information". Both headers are needed.VOLUMEdirective made the token cache root-owned and gave a fresh anonymous volume perdocker run, so the cache could never persist.docker execbypasses the ENTRYPOINT, leavingBAO_ADDRunset —baothen fell back to its own default, which is the same port on https, failing confusingly.Cluster prerequisites
A public
toolboxDex client, that audience in oauth2-proxy'soidc_extra_audiencesand ArgoCD'sallowedAudiences, and jwt-type OpenBao roles bound to it — GlueOps/platform-helm-chart-platform#1485 and GlueOps/terraform-module-kubernetes-hashicorp-vault-configuration#68.ghcr.io/glueops/toolboxdoes not exist until this merges; PR builds don't push.