[AWS][AppSec][Medium] Bucket logging not enabled in sample template (#2243)
Add S3 bucket logging not enabled in sample template
This commit is contained in:
@@ -37,7 +37,7 @@ env = core.Environment(account=ACCOUNT, region=REGION)
|
||||
|
||||
app = core.App()
|
||||
|
||||
core = AWSCore(
|
||||
core_construct = AWSCore(
|
||||
app,
|
||||
id_=f'{PROJECT_FEATURE_NAME}-Construct',
|
||||
project_name=PROJECT_NAME,
|
||||
@@ -46,20 +46,19 @@ core = AWSCore(
|
||||
)
|
||||
|
||||
# Below is the Core example stack which is provided for working with AWSCore ScriptCanvas examples.
|
||||
# It also provided as an example how to reference properties across stacks in the same CDK applications
|
||||
# Note: This will make the consuming stack a dependent stack on core
|
||||
# CDK will deploy the dependent stack first and then the core stack
|
||||
# It also provided as an example how to reference resources across stacks via stack outputs.
|
||||
# See https://docs.aws.amazon.com/cdk/latest/guide/resources.html#resource_stack
|
||||
core_properties = core.properties
|
||||
|
||||
example = ExampleResources(
|
||||
example_stack = ExampleResources(
|
||||
app,
|
||||
id_=f'{PROJECT_FEATURE_NAME}-Example-{env.region}',
|
||||
props_=core_properties,
|
||||
project_name=f'{PROJECT_NAME}',
|
||||
feature_name=FEATURE_NAME,
|
||||
tags={Constants.O3DE_PROJECT_TAG_NAME: PROJECT_NAME, Constants.O3DE_FEATURE_TAG_NAME: FEATURE_NAME},
|
||||
env=env
|
||||
)
|
||||
#
|
||||
# Add the common stack as a dependency of the feature stack
|
||||
example_stack.add_dependency(core_construct.common_stack)
|
||||
|
||||
app.synth()
|
||||
|
||||
@@ -42,3 +42,7 @@ class AWSCore(core.Construct):
|
||||
@property
|
||||
def properties(self):
|
||||
return self._feature_stack.properties
|
||||
|
||||
@property
|
||||
def common_stack(self):
|
||||
return self._feature_stack
|
||||
|
||||
@@ -8,11 +8,11 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
from aws_cdk import (
|
||||
core,
|
||||
aws_iam as iam,
|
||||
aws_s3 as s3,
|
||||
aws_resourcegroups as resource_groups,
|
||||
)
|
||||
|
||||
from constants import Constants
|
||||
from core_stack_properties import CoreStackProperties
|
||||
|
||||
|
||||
class CoreStack(core.Stack):
|
||||
@@ -60,6 +60,17 @@ class CoreStack(core.Stack):
|
||||
type='TAG_FILTERS_1_0')
|
||||
)
|
||||
|
||||
# Create an S3 bucket for Amazon S3 server access logging
|
||||
# See https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html
|
||||
self._server_access_logs_bucket = s3.Bucket(
|
||||
self,
|
||||
f'{self._project_name}-{self._feature_name}-Access-Log-Bucket',
|
||||
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
|
||||
encryption=s3.BucketEncryption.S3_MANAGED,
|
||||
access_control=s3.BucketAccessControl.LOG_DELIVERY_WRITE
|
||||
)
|
||||
self._server_access_logs_bucket.grant_read(self._admin_group)
|
||||
|
||||
# Define exports
|
||||
# Export resource group
|
||||
self._resource_group_output = core.CfnOutput(
|
||||
@@ -83,9 +94,10 @@ class CoreStack(core.Stack):
|
||||
export_name=f"{self._project_name}:AdminGroup",
|
||||
value=self._admin_group.group_arn)
|
||||
|
||||
@property
|
||||
def properties(self) -> CoreStackProperties:
|
||||
_props = CoreStackProperties()
|
||||
_props.user_group = self._user_group
|
||||
_props.admin_group = self._admin_group
|
||||
return _props
|
||||
# Export access log bucket name
|
||||
self._server_access_logs_bucket_output = core.CfnOutput(
|
||||
self,
|
||||
id=f'ServerAccessLogsBucketOutput',
|
||||
description='Name of the S3 bucket for storing server access logs generated by the sample CDK application(s)',
|
||||
export_name=f"{self._project_name}:ServerAccessLogsBucket",
|
||||
value=self._server_access_logs_bucket.bucket_name)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
Copyright (c) Contributors to the Open 3D Engine Project.
|
||||
For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
"""
|
||||
|
||||
from aws_cdk import (
|
||||
core,
|
||||
aws_iam as iam
|
||||
)
|
||||
|
||||
|
||||
class CoreStackProperties(core.StackProps):
|
||||
"""
|
||||
Support for cross stack references in the application.
|
||||
|
||||
Define any properties from the CoreStack other stacks in this application
|
||||
may need to consume.
|
||||
"""
|
||||
# Common IAM group for users
|
||||
user_group: iam.Group
|
||||
|
||||
# Common IAM group for Admin users
|
||||
admin_group: iam.Group
|
||||
@@ -8,13 +8,13 @@ import os
|
||||
|
||||
from aws_cdk import (
|
||||
aws_lambda as lambda_,
|
||||
aws_iam as iam,
|
||||
aws_s3 as s3,
|
||||
aws_s3_deployment as s3_deployment,
|
||||
aws_dynamodb as dynamo,
|
||||
core
|
||||
)
|
||||
|
||||
from core_stack_properties import CoreStackProperties
|
||||
from .auth import AuthPolicy
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ class ExampleResources(core.Stack):
|
||||
* A python 'echo' lambda
|
||||
* A small dynamodb table with the a primary 'id': str key
|
||||
"""
|
||||
def __init__(self, scope: core.Construct, id_: str, project_name: str, feature_name: str,
|
||||
props_: CoreStackProperties, **kwargs) -> None:
|
||||
def __init__(self, scope: core.Construct, id_: str, project_name: str, feature_name: str, **kwargs) -> None:
|
||||
super().__init__(scope, id_, **kwargs,
|
||||
description=f'Contains resources for the AWSCore examples as part of the '
|
||||
f'{project_name} project')
|
||||
@@ -42,17 +41,74 @@ class ExampleResources(core.Stack):
|
||||
self.__create_outputs()
|
||||
|
||||
# Finally grant cross stack references
|
||||
self.__grant_access(props=props_)
|
||||
self.__grant_access()
|
||||
|
||||
def __grant_access(self, props: CoreStackProperties):
|
||||
self._s3_bucket.grant_read(props.user_group)
|
||||
self._s3_bucket.grant_read(props.admin_group)
|
||||
def __grant_access(self):
|
||||
user_group = iam.Group.from_group_arn(
|
||||
self,
|
||||
f'{self._project_name}-{self._feature_name}-ImportedUserGroup',
|
||||
core.Fn.import_value(f'{self._project_name}:UserGroup')
|
||||
)
|
||||
admin_group = iam.Group.from_group_arn(
|
||||
self,
|
||||
f'{self._project_name}-{self._feature_name}-ImportedAdminGroup',
|
||||
core.Fn.import_value(f'{self._project_name}:AdminGroup')
|
||||
)
|
||||
|
||||
self._lambda.grant_invoke(props.user_group)
|
||||
self._lambda.grant_invoke(props.admin_group)
|
||||
# Provide the admin and user groups permissions to read the example S3 bucket.
|
||||
# Cannot use the grant_read method defined by the Bucket structure since the method tries to add to
|
||||
# the resource-based policy but the imported IAM groups (which are tokens from Fn.ImportValue) are
|
||||
# not valid principals in S3 bucket policies.
|
||||
# Check https://aws.amazon.com/premiumsupport/knowledge-center/s3-invalid-principal-in-policy-error/
|
||||
user_group.add_to_principal_policy(
|
||||
iam.PolicyStatement(
|
||||
actions=[
|
||||
"s3:GetBucket*",
|
||||
"s3:GetObject*",
|
||||
"s3:List*"
|
||||
],
|
||||
effect=iam.Effect.ALLOW,
|
||||
resources=[self._s3_bucket.bucket_arn, f'{self._s3_bucket.bucket_arn}/*']
|
||||
)
|
||||
)
|
||||
admin_group.add_to_principal_policy(
|
||||
iam.PolicyStatement(
|
||||
actions=[
|
||||
"s3:GetBucket*",
|
||||
"s3:GetObject*",
|
||||
"s3:List*"
|
||||
],
|
||||
effect=iam.Effect.ALLOW,
|
||||
resources=[self._s3_bucket.bucket_arn, f'{self._s3_bucket.bucket_arn}/*']
|
||||
)
|
||||
)
|
||||
|
||||
self._table.grant_read_data(props.user_group)
|
||||
self._table.grant_read_data(props.admin_group)
|
||||
# Provide the admin and user groups permissions to invoke the example Lambda function.
|
||||
# Cannot use the grant_invoke method defined by the Function structure since the method tries to add to
|
||||
# the resource-based policy but the imported IAM groups (which are tokens from Fn.ImportValue) are
|
||||
# not valid principals in Lambda function policies.
|
||||
user_group.add_to_principal_policy(
|
||||
iam.PolicyStatement(
|
||||
actions=[
|
||||
"lambda:InvokeFunction"
|
||||
],
|
||||
effect=iam.Effect.ALLOW,
|
||||
resources=[self._lambda.function_arn]
|
||||
)
|
||||
)
|
||||
admin_group.add_to_principal_policy(
|
||||
iam.PolicyStatement(
|
||||
actions=[
|
||||
"lambda:InvokeFunction"
|
||||
],
|
||||
effect=iam.Effect.ALLOW,
|
||||
resources=[self._lambda.function_arn]
|
||||
)
|
||||
)
|
||||
|
||||
# Provide the admin and user groups permissions to read from the DynamoDB table.
|
||||
self._table.grant_read_data(user_group)
|
||||
self._table.grant_read_data(admin_group)
|
||||
|
||||
def __create_s3_bucket(self) -> s3.Bucket:
|
||||
# Create a sample S3 bucket following S3 best practices
|
||||
@@ -60,11 +116,21 @@ class ExampleResources(core.Stack):
|
||||
# 1. Block all public access to the bucket
|
||||
# 2. Use SSE-S3 encryption. Explore encryption at rest options via
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html
|
||||
# 3. Enable Amazon S3 server access logging
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html
|
||||
server_access_logs_bucket = s3.Bucket.from_bucket_name(
|
||||
self,
|
||||
f'{self._project_name}-{self._feature_name}-ImportedAccessLogsBucket',
|
||||
core.Fn.import_value(f"{self._project_name}:ServerAccessLogsBucket")
|
||||
)
|
||||
|
||||
example_bucket = s3.Bucket(
|
||||
self,
|
||||
f'{self._project_name}-{self._feature_name}-Example-S3bucket',
|
||||
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
|
||||
encryption=s3.BucketEncryption.S3_MANAGED
|
||||
encryption=s3.BucketEncryption.S3_MANAGED,
|
||||
server_access_logs_bucket=server_access_logs_bucket,
|
||||
server_access_logs_prefix=f'{self._project_name}-{self._feature_name}-{self.region}-AccessLogs'
|
||||
)
|
||||
|
||||
s3_deployment.BucketDeployment(
|
||||
|
||||
@@ -28,14 +28,14 @@ class AWSMetrics(core.Construct):
|
||||
|
||||
# Check context variables to get enabled optional features
|
||||
optional_features = {
|
||||
'batch_processing': self.node.try_get_context("batch_processing") == 'true'
|
||||
'batch_processing': self.node.try_get_context("batch_processing") == 'true',
|
||||
'server_access_logs_bucket': self.node.try_get_context("server_access_logs_bucket")
|
||||
}
|
||||
|
||||
# Deploy AWS Metrics Stack
|
||||
self._feature_stack = AWSMetricsStack(
|
||||
scope,
|
||||
stack_name,
|
||||
stack_name=stack_name,
|
||||
application_name=application_name,
|
||||
description=f'Contains resources for the AWS Metrics Gem Feature stack as part of the {project_name} project',
|
||||
optional_features=optional_features,
|
||||
|
||||
@@ -43,9 +43,11 @@ class AWSMetricsStack(core.Stack):
|
||||
)
|
||||
|
||||
batch_processing_enabled = optional_features.get('batch_processing', False)
|
||||
server_access_logs_bucket = optional_features.get('server_access_logs_bucket')
|
||||
self._data_lake_integration = DataLakeIntegration(
|
||||
self,
|
||||
application_name=application_name
|
||||
application_name=application_name,
|
||||
server_access_logs_bucket=server_access_logs_bucket
|
||||
) if batch_processing_enabled else None
|
||||
|
||||
self._batch_processing = BatchProcessing(
|
||||
|
||||
@@ -19,9 +19,11 @@ class DataLakeIntegration:
|
||||
"""
|
||||
Create the AWS resources including the S3 bucket, Glue database, table and crawler for data lake integration
|
||||
"""
|
||||
def __init__(self, stack: core.Construct, application_name: str) -> None:
|
||||
def __init__(self, stack: core.Construct, application_name: str,
|
||||
server_access_logs_bucket: str = None) -> None:
|
||||
self._stack = stack
|
||||
self._application_name = application_name
|
||||
self._server_access_logs_bucket = server_access_logs_bucket
|
||||
|
||||
self._create_analytics_bucket()
|
||||
self._create_events_database()
|
||||
@@ -34,6 +36,14 @@ class DataLakeIntegration:
|
||||
The bucket uses server-side encryption with a CMK managed by S3:
|
||||
https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html
|
||||
"""
|
||||
# Enable server access logging if the server access logs bucket is provided following S3 best practices.
|
||||
# See https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html
|
||||
server_access_logs_bucket = s3.Bucket.from_bucket_name(
|
||||
self._stack,
|
||||
f'{self._stack.stack_name}-ImportedAccessLogsBucket',
|
||||
self._server_access_logs_bucket,
|
||||
) if self._server_access_logs_bucket else None
|
||||
|
||||
# Bucket name cannot contain uppercase characters
|
||||
# Do not specify the bucket name here since bucket name is required to be unique globally. If we set
|
||||
# a specific name here, only one customer can deploy the bucket successfully.
|
||||
@@ -46,7 +56,9 @@ class DataLakeIntegration:
|
||||
block_public_policy=True,
|
||||
ignore_public_acls=True,
|
||||
restrict_public_buckets=True
|
||||
)
|
||||
),
|
||||
server_access_logs_bucket=server_access_logs_bucket,
|
||||
server_access_logs_prefix=f'{self._stack.stack_name}-AccessLogs' if server_access_logs_bucket else None
|
||||
)
|
||||
|
||||
# For Amazon S3 buckets, you must delete all objects in the bucket for deletion to succeed.
|
||||
|
||||
Reference in New Issue
Block a user