

# Implement SaaS tenant isolation for Amazon S3 by using an AWS Lambda token vending machine
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine"></a>

*Tabby Ward, Thomas Davis, and Sravan Periyathambi, Amazon Web Services*

## Summary
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-summary"></a>

Multitenant SaaS applications must implement systems to ensure that tenant isolation is maintained. When you store tenant data on the same AWS resource—such as when multiple tenants store data in the same Amazon Simple Storage Service (Amazon S3) bucket—you must ensure that cross-tenant access cannot occur. Token vending machines (TVMs) are one way to provide tenant data isolation. These machines provide a mechanism for obtaining tokens while abstracting the complexity of how these tokens are generated. Developers can use a TVM without having detailed knowledge of how it produces tokens.

This pattern implements a TVM by using AWS Lambda. The TVM generates a token that consists of temporary security token service (STS) credentials that limit access to a single SaaS tenant's data in an S3 bucket.

TVMs, and the code that’s provided with this pattern, are typically used with claims that are derived from JSON Web Tokens (JWTs) to associate requests for AWS resources with a tenant-scoped AWS Identity and Access Management (IAM) policy. You can use the code in this pattern as a basis to implement a SaaS application that generates scoped, temporary STS credentials based on the claims provided in a JWT token.

## Prerequisites and limitations
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-prereqs"></a>

**Prerequisites **
+ An active AWS account.
+ AWS Command Line Interface (AWS CLI) [version 1.19.0 or later](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv1.html), installed and configured on macOS, Linux, or Windows. Alternatively, you can use AWS CLI [version 2.1 or later](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).

**Limitations **
+ This code runs in Java and doesn’t currently support other programming languages. 
+ The sample application doesn’t include AWS cross-Region or disaster recovery (DR) support. 
+ This pattern demonstrates how a Lambda TVM for a SaaS application can provide scoped tenant access. This pattern is not intended to be used in production environments without additional security testing as a part of your specific application or use case.

## Architecture
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-architecture"></a>

**Target technology stack  **
+ AWS Lambda
+ Amazon S3
+ IAM
+ AWS Security Token Service (AWS STS)

**Target architecture **

![\[Generating a token to gain temporary STS credentials to access data in an S3 bucket.\]](http://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/images/pattern-img/97a34c8e-d04e-40b6-acbf-1baa176d22a9/images/14d0508a-703b-4229-85e6-c5094de7fe01.png)


 

## Tools
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-tools"></a>

**AWS services**
+ [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) is an open-source tool that helps you interact with AWS services through commands in your command-line shell.
+ [AWS Identity and Access Management (IAM)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) helps you securely manage access to your AWS resources by controlling who is authenticated and authorized to use them.
+ [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) is a compute service that helps you run code without needing to provision or manage servers. It runs your code only when needed and scales automatically, so you pay only for the compute time that you use.
+ [AWS Security Token Service (AWS STS)](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) helps you request temporary, limited-privilege credentials for users.
+ [Amazon Simple Storage Service (Amazon S3)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) is a cloud-based object storage service that helps you store, protect, and retrieve any amount of data.

**Code**

The source code for this pattern is available as an attachment and includes the following files:
+ `s3UploadSample.jar` provides the source code for a Lambda function that uploads a JSON document to an S3 bucket.
+ `tvm-layer.zip` provides a reusable Java library that supplies a token (STS temporary credentials) for the Lambda function to access the S3 bucket and upload the JSON document.
+ `token-vending-machine-sample-app.zip` provides the source code used to create these artifacts and compilation instructions.

To use these files, follow the instructions in the next section.

## Epics
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-epics"></a>

### Determine variable values
<a name="determine-variable-values"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Determine variable values. | The implementation of this pattern includes several variable names that must be used consistently. Determine the values that should be used for each variable, and provide that value when requested in subsequent steps.[\[See the AWS documentation website for more details\]](http://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine.html) | Cloud administrator | 

### Create an S3 bucket
<a name="create-an-s3-bucket"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Create an S3 bucket for the sample application. | Use the following AWS CLI command to create an S3 bucket. Provide the `<sample-app-bucket-name>`** **value in the code snippet:<pre>aws s3api create-bucket --bucket <sample-app-bucket-name></pre>The Lambda sample application uploads JSON files to this bucket. | Cloud administrator | 

### Create the IAM TVM role and policy
<a name="create-the-iam-tvm-role-and-policy"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Create a TVM role. | Use one of the following AWS CLI commands to create an IAM role. Provide the `<sample-tvm-role-name>`** **value in the command.For macOS or Linux shells:<pre>aws iam create-role \<br />--role-name <sample-tvm-role-name> \<br />--assume-role-policy-document '{<br />    "Version": "2012-10-17",		 	 	 <br />    "Statement": [<br />        {<br />            "Effect": "Allow",<br />            "Action": [<br />                "sts:AssumeRole"<br />            ],<br />            "Principal": {<br />                "Service": [<br />                    "lambda.amazonaws.com"<br />                ]<br />            },<br />            "Condition": {<br />                "StringEquals": {<br />                    "aws:SourceAccount": "<AWS Account ID>"<br />                }<br />            }<br />        }<br />    ]<br />}'</pre>For the Windows command line:<pre>aws iam create-role ^<br />--role-name <sample-tvm-role-name> ^<br />--assume-role-policy-document "{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\", \"Action\": [\"sts:AssumeRole\"], \"Principal\": {\"Service\": [\"lambda.amazonaws.com\"]}, \"Condition\": {\"StringEquals\": {\"aws:SourceAccount\": \"<AWS Account ID>\"}}}]}"</pre>The Lambda sample application assumes this role when the application is invoked. The capability to assume the application role with a scoped policy gives the code broader permissions to access the S3 bucket. | Cloud administrator | 
| Create an inline TVM role policy. | Use one of the following AWS CLI commands to create an IAM policy. Provide the `<sample-tvm-role-name>`,** **`<AWS Account ID>`, and `<sample-app-role-name>` values in the command.For macOS or Linux shells:<pre>aws iam put-role-policy \<br />--role-name <sample-tvm-role-name> \<br />--policy-name assume-app-role \<br />--policy-document '{<br />    "Version": "2012-10-17",		 	 	  <br />    "Statement": [<br />        {<br />            "Effect": "Allow", <br />            "Action": "sts:AssumeRole", <br />            "Resource": "arn:aws:iam::<AWS Account ID>:role/<sample-app-role-name>"<br />        }<br />    ]}'</pre>For the Windows command line:<pre>aws iam put-role-policy ^<br />--role-name <sample-tvm-role-name> ^<br />--policy-name assume-app-role ^<br />--policy-document "{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\", \"Action\": \"sts:AssumeRole\", \"Resource\": \"arn:aws:iam::<AWS Account ID>:role/<sample-app-role-name>\"}]}"</pre>This policy is attached to the TVM role. It gives the code the capability to assume the application role, which has broader permissions to access the S3 bucket. | Cloud administrator | 
| Attach the managed Lambda policy. | Use the following AWS CLI command to attach the `AWSLambdaBasicExecutionRole` IAM policy. Provide the `<sample-tvm-role-name>` value in the command:<pre>aws iam attach-role-policy \<br />--role-name <sample-tvm-role-name> \<br />--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole</pre>For the Windows command line:<pre>aws iam attach-role-policy ^<br />--role-name <sample-tvm-role-name> ^<br />--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole</pre>This managed policy is attached to the TVM role to permit Lambda to send logs to Amazon CloudWatch. | Cloud administrator | 

### Create the IAM application role and policy
<a name="create-the-iam-application-role-and-policy"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Create the application role. | Use one of the following AWS CLI commands to create an IAM role. Provide the `<sample-app-role-name>`, `<AWS Account ID>`, and `<sample-tvm-role-name>` values in the command.For macOS or Linux shells:<pre>aws iam create-role \<br />--role-name <sample-app-role-name> \<br />--assume-role-policy-document '{<br />    "Version": "2012-10-17",		 	 	  <br />    "Statement": [<br />        {<br />            "Effect": <br />            "Allow",<br />            "Principal": {<br />                "AWS": "arn:aws:iam::<AWS Account ID>:role/<sample-tvm-role-name>"<br />            },<br />            "Action": "sts:AssumeRole"<br />        }<br />    ]}'</pre>For the Windows command line:<pre>aws iam create-role ^<br />--role-name <sample-app-role-name> ^<br />--assume-role-policy-document "{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\",\"Principal\": {\"AWS\": \"arn:aws:iam::<AWS Account ID>:role/<sample-tvm-role-name>\"},\"Action\": \"sts:AssumeRole\"}]}"</pre>The Lambda sample application assumes this role with a scoped policy to get tenant-based access to an S3 bucket. | Cloud administrator | 
| Create an inline application role policy. | Use one of the following AWS CLI mmands to create an IAM policy. Provide the `<sample-app-role-name>` and `<sample-app-bucket-name>`** **values in the command.For macOS or Linux shells:<pre>aws iam put-role-policy \<br />--role-name <sample-app-role-name> \<br />--policy-name s3-bucket-access \<br />--policy-document '{<br />    "Version": "2012-10-17",		 	 	  <br />    "Statement": [<br />        {<br />            "Effect": "Allow", <br />            "Action": [<br />                "s3:PutObject", <br />                "s3:GetObject", <br />                "s3:DeleteObject"<br />            ], <br />            "Resource": "arn:aws:s3:::<sample-app-bucket-name>/*"<br />        }, <br />        {<br />            "Effect": "Allow", <br />            "Action": ["s3:ListBucket"], <br />            "Resource": "arn:aws:s3:::<sample-app-bucket-name>"<br />        }<br />    ]}'</pre>For the Windows command line:<pre>aws iam put-role-policy ^<br />--role-name <sample-app-role-name> ^<br />--policy-name s3-bucket-access ^<br />--policy-document "{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\", \"Action\": [\"s3:PutObject\", \"s3:GetObject\", \"s3:DeleteObject\"], \"Resource\": \"arn:aws:s3:::<sample-app-bucket-name>/*\"}, {\"Effect\": \"Allow\", \"Action\": [\"s3:ListBucket\"], \"Resource\": \"arn:aws:s3:::<sample-app-bucket-name>\"}]}"</pre>This policy is attached to the application role. It provides broad access to objects in the S3 bucket. When the sample application assumes the role, these permissions are scoped to a specific tenant with the TVM's dynamically generated policy. | Cloud administrator | 

### Create the Lambda sample application with TVM
<a name="create-the-lam-sample-application-with-tvm"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Download the compiled source files. | Download the `s3UploadSample.jar` and `tvm-layer.zip`** **files, which are included as attachments. The source code used to create these artifacts and compilation instuctions are provided in `token-vending-machine-sample-app.zip`. | Cloud administrator | 
| Create the Lambda layer. | Use the following AWS CLI command to create a Lambda layer, which makes the TVM accessible to Lambda. If you aren’t running this command from the location where you downloaded` tvm-layer.zip`, provide the correct path to `tvm-layer.zip` in the `--zip-file` parameter. <pre>aws lambda publish-layer-version \<br />--layer-name sample-token-vending-machine \<br />--compatible-runtimes java11 \<br />--zip-file fileb://tvm-layer.zip</pre>For the Windows command line:<pre>aws lambda publish-layer-version ^<br />--layer-name sample-token-vending-machine ^<br />--compatible-runtimes java11 ^<br />--zip-file fileb://tvm-layer.zip</pre>This command creates a Lambda layer that contains the reusable TVM library. | Cloud administrator, App developer | 
| Create the Lambda function. | Use the following AWS CLI command to create a Lambda function. Provide the `<sample-app-function-name>`, `<AWS Account ID>`, `<AWS Region>`, `<sample-tvm-role-name>`, `<sample-app-bucket-name>`, and `<sample-app-role-name>` values in the command. If you aren’t running this command from the location where you downloaded `s3UploadSample.jar`, provide the correct path to `s3UploadSample.jar` in the `--zip-file` parameter. <pre>aws lambda create-function \<br />--function-name <sample-app-function-name>  \<br />--timeout 30 \<br />--memory-size 256 \<br />--runtime java11 \<br />--role arn:aws:iam::<AWS Account ID>:role/<sample-tvm-role-name> \<br />--handler com.amazon.aws.s3UploadSample.App \<br />--zip-file fileb://s3UploadSample.jar \<br />--layers arn:aws:lambda:<AWS Region>:<AWS Account ID>:layer:sample-token-vending-machine:1 \<br />--environment "Variables={S3_BUCKET=<sample-app-bucket-name>,<br />ROLE=arn:aws:iam::<AWS Account ID>:role/<sample-app-role-name>}"</pre>For the Windows command line:<pre>aws lambda create-function ^<br />--function-name <sample-app-function-name>  ^<br />--timeout 30 ^<br />--memory-size 256 ^<br />--runtime java11 ^<br />--role arn:aws:iam::<AWS Account ID>:role/<sample-tvm-role-name> ^<br />--handler com.amazon.aws.s3UploadSample.App ^<br />--zip-file fileb://s3UploadSample.jar ^<br />--layers arn:aws:lambda:<AWS Region>:<AWS Account ID>:layer:sample-token-vending-machine:1 ^<br />--environment "Variables={S3_BUCKET=<sample-app-bucket-name>,ROLE=arn:aws:iam::<AWS Account ID>:role/<sample-app-role-name>}"</pre>This command creates a Lambda function with the sample application code and the TVM layer attached. It also sets two environment variables: `S3_BUCKET` and `ROLE`. The sample application uses these variables to determine the role to assume and the S3 bucket to upload JSON documents to. | Cloud administrator, App developer | 

### Test the sample application and TVM
<a name="test-the-sample-application-and-tvm"></a>


| Task | Description | Skills required | 
| --- | --- | --- | 
| Invoke the Lambda sample application. | Use one of the following AWS CLI commands to start the Lambda sample application with its expected payload. Provide the `<sample-app-function-name>` and `<sample-tenant-name>` values in the command.For macOS and Linux shells:<pre>aws lambda invoke \<br />--function <sample-app-function-name> \<br />--invocation-type RequestResponse \<br />--payload '{"tenant": "<sample-tenant-name>"}' \<br />--cli-binary-format raw-in-base64-out response.json</pre>For the Windows command line:<pre>aws lambda invoke ^<br />--function <sample-app-function-name> ^<br />--invocation-type RequestResponse ^<br />--payload "{\"tenant\": \"<sample-tenant-name>\"}" ^<br />--cli-binary-format raw-in-base64-out response.json</pre>This command calls the Lambda function and returns the result in a `response.json` document. On many Unix-based systems, you can change `response.json` to `/dev/stdout` to output the results directly to your shell without creating another file. Changing the `<sample-tenant-name>` value in subsequent invocations of this Lambda function alters the location of the JSON document and the permissions the token provides. | Cloud administrator, App developer | 
| View the S3 bucket to see created objects. | Browse to the S3 bucket ( `<sample-app-bucket-name>`) that you created earlier. This bucket contains an S3 object prefix with the value of `<sample-tenant-name>`. Under that prefix, you will find a JSON document named with a UUID. Invoking the sample application multiple times adds more JSON documents. | Cloud administrator | 
| View the logs for the sample application in CloudWatch Logs. | View the logs that are associated with the Lambda function named `<sample-app-function-name>` in CloudWatch Logs. For instructions, see [Sending Lambda function logs to CloudWatch Logs](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html) in the Lambda documentation. You can view the tenant-scoped policy generated by the TVM in these logs. This tenant-scoped policy gives permissions for the sample application to the Amazon S3 **PutObject**, **GetObject**, **DeleteObject**, and **ListBucket** APIs, but only for the object prefix that’s associated with `<sample-tenant-name>`. In subsequent invocations of the sample application, if you change `<sample-tenant-name>`, the TVM updates the scoped policy to correspond to the tenant provided in the invocation payload. This dynamically generated policy shows how tenant-scoped access can be maintained with a TVM in SaaS applications. The TVM functionality is provided in a Lambda layer so that it can be attached to other Lambda functions used by an application without having to replicate the code.For an illustration of the dynamically generated policy, see the [Additional information](#implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-additional) section. | Cloud administrator | 

## Related resources
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-resources"></a>
+ [Isolating Tenants with Dynamically Generated IAM Policies](https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/) (blog post)
+ [Applying Dynamically Generated Isolation Policies in SaaS Environments](https://aws.amazon.com/blogs/apn/applying-dynamically-generated-isolation-policies-in-saas-environments/) (blog post)
+ [SaaS on AWS](https://aws.amazon.com/saas/)

## Additional information
<a name="implement-saas-tenant-isolation-for-amazon-s3-by-using-an-aws-lambda-token-vending-machine-additional"></a>

The following log shows the dynamically generated policy produced by the TVM code in this pattern. In this screenshot, the `<sample-app-bucket-name>` is `DOC-EXAMPLE-BUCKET` and the `<sample-tenant-name>` is `test-tenant-1`. The STS credentials returned by this scoped policy are unable to perform any actions on objects in the S3 bucket except for objects that are associated with the object key prefix `test-tenant-1`.

![\[Log showing a dynamically generated policy produced by the TVM code.\]](http://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/images/pattern-img/97a34c8e-d04e-40b6-acbf-1baa176d22a9/images/d4776ebe-fb8f-41ac-b8c5-b4f97a821c8c.png)


## Attachments
<a name="attachments-97a34c8e-d04e-40b6-acbf-1baa176d22a9"></a>

To access additional content that is associated with this document, unzip the following file: [attachment.zip](samples/p-attach/97a34c8e-d04e-40b6-acbf-1baa176d22a9/attachments/attachment.zip)