Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

--duration option to create time-limited credentials (using sts.assume_role()) #27

Closed
simonw opened this issue Nov 10, 2021 · 6 comments
Labels
enhancement New feature or request
Milestone

Comments

@simonw
Copy link
Owner

simonw commented Nov 10, 2021

See #26 for the research. It looks like the way to do this is:

  1. Ensure a dedicated role with arn:aws:iam::aws:policy/AmazonS3FullAccess exists - if it does not, create it. It needs to have a known name - I propose using s3-credentials.AmazonS3FullAccess here, and also populating the Description field. The role needs to be assumable by the current account, see AssumeRolePolicyDocument example in Research creating expiring credentials using sts.assume_role() #26 (comment)
  2. Call sts.assume_role() against that role, passing in as a policy the same inline policy document used for non-expiring credentials, using the code in policies.py.
  3. Return the AccessKeyId, SecretAccessKey AND the SessionToken - all three are needed to make authenticated calls.
@simonw simonw added the enhancement New feature or request label Nov 10, 2021
@simonw
Copy link
Owner Author

simonw commented Nov 10, 2021

I'm going to add this to the existing s3-credentials create command as a --duration option, which can be 5 for 5 seconds or 10m for ten minutes - so there is an optional suffix which, if omitted, is assumed to be seconds.

@simonw simonw changed the title Option to create time-limited credentials (using sts.assume_role()) --duration option to create time-limited credentials (using sts.assume_role()) Nov 10, 2021
@simonw
Copy link
Owner Author

simonw commented Nov 10, 2021

def ensure_s3_role_exists(iam, sts):
    role_name = "s3-credentials.AmazonS3FullAccess"
    account_id = sts.get_caller_identity()["Account"]
    try:
        role = iam.get_role(RoleName=role_name)
        return role["Role"]["Arn"]
    except iam.exceptions.NoSuchEntityException:
        create_role_response = iam.create_role(
            Description="Role used by the s3-credentials tool to create time-limited credentials that are restricted to specific buckets",
            RoleName=role_name,
            AssumeRolePolicyDocument=json.dumps(
                {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": "arn:aws:iam::{}:root".format(account_id)
                            },
                            "Action": "sts:AssumeRole",
                            "Condition": {},
                        }
                    ],
                }
            ),
        )
        # Attach AmazonS3FullAccess to it - note that even though we use full access
        # on the role itself any time we call sts.assume_role() we attach an additional
        # policy to ensure reduced access for the temporary credentials
        iam.attach_role_policy(
            RoleName="s3-credentials.AmazonS3FullAccess",
            PolicyArn="arn:aws:iam::aws:policy/AmazonS3FullAccess",
        )
        return create_role_response["Role"]["Arn"]

@simonw
Copy link
Owner Author

simonw commented Nov 10, 2021

I'm going to change how policies work a little bit.

The existing code attaches one policy per bucket to the created user:

# Add inline policies to the user so they can access the buckets
for bucket in buckets:
policy_name = "s3.{permission}.{bucket}".format(
permission="custom" if policy else permission,
bucket=bucket,
)
if policy:
policy_dict = json.loads(policy.replace("$!BUCKET_NAME!$", bucket))
else:
if permission == "read-write":
policy_dict = policies.read_write(bucket)
elif permission == "read-only":
policy_dict = policies.read_only(bucket)
elif permission == "write-only":
policy_dict = policies.write_only(bucket)
else:
assert False, "Unknown permission: {}".format(permission)
iam.put_user_policy(
PolicyDocument=json.dumps(policy_dict),
PolicyName=policy_name,
UserName=username,
)

With sts.assume_role() I only get to pass through a single policy document - so instead of generating a policy per bucket I'm going to generate "allow" blocks for each bucket and assemble those into a single policy.

This will likely change the way policies.py works.

@simonw
Copy link
Owner Author

simonw commented Nov 10, 2021

As such, further work on this issue is blocked on the redesigned policies from #15.

@simonw
Copy link
Owner Author

simonw commented Nov 10, 2021

Tested my prototype with a 15 minute duration, when I tried to make a call more than fifteen minutes later I got:

botocore.exceptions.ClientError: An error occurred (ExpiredToken) when calling the ListObjectsV2 operation: The provided token has expired.

@simonw
Copy link
Owner Author

simonw commented Nov 11, 2021

simonw added a commit that referenced this issue Nov 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant