Repository: cloud-custodian/cloud-custodian

Issue:
S3 BucketKey set to True
**Is your feature request related to a problem? Please describe.**
Configuring your bucket to use an S3 Bucket Key with SSE-KMS for new objects to reduce the cost of sse-kms

**Describe the solution you'd like**
- name: s3-enable-default-encryption-kms
            resource: s3
            actions:
              - type: set-bucket-encryption
              # enabled: true <------ optional (true by default)
                crypto: aws:kms
                key: 1234abcd-12ab-34cd-56ef-1234567890ab
                **bucketkey: true**

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion:
This looks straight forward here, docs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html

Only caveat is that users need to be aware that the calling principal also needs kms:Decrypt permissions, but with the default key and SSE it's not an issue
i'm somewhat shocked this wasn't the service default here, most other services naturally enable this for their kms integration, espcially data plane heavy ones (sqs for an example). we should do this post-haste :-)

Resolving patch:
diff --git a/c7n/resources/s3.py b/c7n/resources/s3.py
--- a/c7n/resources/s3.py
+++ b/c7n/resources/s3.py
@@ -3146,9 +3146,14 @@ def resolve_keys(self, buckets):
         for r in regions:
             client = local_session(self.manager.session_factory).client('kms', region_name=r)
             try:
-                self.arns[r] = client.describe_key(
+                key_meta = client.describe_key(
                     KeyId=self.data.get('key')
-                ).get('KeyMetadata').get('Arn')
+                ).get('KeyMetadata', {})
+                self.arns[r] = {
+                    'Arn': key_meta.get('Arn'),
+                    'KeyManager': key_meta.get('KeyManager'),
+                    'Description': key_meta.get('Description')
+                }
             except ClientError as e:
                 self.log.error('Error resolving kms ARNs for set-bucket-encryption: %s key: %s' % (
                     e, self.data.get('key')))
@@ -3260,7 +3265,7 @@ def filter_bucket(self, b, sse):
             return True
         elif crypto == 'aws:kms' and algo == 'aws:kms':
             if key:
-                if rule.get('KMSMasterKeyID') == key:
+                if rule.get('KMSMasterKeyID') == key['Arn']:
                     return True
                 else:
                     return False
@@ -3273,9 +3278,19 @@ class SetBucketEncryption(KMSKeyResolverMixin, BucketActionBase):
     """Action enables default encryption on S3 buckets
 
     `enabled`: boolean Optional: Defaults to True
+
     `crypto`: aws:kms | AES256` Optional: Defaults to AES256
+
     `key`: arn, alias, or kms id key
 
+    `bucket-key`: boolean Optional:
+    Defaults to True.
+    Reduces amount of API traffic from Amazon S3 to KMS and can reduce KMS request
+    costsby up to 99 percent. Requires kms:Decrypt permissions for copy and upload
+    on the AWS KMS Key Policy.
+
+    Bucket Key Docs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html
+
     :example:
 
     .. code-block:: yaml
@@ -3288,6 +3303,7 @@ class SetBucketEncryption(KMSKeyResolverMixin, BucketActionBase):
                   # enabled: true <------ optional (true by default)
                     crypto: aws:kms
                     key: 1234abcd-12ab-34cd-56ef-1234567890ab
+                    bucket-key: true
 
               - name: s3-enable-default-encryption-kms-alias
                 resource: s3
@@ -3296,11 +3312,13 @@ class SetBucketEncryption(KMSKeyResolverMixin, BucketActionBase):
                   # enabled: true <------ optional (true by default)
                     crypto: aws:kms
                     key: alias/some/alias/key
+                    bucket-key: true
 
               - name: s3-enable-default-encryption-aes256
                 resource: s3
                 actions:
                   - type: set-bucket-encryption
+                  # bucket-key: true <--- optional (true by default for AWS SSE)
                   # crypto: AES256 <----- optional (AES256 by default)
                   # enabled: true <------ optional (true by default)
 
@@ -3318,7 +3336,8 @@ class SetBucketEncryption(KMSKeyResolverMixin, BucketActionBase):
             'type': {'enum': ['set-bucket-encryption']},
             'enabled': {'type': 'boolean'},
             'crypto': {'enum': ['aws:kms', 'AES256']},
-            'key': {'type': 'string'}
+            'key': {'type': 'string'},
+            'bucket-key': {'type': 'boolean'}
         },
         'dependencies': {
             'key': {
@@ -3345,21 +3364,36 @@ def process(self, buckets):
                                    futures[future]['Name'])
 
     def process_bucket(self, bucket):
+        default_key_desc = 'Default master key that protects my S3 objects when no other key is defined' # noqa
         s3 = bucket_client(local_session(self.manager.session_factory), bucket)
         if not self.data.get('enabled', True):
             s3.delete_bucket_encryption(Bucket=bucket['Name'])
             return
         algo = self.data.get('crypto', 'AES256')
-        config = {'Rules': [
-            {'ApplyServerSideEncryptionByDefault': {
-                'SSEAlgorithm': algo}}
-        ]}
+
+        # bucket key defaults to True for alias/aws/s3 and AES256 (Amazon SSE)
+        # and ignores False values for that crypto
+        bucket_key = self.data.get('bucket-key', True)
+        config = {
+            'Rules': [
+                {
+                    'ApplyServerSideEncryptionByDefault': {
+                        'SSEAlgorithm': algo,
+                    },
+                    'BucketKeyEnabled': bucket_key
+                }
+            ]
+        }
+
         if algo == 'aws:kms':
             key = self.get_key(bucket)
             if not key:
                 raise Exception('Valid KMS Key required but does not exist')
-            (config['Rules'][0]['ApplyServerSideEncryptionByDefault']
-                ['KMSMasterKeyID']) = key
+
+            config['Rules'][0]['ApplyServerSideEncryptionByDefault'] = {
+                'KMSMasterKeyID': key['Arn'],
+                'BucketKeyEnabled': bucket_key
+            }
         s3.put_bucket_encryption(
             Bucket=bucket['Name'],
             ServerSideEncryptionConfiguration=config


Test patch:
diff --git a/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_1.json b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_1.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 404,
+    "data": {
+        "Error": {
+            "Code": "ServerSideEncryptionConfigurationNotFoundError",
+            "Message": "The server side encryption configuration was not found",
+            "BucketName": "custodian-test-bucket-encryption-key"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_2.json b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.GetBucketEncryption_2.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "ServerSideEncryptionConfiguration": {
+            "Rules": [
+                {
+                    "ApplyServerSideEncryptionByDefault": {
+                        "SSEAlgorithm": "AES256"
+                    },
+                    "BucketKeyEnabled": true
+                }
+            ]
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.ListBuckets_1.json b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.ListBuckets_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.ListBuckets_1.json
@@ -0,0 +1,25 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "Buckets": [
+            {
+                "Name": "custodian-test-bucket-encryption-key",
+                "CreationDate": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 6,
+                    "day": 28,
+                    "hour": 17,
+                    "minute": 18,
+                    "second": 18,
+                    "microsecond": 0
+                }
+            }
+        ],
+        "Owner": {
+            "DisplayName": "aws-fossalytics",
+            "ID": "07fa29f4277968d4e6b54603e37c62999cb1f4b1a53c20c3be5252554fb9684e"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.PutBucketEncryption_1.json b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.PutBucketEncryption_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_s3_bucket_encryption_bucket_key/s3.PutBucketEncryption_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_s3.py b/tests/test_s3.py
--- a/tests/test_s3.py
+++ b/tests/test_s3.py
@@ -318,6 +318,39 @@ def test_s3_filter_bucket_encryption_disabled(self):
         resources = p.run()
         self.assertEqual(len(resources), 0)
 
+    def test_s3_bucket_encryption_bucket_key(self):
+        session_factory = self.replay_flight_data("test_s3_bucket_encryption_bucket_key")
+
+        bname = "custodian-test-bucket-encryption-key"
+
+        self.patch(s3.S3, "executor-factory", MainThreadExecutor)
+        self.patch(s3, "S3_AUGMENT_TABLE", [])
+        policy = self.load_policy(
+            {
+                "name": "test_s3_bucket_encryption_bucket_key",
+                "resource": "s3",
+                "filters": [
+                    {
+                        "Name": bname
+                    },
+                    {
+                        "type": "bucket-encryption",
+                        "state": False
+                    }
+                ],
+                "actions": [
+                    {
+                        "type": "set-bucket-encryption"
+                    }
+                ]
+            }, session_factory=session_factory
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        client = session_factory().client("s3")
+        resp = client.get_bucket_encryption(Bucket=bname)
+        self.assertTrue(resp['ServerSideEncryptionConfiguration']['Rules'][0]['BucketKeyEnabled'])
+
 
 class BucketInventory(BaseTest):