cloudflare/cloudflared · error

metadata JWT verification failed: %w

Error message

metadata JWT verification failed: %w

What it means

GetAppInfo wraps verifyMetadataWithRetry errors: the metadata JWT's signature could not be verified against the JWKS published by the auth domain (including disk cache and retry attempts). This prevents an attacker-controlled server from spoofing app identity — only JWTs signed by the account's keys are accepted.

Source

Thrown at token/token.go:445

		return nil, err
	}

	// Decode without verification to extract auth_domain for JWKS lookup.
	unverified, err := decodeMetadataUnverified(rawJWT)
	if err != nil {
		return nil, err
	}

	// Parse auth_domain into the canonical hostname used for JWKS lookup.
	authDomain, err := parseAuthDomain(unverified.AuthDomain)
	if err != nil {
		return nil, fmt.Errorf("metadata JWT auth_domain validation failed: %w", err)
	}

	// Verify the JWT signature against the JWKS (with disk cache + retry).
	claims, err := verifyMetadataWithRetry(rawJWT, authDomain)
	if err != nil {
		return nil, fmt.Errorf("metadata JWT verification failed: %w", err)
	}

	// Verify the hostname in the JWT matches the URL we actually requested.
	if !strings.EqualFold(claims.Hostname, reqURL.Hostname()) {
		return nil, fmt.Errorf("metadata JWT hostname %q does not match request host %q", claims.Hostname, reqURL.Hostname())
	}
	if claims.Type != metadataMatchType {
		return nil, fmt.Errorf("metadata JWT type %q is not match", claims.Type)
	}
	if claims.AUD == "" {
		return nil, errors.New("metadata JWT aud is empty")
	}
	if err := validateMetadataIssuedAt(claims.IAT, time.Now()); err != nil {
		return nil, err
	}

	appHostname := claims.AppHostname
	if appHostname == "" {

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Clear the cached JWKS file used by the disk cache so fresh keys are fetched, then retry.
  2. Confirm the JWKS endpoint is reachable: curl https://<team-domain>/cdn-cgi/access/certs.
  3. Remove any TLS-intercepting proxy or add its CA properly so JWKS fetches are not tampered with.
  4. Check /etc/hosts and DNS for the auth domain — it must resolve to Cloudflare.
  5. Retry after a few seconds (transient edge issues) and update cloudflared if the edge switched signing algorithms.

Example fix

// before: stale key cache causes verification failure
info, err := token.GetAppInfo(appURL)
// after: verify JWKS reachability and clear stale cache
resp, err := http.Get("https://" + authDomain + "/cdn-cgi/access/certs")
if err != nil || resp.StatusCode != 200 {
    return fmt.Errorf("JWKS unreachable for %s", authDomain)
}
os.Remove(jwksCachePath)
info, err := token.GetAppInfo(appURL)
Defensive patterns

Strategy: retry

Validate before calling

resp, err := http.Get("https://" + authDomain + "/cdn-cgi/access/certs")
if err != nil || resp.StatusCode != http.StatusOK {
    return fmt.Errorf("JWKS endpoint unreachable for auth domain %s", authDomain)
}

Try / catch

var info *token.AppInfo
var err error
for i := 0; i < 3; i++ {
    info, err = token.GetAppInfo(appURL)
    if err == nil || !strings.Contains(err.Error(), "metadata JWT verification failed") {
        break
    }
    os.Remove(jwksCachePath) // drop possibly stale cached keys
    time.Sleep(time.Duration(1<<i) * time.Second)
}
if err != nil {
    return err
}

Prevention

When it happens

Trigger: Calling GetAppInfo when the JWT's kid is absent from the fetched JWKS, the JWKS endpoint is unreachable or returns errors, the JWT is malformed/tampered, keys were rotated and the disk cache is stale, or the auth domain resolved to the wrong Cloudflare account.

Common situations: Cloudflare key rotation while a cached stale JWKS is on disk, DNS/hosts file pointing the auth domain elsewhere, an intercepting TLS proxy (Zscaler/Netskope) breaking JWKS retrieval, firewalls blocking the team domain's /cdn-cgi/access/certs endpoint, or clock skew affecting token claims.

Related errors


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/c3d8b22244928323. Report an issue: GitHub.