A Serverless API to validate AWS Access Keys based on AWS SAM
Tags: aws, serverless, security, cloud
Published: 2022-01-20 13:02:00

**Notification 1: **

All code related to this blog post can be viewed at https://github.com/turjachaudhuri/AWS-Serverless/tree/ValidateAccessKey

## <u>So , what is this about?</u>

While working on AWS projects with junior members in my team , I found that many of them are creating sample users on their personal DEV accounts , and hardcoding the access keys and secret keys in their applications . 
So , I explained them how that is a security vulnerability and should be avoided always . During the discussion , a junior asked me , whether there is an easy way to check whether an access key is valid or not ?

This got me thinking .Obviously one way was to go to the IAM console , check each user separately and check their security credentials one by one to see whether the accesskey we need to verify is in the list or not . 

However , I actually tried it and found that for a large number of users , this is quite time consuming . Also , many times , we don't have access to the IAM console for security reasons.So , obviously this is not a good solution.

That is when I thought about writing a serverless app which will expose an API powered by an AWS Lambda backend that someone can call to verify whether a particular AccessKeyID is valid or not . This blog is about that only.

## <u>Wait , can we not use the Credential report provided by AWS?</u>

Ok , so AWS has a functionality called Credential Report which is kind of like an audit report about your user management that can be presented to external auditors also .It contains a ton of information about users , when their passwords changed and so on.

However , in my particular use case this was not a good fit due to the below reasons:

- To download the credential report you had to navigate to the IAM console , which not everyone will have access to .
- The credential report has information about all the users in your account . I don't want my developers to have access to that kind of information. I just want to provide them the ability to check whether the accesskey they have is valid or not.
- The report does not display the AccessKeyID anywhere , so it is not possible to validate a single AccessKeyID using that report data.

## <u>So , what to do now?</u>

Actually the programming part of this is quite simple . AWS has exposed API's that you can consume to list users , list AccessKeyID(s) for an user and so on . So , my plan was to create a lambda function that will first list all users in the system , then for each of them list all the access keys that they have , and build them all into a list . 
Then , the app will parse the request body of the incoming API call , find the AccessKeyID provided in the request , and validate it against the list of valid AccessKeyID(s)  that we already have by then , and check accordingly.

Obviously , rather than calling the Rest API(s) by ourselves , we will use an AWS SDK for our favorite language which will facilitate the whole process , essentially acting as wrappers for the lower level API calls. I used the AWS SDK for .NET V3 , but please feel free to follow along with your favorite programming language.

## <u>Ok , so show me the code !</u>

Essentially , all the API(s) you need are in the AWSSDK.IdentityManagement nuget package . Add them as a dependency to your project.

Then , create an instance of the identity management client that you will use to invoke the API(s) as and when needed.

```
iamClient = new AmazonIdentityManagementServiceClient();
```
Then , get a list of all the users
```
var userRequest = new ListUsersRequest { MaxItems = 20 } ;
```
Then , iterate over them one by one , and process each user to retrieve their AccessKeyID(s) and build up the list.

```
foreach (var user in allUsersListResponse.Users)
{
ListAccessKeys(user.UserName, 20, accessKeyMetadataList);
}
```
For each user get the AccessKeyID and other metadata information associated with them.
```
ListAccessKeysResponse accessKeysResponse = new ListAccessKeysResponse();
var accessKeysRequest = new ListAccessKeysRequest
{
// Use the user created in the CreateAccessKey example
UserName = userName,
MaxItems = maxItems
};
do
{
accessKeysResponse = iamClient.ListAccessKeysAsync(accessKeysRequest).GetAwaiter().GetResult();
foreach (var accessKey in accessKeysResponse.AccessKeyMetadata)
{
Model.AccessKeyMetadata accesskeymetadata = new Model.AccessKeyMetadata();
accesskeymetadata.AccessKeyId = accessKey.AccessKeyId;
accesskeymetadata.CreateDate = accessKey.CreateDate.ToLongDateString();
accesskeymetadata.Status = accessKey.Status;
accesskeymetadata.UserName = accessKey.UserName;

GetAccessKeyLastUsedRequest request = new GetAccessKeyLastUsedRequest()
{ AccessKeyId = accessKey.AccessKeyId };

GetAccessKeyLastUsedResponse response =
iamClient.GetAccessKeyLastUsedAsync(request).GetAwaiter().GetResult();

accesskeymetadata.LastUsedDate = response.AccessKeyLastUsed.LastUsedDate.ToLongDateString();
AccessKeyMetadataList.Add(accesskeymetadata);
}
accessKeysRequest.Marker = accessKeysResponse.Marker;
} while (accessKeysResponse.IsTruncated);
```

Finally , check that the AccessKeyID provided in the input JSON is part of the previously built list or not. If yes , return the details of the AccessKeyID  like its Status , creation date , last used date and so on.

```
Model.AccessKeyMetadata validAcessDetails =
AccessKeyMetadataList.Where(x =&gt; x.AccessKeyId == requestObj.AccessKeyID).FirstOrDefault();

response = new APIGatewayProxyResponse
{
Body = JsonConvert.SerializeObject(validAcessDetails),
StatusCode = (int)HttpStatusCode.OK,
Headers = new Dictionary { { "Content-Type", "application/json" } }
};
```
Ok , so that was pretty straightforward . 
You can find the whole code with unit tests and a VStudio solution at https://github.com/turjachaudhuri/AWS-Serverless/tree/ValidateAccessKey

<u>**Somethings to watch out for !!**</u>

Most of the IAM API(s) do not return all results at once . For example , the ListUsers call takes in a parameter called MaxItems . If you don't pass it as an argument , the default is 100 .
```
requestUsers = new ListUsersRequest() { MaxItems = 10 };
```
This is a common concept seen in many AWS API(s) which support pagination . Not all data is returned at the first call . Some data is returned , and a marker is returned that can be used in the subsequent API calls to fetch the remaining data . As a result some sort of looping is required to fetch all the data via these API(s) .

So , be aware as your unit tests might pass due to a limited number of users in your system , but the code will fail when the number of users will exceed a particular limit . A easy way to get around this can be found in AWS documentation only , and is shown below.

```
do
{
allUsersListResponse = iamClient.ListUsersAsync(userRequest).GetAwaiter().GetResult();
ProcessUserDetails(allUsersListResponse, AccessKeyMetadataList);
userRequest.Marker = allUsersListResponse.Marker;
} while (allUsersListResponse.IsTruncated);
```

2. When we initialize AWS SDK client classes , we need to provide some sort of AWS Credentials so that our account , and only our account is affected using the roles and policies that we assign to the client class.

However , when initializing AWS clients in lambda , this is not needed and we can use the default constructor for those classes .
```
iamClient = new AmazonIdentityManagementServiceClient();
```
This is because when we deploy a lambda function we associate a role to it , and it is that role that provides the AWS context (credentials) for all the lambda code . So , the lambda function basically assumes that role , and all the client classes defined using the lambda function , use the context of that role . So , there is no need to define anything separately.

However , when we are doing unit testing via our IDE , we are not in an actual lambda function , and we don't have any role for the lambda that the unit test framework can assume as such . In those cases , we need to initialize the AWS SDK client classes with an AWS credentials of some sort . I simply use this :

```
if (isLocalDebug) //I am debugging locally
{
var chain = new CredentialProfileStoreChain();
AWSCredentials awsCredentials;
if (chain.TryGetAWSCredentials(Constants.AWSProfileName, out awsCredentials))
{
// use awsCredentials
iamClient = new AmazonIdentityManagementServiceClient(
awsCredentials, Amazon.RegionEndpoint.APSouth1);
}
}
```
Here Constants.AWSProfileName is the AWS profile that we want the code to assume , meaning the privileges that are possessed by that AWS profile will be assumed by the code while running in VStudio.

This is explained in detail here https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html

Always use AWS Profiles , and never hard-code credentials in your source code . Never .

## <u>Permissions for the lambda function</u>

The lambda function needs to be associated with a role that has privileges to call the IAM API(s) mentioned above. So , the BasicLambdaExecution policy will not suffice. We simply need the below actions in our policies so that the API calls can be made.

1. iam:ListAccessKeys
2. iam:GetAccessKeyLastUsed
3. iam:ListUsers

The actual JSON template can be found in template.json in the git repo at https://github.com/turjachaudhuri/AWS-Serverless/blob/ValidateAccessKey/template.json

One thing to note here is that there is no Create/Update permission , simply read or list permissions . As a result this lambda function role cannot create/alter anything in IAM , simply fetch data .

For detailed idea on this topic , please check https://docs.aws.amazon.com/IAM/latest/UserGuide/access_permissions-required.html

## <u>How to deploy the project?</u>

This app uses the AWS SAM template . So , you can simply clone the code , and get started using a few basic commands

- Create a bucket to store the packaged code .
  aws s3 mb s3://[put your bucket name here] --region [put your region here] --profile [put your profile here]
-  This will validate the template for you
   sam validate --template template.json --profile [put your profile here]
-  This will package the code and push it to the bucket
   sam package --profile [put your profile here] --template-file template.json --output-template-file serverless-output.yaml --s3-bucket [put your bucket name here] --force-upload
-  This will create a Cloudformation stack and deploy all resources to your AWS Account
  sam deploy --profile [put your profile here] --template-file serverless-output.yaml --stack-name [put your CF stack name here] --capabilities CAPABILITY_IAM --region [put your region here]

## <u>Enough talk ! Show me the money</u>

Once the app gets deployed as a Cloudformation stack , you will get an API endpoint that you can use to test that everything works as promised.

This is how the API will behave when an input AccessKeyID is provided that is present.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9i8le6hrdzy8wsaynvq.png)
This is how it will behave for an API key that is unavailable.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q9fr0z0024v3d0bekygz.png)
If you need to debug AWS API calls with Postman where AWS_IAM auth is enabled , check out the detailed set of steps here : https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-use-postman-to-call-api.html

## <u>Wait a minute , can anybody call my API?</u>

Unless your API has some sort of authentication to filter users who can invoke it , everyone will be able to invoke your API . This will essentially mean , that a person who does not have any IAM credentials or AWS privileges can by calling my API actually get information about my AWS resources . This is not acceptable.

This can simply be provided by changing the Authorization settings of your API from None to AWS_IAM in the API Gateway console , and deploying the API again . As soon as the API gets deployed again , you will need to authenticate yourself while invoking the API by signing all API requests with your access key and secret key using Sigv4 protocol.

However , changing the Authorization type from the AWS Console is not an option for us as we want to do it via our SAM template that has defined the resources for this app.  In our SAM templates , we can define API resources either implicitly as lambda function event sources or explicitly as separate AWS::Serverless::Api resources .

However , AWS_IAM authorization is not supported in the implicit API declaration as detailed here :https://github.com/awslabs/serverless-application-model/issues/25

As a result I changed my AWS API Gateway resource declaration from implicit to explicit , and made necessary modifications to get it working . Please check out my git repo for the detailed template at https://github.com/turjachaudhuri/AWS-Serverless/blob/ValidateAccessKey/template.json

## <u>So, who can call my API?</u>

Since your API calls are now AWS_IAM authorized , not everyone can call your API . You need to sign your API call request using your accesskey and secret key to consume the API . This does not mean that anybody in AWS can call your API . The user who is calling the API needs to have permission to invoke the particular API . This is how you can ensure that only developers in your team who need access to this functionality can access this API . Others will not be able to , providing you a level of security that is needed .

If an user who does not have explicit permission to consume this API , tries to invoke this API , he will see the following error:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/krnffr7e3h0glb587y9v.png)

## <u>Done and dusted . Where to go from here ?</u>

I want to submit this app to the AWS Serverless Repo but currently can't do so since AWS Serverless Repo only supports a few predefined policy templates that you can use . You cannot create custom roles or policies and attach to resources . And currently , there is no IAM Policy template in the AWS SAM approved list of templates that I can use .

So , I am working on raising a Pull Request to the aws sam git repo that might help me out in this case.

But , you guys continue . Fork my repo / clone my code . Do whatever you need to , and be sure to let me know what magic you created from my crappy code.

All feedback is appreciated . Cheers !!