Search code examples
aws-sdkamazon-cloudfrontaws-sdk-nodejs

Debugging CloudFront Signed URLs using Trusted Signers and Node.js SDK


AWS can have annoyingly few details about errors and I've found myself stuck without a clear path forward.

I have code in my Node.js app that seems to be generating both signed URLs and signed cookies, but neither are allowing me to access resources on my private CloudFront distribution. This is the 403 response I get, instead:

<Error>
  <Code>InvalidKey</Code>
  <Message>Unknown Key</Message>
</Error>

My solution is spread across two AWS accounts.

  • Master account
    • S3 bucket holding my private content
    • Private CloudFront distribution pointed at the S3 bucket
    • Public key paired with the private key used in Elastic Beanstalk
    • Route53 record with custom subdomains for all distributions (master & members) & API
      • privatedistro.mydomain.com
      • reactapp.mydomain.com
      • api.mydomain.com
  • Member account(s)
    • Node.js-based API running on Elastic Beanstalk (this does the signing & returns cookies on login)
    • S3 bucket holding front-end react app
    • Public CloudFront distribution to serve React app
    • I tried adding a public key to CloudFront here and using its ID too

The key pair ID and private key are configured as environment variables using the Elastic Beanstalk console. Here's my code (it's executing without errors):

const cloudFront =
  sails.config.custom.cloudfront.keyPairId &&
  sails.config.custom.cloudfront.privateKey &&
  new AWS.CloudFront.Signer(
    sails.config.custom.cloudfront.keyPairId,
    sails.config.custom.cloudfront.privateKey.replace(/\\n/g, '\n')  // environment variables passed through Elastic Beanstalk get altered
  );


        // Set Cookies after successful verification
        const policy = JSON.stringify({
          Statement: [
            {
              Resource: 'https://privatedistro.mydomain.com/*',
              Condition: {
                DateLessThan: {
                  'AWS:EpochTime':
                    Math.floor(new Date().getTime() / 1000) + 60 * 60 * 24 // Current Time in UTC + time in seconds, (1 day)
                }
              }
            }
          ]
        });
        cookie = cloudFront.getSignedCookie({ policy });

        this.res.cookie(
          'CloudFront-Key-Pair-Id',
          cookie['CloudFront-Key-Pair-Id'],
          {
            domain: '.mydomain.com',
            path: '/',
            httpOnly: true
          }
        );

        this.res.cookie(
          'CloudFront-Expires',
          Math.floor(new Date().getTime() / 1000) + 60 * 60 * 24, // Current Time in UTC + time in seconds, (60 * 60 * 24 = 1 day)
          {
            domain: '.mydomain.com',
            path: '/',
            httpOnly: true
          }
        );

        this.res.cookie(
          'CloudFront-Signature',
          cookie['CloudFront-Signature'],
          {
            domain: '.mydomain.com',
            path: '/',
            httpOnly: true
          }
        );

The cookies that are returned look like this:

  • CloudFront-Expires=1593487645
  • CloudFront-Key-Pair-Id=K48HZXSAAM76P (not EC2/IAM: key-0847abcde123456)
  • CloudFront-Signature=[345-character string ending in __]

enter image description here

In the Default Cache Behavior Settings of my private distribution, I've selected Yes for Restrict Viewer Access, selected both self and selected accounts as Trusted Signers and entered the 13-digit number of the member account in the AWS Account Numbers field. Before I turned on Restrict Viewer Access, everything was being served nicely, so I don't think there's an issue with other parts of the CloudFront setup.

Any ideas where I went wrong?


Solution

  • The key you're using looks incorrect, to work with CloudFront signed URL, you need to use CloudFront key pair, not the EC2 one, mentioned in below doc:

    Generate Signed URL CloudFront

    The CloudFront key-ID looks more like a Access key of IAM.