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:
- Within the browser a user should be able to read from DyanmoDB all items with a hash key beginning
PUBLIC#
. - 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 allPUBLIC#
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 👍
Serverless Stack@serverlessstackNext, is this great multi-tenant setup by @simon_reilly_.
It has a detailed blog post, a live demo, and a GitHub repo (that's deployed to @SEED_run)!
github.com/simonireilly/s…
Multi-tenancy can be tricky, so a resource like this is really appreciated.
blog.simonireilly.com/posts/iam-mult…18:04 PM - 15 Nov 2021
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.
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:
- Create an account per tenant with no share infrastructure.
- 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:
Create a new resource (API Gateway, DynamoDB) per tenant.
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.
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:
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"
.
- The
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"
}
]
}
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.
- A user signs up, and gets saved in cognito user pool.
- The user gets the auto-confirm email.
- A post-confirmation lambda is triggered
- The post-confirmation lambda tells cognito to insert the UUID of the organisation as an immutable custom attribute on the user.
- The user exchanges their JWT for temporary credentials associated with the identity pool role.
- 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!
- 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 🧂
Top comments (0)