Resolve Lambda URL Error - signature not match when using POST/PUT

Vuong Bach Doan - Aug 31 - - Dev Community
{
   "message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."
}
Enter fullscreen mode Exit fullscreen mode

I. Current issue

šŸ’„Ā A guy with same problem posted a question at this link:
https://repost.aws/questions/QUbHCI9AfyRdaUPCCo_3XKMQ/lambda-function-url-behind-cloudfront-invalidsignatureexception-only-on-post


Describe:

  • He using CloudFront in front of Lambda Function URL, but he can only using GET method, POST/PUT request is rejected

    Image description


Analysis problem:

āš ļø Lambda URL have 2 type of authentication:

  • NONE: anyone with URL can access function
  • AWS_IAM: require signed-header

Image description

If you are using AWS_IAM, all user canā€™t directly access Lambda URL. It require signed-header as x-amz-content-sha256

II. Solution

Solution 1: Create signed-header with boto3 session

The custom header is add manually from client side.

Note that with this solution, using CloudFront is optional as we do nothing to sign header at CloudFront side, we do it from client side.

CloudFront in this case only for CDN purpose.

Using CloudFront, you can using GET method even though you donā€™t have x-amz-content-sha256 in header. But if you call directly to Lambda URL, it will reject all METHODs immediately.


Here is how you can sign request:

Step 1. Create boto3 session to sign header

import boto3
from botocore import crt, awsrequest

class SigV4ASign:

    def __init__(self, boto3_session=boto3.Session()):
        self.session = boto3_session

    def get_headers(self, service, region, aws_request_config):
        sigV4A = crt.auth.CrtS3SigV4AsymAuth(self.session.get_credentials(), service, region)
        request = awsrequest.AWSRequest(**aws_request_config)
        sigV4A.add_auth(request)
        prepped = request.prepare()

        return prepped.headers

    def get_headers_basic(self, service, region, method, url):
        sigV4A = crt.auth.CrtS3SigV4AsymAuth(self.session.get_credentials(), service, region)
        request = awsrequest.AWSRequest(method=method, url=url)
        sigV4A.add_auth(request)
        prepped = request.prepare()

        return prepped.headers
Enter fullscreen mode Exit fullscreen mode

Step 2: Using header in our request

from sigv4a_sign import SigV4ASign
import requests

service = 'lambda'
region = '*'
method = 'GET'
url = 'https://4xmze5deqxjjy4ltw2ze3h7gr40tlvcp.lambda-url.us-east-1.on.aws'

headers = SigV4ASign().get_headers_basic(service, region, method, url)
r = requests.get(url, headers=headers)
print(f'status_code: {r.status_code} \nobject text: {r.text}')

Enter fullscreen mode Exit fullscreen mode

Solution 2: Bypass signed-header by create LambdaEdge to assign token at CloudFront

With this solution, all traffic is signed at CloudFront by LambdaEdge no matter who send the request. It means that everyone can call Lambda URL through CloudFront distribution domain.

It only helpful to prevent traffic go directly to Lambda function, but not validate if a user go through CloudFront.


You can implement a solution to sign custom header by follow this document:

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-how-it-works-tutorial.html

Futher Read - Why only GET method can bypass CloudFront to invoke Lambda URL

ā‰ļøĀ We know that Lambda URL requires all methods to have signed header, but why GET method can go through CloudFront to invoke function?

Here is message when I test with GET method at CloudFront, as you see it includes the Headers, and it included x-amz-content-sha256 .

{
    "message": "Hello from Lambda!",
    "headers": {
        "x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        "cloudfront-is-android-viewer": "false",
        "content-length": "0",
        "x-amzn-tls-version": "TLSv1.3",
        "cloudfront-viewer-country": "US",
        "postman-token": "2b178115-dd65-4567-8cfb-07b95ed45d6e",
        "x-amz-source-account": "058264411535",
        "cloudfront-viewer-tls": "TLSv1.3:TLS_AES_128_GCM_SHA256:sessionResumed",
        "x-forwarded-port": "443",
        "x-amz-security-token": "IQoJb3JpZ2luX2VjED0aCXVzLWVhc3QtMSJHMEUCIQDVVeJvqePhgQHPp2rQwVkuB1eNg7lIwpqLd7UpIw4ihwIgBjkJsp9pna8XikOc8Oo0sIW5wsKteL1gc/2PvgVwcu8qtQIIVhAAGgw4NTYzNjkwNTMxODEiDOcCEWCBKujgcCE8KCqSAk3VAG17dDtr7yyyni1Eclx3bOChaK3Lq8ZKhz3LDFZ9KwHErLm5CVFMLg7fHmqHbxYPFxRft6rFJfnePq3XV2x12r8WVNiPMr4rrrNevzB7Qyn2n2CdwOalsFYm7I3fLB4oT5hR7CNN61tzJpupy5XnzbkQJ1HmjnlXRmbQTnkWjpooWuZXZF5hmNKpOAH7+nrn+L97+9xSkYKGs5Yj66BD/TxeoUKBD1extNk8Dz49yvyX3Le9O7rUwWuMof8Qih6aABL8IRQQs5qo2tpuCIODe+nRi2F1j7CJ85YW5MPp2FVvCLOCi0y7ApAiIpzn+g3fQCd830DqEwA1FJ3Mc+eDDy+SZ3j0oDK97lOgAJFtOMQw5JXFtgY6jwGzvIJEeEV29B3yHCpalJciUNzGxWtt2Vw7f1Btv02KgbnGyVzFvs4Dh3Ia6ldLAWevM6ZB/h62870hj0XKBUw5Q/sZR+LqJGDFyD/UfMWb6zSbv6mCc3xtbO/HoxUW7qwNiBGwqDvxvls9383cnpTT8rHPviOGv78QAnNF7qGmKGVPlGWignUvEzYx2Tur8g==",
        "via": "1.1 eb8674b99d3dfcc6867fb20af353442a.cloudfront.net (CloudFront)",
        "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256",
        "cloudfront-viewer-asn": "14618",
        "cloudfront-is-desktop-viewer": "true",
        "host": "4xmze5deqxjjy4ltw2ze3h7gr40tlvcp.lambda-url.us-east-1.on.aws",
        "cache-control": "no-cache",
        "cloudfront-viewer-city": "Ashburn",
        "cloudfront-viewer-http-version": "1.1",
        "cloudfront-viewer-address": "54.86.50.139:4863",
        "x-amz-date": "20240830T061644Z",
        "x-forwarded-proto": "https",
        "cloudfront-is-ios-viewer": "false",
        "x-forwarded-for": "54.86.50.139",
        "accept": "*/*",
        "x-amz-source-arn": "arn:aws:cloudfront::058264411535:distribution/E1ZAD106XSWJF5",
        "cloudfront-is-smarttv-viewer": "false",
        "x-amzn-trace-id": "Self=1-66d163cc-0bda717d14f2a84850a6eed2;Root=1-66d163cc-711a9a2d2cad036f1be882a4",
        "cloudfront-is-tablet-viewer": "false",
        "cloudfront-forwarded-proto": "https",
        "accept-encoding": "gzip, deflate, br",
        "x-amz-cf-id": "89c8mF_r2r7fOOOaYREIJ5ZZW4mfurh81xCmzYA5VHbyDTy56i0Crg==",
        "user-agent": "PostmanRuntime/7.41.2",
        "cloudfront-is-mobile-viewer": "false"
    }
}
Enter fullscreen mode Exit fullscreen mode

I did not sign the request before, so we can imagine that CloudFront automatically sign request header for requests that using GET method.

But when I call POST method it show error:

{
    "message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."
}
Enter fullscreen mode Exit fullscreen mode

It clearly that the POST method is not signed by default by CloudFront, we need to create signed token to header.


In this document it saids about why GET is enable by default ( https://community.aws/content/2fuBTcoVg7nnRIVLnqjIsIC8LAi/enhancing-security-for-lambda-function-urls?lang=en ).

To summary, it saids that the solution is for easier access if we enable for GET requests at CloudFront, POST requests still require signed payloads.


In my view, bypass GET can help us with less effort to sign request header, and we know that GET is very common method so if every request need to be signed will take a lot of cost and time. Disadvantage is less secure.

Refs

To read more about solution, I have some helpful link below:

https://github.com/vuongbachdoan/sigv4a-signing-examples/tree/main/python

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-how-it-works-tutorial.html

https://community.aws/content/2fuBTcoVg7nnRIVLnqjIsIC8LAi/enhancing-security-for-lambda-function-urls?lang=en

. . . . . . . . . . . . . . .