Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Jenkins By James Joyner IV · · 9 min read

Jenkins Error: 'HTTP ERROR 403 No valid crumb was included in the request' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix Jenkins 'HTTP ERROR 403 No valid crumb was included in the request': fetch a CSRF crumb, use API tokens, and stop your reverse proxy stripping headers.

  • #jenkins
  • #ci-cd
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

Jenkins rejected a state-changing HTTP request because it did not carry a valid CSRF token — a “crumb”. Jenkins protects POST endpoints (triggering builds, creating jobs, running Script Console, most REST/CLI writes) with cross-site request forgery prevention: the client must present a crumb tied to its session or authentication. Without it you get:

HTTP ERROR 403 No valid crumb was included in the request
URI: /job/my-app/build
STATUS: 403

This is a security control working as designed, not a broken Jenkins. The fix is to obtain a crumb from the crumb issuer and send it (or use an API token, which the modern crumb issuer honors), and to make sure nothing between your client and Jenkins — a reverse proxy, a load balancer, a caching layer — is stripping the header or breaking session affinity.

Symptoms

  • Scripted curl/REST calls that POST to Jenkins return 403 No valid crumb was included in the request.
  • jenkins-cli over HTTP fails with a 403 while the same operation works in the web UI.
  • Automation that worked before breaks after an upgrade, a proxy change, or enabling CSRF protection.
  • The request succeeds intermittently — working on one node of a Jenkins cluster and failing on another.
  • Fetching the crumb succeeds, but the follow-up POST still 403s (the crumb and the POST used different sessions).

Common Root Causes

  • No crumb sent at all — the client POSTs directly without first requesting a crumb from the crumb issuer.
  • Crumb/session mismatch — the crumb was fetched in one HTTP session but the POST was made in another, so the crumb no longer matches (very common when cookies are not persisted between the two calls).
  • Reverse proxy strips the crumb header — nginx/Apache/HAProxy in front of Jenkins drops or rewrites the Jenkins-Crumb header, so it never reaches Jenkins.
  • No sticky sessions behind a load balancer — the crumb GET hits one Jenkins/backend and the POST hits another; without session affinity the crumb is not recognized.
  • Using a password instead of an API token for automation — session-based auth needs the crumb dance; an API token is the supported path for scripts.
  • Proxy compatibility setting mismatch — the crumb is bound to the client IP but the proxy presents a different source address on the second request.

How to diagnose

Confirm CSRF protection is on and check its settings under Manage Jenkins → Security → CSRF Protection. Then reproduce the crumb fetch directly against Jenkins.

Fetch a crumb from the crumb issuer API and see what header name Jenkins expects:

# Ask the crumb issuer for a crumb (name:value)
curl -s -u "user:<api-token>" \
  "https://jenkins.example.com/crumbIssuer/api/json"
# => {"crumb":"<crumb-value>","crumbRequestField":"Jenkins-Crumb", ...}

Verify the header actually reaches Jenkins (i.e. the proxy is not stripping it) by checking the controller access/log or echoing headers at the proxy. From the Script Console you can confirm which crumb issuer is active:

// Which crumb issuer is configured, and is it proxy-compatible? (Script Console)
def issuer = Jenkins.instance.crumbIssuer
println issuer?.class?.name
if (issuer instanceof hudson.security.csrf.DefaultCrumbIssuer) {
  println "excludeClientIPFromCrumb = ${issuer.isExcludeClientIPFromCrumb()}"
}

If POSTs still fail, check whether the load balancer keeps you on one backend:

# Are the crumb GET and the POST hitting the same backend? Compare Set-Cookie / backend headers.
curl -sI -u "user:<api-token>" https://jenkins.example.com/crumbIssuer/api/json | grep -i 'set-cookie\|x-backend'

Fixes

1. Fetch a crumb and reuse the same session for the POST

The reliable pattern is: GET the crumb and the POST in one cookie jar so the session matches. With curl:

# Persist cookies so the crumb and the POST share one session
CRUMB=$(curl -s -c cookies.txt -u "user:<api-token>" \
  'https://jenkins.example.com/crumbIssuer/api/json' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["crumb"])')

curl -s -b cookies.txt -u "user:<api-token>" \
  -H "Jenkins-Crumb: ${CRUMB}" \
  -X POST 'https://jenkins.example.com/job/my-app/build'

The -c/-b cookies.txt is the important part — without a shared cookie jar the crumb is bound to a session you throw away.

2. Use an API token (the supported path for automation)

Generate a per-user API token under your user → Security → API Token → Add new Token, and authenticate with it. Modern Jenkins accepts an API token on the REST API without a separate crumb for token-authenticated requests in many endpoints; where a crumb is still required, combine both as above.

# jenkins-cli authenticated with an API token
java -jar jenkins-cli.jar -s https://jenkins.example.com/ \
  -auth user:<api-token> build my-app -f -s

Prefer the SSH CLI transport for jenkins-cli where possible — it does not go through the HTTP CSRF path at all:

java -jar jenkins-cli.jar -s https://jenkins.example.com/ \
  -ssh -user user build my-app

3. Stop the reverse proxy stripping the crumb header

Ensure the proxy passes the crumb header and the Cookie/Authorization headers through unchanged. For nginx:

location / {
    proxy_pass          http://127.0.0.1:8080;
    proxy_set_header    Host              $host;
    proxy_set_header    X-Real-IP         $remote_addr;
    proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto $scheme;
    proxy_pass_request_headers on;      # do not drop Jenkins-Crumb
    proxy_buffering     off;
}

Underscore-containing headers are rarely an issue here (Jenkins-Crumb uses hyphens), but if you use a custom header, remember nginx drops underscored headers unless underscores_in_headers on;.

4. Make the crumb tolerant of proxied client IPs

If the proxy presents a different source IP on the second request, the default issuer’s IP binding rejects the crumb. Either enable sticky sessions on the load balancer, or configure the crumb issuer to exclude the client IP. Via Configuration as Code:

jenkins:
  crumbIssuer:
    standard:
      excludeClientIPFromCrumb: true

Excluding the client IP is the standard fix behind a proxy/load balancer. Do not disable CSRF protection wholesale — that removes the control instead of fixing the transport.

What to watch out for

  • Never “fix” this by turning off CSRF protection. It defeats a real security control; the correct fix is sending the crumb or using a token.
  • The crumb header field name is not always Jenkins-Crumb — read crumbRequestField from the crumb issuer response and use exactly that.
  • A crumb without its session is worthless. If you fetch and POST in separate processes, carry the cookie too, or use API-token auth.
  • Behind a load balancer, enable session affinity (sticky sessions) or excludeClientIPFromCrumb, otherwise POSTs will fail non-deterministically.
  • API tokens are per-user and revocable — scope them to a service account, never embed a human password in automation.
  • The Jenkins guides hub collects related auth and pipeline fixes.
Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.