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

updating module S3 Bucket Keys for SSE-KMS #882

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion plugins/modules/s3_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@
description: KMS master key ID to use for the default encryption. This parameter is allowed if I(encryption) is C(aws:kms). If
not specified then it will default to the AWS provided KMS key.
type: str
bucket_key_enabled:
description:
- Enable S3 Bucket Keys for SSE-KMS on new objects.
- See the AWS documentation for more information
U(https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html).
- Bucket Key encryption is only supported if I(encryption=aws:kms).
required: false
type: bool

tremble marked this conversation as resolved.
Show resolved Hide resolved
chirag1603 marked this conversation as resolved.
Show resolved Hide resolved
public_access:
description:
- Configure public access block for S3 bucket.
Expand Down Expand Up @@ -214,6 +223,11 @@
encryption: "aws:kms"
encryption_key_id: "arn:aws:kms:us-east-1:1234/5678example"

# Create a bucket with aws:kms encryption, Bucket key
- amazon.aws.s3_bucket:
name: mys3bucket
bucket_key_enabled: true
chirag1603 marked this conversation as resolved.
Show resolved Hide resolved

# Create a bucket with aws:kms encryption, default key
- amazon.aws.s3_bucket:
name: mys3bucket
Expand Down Expand Up @@ -284,6 +298,7 @@
type: dict
returned: I(state=present)
sample: {

chirag1603 marked this conversation as resolved.
Show resolved Hide resolved
"Statement": [
{
"Action": "s3:GetObject",
Expand Down Expand Up @@ -359,6 +374,7 @@ def create_or_update_bucket(s3_client, module, location):
versioning = module.params.get("versioning")
encryption = module.params.get("encryption")
encryption_key_id = module.params.get("encryption_key_id")
bucket_key_enabled = module.params.get("bucket_key_enabled")
public_access = module.params.get("public_access")
delete_public_access = module.params.get("delete_public_access")
delete_object_ownership = module.params.get("delete_object_ownership")
Expand Down Expand Up @@ -535,8 +551,17 @@ def create_or_update_bucket(s3_client, module, location):
current_encryption = put_bucket_encryption_with_retry(module, s3_client, name, expected_encryption)
changed = True

if bucket_key_enabled is not None:
current_encryption_algorithm = current_encryption.get('SSEAlgorithm') if current_encryption else None
if current_encryption_algorithm == 'aws:kms':
if get_bucket_key(s3_client, name) != bucket_key_enabled:
if bucket_key_enabled:
expected_encryption = True
else:
expected_encryption = False
current_encryption = put_bucket_key_with_retry(module, s3_client, name, expected_encryption)
changed = True
result['encryption'] = current_encryption

# Public access clock configuration
current_public_access = {}

Expand Down Expand Up @@ -701,6 +726,17 @@ def get_bucket_encryption(s3_client, bucket_name):
return None


@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
def get_bucket_key(s3_client, bucket_name):
try:
result = s3_client.get_bucket_encryption(Bucket=bucket_name)
return result.get('ServerSideEncryptionConfiguration', {}).get('Rules', [])[0].get('BucketKeyEnabled')
except is_boto3_error_code('ServerSideEncryptionConfigurationNotFoundError'):
return None
except (IndexError, KeyError):
return None


def put_bucket_encryption_with_retry(module, s3_client, name, expected_encryption):
max_retries = 3
for retries in range(1, max_retries + 1):
Expand All @@ -726,6 +762,38 @@ def put_bucket_encryption(s3_client, bucket_name, encryption):
s3_client.put_bucket_encryption(Bucket=bucket_name, ServerSideEncryptionConfiguration=server_side_encryption_configuration)


def put_bucket_key_with_retry(module, s3_client, name, expected_encryption):
max_retries = 3
for retries in range(1, max_retries + 1):
try:
tremble marked this conversation as resolved.
Show resolved Hide resolved
put_bucket_key(s3_client, name, expected_encryption)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Failed to set bucket Key")
result = s3_client.get_bucket_encryption(Bucket=name)
current_encryption = wait_bucket_key_is_applied(module, s3_client, name, expected_encryption,
should_fail=(retries == max_retries), retries=5)
if current_encryption == expected_encryption:
return result

# We shouldn't get here, the only time this should happen is if
# current_encryption != expected_encryption and retries == max_retries
# Which should use module.fail_json and fail out first.
module.fail_json(msg='Failed to set bucket key',
current=current_encryption, expected=expected_encryption, retries=retries)


@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
def put_bucket_key(s3_client, bucket_name, encryption):
# server_side_encryption_configuration ={'Rules': [{'BucketKeyEnabled': encryption}]}
encryption_status = s3_client.get_bucket_encryption(Bucket=bucket_name)
encryption_status['ServerSideEncryptionConfiguration']['Rules'][0]['BucketKeyEnabled'] = encryption
s3_client.put_bucket_encryption(
Bucket=bucket_name,
ServerSideEncryptionConfiguration=encryption_status[
'ServerSideEncryptionConfiguration']
)


@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted'])
def delete_bucket_tagging(s3_client, bucket_name):
s3_client.delete_bucket_tagging(Bucket=bucket_name)
Expand Down Expand Up @@ -835,6 +903,23 @@ def wait_encryption_is_applied(module, s3_client, bucket_name, expected_encrypti
return encryption


def wait_bucket_key_is_applied(module, s3_client, bucket_name, expected_encryption, should_fail=True, retries=12):
for dummy in range(0, retries):
try:
encryption = get_bucket_key(s3_client, bucket_name)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.fail_json_aws(e, msg="Failed to get updated encryption for bucket")
if encryption != expected_encryption:
time.sleep(5)
else:
return encryption

if should_fail:
module.fail_json(msg="Bucket Key failed to apply in the expected time",
requested_encryption=expected_encryption, live_encryption=encryption)
return encryption


def wait_versioning_is_applied(module, s3_client, bucket_name, required_versioning):
for dummy in range(0, 24):
try:
Expand Down Expand Up @@ -1009,6 +1094,7 @@ def main():
ceph=dict(default=False, type='bool'),
encryption=dict(choices=['none', 'AES256', 'aws:kms']),
encryption_key_id=dict(),
bucket_key_enabled=dict(type='bool'),
public_access=dict(type='dict', options=dict(
block_public_acls=dict(type='bool', default=False),
ignore_public_acls=dict(type='bool', default=False),
Expand Down Expand Up @@ -1070,6 +1156,7 @@ def main():
state = module.params.get("state")
encryption = module.params.get("encryption")
encryption_key_id = module.params.get("encryption_key_id")
bucket_key_enabled = module.params.get("bucket_key_enabled")
delete_object_ownership = module.params.get('delete_object_ownership')
object_ownership = module.params.get('object_ownership')

Expand Down
1 change: 1 addition & 0 deletions tests/integration/targets/s3_bucket/inventory
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ complex
dotted
tags
encryption_kms
encryption_bucket_key
encryption_sse
public_access
acl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
- module_defaults:
group/aws:
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_token: "{{ security_token | default(omit) }}"
region: "{{ aws_region }}"
block:
- name: Set facts for encryption_bucket_key test
set_fact:
local_bucket_name: "{{ bucket_name | hash('md5') }}-bucket-key"
# ============================================================

- name: "Create a simple bucket"
s3_bucket:
name: "{{ local_bucket_name }}"
state: present
register: output

- name: "Enable aws:kms encryption with KMS master key"
s3_bucket:
name: "{{ local_bucket_name }}"
state: present
encryption: "aws:kms"
register: output

- name: "Enable bucket key for bucket with aws:kms encryption"
s3_bucket:
name: "{{ local_bucket_name }}"
state: present
encryption: "aws:kms"
bucket_key_enabled: true
register: output

- name: Assert for 'Enable bucket key for bucket with aws:kms encryption'
assert:
that:
- output.changed
- output.encryption
Comment on lines +38 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the output:

2022-06-30 12:33:30.543586 | controller | TASK [s3_bucket : Enable bucket key for bucket with aws:kms encryption] ********
2022-06-30 12:33:30.548091 | controller | task path: /home/zuul/.ansible/collections/ansible_collections/amazon/aws/tests/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_bucket_key.yml:27
2022-06-30 12:33:30.548947 | controller | changed: [encryption_bucket_key] => {
2022-06-30 12:33:30.548970 | controller |     "changed": true,
2022-06-30 12:33:30.548979 | controller |     "encryption": {
2022-06-30 12:33:30.548986 | controller |         "ResponseMetadata": {
2022-06-30 12:33:30.548993 | controller |             "HTTPHeaders": {
2022-06-30 12:33:30.549000 | controller |                 "date": "Thu, 30 Jun 2022 12:33:30 GMT",
2022-06-30 12:33:30.549014 | controller |                 "server": "AmazonS3",
2022-06-30 12:33:30.549022 | controller |                 "transfer-encoding": "chunked",
2022-06-30 12:33:30.549029 | controller |                 "x-amz-id-2": "MrzpYivR6wKskVGQkANi9QR2P8r/VbcwDo6tTG7SHyrn6WVIQKwBwmFM/si5KXLmPrG1f+jV3sg=",
2022-06-30 12:33:30.549038 | controller |                 "x-amz-request-id": "DDXSP72CXZ3PW81Y"
2022-06-30 12:33:30.549045 | controller |             },
2022-06-30 12:33:30.549052 | controller |             "HTTPStatusCode": 200,
2022-06-30 12:33:30.549067 | controller |             "HostId": "MrzpYivR6wKskVGQkANi9QR2P8r/VbcwDo6tTG7SHyrn6WVIQKwBwmFM/si5KXLmPrG1f+jV3sg=",
2022-06-30 12:33:30.549075 | controller |             "RequestId": "DDXSP72CXZ3PW81Y",
2022-06-30 12:33:30.549082 | controller |             "RetryAttempts": 0
2022-06-30 12:33:30.549089 | controller |         },
2022-06-30 12:33:30.549097 | controller |         "ServerSideEncryptionConfiguration": {
2022-06-30 12:33:30.549104 | controller |             "Rules": [
2022-06-30 12:33:30.549111 | controller |                 {
2022-06-30 12:33:30.549119 | controller |                     "ApplyServerSideEncryptionByDefault": {
2022-06-30 12:33:30.549126 | controller |                         "SSEAlgorithm": "aws:kms"
2022-06-30 12:33:30.549133 | controller |                     },
2022-06-30 12:33:30.549140 | controller |                     "BucketKeyEnabled": true
2022-06-30 12:33:30.549148 | controller |                 }
2022-06-30 12:33:30.549155 | controller |             ]
2022-06-30 12:33:30.549162 | controller |         }
2022-06-30 12:33:30.549169 | controller |     },
2022-06-30 12:33:30.549177 | controller |     "invocation": {
2022-06-30 12:33:30.549184 | controller |         "module_args": {
2022-06-30 12:33:30.549190 | controller |             "acl": null,
2022-06-30 12:33:30.549197 | controller |             "aws_access_key": "ASIA6CCDWXDOLAKDTBC7",
2022-06-30 12:33:30.549204 | controller |             "aws_ca_bundle": null,
2022-06-30 12:33:30.549210 | controller |             "aws_config": null,
2022-06-30 12:33:30.549217 | controller |             "aws_secret_key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
2022-06-30 12:33:30.549223 | controller |             "bucket_key_enabled": true,
2022-06-30 12:33:30.549230 | controller |             "ceph": false,
2022-06-30 12:33:30.549237 | controller |             "debug_botocore_endpoint_logs": true,
2022-06-30 12:33:30.549243 | controller |             "delete_object_ownership": false,
2022-06-30 12:33:30.549250 | controller |             "delete_public_access": false,
2022-06-30 12:33:30.549256 | controller |             "ec2_url": null,
2022-06-30 12:33:30.549263 | controller |             "encryption": "aws:kms",
2022-06-30 12:33:30.549270 | controller |             "encryption_key_id": null,
2022-06-30 12:33:30.549277 | controller |             "force": false,
2022-06-30 12:33:30.549284 | controller |             "name": "103bcc7ff588ca72c92b997bf1fcf869-bucket-key",
2022-06-30 12:33:30.549291 | controller |             "object_ownership": null,
2022-06-30 12:33:30.549298 | controller |             "policy": null,
2022-06-30 12:33:30.549305 | controller |             "profile": null,
2022-06-30 12:33:30.549312 | controller |             "public_access": null,
2022-06-30 12:33:30.549319 | controller |             "purge_tags": true,
2022-06-30 12:33:30.549326 | controller |             "region": "us-east-1",
2022-06-30 12:33:30.549333 | controller |             "requester_pays": null,
2022-06-30 12:33:30.549340 | controller |             "s3_url": null,
2022-06-30 12:33:30.549347 | controller |             "security_token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
2022-06-30 12:33:30.549353 | controller |             "state": "present",
2022-06-30 12:33:30.549360 | controller |             "tags": null,
2022-06-30 12:33:30.549367 | controller |             "validate_bucket_name": true,
2022-06-30 12:33:30.549374 | controller |             "validate_certs": true,
2022-06-30 12:33:30.549381 | controller |             "versioning": null
2022-06-30 12:33:30.549387 | controller |         }
2022-06-30 12:33:30.549395 | controller |     },
2022-06-30 12:33:30.549402 | controller |     "name": "103bcc7ff588ca72c92b997bf1fcf869-bucket-key",
2022-06-30 12:33:30.549409 | controller |     "object_ownership": null,
2022-06-30 12:33:30.549416 | controller |     "policy": null,
2022-06-30 12:33:30.549423 | controller |     "requester_pays": null,
2022-06-30 12:33:30.549430 | controller |     "resource_actions": [
2022-06-30 12:33:30.549438 | controller |         "s3:PutBucketEncryption",
2022-06-30 12:33:30.549445 | controller |         "s3:GetBucketOwnershipControls",
2022-06-30 12:33:30.549452 | controller |         "s3:GetPublicAccessBlock",
2022-06-30 12:33:30.549463 | controller |         "s3:GetBucketVersioning",
2022-06-30 12:33:30.549471 | controller |         "s3:HeadBucket",
2022-06-30 12:33:30.549478 | controller |         "s3:GetBucketTagging",
2022-06-30 12:33:30.549485 | controller |         "s3:GetBucketEncryption",
2022-06-30 12:33:30.549492 | controller |         "s3:GetBucketRequestPayment",
2022-06-30 12:33:30.549500 | controller |         "s3:GetBucketPolicy"
2022-06-30 12:33:30.549507 | controller |     ],
2022-06-30 12:33:30.549514 | controller |     "tags": {},
2022-06-30 12:33:30.549521 | controller |     "versioning": {
2022-06-30 12:33:30.549529 | controller |         "MfaDelete": "Disabled",
2022-06-30 12:33:30.549536 | controller |         "Versioning": "Disabled"
2022-06-30 12:33:30.549543 | controller |     }
2022-06-30 12:33:30.549550 | controller | }

This is returning something incompatible with the "encryption" output from simply enabling SSE with KMS:

2022-06-30 12:33:29.348311 | controller |     "encryption": {
2022-06-30 12:33:29.348319 | controller |         "SSEAlgorithm": "aws:kms"
2022-06-30 12:33:29.348327 | controller |     },


- name: "Re-enable bucket key for bucket with aws:kms encryption (idempotent)"
s3_bucket:
name: "{{ local_bucket_name }}"
encryption: "aws:kms"
bucket_key_enabled: true
register: output

- name: Assert for 'Re-enable bucket key for bucket with aws:kms encryption (idempotent)''
assert:
that:
- not output.changed
- output.encryption

# ============================================================

- name: Disable encryption from bucket
s3_bucket:
name: "{{ local_bucket_name }}"
encryption: none
bucket_key_enabled: false
register: output

- name: Assert for 'Disable encryption from bucket'
assert:
that:
- output.changed
- not output.encryption

- name: Disable encryption from bucket (idempotent)
s3_bucket:
name: "{{ local_bucket_name }}"
bucket_key_enabled: true
register: output

- name: Assert for 'Disable encryption from bucket (idempotent)'
assert:
that:
- output is not changed
- not output.encryption

# ============================================================

- name: Delete encryption test s3 bucket
s3_bucket:
name: "{{ local_bucket_name }}"
state: absent
register: output

- name: Assert for 'Delete encryption test s3 bucket'
assert:
that:
- output.changed

# ============================================================
always:
- name: Ensure all buckets are deleted
s3_bucket:
name: "{{ local_bucket_name }}"
state: absent
failed_when: false