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 *not* this page 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 tiles below are the place to look. - Then this is not 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 rather than your client's exception, because AWS names the element that did not match. Credential should be scoped to correct service: '<service>' means the signing name is wrong: it is the name of whatever fronts the server — lambda for a Lambda function URL, execute-api for API Gateway, bedrock-agentcore for Bedrock AgentCore — and never mcp. Credential should be scoped to a valid Region means the Region in the credential scope. Signature expired means a clock or a queued retry, not a signing bug. A bare mismatch with all three of those 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. Once the signer is right, look for a hop that changed host or another signed header after signing.
Have your agent fix this
Copies a prompt that sends your coding agent to this page, then has it 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 very 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 Do not 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-agentandx-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 If you are on
streamablehttp_client_with_sigv4, know what you are debugging: its own module docstring calls it a "Backwards compatibility wrapper" that forwards toaws_iam_streamablehttp_client from mcp-proxy-for-aws. Both take the same three inputs — endpoint, service, region — so a mismatch is in one of those three and not in the transport.
MCP TypeScript SDK (SigV4 transport)
If your agent uses StreamableHTTPClientWithSigV4Transport or createSigV4Fetch
- 1 Two options decide the credential scope and nothing else does:
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 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 if the URL your client dials is a custom domain, a load balancer or a CDN that forwards to a different origin host, you have 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 Unlike botocore, this path publishes the payload hash on the wire.
@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". The header is then itself signed, so anything that rewrites the body after signing breaks the hash and the header together. - 4 Every header you put on the transport is 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-agentandx-amzn-trace-id, plus anything matching/^proxy-/ior/^sec-/i.mcp-session-idandmcp-protocol-versionare not on it, so a hop that adds or rewrites either after signing is a mismatch. - 5 Re-signing a request has to start from a clean header set. smithy's
prepareRequestdeletesauthorization,x-amz-dateanddatebefore 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 No MCP client signs SigV4, so nothing here is in your server. AWS's answer is
mcp-proxy-for-aws, a local stdio process launched by the client that signs from your own credentials — which means 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 is<url-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 recognise: 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 "It worked this morning" is usually 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
hostis signed on every request — AWS requires it in the canonical headers — so any 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 The volatile headers are the other half, and 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, its own configuration 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 cannot sign and cannot be taken out of the path, IAM signing is not the 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 worth naming: this is not a permissions problem — a verified signature that IAM then denies is also a 403, and only the body distinguishes them — and it is not something the MCP spec can help with, because request signing sits outside the OAuth profile there, so there is no challenge to read and no discovery to fall back on.
Why it happens
Nothing is being guessed at either end. You build a canonical request — method, URI, query string, the canonical headers, the list of 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 then does the same thing to 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 never about credentials being invalid; it is about the two canonical requests differing by one field, and there are only four places that field can be. The credential scope, where the service code or the Region is wrong — AWS answers those with their own messages rather than a bare mismatch, which is why reading the body is the first move. The payload hash, which is what makes MCP different from a normal AWS API call: a REST GET has no body, and an MCP request always does, so a signer that is handed the URL and headers but not the JSON-RPC body signs e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 — the empty-string hash — while AWS hashes the message. The signed headers, where a hop between you and AWS rewrote one; AWS's own troubleshooting note is a single line: "You can also verify that you didn't send the request through a proxy that modifies the headers or the request." And the signature itself, from a derived key or a secret that does not match the access key ID. One thing this error 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 spec rather than in conflict with it — authorization there is **OPTIONAL**, and "Implementations using an HTTP-based transport **SHOULD** conform to this specification" — so no MCP mechanism helps you here. 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." There is no challenge to follow and no discovery to fall back on.
The exchange
Each step in the exchange, in order.
- 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 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 two canonical requests, one field apartFlow stops here — MCP server not reached
What lands on you next
A verified signature tells you one thing: which IAM principal sent this request. Everything the reader asks next is about callers that have no principal, and about what a principal may do once it is through the door.
-
The signature verified. Which of our tools may that role call?
-
mcp-capability-filter-inboundA signed caller is not an authorized one. The filter allow-lists capabilities per route and blocks the rest with
MethodNotFoundbefore the request reaches your server. It is a static per-route list, not a per-user one. -
A partner's agent has no AWS account and cannot sign a thing.
-
mcp-oauth-inboundThe inbound method is a policy rather than server code, so the same route can authenticate a browser login against an OIDC provider you configure. 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_rejectedEvery rejected request emits its own event 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 spec 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.
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.