STS Assume Role and Revoke

AWS STS is the security token service that enables the assumption of roles in AWS. Using this service can provide temporary credentials to:

  • IAM Users
  • Web Identity Federation Users
    • Twitter
    • Google
    • Facebook
    • Others
  • AWS Services (EC2, Lambda, etc…)

The service can also be called manually from the command line using the following command:

aws sts assume-role --role-arn "arn:aws:iam::[REDACTED]:role/Role_Allow_S3_Access" --role-session-name "tricky" --profile profile_name'

The session name above, tricky, is just an arbitrary name that I gave to the session.

This command will return credentials and a token:

{
    "Credentials": {
        "AccessKeyId": "ASIAQHJ236EMZC3GYYF6",
        "SecretAccessKey": "YJ+JCWzTSnfFjUcFZdopuLyjTF/Ag7h5VwRY4YkK",
        "SessionToken": "IQoJb3JpZ2luX2VjEJz//////////wEaCXVzLWVhc3QtMSJIMEYCIQCZu+rtJ+B/YJvox8y8rjBbp7cs6sY7OgjypE1jap7KrQIhAM7PjxzrmQqXmcNzNVlRuSh6+5YehGPKFfkQRZia+SdzKpwCCOT//////////wEQABoMMDE1NjkyNzIyNDU3Igzf6qxY02XYZ5XQZIcq8AFVOuyvEsJF1cIsgg37zdSuRrRQ12TZXfeF3129ntIsqzVup11ER6cNwHjxN1M05AuE/9TsMlO7ekQ1PwyW8ZSs2hdZnsD8cD7p0R2dVWdIUA1VPeVQ6miyMUUxovwQfBa3fX974y35jyevzL0CifBwineW1umTd6QoUDUygh4uFnuFMVJAPB2mcpwnXi//asJOf4nhTPXUnIyWUxbDo47GoH6legKHwispqMPHs+aPnysKEDRA6+Khigk7TsvbeQHReOuJCNkC4gjwCdNXmXm5eeGFMy0FtQU38Uz23XJITKmEv22Hd5aJ7g0SaL9Qs9EwutvTnQY6nAFtR6PZHc9c1VPYwXpCLswJwaQ1bfjNpdPQNhcPpCm69vBwkxLxjplHsI2w0trpkklse9Z6HKP4Jb1ldSKiVKG5wwCILnMzdDDUcRWpVux2Sr1IluQPLiPNWWZ8pww2psoyWQKfS/OWZrZf0b4BSJ452lT6fqh16Rd/jHCpxm2NImW6RFcO6JSISTuMco98MeatQcT0kkT4xp5AeYY=",
        "Expiration": "2023-01-04T04:08:42+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAQHJ236EMX4GDIO2JP:tricky",
        "Arn": "arn:aws:sts::[REDACTED]:assumed-role/Role_Allow_S3_Access/tricky"
    }
}

(for the example later in this article I have saved the original output of this file to a file called credentials.json)

In this case the managed policy AmazonS3FullAccess was added to the role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*",
                "s3-object-lambda:*"
            ],
            "Resource": "*"
        }
    ]
}

For a role to be assumable, the principal that is assuming the role must be in the trust policy for the role. In this case, the user iamadmin is the user that ran sts assume-role, and that user is in this trust policy.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowS3ForUser",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::REDACTED:user/iamadmin"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

In the code below, we use the credentials provided by the assume role to read the content of a file in S3.

import boto3
import json
from botocore.exceptions import ClientError

# read the credentials json 
with open("./credentials.json", "r") as f:
    credentials = json.load(f)['Credentials']

# connect using the credentials from the aws sts assume-role command
s3_client = boto3.client(
    's3',
    aws_access_key_id=credentials['AccessKeyId'],
    aws_secret_access_key=credentials['SecretAccessKey'],
    aws_session_token=credentials['SessionToken'])

try:
    # get an s3 client and list the buckets
    response = s3_client.list_buckets()
    if response['ResponseMetadata']['HTTPStatusCode'] == 200:
        # get a list of bucket names
        bucket_names=[bucket['Name'] for bucket in response['Buckets']]
        for bucket in bucket_names:
            print(f'Bucket Name - {bucket}')
            # get a list of objects in the bucket
            response=s3_client.list_objects_v2(Bucket=bucket)
            if response['ResponseMetadata']['HTTPStatusCode'] == 200:
                # get the keys from the objects
                keys = [object['Key'] for object in response['Contents']]
                for key in keys:
                    print(f'Key Name - {key}')
                    # get the object for the key
                    response =s3_client.get_object(Bucket = bucket, Key = key)
                    if response['ResponseMetadata']['HTTPStatusCode'] == 200:
                        file_contents = response['Body']
                        # display the contents of the object
                        print(file_contents.read().decode('utf-8'))
except ClientError as ex:
    print(ex)

Output:

❯ python main.py
Bucket Name - jeffgoldenme-secret-bucket
Key Name - secret_file.txt
Secret Data

The credentials exposed through this command could accidentally be commited to source code or otherwise become exposed.

If this happens, we need a way to mitigate the issue. But we also don’t want to impact legitimate users of the role in the process (or at least minimize the impact on those users).

Bad Ideas:

  • Remove the role.
  • Change the role permissions.

These ideas could be better because if other principals use the role, it will require code or process changes to rectify the issue.

You might think you can change the trust policy for the role and remove the user who leaked the credential. But this isn’t effective. Once credentials have been provided through the assume role function, those credentials no longer depend on the trust policy. The role and permissions will continue to work for the supplied credentials. There is also no way of actually causing the credentials to be invalidated or expire early.

The best option is to use the revoke sessions option to add a policy to revoke sessions established before the current time. It does this using a conditional deny policy for all permissions on all resources. This condition denies access only if the token was granted before the current time.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Action": [
                "*"
            ],
            "Resource": [
                "*"
            ],
            "Condition": {
                "DateLessThan": {
                    "aws:TokenIssueTime": "[policy creation time]"
                }
            }
        }
    ]
}

The same code, from above, after running the revoke sessions function and adding the above policy would prevent further access.

❯ python main.py
An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

However, the unfortunate side effect of this is that anyone using credentials provided by this role would need to use sts assume-role again to get new credentials. If EC2 instances use the role in an instance profile, they will need to be restarted to force the instance to get new credentials. But this would stop the credentials from being used unauthorized and not require code or configuration changes to existing processes or for existing users.