On this page
SigV4 signature fails to verify
The request signature we calculated does not match the signature you provided AWS recomputed the signature over the request it actually received, got a different value from the one you sent, and answered 403 before your MCP server ran — so nothing about your tools, your handler, or your IAM policy is implicated yet.
- MCP specification
- 2026-07-28
Is this you?
Sign one request with a known-good signer and read the body AWS sends back, then prove the credentials and the clock separately. That splits the three things this message covers: a wrong service or Region, a canonical request that differs from what arrived, and credentials that were never going to work.
- This error
- A
403whose body carriesThe request signature we calculated does not match the signature you providedis this page. A403whose body isCredential should be scoped to correct service: '<service>'orCredential should be scoped to a valid Region, not <region-code>is the same page with the answer already in it — change that one value.Signature expired: <date> is now earlier than <date>is a clock or a request that sat in a retry queue, not a signing bug. Three responses are none of the above and need a different fix:Missing Authentication Token, which means the request arrived unsigned; a403carryingAccessDeniedExceptionor AgentCore'sACCESS_DENIED, which means the signature verified and the IAM policy said no — on a new function URL that needs bothlambda:InvokeFunctionUrlandlambda:InvokeFunction; and a403from step 2 as well as step 1, which means the credentials or the clock are wrong for every AWS call you make and the MCP endpoint is incidental. - Then the fix is the next section.
- Working
- Step 1 prints
200withapplication/jsonortext/event-streamand a JSON-RPC result, and aSignedHeaderslist that starts withcontent-type;host;x-amz-date—hostandx-amz-dateare required on every signed request. Step 2 prints the ARN you expect. Step 3's two timestamps agree to within a few seconds. If step 1 succeeds while your MCP client still fails, the fault is entirely in your client's signing path and the platform tiles are the place to look. - Then this isn't your problem — try the error reference.
7 other strings clients print for this
- SignatureDoesNotMatch
- Credential should be scoped to correct service: '<service>'
- Credential should be scoped to a valid Region, not <region-code>
- Signature expired: <date> is now earlier than <date>
- Signature not yet current: <date> is still later than <date>
- Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP
- The signature contains an invalid key=value pair (missing equal-sign) in Authorization header
Dotted spans such as <url> are placeholders — your client prints your own value there.
Fix it
Read the response body, not your client's exception: AWS names the element that did not match. Credential should be scoped to correct service: '<service>' means the signing name — whatever fronts the server, so lambda for a Lambda function URL, execute-api for API Gateway, bedrock-agentcore for Bedrock AgentCore, never mcp. Credential should be scoped to a valid Region means the Region in that scope; Signature expired means a clock or a queued retry, not a signing bug. A bare mismatch with all three right means the canonical request itself differs, and for MCP that is almost always the payload hash — every JSON-RPC message is a POST with a body, and a signer handed no body signs the SHA-256 of the empty string. Then look for a hop that changed host or another signed header after signing.
Have your agent fix this
Copy a prompt that sends your coding agent to this page to find and apply the right fix for your project.
The cause depends on your platform. Pick yours.
Where is the signature going wrong?
MCP Python SDK (botocore signer)
If the signing happens in Python — aws_iam_streamablehttp_client, streamablehttp_client_with_sigv4, or your own SigV4Auth
- 1 Check the body reached the signer before anything else.
SigV4Auth.payload()hashesrequest.body, and with no body it returns botocore'sEMPTY_SHA256_HASH—e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Build anAWSRequestwithoutdata=, sign it, thenPOSTthe JSON-RPC message withrequestsorhttpx, and the canonical request you signed claims an empty payload while AWS hashes the message. Every MCP call is aPOSTwith a body, so this fails on the first request and never works once. - 2 Copy the whole signed header set onto the outgoing request, not just
Authorization. botocore's_modify_request_before_signingaddsX-Amz-Security-Tokenitself when the credentials carry a session token, and that header is part of the canonical request — cherry-pickAuthorizationout of the signedAWSRequestand the token is silently dropped. AWS's own signing reference is explicit that temporary credentials need it: "When you use temporary security credentials, you must addX-Amz-Security-Tokento the Authorization header or include it in the query string to hold the session token." - 3 Pass the service name rather than letting it be inferred.
aws_iam_streamablehttp_clienttakesaws_serviceandaws_regionexplicitly; AWS's proxy in front of it does infer, and its own troubleshooting entry for this error explains why to override that: "We try to autodetect the service from the url, sometimes this fails, ensure that--serviceis set correctly to the service you are attempting to connect to. Otherwise the SigV4 signing will not be able to be verified by the service you connect to, resulting in this error." - 4 Don't hand botocore headers that change in flight. Its
SIGNED_HEADERS_BLACKLISTalready skipsconnection,expect,keep-alive,proxy-authenticate,proxy-authorization,te,trailer,transfer-encoding,upgrade,user-agent, andx-amzn-trace-id, and AWS's signing reference says why: "Do not include hop-by-hop headers that are frequently altered during transit across a complex system." AWS's proxy strips one by hand for exactly this reason — its comment reads "Header 'connection' = 'keep-alive' is not used in calculating the request signature on the server-side, and results in a signature mismatch if included". - 5 On
streamablehttp_client_with_sigv4, check the three inputs rather than the transport. Its own module docstring calls it a "Backwards compatibility wrapper" that forwards toaws_iam_streamablehttp_client from mcp-proxy-for-aws, and both take the same three: endpoint, service, and region.
MCP TypeScript SDK (SigV4 transport)
If your agent uses StreamableHTTPClientWithSigV4Transport or createSigV4Fetch
- 1 Check the two options that decide the credential scope,
regionandservice. The transport's own comment names the values it expects — "AWS service name for signing requests (e.g., 'lambda', 'execute-api')" — and AgentCore addsbedrock-agentcore. Get either wrong and AWS answers with the element it wanted rather than a bare mismatch:Credential should be scoped to correct service: '<service>', orCredential should be scoped to a valid Region, not <region-code>. - 2 Sign the origin host, not a front door. The signer takes the signed
hostfrom the URL you construct it with — it seeds the header map withhost: urlObj.hostname— andhostis required in every canonical request, so a custom domain, a load balancer, or a CDN that forwards to a different origin host means you signed the wrong host. AWS documents this exact failure for a function URL behind CloudFront: "Lambda will validate the signature against the host of the Lambda URL domain. If the signature isn't based on the Lambda URL domain, the host in the signature won't match the host used by the Lambda URL origin. This means the request will fail, resulting in a signature validation error." - 3 Read the payload hash off the wire, which botocore doesn't give you.
@smithy/signature-v4setsx-amz-content-sha256when the header is absent andapplyChecksumis on, andapplyChecksumis on by default — the source comment is "default to true if applyChecksum isn't set". That header is itself signed, so anything that rewrites the body after signing breaks the hash and the header together. - 4 Expect every header you put on the transport to be signed unless the signer considers it unsignable, and the exempt list is fixed:
ALWAYS_UNSIGNABLE_HEADERScoversauthorization,cache-control,connection,expect,from,keep-alive,max-forwards,pragma,referer,te,trailer,transfer-encoding,upgrade,user-agent, andx-amzn-trace-id, plus anything matching/^proxy-/ior/^sec-/i. The MCP transport headers are not on it —mcp-protocol-versionandmcp-methodon every request, plusmcp-nameon the methods that carry a name (tools/call,resources/read,prompts/get), all of which2026-07-28made mandatory — so a hop that adds or rewrites any of them after signing is a mismatch. - 5 Start every re-sign from a clean header set. smithy's
prepareRequestdeletesauthorization,x-amz-date, anddatebefore it signs, and a caller that restores its own saved headers afterwards puts the previous signature back over the new body. LiteLLM shipped a fix for precisely that shape: a retry re-signed a modified body, the first attempt'sAuthorizationandX-Amz-Datewere restored on top, and AWS returned403 SignatureDoesNotMatch.
Claude Code
If a chat client is the caller and AWS's local signing proxy is doing the signing for it
- 1 Look in the proxy, not in your server: no MCP client signs SigV4. AWS's answer is
mcp-proxy-for-aws, a local stdio process launched by the client that signs from your own credentials, so every value in this error comes from that process's arguments and environment. - 2 Set
--serviceexplicitly. The flag's own documentation is "AWS service name for SigV4 signing, if omitted we try to infer this from the url", and the proxy's troubleshooting entry for this error is about that inference failing. A function URL host isURL_ID.lambda-url.REGION.on.aws, which carries no service label to infer from at all. - 3 Set the Region the same way.
--region"UsesAWS_REGIONenvironment variable if not set", and a process a client spawns does not necessarily inherit your shell's exports — put the value inargs, or in the config'senvblock. A separate symptom to recognize: no Region resolved at all surfaces asNoRegionError, which the proxy's docs note MCP clients show as-32602: Invalid request parameters, not as a signature error. - 4 Rule out stale credentials, which usually are not this error. The proxy "reads fresh credentials from disk on every request, so credential refreshes and account switches take effect immediately — no restart or retry required", and expired credentials produce
Signature expired: <date> is now earlier than <date>rather than a mismatch. Runaws sso loginand retry without restarting anything. If the message is still the mismatch, the service or the Region is wrong.
AWS API Gateway (IAM auth)
If the MCP route is an API Gateway method whose authorizationType is AWS_IAM
- 1 The service code is
execute-api, notapigatewayand notlambda. AWS's permissions model page puts it plainly: the invoking actions are "execute-api:Invokeorexecute-api:InvalidateCache, whereexecute-apidesignates the underlying API execution component of API Gateway." Sign with the wrong one and the credential-scope check fails before the signature is compared, giving youCredential should be scoped to correct service: '<service>'. - 2 The stage is part of the path you sign. AWS's annotated canonical request marks the second line as "Path. For API stage endpoint, it should be /{stage-name}/{resource-path}", against a host of the form
<api-id>.execute-api.<region>.amazonaws.com. A client configured with the bare invoke URL and the stage stitched on elsewhere signs a path the gateway never sees. - 3 Sign the Region the API lives in, which the host already tells you. Both signature versions are accepted — "API Gateway supports Signature Version 4a (SigV4a) and Signature Version 4 (SigV4) to authenticate the user's credentials" — but the credential scope has to match either way, and with SigV4a the Region moves out of the scope and into an
X-Amz-Region-Setheader that is signed instead. - 4 Separate this from an unsigned request. AWS: "Make sure that the API request is signed with SigV4. If the API request isn't signed, then you might receive the error:
Missing Authentication Token." That is a client that never signed at all, which is a different fix from a signature that did not verify.
A load balancer or proxy in the path
If anything sits between the code that signs and the AWS service that verifies — a load balancer, a CDN, a service-mesh sidecar, or a gateway re-signing on your behalf
- 1 Start here when the same request succeeds against the AWS hostname and fails through your own. AWS's troubleshooting note is one sentence and it is the whole diagnosis: "You can also verify that you didn't send the request through a proxy that modifies the headers or the request."
- 2 Check what
Hosteach hop presents.hostis signed on every request — AWS requires it in the canonical headers — so a hop that presents a differentHostto the verifying service breaks the signature by construction, before anything about your payload matters. AWS spells this out for a function URL behind CloudFront: "If the signature isn't based on the Lambda URL domain, the host in the signature won't match the host used by the Lambda URL origin. This means the request will fail, resulting in a signature validation error." - 3 Take the volatile headers out of what you sign. AWS names them: "Do not include hop-by-hop headers that are frequently altered during transit across a complex system. This includes all volatile transport headers that are mutated by proxies, load balancers, and the nodes in a distributed system, including
connection,x-amzn-trace-id,user-agent,keep-alive,transfer-encoding,TE,trailer,upgrade,proxy-authorization, andproxy-authenticate." A tracing sidecar that stampsx-amzn-trace-idafter signing is enough, if your signer included it. - 4 Where the hop signs for you, check its configuration instead of yours: it owns the service and the Region. AgentCore Gateway is the documented case: for an MCP server or OpenAPI target under IAM outbound authorization you supply an
iamCredentialProviderwhoseserviceis "The AWS service name used for SigV4 signing. For example,bedrock-agentcorefor MCP servers hosted on Amazon Bedrock AgentCore", and whoseregionis optional — "If you don't specify a Region, the gateway uses its own Region." A target in a different Region from the gateway therefore gets signed for the wrong one by default. Lambda, API Gateway, and Smithy targets take noiamCredentialProviderat all. - 5 If the hop can't sign and can't be taken out of the path, pick a different method for that route. A
PUTorPOSTthrough CloudFront's origin access control needs the caller to supply the hash: "your users must compute the SHA256 of the body and include the payload hash value of the request body in thex-amz-content-sha256header when sending the request to CloudFront. Lambda doesn't support unsigned payloads." Every MCP message is aPOSTwith a body, so that requirement applies to all of them.
Something else
Whatever the components are, this error has one shape: two canonical requests differ by one field, and only three things can be changed — what you sign, what arrives, or the scope you sign it under. Work it in that order. Read the response body first, because AWS names the service, the Region, or the date when one of those is what differed, and a bare mismatch is the only case that needs investigating. Then reproduce with a known-good signer over the same body, since every MCP message is a POST and a signer handed no body signs the empty-string hash. Then bisect the hops: sign for the AWS hostname, then for each proxy, ingress, or domain in front of it, and fix the layer where the request stopped matching rather than the signer. Two dead ends: this is not a permissions problem, though an IAM denial returns the same 403 and only the body distinguishes them, and it is not something the MCP specification can help with, because request signing sits outside the OAuth profile there.
Why it happens
Both ends compute the same thing. You build a canonical request — method, URI, query string, the canonical headers, the names in SignedHeaders, and a hex SHA-256 of the payload — hash it, prepend a credential scope that pins the date, the Region, and the service code, and sign that. AWS repeats the calculation on the bytes that arrived: "When the AWS service receives a signed request, it recalculates the signature. If there are differences in the values, then the signatures don't match."
So the message is not about invalid credentials. It is two canonical requests one field apart, and that field sits in one of four places.
The scope names the wrong service code or Region, which AWS reports with a message of its own rather than a bare mismatch. The payload hash differs, which is the MCP-specific case: every MCP request carries a body, and a signer handed the URL and headers but not the JSON-RPC body signs e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, the empty-string hash, while AWS hashes the message.
A hop in the path rewrote a signed header — AWS's troubleshooting note: "You can also verify that you didn't send the request through a proxy that modifies the headers or the request." Or the key is wrong, and the signature came from a derived key or a secret that doesn't match the access key ID.
This is not an authorization failure: a signature that verifies and an IAM policy that denies both return 403, and only the body tells them apart. SigV4 also sits outside the MCP authorization specification — authorization there is OPTIONAL, and "Implementations using an HTTP-based transport SHOULD conform to this specification" — so no MCP mechanism helps you here, and AgentCore says as much in its own protocol contract: "SigV4-configured agents return HTTP 403 with an ACCESS_DENIED error and do not include WWW-Authenticate headers."
The exchange
- Your agent IAM role credentials
- AWS function URL, API Gateway, or AgentCore
- MCP server never invoked
-
No token is fetched and nothing is cached. The signature covers the host header, every name listed in SignedHeaders, and a SHA-256 of the JSON-RPC body, and the credential scope pins the date, the Region, and the service code.
Inside Your agent
Sign the canonical request scope: YYYYMMDD/region/service/aws4_request -
Your agent to AWS
POST /mcp (carries the credential) Authorization: AWS4-HMAC-SHA256 … -
AWS replicates the whole calculation on the bytes it received. Because the check runs before the integration is invoked, the MCP server never executes and nothing in its logs will show the request.
Inside AWS
Recompute over what arrived host + SignedHeaders + payload hash -
AWS to Your agent
403 SignatureDoesNotMatch (this step fails) two canonical requests, one field apartFlow stops here — MCP server not reached
A verified signature tells you which IAM principal sent the request, and nothing about what that principal can do or about callers that have no principal at all.
-
The signature verified. Which of our tools can that role call?
-
mcp-capability-filter-inboundOnly what the route allowlists — a verified signature got the caller in, nothing more. The rest is blocked with
MethodNotFoundbefore the request reaches your server, andaccessControl.mode: rolesAndGroupsnarrows the list per caller — though an IAM role is one caller for every workload that assumes it. -
A partner's agent has no AWS account and cannot sign a thing.
-
mcp-oauth-inboundThey don't have to: the same route can authenticate a browser login against an OIDC provider you configure, because the inbound method is a policy rather than server code. Exactly one such policy per project — the runtime rejects a second.
-
A bad signature and an IAM denial are both 403. Which was it?
-
mcp_request_rejectedThe event says which: every rejected request emits one carrying the reason, so a refusal resolves to a named outcome instead of a status line you have to infer a cause from.
-
Each API behind this server needs its own credential. Do the agents hold them all?
-
mcp-token-exchange-inboundNo. The specification forbids passthrough with a MUST NOT. The inbound credential is dropped and an independent upstream one is minted per user or per gateway.
All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out. Your MCP server keeps
the code and the authentication it has today.
Stop debugging auth on every MCP server
A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.