simonw/s3-credentials

Command for creating roles

simonw opened this issue · 18 comments

If you want to access S3 from a Lambda function, AWS recommend you create a dedicated role that the Lambda function can then use. But... you still need to attach JSON policies to it!

https://aws.amazon.com/premiumsupport/knowledge-center/lambda-execution-role-s3-bucket/

A similar mechanism is available for EC2 instances: https://aws.amazon.com/premiumsupport/knowledge-center/ec2-instance-access-s3-bucket/

So a command which can create a role using the same --read-only and suchlike options as the other commands would be really useful.

Maybe something like:

s3-credentials create-role name-of-role name-of-bucket1 name-of-bucket2 --read-only

Probably need a list-roles command too for this, which could get a bit weird because it will list roles outside of the domain of S3 buckets.

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.create_role

response = client.create_role(
    Path='string',
    RoleName='string',
    AssumeRolePolicyDocument='string',
    Description='string',
    MaxSessionDuration=123,
    PermissionsBoundary='string',
    Tags=[
        {
            'Key': 'string',
            'Value': 'string'
        },
    ]
)

Looks like only RoleName and AssumeRolePolicyDocument are required (the latter is the JSON policy string).

Tempted to add a --tag key value multi option which can be used to add tags to the created role - maybe add that to other commands for e.g. tagging the bucket created by s3-credentials create as well.

If you run this command twice:

s3-credentials create-role name-of-role name-of-bucket1 --read-only
s3-credentials create-role name-of-role name-of-bucket1 name-of-bucket2

Should it redefine the name-of-role role to instead grant read-write access to those two buckets - essentially replacing the JSON policy for the existing role?

I think it should... but it should maybe show an interactive warning that you are about to replace an existing role (since the command is called create-role) - and -y can be used to automatically say yes to that prompt.

I need to try this out myself first - ideally by shipping a lambda function that can read/write to an S3 bucket via a role created using the procedure I would use to implement this command.

Maybe name the command role instead of create-role to solve for that confusion about whether it's creating a new role or updating an existing one.

Current list of commands from s3-credentials --help:

  create              Create and return new AWS credentials for specified...
  delete-user         Delete specified users, their access keys and their...
  get-object          Download an object from an S3 bucket
  list-bucket         List content of bucket
  list-buckets        List buckets - defaults to all, or pass one or more...
  list-user-policies  List inline policies for specified user
  list-users          List all users
  policy              Generate JSON policy for one or more buckets
  put-object          Upload an object to an S3 bucket
  whoami              Identify currently authenticated user

I think role would fit with the existing commands well.

If I do this it would be useful to be able to try out the newly created roles with the list-bucket and get-object and put-object commands.

They could grow a --role option which uses STS.assume_role() to assume the role and then performs their actions under that role.

Also maybe have a way of returning temporary authentication credentials for a specified role. Something like this:

s3-credentials auth-role name-of-role 15m
# Returns JSON or INI credentials

Could also offer an option to s3-credentials role which causes it to output temporary credentials once the role has been created - not sure what I would call that option though since --auth is already taken. Probably better to leave it as a separate command.

This is beginning to make the create command look like it has the wrong name. Might fix that and set up the old name as an alias.

Annoying that assume_role() requires an ARN, not just a role name - something like RoleArn='arn:aws:iam::123456789012:role/demo' - so code will have to look up that (maybe construct it using the account ID) if it's going to allow users to use the roles name directly with --role.

AssumeRolePolicyDocument is not what I thought it was. I thought it was where you put the kind of policy document I've been generating for this tool - but actually looking at examples using my list-roles prototype command they ALL look like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "access-analyzer.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

So they aren't policies for what the role is allowed to do - they are policies that control who or what is allowed to assume that role.

Re-reading the docs, that field is described as "The trust relationship policy document that grants an entity permission to assume the role." - but it's also a required field! So you can't create a role without having that document. I guess that means the s3-credentials role command needs a --service access-analyzer.amazonaws.com option or similar to help populate the assume role policy?

OK, it looks like you have to create a role, then attach policies to it using https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.attach_role_policy

response = client.attach_role_policy(
    RoleName='string',
    PolicyArn='string'
)

That requires a policy ARN - but the docs say:

Use this operation to attach a managed policy to a role. To embed an inline policy in a role, use PutRolePolicy . For more information about policies, see Managed policies and inline policies in the IAM User Guide .

So it looks like I should use put_role_policy() instead: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.put_role_policy

response = client.put_role_policy(
    RoleName='string',
    PolicyName='string',
    PolicyDocument='string'
)

The PolicyName doesn't matter, similar to the code I wrote here:

policy_name = "s3.{permission}.{bucket}".format(
permission="custom" if policy else permission,
bucket=bucket,
)

iam.put_user_policy(
PolicyDocument=json.dumps(user_policy),
PolicyName=policy_name,
UserName=username,
)

Looking at my existing code, I have this:

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": {},
}
],
}
),
)

Where did I get that AssumeRolePolicyDocument JSON from? That assigns a "Principal" of "AWS": "arn:aws:iam::{}:root".format(account_id)?

That came out of my research in:

After puzzling over the AssumeRolePolicyDocument for a while, I tried creating a role using the AWS web console and then retrieving that role definition using get_role() - which is how I found that roles created directly within that console use that as their AssumeRolePolicyDocument.

A useful clue: in the web console it's called trusted entities, as seen in this screenshot:

image

Here's what the AWS CLI create-role command looks like: https://docs.aws.amazon.com/cli/latest/reference/iam/create-role.html#create-role

  create-role
[--path <value>]
--role-name <value>
--assume-role-policy-document <value>
[--description <value>]
[--max-session-duration <value>]
[--permissions-boundary <value>]
[--tags <value>]
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]

Design question: what should the options to the command be for generating the AssumeRolePolicyDocument?

I think the following:

  • --root - adds "AWS": "arn:aws:iam::{}:root".format(account_id)
  • --service lightsail.amazonaws.com - can be used multiple times, with different service names
  • --assume-role-policy POLICY - for completely custom policies, similar to s3-credentials create --policy POLICY - which accepts a filename or - or a JSON literal.

At least one of these three options is required, or the command returns an error.

Detailed documentation about what can go in "Principal" here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html

Multiple values can be provided, for example:

"Principal": {
  "AWS": [
    "arn:aws:iam::AWS-account-ID:user/user-name-1", 
    "arn:aws:iam::AWS-account-ID:user/user-name-2"
  ]
}

"Principal": {
    "Service": [
        "ecs.amazonaws.com",
        "elasticloadbalancing.amazonaws.com"
   ]
}

Presumably it's possible to have both AWS and Service keys, though I would have to test that to make sure.

I could offer --arn "arn:aws:iam::AWS-account-ID:user/user-name-1" as an option for enabling the role for specific ARNs - in this case specific user IDs.