argocd-mcp is the Model Context Protocol server for Argo CD from argoproj-labs: you point an agent at it, and the agent can list applications, read logs, create Applications and trigger syncs. Version 0.8.0, released on June 11, 2026, started its HTTP transport on every network interface and let anyone who reached it open a session without a credential. Every tool call then went to Argo CD with the operator's token.
The fix is 0.9.0, out since August 11 together with advisory GHSA-rp45-5x3v-48mr. VulnCheck assigned CVE-2026-82456 on August 29, CVSS 10.0 in both 3.1 and 4.0. Upgrading takes one command. The more useful part is why it happened, because the same mistake is easy to make in any MCP server that sits next to infrastructure.▸The 60-second version, flip through the deck9 slides · swipe →
Two tokens, one variable
An MCP server for Argo CD has two conversations. The agent talks to the server. The server talks to the Argo CD API. Each of those needs its own answer to "who are you?".
In 0.8.0 only the second hop had a credential. This is the line the advisory points at, in src/server/transport.ts:
const argocdApiToken =
(req.headers['x-argocd-api-token'] as string) ||
process.env.ARGOCD_API_TOKEN ||
'';
If the request carries no token, the server falls back to its own. The listener itself was app.listen(port) with no host argument, which in Node means every interface. There was no Host or Origin validation either, so the advisory also flags DNS rebinding: a web page in your browser could reach a listener on your laptop.
The irony is that 0.8.0's headline change, PR #122, was a credential-hardening PR. It moved the token strictly into the transport layer so it would never land in prompts or tool-call logs. That is a real improvement for the second hop. Nobody was looking at the first.
The attack, as the advisory describes it
The reporter reproduced it against the published package with a local HTTP sink instead of a real Argo CD. No cluster was touched. The chain:
The sink recorded exactly what Argo CD would have received:
POST /api/v1/applications
Authorization: Bearer TEST-ARGOCD-TOKEN
POST /api/v1/applications/attacker-controlled/sync
Authorization: Bearer TEST-ARGOCD-TOKEN
What I saw when I ran it
I wanted to see the bind for myself rather than trust a CWE label. Each version ran from npm on my Mac with a placeholder token and ARGOCD_BASE_URL pointing at a closed local port, so nothing could ever reach a real Argo CD. Each listener lived for a few seconds.
export ARGOCD_BASE_URL=http://127.0.0.1:9 ARGOCD_API_TOKEN=PLACEHOLDER
npx [email protected] http --port 3800 &
lsof -nP -iTCP:3800 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 35096 olala 12u IPv6 0xf88f87c35a211587 0t0 TCP *:3800 (LISTEN)
Then an initialize with no credential of any kind:
curl -s -D - -o /dev/null http://127.0.0.1:3800/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"1"}}}'
HTTP/1.1 200 OK
mcp-session-id: 3dd1a05c-fa39-49ce-be1a-f153025ed8aa
0.9.0 with no extra settings:
node 35328 olala 12u IPv4 0x2186f97f9d90637b 0t0 TCP 127.0.0.1:3900 (LISTEN)
{"level":30,...,"msg":"Http Stream transport listening on 127.0.0.1:3900"}
0.9.0 asked to listen on all interfaces without an inbound token refuses, with exit code 1:
$ MCP_BIND_ADDRESS=0.0.0.0 npx [email protected] http --port 3901
{"level":50,...,"msg":"Refusing to bind to 0.0.0.0 without inbound authentication. The MCP listener exposes every ArgoCD tool to anything that can reach it, and ARGOCD_API_TOKEN authenticates this server to ArgoCD, not the caller. Either set MCP_AUTH_TOKEN to require an inbound \"Authorization: Bearer <token>\" header, or pass --allow-unauthenticated if the listener is already protected by an external layer (a sidecar proxy, service mesh, or network policy). To listen locally only, set --bind-address to 127.0.0.1."}
That error message is the best one-paragraph summary of the whole CVE, and the maintainers wrote it into the binary.
Then I tried 0.7.0, because the advisory's affected range is = 0.8.0 and I wanted to know whether older pins were actually safe:
| Version | Listener | initialize, no credential |
|---|---|---|
| 0.7.0 | *:3811 | HTTP 200 |
| 0.8.0 | *:3800 | HTTP 200 |
| 0.9.0, defaults | 127.0.0.1:3900 | loopback only |
0.9.0, 0.0.0.0 without MCP_AUTH_TOKEN | none | refuses to start, exit 1 |
0.9.0, 0.0.0.0 with MCP_AUTH_TOKEN | *:3902 | HTTP 401, Missing or invalid inbound bearer token |
| 0.9.0, with the bearer header | *:3902 | HTTP 200 |
The same env-var fallback and the bare app.listen(port) are in transport.ts at the v0.4.0, v0.5.0, v0.6.0 and v0.7.0 tags. I only ran 0.7.0 end to end (the 0.4.0 package wouldn't even start for me), and I only checked session creation, not tool calls. But "the advisory lists 0.8.0" is not the same statement as "my 0.6.0 pin is fine". Go to 0.9.0.
"Local, just for the agent" is how you started it. The socket never heard about it.
What 0.9.0 changed
--bind-address / MCP_BIND_ADDRESS defaults to 127.0.0.1. A sidecar in the same pod still reaches it.bindMCP_AUTH_TOKEN, checked as Authorization: Bearer in constant time. Separate from ARGOCD_API_TOKEN.auth--allow-unauthenticated exits non-zero instead of running exposed.startup/healthz stays open for probes.browserOne detail from the README is worth knowing before you design around it: MCP_AUTH_TOKEN is a single shared secret. Anyone holding it can reach every Argo CD instance the server knows about. If you need different callers with different reach, the maintainers' answer is separate server instances.
If you run it in Kubernetes and something outside the pod has to reach it, the release notes spell out the two settings you now need together:
-e MCP_BIND_ADDRESS=0.0.0.0 -e MCP_AUTH_TOKEN=<inbound_token>
MCP_READ_ONLY only filters the tool list
MCP_READ_ONLY=true looks like the safe switch. Here is what it does on 0.9.0, summarized from two tools/list calls over stdio:
MCP_READ_ONLY=false 16 tools
MCP_READ_ONLY=true 11 tools
hidden: create_application, update_application, delete_application,
sync_application, run_resource_action
The server stops advertising the write tools. The token in its environment can still do everything it could before. If that token is the admin one somebody generated on day one, a read-only MCP server is one bug away from a read-write one, and this CVE was that bug.
The real fix lives in Argo CD itself: a dedicated local account, its own API key, and RBAC that matches what the agent actually needs:
# argocd-cm
data:
accounts.mcp-agent: apiKey
---
# argocd-rbac-cm
data:
policy.csv: |
g, mcp-agent, role:readonly
argocd account generate-token --account mcp-agent
If the agent really must sync, give it sync on one AppProject, not *. And look at what your default project allows, because the advisory's chain works precisely because default permits any repo and any destination.
Four questions for every MCP server next to infra
This is the checklist I now run, on argocd-mcp and on everything shaped like it:
- Interface. What does it actually bind to? Run
lsoforss -ltnpon the host or in the pod, don't read the README. - Inbound auth. What does a caller have to present? "Nothing, it's local" is an answer about today's network, not about the server.
- Token owner. Whose credential sits in its environment? Yours, an admin's, or a dedicated account that exists only for this server?
- Scope. What can that credential write, with every tool enabled? Assume the tool filter will fail one day.
- Network. If it has to be reachable, what else stands in front: a sidecar proxy, a NetworkPolicy, a mesh policy?
In July I wrote about AI code and trust boundaries, and said that the part of my own Claude Code + MCP pipeline I trust least is the credentials sitting next to it. This CVE is the clean version of that worry, and the model had nothing to do with it. A small Node process held a powerful token and listened on the wrong address.
My position: an MCP server that holds infrastructure credentials gets the same review as any other service that holds them. Interface, auth, owner, scope. That it was started "for the agent" changes nothing.
Which MCP servers in your setup hold a token right now, and have you looked at what they bind to?