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 want to talk directly to dynamoDB from the browser for reads and be able to cache all my API Gateway GET requests.
But I ask you to think, how could you do this multi-tenant safe? See below for the results:
Example website: https://d1mmk10lcvudwp.cloudfront.net
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:
This blog only thinks about number 2.
So if we want to share an AWS Account we still have some options:
🧠 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 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:
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.
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.amazonaws.com:aud"
string is exactly equal to "eu-west-2:e43159e7-b1bd-4cd2-8cbf-xxxxxxxxxxxx"
."cognito-identity.amazonaws.com:amr"
has any of its values as "authenticated"
.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": [
// Wait wait wait
// what the heck is this thing?
"${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}#
Please see the saas-stack for a full working Infrastructure as Code example.
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 🧂