s3-credentials create command

Closes #3. Also added a warning and request for security review, refs #7
pull/16/head
Simon Willison 2021-11-02 18:54:31 -07:00 zatwierdzone przez GitHub
rodzic 5eea11994c
commit 1f0cc51142
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 231 dodań i 2 usunięć

Wyświetl plik

@ -5,10 +5,14 @@
[![Tests](https://github.com/simonw/s3-credentials/workflows/Test/badge.svg)](https://github.com/simonw/s3-credentials/actions?query=workflow%3ATest)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/s3-credentials/blob/master/LICENSE)
NOT YET USABLE.
A tool for creating credentials for accessing S3 buckets
## ⚠️ Warning
I am not an AWS security expert. You shoud review how this tool works carefully before using it against with own AWS account.
If you are an AWS security expert I would [love to get your feedback](https://github.com/simonw/s3-credentials/issues/7)!
## Installation
Install this tool using `pip`:
@ -17,12 +21,55 @@ Install this tool using `pip`:
## Usage
The `s3-credentials create` command is the core feature of this tool. Pass it one or more S3 bucket names and it will create a new user with permission to access just those specific buckets, then create access credentials for that user and output them to your console.
Make sure to record the `SecretAccessKey` because it will only be displayed once and cannot be recreated later on.
In this example I create credentials for reading and writing files in my `static.niche-museums.com` S3 bucket:
```
% s3-credentials create static.niche-museums.com
Created user: s3.read-write.static.niche-museums.com with permissions boundary: arn:aws:iam::aws:policy/AmazonS3FullAccess
Attached policy s3.read-write.static.niche-museums.com to user s3.read-write.static.niche-museums.com
Created access key for user: s3.read-write.static.niche-museums.com
{
"UserName": "s3.read-write.static.niche-museums.com",
"AccessKeyId": "AKIAWXFXAIOZOYLZAEW5",
"Status": "Active",
"SecretAccessKey": "...",
"CreateDate": "2021-11-03 01:38:24+00:00"
}
```
The command has several additional options:
- `--username TEXT`: The username to use for the user that is created by the command (or the username of an existing user if you do not want to create a new one). If ommitted a default such as `s3.read-write.static.niche-museums.com` will be used.
- `-c, --create-bucket`: Create the buckts if they do not exist. Without this any missing buckets will be treated as an error.
- `--read-only`: The user should only be allowed to read files from the bucket.-
- `--write-only`: The user should only be allowed to write files to the bucket, but not read them. This is useful for logging use-cases.
- `--bucket-region`: If creating buckets, the region in which they should be created.
- `--silent`: Don't output details of what is happening, just output the JSON for the created access credentials at the end.
`--user-permissions-boundary`: Custom [permissions boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to use for users created by this tool. This will default to restricting those users to only interacting with S3, taking the `--read-only` option into account. Use `none` to create users without any permissions boundary at all.
Here's the full sequence of events that take place when you run this command:
1. Confirm that each of the specified buckets exists. If they do not and `--create-bucket` was passed create them - otherwise exit with an error.
2. If a username was not specified, determine a username using the `s3.$permission.$buckets` format.
3. If a user with that username does not exist, create one with an S3 permissions boundary that respects the `--read-only` option - unless `--user-permissions-boundary=none` was passed (or a custom permissions boundary string).
4. For each specified bucket, add an inline IAM policy to the user that gives them permission to either read-only, write-only or read-write against that bucket.
5. Create a new access key for that user and output the key and its secret to the console.
## Other commands
### whoami
To see which user you are authenticated as:
$ s3-credentials whoami
This will output JSON representing the currently authenticated user.
### list-users
To see a list of all users that exist for your AWS account:
$ s3-credentials list-users

Wyświetl plik

@ -1,6 +1,24 @@
import boto3
import botocore
import click
import json
from . import policies
def bucket_exists(s3, bucket):
try:
s3.head_bucket(Bucket=bucket)
return True
except botocore.exceptions.ClientError:
return False
def user_exists(iam, username):
try:
iam.get_user(UserName=username)
return True
except iam.exceptions.NoSuchEntityException:
return False
@click.group()
@ -9,6 +27,141 @@ def cli():
"A tool for creating credentials for accessing S3 buckets"
@cli.command()
@click.argument(
"buckets",
nargs=-1,
required=True,
)
@click.option("--username", help="Username to create or existing user to use")
@click.option(
"-c",
"--create-bucket",
help="Create buckets if they do not already exist",
is_flag=True,
)
@click.option("--read-only", help="Only allow reading from the bucket", is_flag=True)
@click.option("--write-only", help="Only allow writing to the bucket", is_flag=True)
@click.option("--bucket-region", help="Region in which to create buckets")
@click.option("--silent", help="Don't show performed steps", is_flag=True)
@click.option(
"--user-permissions-boundary",
help=(
"Custom permissions boundary to use for created users, or 'none' to "
"create without. Defaults to limiting to S3 based on "
"--read-only and --write-only options."
),
)
def create(
buckets,
username,
create_bucket,
read_only,
write_only,
bucket_region,
user_permissions_boundary,
silent,
):
"Create and return new AWS credentials for specified S3 buckets"
if read_only and write_only:
raise click.ClickException(
"Cannot use --read-only and --write-only at the same time"
)
def log(message):
if not silent:
click.echo(message, err=True)
permission = "read-write"
if read_only:
permission = "read-only"
if write_only:
permission = "write-only"
s3 = boto3.client("s3")
iam = boto3.client("iam")
# Verify buckets
for bucket in buckets:
# Create bucket if it doesn't exist
if not bucket_exists(s3, bucket):
if not create_bucket:
raise click.ClickException(
"Bucket does not exist: {} - try --create-bucket to create it".format(
bucket
)
)
if create_bucket:
kwargs = {}
if bucket_region:
kwargs = {
"CreateBucketConfiguration": {
"LocationConstraint": bucket_region
}
}
s3.create_bucket(Bucket=bucket, **kwargs)
info = "Created bucket: {}".format(bucket)
if bucket_region:
info += "in region: {}".format(bucket_region)
log(info)
# Buckets created - now create the user, if needed
if not username:
# Default username is "s3.read-write.bucket1,bucket2"
username = "s3.{permission}.{buckets}".format(
permission=permission, buckets=",".join(buckets)
)
if not user_exists(iam, username):
kwargs = {"UserName": username}
if user_permissions_boundary != "none":
# This is a user-account level limitation, it does not grant
# permissions on its own but is a useful extra level of defense
# https://github.com/simonw/s3-credentials/issues/1#issuecomment-958201717
if not user_permissions_boundary:
# Pick one based on --read-only/--write-only
if read_only:
user_permissions_boundary = (
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
)
else:
# Need full access in order to be able to write
user_permissions_boundary = (
"arn:aws:iam::aws:policy/AmazonS3FullAccess"
)
kwargs["PermissionsBoundary"] = user_permissions_boundary
iam.create_user(**kwargs)
info = "Created user: {}".format(username)
if user_permissions_boundary != "none":
info += " with permissions boundary: {}".format(user_permissions_boundary)
log(info)
# Add inline policies to the user so they can access the buckets
for bucket in buckets:
policy_name = "s3.{permission}.{bucket}".format(
permission=permission,
bucket=bucket,
)
policy = {}
if permission == "read-write":
policy = policies.read_write(bucket)
elif permission == "read-only":
policy = policies.read_only(bucket)
elif permission == "write-only":
policy = policies.write_only(bucket)
else:
assert False, "Unknown permission: {}".format(permission)
iam.put_user_policy(
PolicyDocument=json.dumps(policy),
PolicyName=policy_name,
UserName=username,
)
log("Attached policy {} to user {}".format(policy_name, username))
# Retrieve and print out the credentials
response = iam.create_access_key(
UserName=username,
)
log("Created access key for user: {}".format(username))
click.echo(json.dumps(response["AccessKey"], indent=4, default=str))
@cli.command()
def whoami():
"Identify currently authenticated user"

Wyświetl plik

@ -2,6 +2,7 @@ from click.testing import CliRunner
from s3_credentials.cli import cli
import json
import pytest
from unittest.mock import Mock
def test_whoami(mocker):
@ -36,3 +37,31 @@ def test_list_users(mocker, option, expected):
result = runner.invoke(cli, ["list-users"] + ([option] if option else []))
assert result.exit_code == 0
assert result.output == expected
def test_create(mocker):
boto3 = mocker.patch("boto3.client")
boto3.return_value = Mock()
boto3.return_value.create_access_key.return_value = {
"AccessKey": {
"AccessKeyId": "access",
"SecretAccessKey": "secret",
}
}
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli, ["create", "pytest-bucket-simonw-1", "-c"])
assert result.exit_code == 0
assert result.output == (
"Attached policy s3.read-write.pytest-bucket-simonw-1 to user s3.read-write.pytest-bucket-simonw-1\n"
"Created access key for user: s3.read-write.pytest-bucket-simonw-1\n"
'{\n "AccessKeyId": "access",\n "SecretAccessKey": "secret"\n}\n'
)
assert [str(c) for c in boto3.mock_calls] == [
"call('s3')",
"call('iam')",
"call().head_bucket(Bucket='pytest-bucket-simonw-1')",
"call().get_user(UserName='s3.read-write.pytest-bucket-simonw-1')",
'call().put_user_policy(PolicyDocument=\'{"Version": "2012-10-17", "Statement": [{"Sid": "ListObjectsInBucket", "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Sid": "AllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}\', PolicyName=\'s3.read-write.pytest-bucket-simonw-1\', UserName=\'s3.read-write.pytest-bucket-simonw-1\')',
"call().create_access_key(UserName='s3.read-write.pytest-bucket-simonw-1')",
]