Attribute based access control to AWS resources
Part of the platform vision we have at Coinmama includes mandatory tagging of all managed resources in our AWS account. When I say "managed" resources, I refer to resources managed by Infrastructure as Code (usually HashiCorp's Terraform in our case).
Because we use mandatory and uniform tagging across all managed resources that we provision inside AWS, that gives us a trust anchor that we can utilize inside our applications to make for more repeatable and cleaner patterns when creating applicative IAM policies.
That's admittedly a lot of big fancy words, so let me try to break that down a bit.
First of all, I talk about using mandatory and uniform tagging across all managed - e.g., Terraform created - resources. I'm not going to go over the basics of tagging inside AWS here. There are some great resources, including this great whitepaper published by AWS, for getting started with how, why and tag strategies. At Coinmama, our Terraform design assigns a "project name" and "environment" internally to the Terraform workspace names that we use. This helps us maintain individual workspaces for projects which tweak individual settings while leveraging similar (or occasionally the exact same) codebases.
We set the tags globally like this:
# main.tf locals { # … other stuff … aws_tags = { # … other stuff … environment = local.values.environment # Something like "test", "dev", "qa", "prod", "staging" project = local.values.project # Something like "web-front", "user-auth-service", etc }
}
And then reference this aws_tags in all AWS resources like this:
resource "aws_security_group" "sg" { # … other stuff … tags = local.aws_tags } … resource "aws_instance" "instance" { # … other stuff … tags = merge({ Name : "instance" }, local.aws_tags) }
Sometimes we'll merge in other tags - notably the Name tag as seen above - as needed, but will always use the base tags.
If desired we could also enforce the tags on the IAM level, too. For example, we could set the following IAM policy which would validate the environment tag on the AWS side…
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "ec2:CreateTags", "Resource": "arn:aws:ec2:*:*:instance/*", "Condition": { "StringEquals": { "aws:RequestTag/environment": [ "test", "dev", "qa", "prod", "staging" ] }, "ForAllValues:StringEquals": {"aws:TagKeys": "environment"} } } }
Or we could go one step further and require the tags on all resources across an AWS Organization using Tag Policies, which is definitely worth a quick read!
This creates a mandatory and uniform tagging policy across all managed resources that we provision inside AWS.
The next step is to utilize this as a trust anchor inside our applications to make for more repeatable and cleaner patterns. To explain this, let's say that we keep our application configurations stored inside of an S3 bucket. We want to give each project/environment access to its own configuration, while denying access to the rest of the files in the bucket.
One way to accomplish this would be to dynamically generate a role for each cartesian product of project/environment which would grant the specific access we needed inside of the S3 bucket. You can see an example AWS implementation of this technique on this excellent article by AWS - look for the "Permission Templates" heading.
The problem with this is that it requires an individual policy for each role created, and in addition needs to update the S3 bucket policy every time a project or environment is added. (In the example above, one would need to update the S3 bucket policy every time a tenant is added).
However, we can use a technique called Attribute Based Access Control (ABAC) to use a static bucket policy that can leverage the tagging that we implemented above.
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": { "AWS": [ "arn:aws:iam::account-id:root" ] }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/${aws:PrincipalTag/environment}/${aws:PrincipalTag/project}/config.env", "Condition": { "ForAllValues:StringEquals": { "aws:TagKeys": [ "project", "environment" ] } } } }
This will utilize the tagging policy enforced above to dynamically resolve the tags on the IAM entity requesting access to S3. This entity can be a Lambda role, an EC2 instance role, a role assigned to EKS/ECS or even a dynamic IAM entity created by a third party tool, such as Hashicorp Vault.
In this setup, environments and projects can be created, modified or removed without ever needing to make changes to the above S3 bucket role. It is important to note that we have a hardcoded AWS account ID (or list of them for cross-account access) which would still need to be modified to restrict the source accounts (else some third party who knew your naming structure could create an IAM entity with the required tags and gain access to your configuration files).
We now have a static - or nearly static - centrally manageable IAM policy which can be used as a clean and repeatable pattern for our applicative IAM needs which utilizes the trust anchor provided by our mandatory and uniform tagging used across all managed resources that we provision inside AWS.
As I've illustrated, this pattern can help to eliminate external tooling and reduce the changesets required in policy management when introducing changes to an ever-evolving modern web application. It's not a perfect one-size-fits all solution - there's a limit to how much you can do with tags - but hopefully this will help you adhere to important architectural principals, such as the KISS principal.
Has this helped you? Do you have another interesting pattern to share? Questions? Leave your comments below!