Deploying an AWS Lambda Function with Multi-Factor Auth
David Crawford / October 13, 2022
Hot on the heels of my recent passing of the AWS Cloud Practitioner certification exam, I wanted to get right into some interesting and relevant AWS work. This post was made in collaboration with Justin Wheeler, Senior Software Developer at Bravo LT and resident cloud expert who has all AWS certifications, to share a barebones example of how to deploy to AWS Lambda using MFA and SAM CLI, and the Least Privilege IAM policy.
In nearly all guides out there, Lambda functions are deployed without the Least Privilege policy, meaning that everyone assumes that everyone has full admin rights. But this is not a best practice, and in reality is a large security risk.
The security best practices for IAM involve least privilege and MFA. These are extremely important when protecting your account. However, they’re not that easily implemented. AWS has thousands of fine-grained access permissions, with some that we’ll mention as requirements for Lambda deployments. Many other projects require a slew of these permissions to operate, and finding the correct permissions is usually done through tedious trial and error. The risks of not following least privilege can lead to more issues:
- Employees can accidentally (or, in the case of disgruntled employees, not-so accidentally) delete resources they shouldn't have access to
- Employees can create resources that are not compliant
- Employees can change resources that belong to another team/department
- Lost or stolen credentials can provide a larger attack vector for attackers
- It can create a scenario that requires root account or AWS support assistance to resolve
When granting a certain level of write permissions, we would argue that MFA should be added as an extra layer of protection against potential attackers. The likelihood that an attacker will steal your account credentials is low (if you're being cautious), but it's extremely unlikely that an attacker will be able to obtain your MFA device as well. MFA is a necessary inconvenience for the security it provides.
Now, if you’re required to use MFA to use your AWS developer account, how do you deploy anything via CLI? You may have tried, and gotten a lot of credential errors, and are wondering how MFA can be used during deployments. This problem is exactly what we’re solving in this post.
Getting Started
Assumptions:
- You're part of an AWS organization
- You don't have root access, and need to request "least privilege" access to your admin in order to deploy a new Lambda function
- Your organization requires MFA
Step 1: Install dependencies
- Install the AWS CLI
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
Verify that it's installed correctly by running
aws --version
-
Install Docker - This is needed for local testing of AWS SAM projects
-
Install Homebrew - This is needed to install the AWS SAM CLI
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
Verify that it's installed correctly by running
brew --version
- Install the AWS SAM CLI
brew tap aws/tap
brew install aws-sam-cli
Verify that it's installed correctly by running
sam --version
- Clone our specially made barebones repo, and continue the rest of the steps inside of this project
git clone https://github.com/DaveAldon/Barebones-AWS-Lambda-with-MFA
Step 2: Get the minimum roles needed for your AWS account
You'll need the following roles if you're part of an AWS organization that practices the "least-privilege" principle:
iam:CreateRole
iam:AttachRolePolicy
iam:DetachRolePolicy
cloudformation:CreateChangeSet
apigateway:\* SAM needs to associate your Lambda function with an API gateway
Step 3: Invoke the hello-world Lambda function
Run npm run invoke to run the included sample Lambda function locally. To verify that it's working, you should get a similar output to the following:
{
"statusCode": 200,
"body": "{\"message\": \"hello world\"}"
}
The SAM-Project folder is an example Node v16 + Typescript Lambda function originally created via a sam init command. You can run sam init and follow the guided instructions to create your own SAM environment if you desire, based on a variety of languages like Python, Rust, C#, etc.
Step 4: Deploy to AWS Lambda
Run npm run deploy to build and deploy the Lambda function to a new AWS stack called sam-barebones-aws-lambda-with-mfa, and with the authentication configuration called mfa.
This command runs you through the "guided" deployment, which will ask you a few clarifying questions. Here's an example of what this could look like:
Looking for config file [samconfig.toml] : Found
Reading default arguments : Success
Setting default arguments for sam deploy
Stack Name [sam-barebones-aws-lambda-with-mfa]:
AWS Region [us-east-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [Y/n]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [mfa]:
You can take a look at the samconfig.toml file that we created to see where some of these values come from, and where the guided deployment may make updates.
If the deployment preparation is successful, you should see output like this:
Then you'll be asked for a final confirmation to proceed, and if everything is successful, you should see output like this, depending on your region:
Successfully created/updated stack - sam-barebones-aws-lambda-with-mfa in us-east-1
Step 5: Try out the deployed Lambda function
Now that your Lambda function has been deployed, you should try invoking it using its function URL.
You can generate this URL by going to AWS Console -> Lambda -> find the sam-barebones-aws-lambda-with-mfa application -> Configuration -> Function URL -> Create Function URL
For ease of testing purposes, set the auth type to NONE, making your function public. Otherwise you'll need authentication to invoke it.
Then navigate to the URL, and you should see this result in your browser:
{"message":"hello world"}
Troubleshooting
There are a number of random things that can go wrong during this entire Lambda process. I've tried to include some of the most common issues, and how to resolve them.
Deployment Errors
If you get an error like the following during a deployment:
Error: Failed to create managed resources: An error occurred (ExpiredToken) when calling the CreateChangeSet operation: The security token included in the request is expired
You need to rerun the MFA token generation command:
CODE=<YOUR_CODE> npm run mfa
Post-Deployment Errors
If you try invoking the Lambda function via the function URL and get an error like the following:
{
"message": "Internal server error"
}
and an accompanying error in CloudWatch like this:
Unknown application error occurred Runtime.ImportModuleError
{
"errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module 'app'\nRequire stack:\n- /var/runtime/index.mjs",
"stack": [
"Runtime.ImportModuleError: Error: Cannot find module 'app'",
"Require stack:",
"- /var/runtime/index.mjs",
" at _loadUserApp (file:///var/runtime/index.mjs:951:17)",
" at async Object.UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:976:21)",
" at async start (file:///var/runtime/index.mjs:1137:23)",
" at async file:///var/runtime/index.mjs:1143:1"
]
}
This is because there's an issue with the handler. There are a couple things to try depending on your setup:
- Make sure that the handler in the template.yaml config is fully qualified, with its parent folder name included in the path, and that the functions are named correctly
- If you're uploading your file via zip, you'll need to use index instead of app
Investigating Authentication Issues
aws configure list - shows you the current configuration and if your profile is set correctly or not. Example output:
Name Value Type
---
profile <not set> None None
access_key **\*\*\*\***\*\*\*\***\*\*\*\*** shared-credentials-file
secret_key **\*\*\*\***\*\*\*\***\*\*\*\*** shared-credentials-file
region us-east-1 config-file ~/.aws/config
**nano ~/.aws/credentials** - You can use whatever editor you want for this. This shows you the credentials file, separated by profiles, with the tokens/access key information. Example output:
[default]
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXX
[mfa]
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXX
aws_session_token = XXXXXXXXXXXXXXXXXXXX
Conclusion
I want to give a big thanks to Justin Wheeler for helping me work through figuring out this deployment, and for contributing his thoughts on security best-practices. We simulated this work with Justin being the admin, and myself being the Least Privilege developer needing the minimum required rights. Justin has struggled through this before, and also supplied the MFA shell script that I adapted for this project.
Subscribe to the newsletter
Get emails from me about web development, tech, and early access to new articles.