Normal view

There are new articles available, click to refresh the page.
Before yesterdayNCC Group Research

Introduction to AWS Attribute-Based Access Control

2 October 2023 at 12:01

AWS allows tags, arbitrary key-value pairs, to be assigned to many resources. Tags can be used to categorize resources however you like. Some examples:

  • In an account holding multiple applications, a tag called “application” might be used to denote which application is associated with each resource.
  • A tag called “stage” might be used to separate resources belonging to alpha, beta, and production stages within a single account.
  • A tag called “cost-center” might be used to indicate which business unit is responsible for a resource. AWS’ billing can break down bills by tag, allowing customers to allocate costs from a shared account to the appropriate budgets.

Once you have tagged your resources, you can search and filter based on tags. That’s not very interesting from a security perspective. Far more interesting is using tags to implement attribute-based access control (ABAC).

Attribute-Based Access Control

The “normal” AWS authorization scheme is known as Role-Based Access Control (RBAC): you define “roles” corresponding to service or job functions (implemented as IAM Users or Roles) and assign them the privileges necessary for those functions. A disadvantage of this scheme is that when you add new resources to your environment, the privileges assigned to your principals may need to be modified. This doesn’t scale particularly well with large numbers of resources. Using resource-based permission policies rather than identity-based permission policies can help with this, but that doesn’t scale well with large numbers of principals (especially since AWS doesn’t allow permissions to be granted to groups using resource-based permission policies). Also, not all resources support resource-based permission policies.

An alternative authorization scheme is to assign tags to principals and resources then grant permissions based on the combinations of principal and resource tags. For instance, all principals tagged as belonging to project QuarkEagle can be allowed to access resources also tagged as belonging to project QuarkEagle, while principals tagged with project CrunchyNugget can only access resources also tagged CrunchyNugget. This approach isn’t suitable for all scenarios but can result in significantly fewer and smaller permission policies that rarely need to be updated even as new principals and resources are added to accounts. This scheme is known as “attribute-based access control” (ABAC) or “tag-based access control” (TBAC), depending on the source.

In practice, you’re not likely to want a “pure” ABAC environment: most ABAC deployments will combine it with elements of RBAC.

How AWS implements ABAC

Apart from tags themselves, there are no new fundamental concepts in AWS for ABAC. You still have principals and resources with identity-based and resource-based permission policies. However, instead of having a lot of specifics in the resource and principal fields, an ABAC permission policy will have wildcards in those fields with the real logic implemented using conditions. There are four main condition keys that relate to tagging:

  • aws:ResourceTag/<tag-name>: control access based the values of tags attached to the resource being accessed.
  • aws:RequestTag/<tag-name>: control the tag values that can be assigned to or removed from resources.
  • aws:TagKeys: control access based on the tag keys specified in a request. This is a multi-valued condition key.
  • aws:PrincipalTag/<tag-name>: control access based on the values of tags attached to the principal making the API request.

Some examples are in order to clarify how these condition keys are be used.

If a principal is only permitted to publish messages to SNS Topics belonging to project QuarkEagle, then it might have this permission policy statement:

{
    "Effect": "Allow",
    "Action": "sns:Publish",
    "Resource": "arn:aws:sns:*:*:*",
    "Condition": {
        "StringEquals": {
            "aws:ResourceTag/project": "QuarkEagle"
        }
    }
}

This allows your principal to publish to any SNS Topic, so long as that Topic has a tag named “project” whose value is “QuarkEagle”. If tiy want to go a step farther, you could tag your principals with their associated projects and then use this permission policy statement instead:

{
    "Effect": "Allow",
    "Action": "sns:Publish",
    "Resource": "arn:aws:sns:*:*:*",
    "Condition": {
        "StringEquals": {
            "aws:ResourceTag/project": "${aws:PrincipalTag/project}"
        }
    }
}

Now any principal that has a tag named “project” with the value “QuarkEagle” can publish to any SNS topic whose “project” tag is also “QuarkEagle” and any principal whose “project” tag is “CrunchyNugget” can publish to any topic that is also tagged “CrunchyNugget” — no need for permission policies that know about every tag value in use.

If your principals can create and delete SNS Topics, then you should make sure that they can only create properly tagged ones and can only delete ones with the proper tags. Similarly, if you allow your principals to set or unset tags, then you probably don’t want to allow them to change the “project” tag values on their resources. To enforce that, you might give them permission policy statements like this:

{
    "Effect": "Allow",
    "Action": [
        "sns:CreateTopic",
        "sns:TagResource"
    ],
    "Resource": "arn:aws:sns:*:*:*",
    "Condition": {
        "StringEquals": {
            "aws:RequestTag/project": "${aws:PrincipalTag/project}",
            "aws:ResourceTag/project": "${aws:PrincipalTag/project}"
        }
    }
},
{
    "Effect": "Allow",
    "Action": "sns:DeleteTopic",
    "Resource": "arn:aws:sns:*:*:*",
    "Condition": {
        "StringEquals": {
            "aws:ResourceTag/project": "${aws:PrincipalTag/project}"
        }
    }
},
{
    "Effect": "Allow",
    "Action": [
        "sns:TagResource",
        "sns:UntagResource"
    ],
    "Resource": "arn:aws:sns:*:*:*",
    "Condition": {
        "StringEquals": {
            "aws:ResourceTag/project": "${aws:PrincipalTag/project}"
        },
        "ForAllValues:StringNotEquals": {
            "aws:TagKeys": "project"
        }
    }
}
  1. The first statement permits the principal to create SNS Topics so long as the each new Topic’s “project” tag has the same value as the principal’s “project” tag. Setting a tag during resource creation requires the same sns:TagResource permission as setting one explicitly later, so we grant permission for both actions. During resource creation, the condition key aws:ResourceTag is set to the value specified for the tag to be created, so the two condition checks are very similar but both are necessary to prevent the principal from using sns:TagResource in unintended ways:
    • Without the check against aws:RequestTag, the principal would be able to assign arbitrary tag values to existing Topics that currently share the principal’s “project” tag (potentially giving away those Topics to someone else; this could allow one principal to elevate the privileges of another, useful in a scenario where someone has compromised two different principals, neither of which can do what the attacker wants).
    • Omitting the aws:ResourceTag check would allow the principal to re-tag arbitrary existing Topics to its “project” tag value (allowing it to take control of other Topics and likely elevating its privileges if the principal has other permissions allowing it to read from or write to Topics that share its “project” tag).
  2. The second statement permits the principal to delete SNS Topics whose “project” tag have the same value as the caller’s “project” tag.
  3. The third statement enables the principal to change the tags on existing SNS Topics. The first condition, using aws:ResourceTag, requires that the target Topic’s “project” tag have the same value as the caller’s “project” tag. The second condition, using aws:TagKeys, prevents the caller from changing the value of the Topic’s “project” tag. Note that due to the first statement, it’s still possible for the principal to set a Topic’s “project” tag, but only if the value is the caller’s “project” tag.

ABAC permissions policies are easy to get wrong. Even Amazon has difficulty with them: AWS’ public documentation contains a number of example permission policies similar to the first statement above that do not contain the “StringEquals”: { “aws:ResourceTag/project”: “${aws:PrincipalTag/project}” } condition. Make sure that any ABAC permission policies that you write or review cover all scenarios.

It’s also important that your principals can’t change the “project” tags on themselves. If you need to allow your principals to call iam:TagUser and iam:UntagUser (or their equivalents for Roles), then you should use similar permission policies to prevent them from removing or changing the values of their “project” tags.

If you want to enforce some order on what tags are applied, then you can use a permission policy statement such as the following to prevent principals from setting any tags other than “department”, “project”, and “stage” on SNS Topics:

{
    "Effect": "Deny",
    "Action": "sns:TagResource",
    "Resource":"arn:aws:sns:*:*:*",
    "Condition": {
        "ForAnyValue:StringNotEquals": {
            "aws:TagKeys": [
                "stage",
                "project",
                "department"
            ]
        }
    }
}

A similar permission policy statement employing aws:RequestTag/… can be used to control the values that may be assigned to tags.

Some services offer a condition key that can be used to make a permission policy statement apply only during resource creation (such as EC2’s ec2:CreateAction); using such a condition in your Policies can make them simpler and easier to understand. For instance, the following permission statement allows a principal to create tagged EC2 resources without allowing any weird ec2:CreateTags abuses:

{
    "Effect": "Allow",
    "Action": [
        "ec2:CreateSecurityGroup",
        "ec2:CreateImage",
        "ec2:CreateVolume"
        "ec2:RunInstances"
    ],
    "Resource":"*",
    "Condition": {
        "StringEquals": {
            "aws:RequestTag/project": "${aws:PrincipalTag/project}"
        }
    }
},
{
    "Effect": "Allow",
    "Action": "ec2:CreateTags",
    "Resource": "arn:aws:ec2:*:*:*",
    "Condition": {
        "StringEquals": {
            "ec2:CreateAction": [
                "CreateSecurityGroup",
                "CreateImage",
                "CreateVolume"
                "RunInstances"
            ]
        }
    }
}

The first statement permits the caller to create a few different EC2 resources so long as they have the same “project” tag value as the caller. Actually setting that tag, even during resource creation, requires a permission for ec2:CreateTags, so the second statement allows the caller to create tags only from those resource-creation API actions. However, this approach has its own flaws: EC2 considers ec2:CreateTags to be a resource-creation action, so make sure that any wildcards that you use in the ec2:CreateAction condition check don’t match ec2:CreateTags.

With proper use of tagging, combined with separate VPCs, it may be possible to put separate applications and separate stages of each application in the same account without allowing, for instance, beta principals to access prod data. Not that I’d recommend this approach: getting the tagging right is a lot of work (and there are many gotchas; see below). If you’re building a new application, it’s usually safer (and easier) to just use separate accounts for each application and stage with cross-account access via Role assumption and VPC peering as needed. But it’s worth considering for big complicated accounts with access control problems: it may be less work to implement ABAC on top of existing applications than it is to disentangle a giant messy account.

Manipulating tags

AWS services that support tagging have API actions to add and remove tags from resources and to list the tags on a resource. Tagging support was added to many AWS services well after they were first released and ABAC support was grafted on later still. As a result, the implementation of tagging isn’t consistent between services.

The most common patterns for API action names are:

  • TagResource / UntagResource / ListTagsForResource: a single set of API actions for all taggable resources in the service. One of the arguments is a resource ARN; if the service supports more than one type of resource, it figures out the resource type and what to do from there based on the ARN. One or more tags can be set or removed at once. This seems to be the most common pattern, used by Lambda, SNS, DynamoDB, and other services. However, there is a lot of variation in the name of the tag-listing operation: sns:ListTagsForResource, lambda:ListTags, dynamodb:ListTagsOfResource, apigateway:GetTags, kms:ListResourceTags, etc. Some services don’t have a tag-listing operation at all, allowing tags to be retrieved only using other resource-description API actions (e.g., Secrets Manager). There are also several variations on this pattern:
    • AddTagsToResource / RemoveTagsFromResource / ListTagsForResource: these API actions tend to work just like in the above case only with different names. This pattern is used by RDS and SSM.
    • AddTags / RemoveTags / ListTags, used by CloudTrail.
    • CreateTags / DeleteTags / DescribeTags, used by EC2 and Workspaces. In EC2’s case, ec2:DescribeTags doesn’t operate on a single resource but rather returns information about every resource in EC2 (with optional filters to limit the response to certain specific resources, resource types, etc.).
  • Tag<Resource> / Untag<Resource> / List<Resource>Tags: separate sets of API actions for each taggable resource. You need to call the correct API action for the type of resource that you want to tag and provide a resource name or ARN. As above, you can generally set or remove one or more tags at once. This pattern is followed by IAM and SQS (some specific examples: iam:TagUser, sqs:UntagQueue, iam:ListRoleTags). Certificate Manager uses a variation on this pattern: acm:AddTagsToCertificate / acm:RemoveTagsFromCertificate / acm:ListTagsForCertificate.

Most resource-creation API actions allow tags to be assigned during resource creation. Assigning a tag this way requires the permission for the tag-setting API action in addition to the resource-creation API action. For instance, to create an SNS Topic, I need permission for the action “sns:CreateTopic“. If I set a tag on a Topic while creating it, then I also need permission for the action “sns:TagResource” even if I never directly call that API action. However, there may still be some resources that support tagging but cannot be tagged at creation or cannot be tagged when created using CloudFormation.

The syntax for setting tags using AWS CLI also differs between services. Most services’ resource-creation and tagging API actions use a “–tags” command-line argument followed by a list of tags to set, but how that list is formatted depends on the service. Some services (including SQS and Lambda) expect “–tags project=QuarkEagle,stage=beta” while others (such as SNS and SSM) expect an argument of the form “–tags Key=project,Value=QuarkEagle Key=stage,Value=beta“. EC2 is an exception; during resource creation, it uses a more elaborate form of the latter syntax: “–tag-specifications ‘ResourceType=security-group,Tags=[{Key=project,Value=QuarkEagle},{Key=stage,Value=beta}]’“.

If you were thinking that it would be really nice for AWS to provide a unified tagging API, you’re not alone. AWS Resource Groups has a tagging API that can operate on most AWS services that support tagging. Besides providing a generic interface to tagging and untagging, this service also provides ways to retrieve all tag keys and values currently in use by an account and to query resources by tag across multiple services. From AWS Console, you can use “aws resourcegrouptaggingapi tag-resources …” to apply tags to arbitrary resources. To do this, you need both a “tag:TagResources” permission and a tagging permission for the resource that you are trying to tag. Untagging is similar. The following is a minimal permission policy to allow a principal to apply tags to a specific SNS Topic (resource constraints aren’t supported on the tag:TagResources permission because the resources are not in the Resource Groups service):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sns:TagResource",
            "Resource": "arn:aws:sns:us-east-1:111111111111:some-topic"
        },
        {
            "Effect": "Allow",
            "Action": "tag:TagResource",
            "Resource": "*"
        }
    ]
}

Special Cases

S3 is the special snowflake; there’s a reason why I didn’t use it for my examples above. While most AWS services have API actions that add or remove individual tags, the S3 tagging API operates on the entire set of tags on the object at once: s3:PutObjectTagging and s3:PutBucketTagging replace the full set of tags on the target resource with the given set, while s3:RemoveObjectTagging and s3:RemoveBucketTagging remove all tags from the target resource. If an object has the tags “application”, “stage”, and “owner” and you want to change the value of “owner”, then your call to s3:PutObjectTagging needs to set the “owner” tag to its new value and the “application” and “stage” tags to their current values in order to not lose the other tags. Tags can be retrieved using s3:GetObjectTagging and s3:GetBucketTagging. Unfortunately, the Resource Groups Tagging API does not support objects in S3 so it does not provide a work-around for S3’s weird tagging implementation.

To make the S3 case even more complicated, both Buckets and objects can be tagged but S3 only supports ABAC on objects. Even that support is incomplete: ABAC is not supported for s3:PutObject, s3:DeleteObject, or s3:DeleteObjectVersion calls. S3 also doesn’t support the normal aws:ResourceTag, aws:RequestTag, and aws:TagKeys condition keys at all: you must use S3-specific condition keys:

  • s3:ExistingObjectTag/<tag-name>: control access based the values of tags attached to objects.
  • s3:RequestObjectTag/<tag-name>: control the tag values that can be assigned to or removed from objects.
  • s3:RequestObjectTagKeys: control access based on the tag keys specified in a request. This is a multi-valued condition key.

There may be other services with unusual tagging implementations; I haven’t checked all of them. EC2 has its own tagging condition key “ec2:ResourceTag” but that seems to be a synonym for aws:ResourceTag.

Limitations of AWS ABAC

Unfortunately, ABAC support across AWS is incomplete (though improving) and has many implementation inconsistencies across services. Quirks of AWS’ ABAC implementation include:

  • Some services do support tagging but don’t support ABAC; others support ABAC on only some types of resources or in some contexts. There may yet be some services that don’t support tagging at all. The following table, describing ABAC support for a number of frequently-encountered AWS services, is summarized from Amazon’s documentation:
    Service ABAC support?
    EC2 Yes
    S3 Partial support for objects using non-standard condition keys; no support for Buckets
    Lambda For Functions but not for other resource types
    DynamoDB No
    RDS Yes
    IAM Users and Roles only, with exceptions
    Certificate Manager Yes
    Secrets Manager Yes
    KMS Yes
    CloudTrail Yes
    CloudWatch Mostly
    CloudFormation Yes
    API Gateway Yes (though not for API authorization)
    CloudFront Yes
    Route 53 No
    SNS Yes
    SQS Yes (added in fall 2022)

    Details on some of those limitations:

    • IAM: it is not possible to limit who can pass a Role using tags on that Role. While it is tempting to try to limit overly-permissive iam:PassRole permissions by allowing principals to pass only Roles with specific tags, that won’t work.
    • S3: it is not possible to limit who can delete or overwrite an S3 object based on tags on that object, nor is it possible to control access to API actions that operate on Buckets using tags. S3 also doesn’t support the normal condition keys for tags, instead using its own.
    • CloudWatch Logs doesn’t support limitations on the tags that can be assigned to Log Groups and doesn’t support aws:ResourceTag/<tag-name> for logs:DescribeLogGroups.
  • As previously noted, the tagging APIs and available condition keys for tagging are not consistent, either in name or function, across all AWS services. As a result, make sure to thoroughly review or test ABAC policies, especially if they contain Deny rules based on tagging. It won’t do to attempt to restrict access to S3 objects using aws:ResourceTag/<tag-name> because that condition key won’t exist: you need to use s3:ExistingObjectTag/<tag-name>. It also won’t do to treat s3:PutObjectTagging like you do iam:TagUser in your permission policies because they work differently.
  • Also as noted above, AWS’ pattern of requiring a tagging permission to assign tags while creating a resource makes it altogether too easy to accidentally give principals excessive tagging permissions. Carefully review all permission policies that permit tagging during resource creation.
  • In most cases, tag names are case-sensitive: “Project” is a different tag than “project“. However, this is not entirely consistent across services. Tags on IAM Users and Roles (though not on other IAM resources) are case-insensitive.
  • Some services (such as Secrets Manager) won’t let a principal change a tag if that change would prevent that principal from accessing the resource after the change. Other services (such as SNS) do not have this restriction.
  • There are some random bugs and missing functionality in various services.  SQS doesn’t support aws:ResourceTag in resource-based permission policies. EC2 doesn’t support aws:ResourceTag checks during resource creation; you must use the EC2-specific condition key ec2:CreateAction to protect your tag-on-create permissions for EC2. CloudTrail seems to support aws:ResourceTag only after Trail creation and doesn’t seem to support aws:RequestTag or aws:TagKeys at all. There are probably a few other similar bugs out there.

Hopefully AWS’ ABAC support will continue to improve over time.

More information

❌
❌