DEV Community

Cover image for AWS Pooled Multi-Tenancy for DynamoDB with IAM & Cognito
Simon
Simon

Posted on • Originally published at blog.simonireilly.com on

AWS Pooled Multi-Tenancy for DynamoDB with IAM & Cognito

This blog covers configurations I have been using to build the 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

TL;DR ✨

It is totally possible 👍

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:

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

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

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

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]

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:

{
   "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"
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

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 for a full working Infrastructure as Code example.

Steps in authentication and JWT authoring flow

  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 🧂

Discussion (0)