Using and Securing Environment Variables with AWS Lambda, KMS and Node.js

·

12 min read

Featured on Hashnode
Using and Securing Environment Variables with AWS Lambda, KMS and Node.js

A well-written function needs to be adaptable, which can be achieved by utilizing environment variables. Environment variables give us the ability to adapt our function's behavior without needing to update code. This article will discuss how to access environment variables in a lambda function using Node.js. For advanced use cases where security is of concern, we will also discuss how to encrypt our data at rest using AWS Key Management Service. Finally, we will discuss how to encrypt our environment variables in transit and decrypt it in our lambda function.

Use Cases for Environment Variables

By using environment variables, a developer does not need to hard-code a value into their functions. Instead, the environment variable will pass to our function a value at runtime. That is great if there is dynamic or changing data. Environment variables also allow us to reuse our functions in development and production environments and give us the ability to adapt our function's behavior based on the environment it is running in.

Another benefit of environment variables is that, when properly used, they keep sensitive information secure. For example, if I need to pass to my function a password to access my database, I would not want this information in plaintext. Environment variables enable us to still have access to sensitive data without revealing the contents to anyone who views our code. We can further secure our sensitive information by leveraging AWS Key Management System (KMS) to encrypt and decrypt our environment variables. We will see how to do this at the end of the article.

Plaintext Environment Variables in Lambda

Environment variables are defined as a key/value pair. Before we create environment variables in Lambda, there are some requirements to keep in mind.

  • Keys must start with a letter and must be at least two characters long
  • Keys can only contain letters, numbers, and the underscore character (_)
  • Keys are not reserved by Lambda
  • The total size of all environment variables can't exceed 4 KB

For our examples, we will set up an environment variable with the key 'DB_PASSWORD' and the value 'helloworld'. There are two common ways to set up environment variables in Lambda, using the console and the command line.

Setting an Environment Variable Using the Console

  1. Open the functions page on the Lambda console and choose a function. If one isn't available, create a new one with the default settings selected. Select Node.js for the runtime.
  2. Choose Configuration > Environment variables.
  3. Select the Edit button.
  4. Select Add environment variable.
  5. Enter the key (which is used to access our variable) and the value (the actual value we want to store).
  6. Keep all the default settings.
  7. Select Save.

Setting an Environment Variable Using the Command Line

Note: This section assumes you have the AWS CLI installed and configured. If not, you can find information on how to do it here. You can follow along with this article by setting your environment variable using the AWS console.

  1. Have the name of the function you want to set the environment variable for on hand. If you don't have one, create a Node.js Lambda function and write down the name of the function.
  2. Enter the following command to your command line, substituting the values between the brackets with the name of your function.
aws lambda update-function-configuration --function-name <my-function-name>  /
 --environment "Variables={DB_PASSWORD=helloworld}"

Notice that in the above command, there is a Variables object where our environment variable is stored. Whenever the update-function-configuration command is run, it will update the entire contents of the Variables object. If you want to keep other existing environment variables when updating the one, be sure to include all variable values in your request.

Accessing Environment Variables in Your Function

Environment variables in Lambda are accessed the same way as environment variables in a Node.js application. The following code snippet shows us how to access an environment variable.

const DB_PASSWORD = process.env.DB_PASSWORD;

Let's update our function to access our environment variable and test to see if we were successful.

  1. Select your lambda function from the Lambda console.
  2. Select the Code tab.

    If you don't see your code in the embedded IDE, double click on the index.js file on the left-hand side of the IDE.

  3. Edit your function with the following code snippet.

    exports.handler = async (event) => {
     const db_password = process.env.DB_PASSWORD;
    
     const response = {
         statusCode: 200,
         body: `The password to the Database is: ${db_password}`
     };
     return response;
    };
    
  4. Select the Test tab.
  5. Keeping all of the default settings, name your test event test.
  6. Run your test event.
  7. You should see a green box that says Execution result: succeeded (logs). Select the Details dropdown and we should see:
    {
    "statusCode": 200,
    "body": "The password to the Database is: helloworld"
    }
    

Yeah! We got our environment variable to work! Of course, we wouldn't want to expose our database password in a production environment. However, in this tutorial, it is ok.

Environment Variables Provided by Lambda

Every lambda function comes with defined environment variables that provide information about the function or runtime. Some helpful ones that can be used to configure the behavior of your function include:

  • AWS_REGION - The region where the lambda function is executed
  • AWS_LAMBDA_FUNCTION_NAME - The name of the lambda function

A complete list can be found here.

Securing Environment Variables with AWS Key Management System

To keep our sensitive information secure, AWS by default encrypts all environment variables at rest using AWS Key Management System (KMS). KMS is a service that enables a user to create and control encryption keys (also called Customer Master Keys or CMKs). These CMKs encrypt and decrypt data in AWS. When an environment variable is created, which is considered an encrypted resource, Lambda will create an AWS managed CMK to encrypt our environment variable. As we have seen in our previous example, we did not need to call a decrypt method to access our environment variables. Lambda handles encryption and decryption for us automatically. Encryption of data using an AWS managed CMK is provided free of charge.

For many developers, using an AWS managed CMK is a solid option. AWS manages encryption/decryption for us, as well as the policies associated with AWS managed CMKs. However, some companies may have higher security standards and may require all CMKs to be company managed. In this case, a developer may create their own CMK to secure their data. This option gives the developer/company more control over the CMK. They can define the permissions on the key and control who can use or manage the CMK.

When securing an environment variable in Lambda, a developer can choose to use an AWS managed CMK or use their own CMK. Let's now discuss how we can create a customer managed CMK and use it to secure environment variables in Lambda.

Note: Creating a customer managed CMK is outside of the free tier and costs $1 USD. If you follow the next part of this tutorial, there are associated with creating a CMK.

Creating a Customer Managed Key

  1. Open the AWS KMS console page.
  2. Click Create a key.
  3. Keep all the default options. (Key type: Symmetrical, Key material origin: KMS)
  4. Select Next.
  5. Choose an Alias, which is a friendly name for your CMK. We will name our alias lambda-encryption-key.
  6. Select Next.
  7. Select the users and roles that can administer your CMK. For this tutorial, you can select the user you are signed in with or the admin role.
  8. Select the users and roles that have permissions to use your CMK. For this tutorial, you can select the user you are signed in with or the admin role.
  9. On the review screen, select Finish.

Encrypting an Environment Variable with a Customer Managed CMK

  1. Open the functions page on the Lambda console and select the function we have been using thus far.
  2. Choose Configuration > Environment variables.
  3. Select the Edit button.
  4. Select Add environment variable.
  5. Enter the key (which is used to access our variable) and the value (the actual value we want to store).
  6. Select the dropdown menu Encryption configuration.
  7. Under AWS KMS key to encrypt at rest, select Use a customer master key.
  8. From the dropdown menu that appears, select the CMK we just created lambda-encryption-key.
  9. Select Save.

Now your environment variables are encrypted and decrypted by Lambda using a customer managed CMK. There is no need to decrypt the data, as Lambda will do this for us.

Encrypting Data in Transit using AWS KMS

For a higher level of security, a company may require that all sensitive data, including environment variables, are encrypted while in transit. This means that our lambda function will receive an encrypted environment variable and will need to decrypt it using kms.decrypt() in the lambda function.

Let's see how we can encrypt our environment variables in transit and decrypt it in our lambda function.

Note: To encrypt in transit, you must have a customer managed CMK.

Encrypting our Environment Variable In Transit

  1. Open the functions page on the Lambda console and select the function we have been using thus far.
  2. Choose Configuration > Environment variables.
  3. Select the Edit button.
  4. Select the Encryption configuration dropdown menu.
  5. Check Enable helpers for encryption in transit.
  6. A box will appear next to your environment variable entitled Encrypt. Select this box.
  7. A popup modal will appear. In the dropdown menu entitled AWS KMS key to encrypt in transit, select the customer managed CMK key we created earlier, lambda-execution-key.
  8. In a separate file, copy and paste the information from the Decrypt secrets snippet. (An edited copy of the Decrypt secrets snippet is available later in the article for your use.)
  9. Select Encrypt.
  10. Select Save.

In the Lambda console, we can see the value of our environment variable is now encrypted.

Decrypting our Environment Variable

To decrypt our environment variable, we first need to give our Lambda execution role IAM permissions to call the KMS service.

Adding KMS Permissions to Our Lambda Role

  1. Open the functions page on the Lambda console and select the function we have been using thus far.
  2. Choose Configuration > Permissions.
  3. Select the execution role Role name. This will open up the IAM console in a new browser window.
  4. Select Attach policies.
  5. Select Add inline policy.
  6. For the Service, select KMS.
  7. For the Actions, select the dropdown menu button next to Write. Select Decrypt.
  8. For Resources, select Specific, then select Add ARN to restrict access. Add the ARN of your KMS CMK. You can get the ARN for your key on the KMS service page.
  9. Select Save changes.
  10. Select Review policy.
  11. Name the policy KMSDecryptPolicyForLambda.
  12. Select Create policy.

Editing Our Lambda Function

Let's edit our lambda function and decrypt our environment variable. In the Lambda console, select the function we have been working on. In the code editor, replace our previous code with the code below. The following code is an edited version of the 'Decrypt secrets snipped` for use in this tutorial.

// Edited decrypt secrets snippet

const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });

const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const encrypted = process.env['DB_PASSWORD'];
let decrypted;


exports.handler = async (event) => {
    if (!decrypted) {
        const kms = new AWS.KMS();
        try {
            const req = {
                CiphertextBlob: Buffer.from(encrypted, 'base64'),
                EncryptionContext: { LambdaFunctionName: functionName },
            };
            const data = await kms.decrypt(req).promise();
            decrypted = data.Plaintext.toString('ascii');
        } catch (err) {
            console.log('Decrypt error:', err);
            throw err;
        }
    }
    return `Our database password is: ${decrypted}`
};

Let's break apart this code and see what it does.

const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });

First, we import the AWS SDK and update the region to us-east-1. Of course, update the region if you are working out of another region.

const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
const encrypted = process.env['DB_PASSWORD'];
let decrypted;

Next, we will set our variables. Our first variable, functionName, uses a Lambda provided environment variable to get the name of our function. The second variable is our encrypted DB_PASSWORD environment variable.

Notice that we are setting our variables outside of the function handler. This is best practice when working with Lambda due to how lambda functions are invoked. Whenever a lambda function is invoked, a container is created and runs our function. This container is not terminated upon completion of the function, but it will hang around for a bit in case other requests come through. If another request comes through during this time, our variables stored outside of the handler function will still be available. This reduces both function duration time and kms:decrypt calls.

Let's examine our function handler and see how we decrypt our encrypted environment variable.

exports.handler = async (event) => {
    if (!decrypted) {
        const kms = new AWS.KMS();
        try {
            const req = {
                CiphertextBlob: Buffer.from(encrypted, 'base64'),
                EncryptionContext: { LambdaFunctionName: functionName },
            };
            const data = await kms.decrypt(req).promise();
            decrypted = data.Plaintext.toString('ascii');
        } catch (err) {
            console.log('Decrypt error:', err);
            throw err;
        }
    }
    return `Our database password is: ${decrypted}`
};

First, we check to see if our environment variable decrypted has a defined value. If not, we construct a KMS service object using new AWS.KMS(). Next, we define a try/catch block. Within the try block, we will define the request object, req. This request object will pass the parameters needed to the kms.decrypt() method. The kms.decrypt() method requires two arguments, CiphertextBlob and EncryptionContext.

  • CiphertextBlob is the text to be decoded and needs to be in base-64. The Buffer.from method does this for us.
  • EncryptionContext specifies the encrypted context to use when decrypting the data, which in our case is the lambda function name.

For more information regarding kms.decrypt, see here.

To use the async/await approach with our decrypt call, we will add the .promise() method. Once we receive our data object as a response, we can access our decrypted variable by calling data.Plaintext. However, data.Plaintext will return a base-64 encoded buffer. To convert it to a string, we will call .toString('ascii') on data.Plaintext. We will save this as our decrypted variable. In our catch block, we will catch and throw any errors in our function. Finally, the function will return a string that will hopefully tell us what our database password is.

To test our function, select the orange Test button above the code editor. Hopefully, you received as a response, "Our database password is: helloworld".

Congratulations! Hopefully, this article helped you understand how to access and secure environment variables in AWS Lambda.

Thank you for reading this article. Your feedback is appreciated! If there is something I missed, something that isn't clear, or you just want to reach out and connect, feel free to leave a comment below or on Twitter.