AWS Pooled Multi-Tenancy for DynamoDB with IAM & Cognito
Tags: aws, cognito, iam, dynamodb
Published: 2022-01-16 23:02:20

---
title: AWS Pooled Multi-Tenancy for DynamoDB with IAM & Cognito
published: true
date: 2021-11-15 00:00:00 UTC
tags: aws, cognito, iam, dynamodb
canonical_url: https://blog.simonireilly.com/posts/iam-multi-tenancy-with-principal-tags
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w11dyor8ipktlnuy5q3c.png
---
 
 
This blog covers configurations I have been using to build the [saas-stack](https://github.com/simonireilly/saas-stack). A multi-tenant boiler plate for Serverless Stack apps. 

When building that application I wanted to push the boundaries of Cognito multi-tenancy. 

I had the following goals:

1. Within the browser a user should be able to read from DyanmoDB all items with a hash key beginning `PUBLIC#`.
2. Within the browser a user should be able to sign in and read from DyanmoDB all items with a hash key beginning with their organisations UUID `<UUID>#` and all `PUBLIC#` data.

But I ask you to think, how could you do this multi-tenant safe?

That requirement took me on a journey through many YouTube videos, and blog's, and [also resulted in upstreaming some changes into the aws-cdk](https://github.com/aws/aws-cdk/issues/15908)

## TL;DR ✨

It is totally possible :+1:

{% twitter 1460307822244212736 %}

You can achieve this by substituing the PrincipalTags from a user into an IAM Policy. When you use that in conjunction, with a federated identity that assumes that policy, then you can achieve things like row-level security in DynamoDB, and restrict API request routes to have speccific URL's such as an organisation or tenant ID.

>_Above is a fairly complex paragraph, read where I will disect AWS IAM and explain identity federation_

This security applies to the temproray access keys sent to the federated identity, so security cannot be overriden within the code 🔒

## Results

See below for the results: 

- Demonstration website: https://d1mmk10lcvudwp.cloudfront.net
- Source code: https://github.com/simonireilly/saas-stack 

In the below the user can access data in their organisation; which is encoded into the JWT, and into the temporary AWS credentials that are sent back for the federated identity.

![Browser side access for reads in dynamoDB](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bs7p6e5hd3wf4lto5h6a.png)
 

## In the beginning 

For a long time my brain has been mulling over multi-tenancy in AWS. 

There are a couple different ways to do Multi-Tenancy in AWS; the first decision: 

1. Create an account per tenant with no share infrastructure. 
2. Have multiple tenants in one AWS account. 

**This blog only thinks about number 2.** 

So if we want to share an AWS Account you still have some options: 

1. Create a new resource (API Gateway, DynamoDB) per tenant. 

2. Share common resources and use IAM to perform strict **data segregation**. 

>🧠 Data segregation in an AWS account has tripped up both of my latest engagements, it takes some pre-meditation to say the least! **This blog only thinks about number 2.** 

So we are going to have shared infrastructure in a single AWS Account. How will we secure everything? IAM to the rescue 🦸 

## AWS IAM 

AWS IAM has a concept called **fine-grained access control**. 

This type of **Role** is designed, with regard to allowing specific **Principals** (what AWS calls the entities assuming the **Role**), to access specific **Resources**. 
At this point, I think we need to hit reset and introduce: 

- AWS IAM **Roles** 
- AWS IAM **Principals** 
- AWS IAM **Trust Relationships** 

### AWS IAM - Basics 

If we think of a straight forward example, here, is a lambda function. 

The lambda function has a **Role**, that it needs to *assume*, in order to do all its lambda things. 

Specifically, if the lambda needs to interact with other AWS services, it needs to have a **Role** that allows it to do that. 

![Trust relationships with assumed roles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kmaw9o6xzarelnzjwvtt.png) 

The lambda function, wants to assume a role, and so, we setup a trust relationship, that says, when the **Principal** is lambda, then it can assumed the **Role**. The lambda then gets access to all the **Actions** in the **Role**, for all of the **Resources** in the **Role**. 

### AWS IAM - Fine grained Access Control 

Fine grained control adds another layer to this relationship. 

With fine grained control, we go beyond relying solely on the principal and add conditions. These conditions can be added to both the trust policy, and the iam policy. Here is an example: 

![Trust relationships with assumed roles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8vh7b5s5vpku9byedzi6.png) 

Summarizing the above: 

- Cognito Identity wants to assume the IAM policy, and this is allowed if: 
  - The `"cognito-identity.amazonaws.com:aud"` string is exactly equal to `"eu-west-2:e43159e7-b1bd-4cd2-8cbf-xxxxxxxxxxxx"`. 
  - The `"cognito-identity.amazonaws.com:amr"` has any of its values as `"authenticated"`. 

### AWS IAM - Principal Tag Mapping 

There is a final piece to the puzzle, **Principal Tags** which AWS defines as... 
>Control what the person making the request (the principal) is allowed to do based on the tags that are attached to that person's IAM user or role. [[1]](https://docs.aws.amazon.com/IAM/latest/UserGuide/access\_iam-tags.html) 

In this use case with Amazon Cognito, we are specifically going to associate **Principal Tags** to the JSON Web Token (JWT) claims that are returned by Cognito. 

These policies enhance the Fine Grained case with conditions of the type:

 ```json
{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Condition":{
            "ForAllValues:StringLike":{
               "dynamodb:LeadingKeys":[
                  "${aws:PrincipalTag/org}#*"
               ]
            }
         },
         "Action":[
            "dynamodb:GetItem",
            "dynamodb:Query"
         ],
         "Resource":"::dynamodb:eu-west-2::table/ExampleTableName",
         "Effect":"Allow",
         "Sid":"AllowPrecedingKeysToDynamoDBOrganisation"
      }
   ]
}
``` 

Now, all we need to do, is setup cognito to map a principal tag. Then, when a user assumes this role, they would only be able to see things in DynamoDB table `ExampleTableName` which have a primary key starting with `${aws:PrincipalTag/org}#` 

## Putting it all together 

>Please see the [saas-stack](https://github.com/simonireilly/saas-stack) for a full working Infrastructure as Code example. 

![Steps in authentication and JWT authoring flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nind0tba2la97o2tcni2.png)

1. A user signs up, and gets saved in cognito user pool. 
2. The user gets the auto-confirm email. 
3. A post-confirmation lambda is triggered 
4. The post-confirmation lambda tells cognito to insert the UUID of the organisation as an **immutable** custom attribute on the user. 
5. The user exchanges their JWT for temporary credentials associated with the identity pool role. 
6. The policy attached to those credentials is already secured to only allow access to PUBLIC resources and the tenants own resources (using the org uuid); nothing else! 
7. The tenant can make requests to API Gateway, and DynamoDB, but will have restricted access. 

## Wrap up 

I have been working on this for a while, and I am still waiting for an upstream change in the aws-cdk to make it fully supported and easy to configure: https://github.com/aws/aws-cdk/issues/15908 Once that hits the stable CDK builds, I think this will be a much more secure way to perform IAM, and keep your lambda code clean. 

**There is a big but!** This is very deep integration into AWS, perhaps as deep as you can go, and its not going to be the most flexible approach; so take this with a pinch of salt 🧂