AUTHENTICATION_ERROR on self-hosted n8n behind k8s ingress, despite identical curl returning 200
Summary
Every authenticated tool call (n8n_list_workflows, n8n_get_workflow,
etc.) returns AUTHENTICATION_ERROR against a self-hosted n8n instance
behind a Kubernetes ingress, while a raw curl with the identical
X-N8N-API-KEY header from the same shell returns 200.
n8n_health_check reports connected: true because it probes /healthz
(unauthenticated), so the diagnostic is misleadingly green.
Reproduced on both 2.54.0 and 2.56.0 (latest at time of report).
Environment
-
n8n-mcp: tested 2.54.0 and 2.56.0 — same failure on both
-
n8n: self-hosted (recent), behind a k8s ingress
-
Topology:
- Internal DNS resolves to an RFC1918 address
- HTTP (not HTTPS) on port 80, ingress terminates and routes by
Host
header
-
API key shape: JWT-style (eyJhbGci…), ~267 chars, generated via
the n8n UI → Settings → n8n API → Create an API key
-
Host OS: macOS, Node v22.17.0, npx-launched MCP via the host
agent
-
MCP configuration for the n8n server (sanitized):
"n8n": {
"command": "npx",
"args": ["-y", "n8n-mcp"],
"env": {
"N8N_API_URL": "http://<internal-n8n-host>",
"N8N_API_KEY": "${N8N_API_KEY}",
"MCP_MODE": "stdio",
"WEBHOOK_SECURITY_MODE": "permissive"
}
}
WEBHOOK_SECURITY_MODE=permissive also exported in the launching
shell (verified reaching the subprocess).
N8N_API_KEY exported in the launching shell (verified: same value
as in the n8n UI; non-empty inside the subprocess).
Repro
npx -y n8n-mcp from a shell with the four env vars above set
From an MCP client, call n8n_list_workflows
Expected: list of workflows
Actual:
{
"success": false,
"error": "Failed to authenticate with n8n. Please check your API key.",
"code": "AUTHENTICATION_ERROR"
}
Smoking gun
From the same shell at the same time, the identical header succeeds:
curl -sS -o /dev/null -w "HTTP %{http_code}\n"
-H "X-N8N-API-KEY: $N8N_API_KEY"
"http:///api/v1/workflows?limit=1"
→ HTTP 200
Re-tested with axios-style UA + accept headers, with empty UA, with
explicit --http1.1 — all return 200 to curl. Only the n8n-mcp axios
client gets 401.
What we ruled out
❌ Not the API key — same key works in curl from the same env
(verified JWT prefix + length).
❌ Not SSRF blocking — WEBHOOK_SECURITY_MODE=permissive is set
AND a SSRF-blocked request would map to REQUEST_ERROR per
utils/n8n-errors.js, not AUTHENTICATION_ERROR. So the request IS
reaching the n8n server.
❌ Not header construction — services/n8n-api-client.js L72
correctly sends X-N8N-API-KEY: apiKey (not Bearer).
❌ Not a redirect — maxRedirects: 0 + /api/v1/workflows
returns 200 directly with no redirect_url.
❌ Not a User-Agent filter on the ingress — curl with axios-style
UA returns 200.
❌ Not an HTTP-version mismatch — curl with --http1.1 returns
200.
❌ Not a Host-header issue from SSRF pinning — pinned-DNS connect
to the resolved IP with the correct Host header returns 200 via curl.
❌ Not a recent regression — same failure on 2.54.0 and
2.56.0.
Conjecture
The bug appears isolated to how the n8n-mcp axios client interacts with
this n8n+ingress combination. Possible investigation directions:
The keepAlive: false pinned agent — subtle connection-reuse or
sequencing issue when the SSRF-pinned lookup callback is in play?
Some interceptor stripping or rewriting the X-N8N-API-KEY header
before the wire? (We did NOT verify with tcpdump on the local
machine; this would be the next concrete diagnostic step.)
An axios HTTP/HTTPS upgrade that breaks the n8n ingress contract on
HTTP-only deployments?
Suggested next steps
A tcpdump capture of the actual on-wire request from the n8n-mcp
subprocess (happy to produce this on request — let us know the
format).
A debug flag that logs the literal request.headers axios emits at
request-interception time would close the gap quickly without
requiring users to tcpdump.
Optional: an N8N_API_DEBUG=1 env var that prints the response
data body for non-2xx responses — currently the error path
swallows the n8n-side message and only surfaces the generic
Failed to authenticate.
Happy to provide additional diagnostics on request — fast turnaround
since we're actively trying to adopt this MCP.
AUTHENTICATION_ERRORon self-hosted n8n behind k8s ingress, despite identical curl returning 200Summary
Every authenticated tool call (
n8n_list_workflows,n8n_get_workflow,etc.) returns
AUTHENTICATION_ERRORagainst a self-hosted n8n instancebehind a Kubernetes ingress, while a raw
curlwith the identicalX-N8N-API-KEYheader from the same shell returns 200.n8n_health_checkreportsconnected: truebecause it probes/healthz(unauthenticated), so the diagnostic is misleadingly green.
Reproduced on both 2.54.0 and 2.56.0 (latest at time of report).
Environment
n8n-mcp: tested
2.54.0and2.56.0— same failure on bothn8n: self-hosted (recent), behind a k8s ingress
Topology:
Hostheader
API key shape: JWT-style (
eyJhbGci…), ~267 chars, generated viathe n8n UI → Settings → n8n API → Create an API key
Host OS: macOS, Node
v22.17.0, npx-launched MCP via the hostagent
MCP configuration for the n8n server (sanitized):
WEBHOOK_SECURITY_MODE=permissive also exported in the launching
shell (verified reaching the subprocess).
N8N_API_KEY exported in the launching shell (verified: same value
as in the n8n UI; non-empty inside the subprocess).
Repro
npx -y n8n-mcp from a shell with the four env vars above set
From an MCP client, call n8n_list_workflows
Expected: list of workflows
Actual:
{
"success": false,
"error": "Failed to authenticate with n8n. Please check your API key.",
"code": "AUTHENTICATION_ERROR"
}
Smoking gun
From the same shell at the same time, the identical header succeeds:
curl -sS -o /dev/null -w "HTTP %{http_code}\n"
-H "X-N8N-API-KEY: $N8N_API_KEY"
"http:///api/v1/workflows?limit=1"
→ HTTP 200
Re-tested with axios-style UA + accept headers, with empty UA, with
explicit --http1.1 — all return 200 to curl. Only the n8n-mcp axios
client gets 401.
What we ruled out
❌ Not the API key — same key works in curl from the same env
(verified JWT prefix + length).
❌ Not SSRF blocking — WEBHOOK_SECURITY_MODE=permissive is set
AND a SSRF-blocked request would map to REQUEST_ERROR per
utils/n8n-errors.js, not AUTHENTICATION_ERROR. So the request IS
reaching the n8n server.
❌ Not header construction — services/n8n-api-client.js L72
correctly sends X-N8N-API-KEY: apiKey (not Bearer).
❌ Not a redirect — maxRedirects: 0 + /api/v1/workflows
returns 200 directly with no redirect_url.
❌ Not a User-Agent filter on the ingress — curl with axios-style
UA returns 200.
❌ Not an HTTP-version mismatch — curl with --http1.1 returns
200.
❌ Not a Host-header issue from SSRF pinning — pinned-DNS connect
to the resolved IP with the correct Host header returns 200 via curl.
❌ Not a recent regression — same failure on 2.54.0 and
2.56.0.
Conjecture
The bug appears isolated to how the n8n-mcp axios client interacts with
this n8n+ingress combination. Possible investigation directions:
The keepAlive: false pinned agent — subtle connection-reuse or
sequencing issue when the SSRF-pinned lookup callback is in play?
Some interceptor stripping or rewriting the X-N8N-API-KEY header
before the wire? (We did NOT verify with tcpdump on the local
machine; this would be the next concrete diagnostic step.)
An axios HTTP/HTTPS upgrade that breaks the n8n ingress contract on
HTTP-only deployments?
Suggested next steps
A tcpdump capture of the actual on-wire request from the n8n-mcp
subprocess (happy to produce this on request — let us know the
format).
A debug flag that logs the literal request.headers axios emits at
request-interception time would close the gap quickly without
requiring users to tcpdump.
Optional: an N8N_API_DEBUG=1 env var that prints the response
data body for non-2xx responses — currently the error path
swallows the n8n-side message and only surfaces the generic
Failed to authenticate.
Happy to provide additional diagnostics on request — fast turnaround
since we're actively trying to adopt this MCP.