aws-cloudformation / cloudformation-coverage-roadmap

The AWS CloudFormation Public Coverage Roadmap
https://aws.amazon.com/cloudformation/
Creative Commons Attribution Share Alike 4.0 International
1.11k stars 56 forks source link

AWS::S3::PublicAccessBlock Account Wide Setting #168

Open leozhad opened 5 years ago

leozhad commented 5 years ago

Instructions for CloudFormation Coverage New Issues Template

Quick Summary:

  1. Title -> AWS::Service::Resource-Attribute-Existing Attribute
  2. Scope of request -> AWS::S3::Bucket PublicAccessBlockConfiguration supports the setting at the bucket level today, but not the account level
  3. Expected behavior -> There should be a resource for turning on Public Access Block for a whole account in CloudFormation
  4. Category tag (optional) -> Storage
  5. Any additional context (optional)
pamu78 commented 4 years ago

any news on this?

eduardomourar commented 4 years ago

The private resource type Community::S3::PublicAccessBlock can be used in the meantime.

Installation instructions:

aws cloudformation register-type \
  --region us-east-1 \
  --type-name "Community::S3::PublicAccessBlock" \
  --schema-handler-package "s3://community-resource-provider-catalog/community-s3-publicaccessblock-0.1.0.zip" \
  --type RESOURCE \
  --execution-role-arn <ROLE_ARN_WITH_ENOUGH_PRIVILEGE>

Usage example:

AWSTemplateFormatVersion: 2010-09-09
Resources:
  S3AccountPublicAccessBlock:
    Type: 'Community::S3::PublicAccessBlock'
    Properties:
      BlockPublicAcls: true
      BlockPublicPolicy: false
      IgnorePublicAcls: true
      RestrictPublicBuckets: true
dlenski commented 3 years ago

This is a frustrating gap in CloudFormation.

As AWS's infrastructure-as-code tool, CloudFormation should be able to build all desired AWS infrastructure in a brand new AWS account using code, right?

But the inability to disable this account-wide feature via officially-supported CFN resource types means that CFN cannot do that when those resources include S3 buckets with public access. :stuck_out_tongue_closed_eyes:

The error message associated with violating this account-wide public access block is also quite unhelpful:

  "MyBucket": {
    "Type": "AWS::S3::Bucket"
  }
  "BucketPolicy": {
    "Type": "AWS::S3::BucketPolicy",
    "Properties": {
      "Bucket": {"Ref": "MyBucket"},
      "PolicyDocument": {
        "Statement":[
          {
            "Action":["s3:GetObject"],
            "Effect":"Allow",
            "Resource": { "Fn::Sub" : "${MyBucket.Arn}/*" },
            "Principal": "*"
          }
        ]
      }  
    }    
  }      

Attempting to create this stack results in the error API: s3:PutBucketPolicy Access Denied. Because of this error, I spent a bunch of time trying to figure out if there was some race condition in the order of creation of the bucket and the policy, or some scenario in which the owner of an S3 bucket can't put a policy to the bucket they just created… when in fact neither of those had anything to do with the problem.

georgealton commented 3 years ago

Just to add, this would be a great addition to use in StackSets applied to an Organization. All new and existing accounts could get BlockPublicAccess as a security baseline.

alextongme commented 2 years ago

any updates on this? ive found some workarounds but wondering if theres something in the works to make this a lot easier

stewartcampbell commented 2 months ago

This can be deployed as a stack set:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Security: S3 Public Access Block Configuration"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Lambda Function Configuration"
        Parameters:
          - LoggingLevel
      - Label:
          default: "S3 Public Access Block Configuration"
        Parameters:
          - BlockPublicAcls
          - BlockPublicPolicy
          - IgnorePublicAcls
          - RestrictPublicBuckets
    ParameterLabels:
      BlockPublicAcls:
        default: "Block Public ACLs"
      BlockPublicPolicy:
        default: "Block Public Policy"
      IgnorePublicAcls:
        default: "Ignore Public ACLs"
      LoggingLevel:
        default: "Logging Level"
      RestrictPublicBuckets:
        default: "Restrict Public Buckets"

Parameters:
  BlockPublicAcls:
    AllowedValues: ["True", "False"]
    Default: "True"
    Description: "S3 will block public access permissions applied to newly added buckets or objects, and prevent the creation of new public access ACLs for existing buckets and objects. This setting doesn't change any existing permissions that allow public access to S3 resources using ACLs."
    Type: String
  BlockPublicPolicy:
    AllowedValues: ["True", "False"]
    Default: "True"
    Description: "S3 will block new bucket and access point policies that grant public access to buckets and objects. This setting doesn't change any existing policies that allow public access to S3 resources."
    Type: String
  IgnorePublicAcls:
    AllowedValues: ["True", "False"]
    Default: "True"
    Description: "S3 will ignore all ACLs that grant public access to buckets and objects."
    Type: String
  LoggingLevel:
    AllowedValues: ["CRITICAL", "DEBUG", "ERROR", "INFO", "WARNING"]
    Default: "INFO"
    Description: "The logging level for the Lambda function."
    Type: String
  RestrictPublicBuckets:
    AllowedValues: ["True", "False"]
    Default: "True"
    Description: "S3 will ignore public and cross-account access for buckets or access points with policies that grant public access to buckets and objects."
    Type: String

Resources:
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Description: "Used by the S3 Public Access Block Lambda to update the account-level S3 public access block configuration."
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"
              - Effect: Allow
                Action:
                  - "s3:DeletePublicAccessBlock"
                  - "s3:PutAccountPublicAccessBlock"
                  - "sts:GetCallerIdentity"
                Resource: "*"
      RoleName: "s3-public-access-block-update"

  S3PublicAccessBlockLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      Architectures:
        - x86_64
      Code:
        ZipFile: |
          import os
          import logging
          import boto3
          from botocore.exceptions import ClientError
          import cfnresponse
          logger = logging.getLogger()
          logger.setLevel(os.environ['LOGGING_LEVEL'])
          s3control_client = boto3.client("s3control")
          sts_client = boto3.client('sts')
          account_id = sts_client.get_caller_identity()["Account"]
          def lambda_handler(event, context):
              logger.info(f"Received event: {event}")
              physical_id = event['LogicalResourceId']
              try:
                  if event['RequestType'] in ['Create', 'Update']:
                      set_public_access_block(event['ResourceProperties'])
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                  elif event['RequestType'] == 'Delete':
                      delete_public_access_block()
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
                  else:
                      logger.error(f"Invalid request type: {event['RequestType']}")
                      cfnresponse.send(event, context, cfnresponse.FAILED, {}, physical_id)
              except Exception as e:
                  logger.error(f"Error: {str(e)}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {}, physical_id)
          def set_public_access_block(properties):
              try:
                  s3control_client.put_public_access_block(
                      PublicAccessBlockConfiguration={
                          'BlockPublicAcls': properties['BlockPublicAcls'].lower() == 'true',
                          'BlockPublicPolicy': properties['BlockPublicPolicy'].lower() == 'true',
                          'IgnorePublicAcls': properties['IgnorePublicAcls'].lower() == 'true',
                          'RestrictPublicBuckets': properties['RestrictPublicBuckets'].lower() == 'true'
                      },
                      AccountId=account_id
                  )
                  logger.info("S3 Public Access Block configuration updated successfully")
              except ClientError as e:
                  logger.error(f"Error updating S3 Public Access Block configuration: {str(e)}")
                  raise
          def delete_public_access_block():
              try:
                  s3control_client.delete_public_access_block(AccountId=account_id)
                  logger.info("S3 Public Access Block configuration deleted successfully")
              except ClientError as e:
                  if e.response['Error']['Code'] == 'NoSuchPublicAccessBlockConfiguration':
                      logger.info("S3 Public Access Block configuration was already deleted or did not exist")
                  else:
                      logger.error(f"Error deleting S3 Public Access Block configuration: {str(e)}")
                      raise
      Description: "Updates and deletes the account-level S3 Public Access Block configuration."
      Environment:
        Variables:
          LOGGING_LEVEL: !Ref LoggingLevel
      FunctionName: "s3-public-access-block-update"
      Handler: index.lambda_handler
      LoggingConfig:
        LogGroup: !Ref S3PublicAccessBlockLambdaLogGroup
      MemorySize: 128
      PackageType: Zip
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: python3.12
      Timeout: 10

  S3PublicAccessBlockLambdaLogGroup:
    Type: "AWS::Logs::LogGroup"
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      RetentionInDays: 7

  S3PublicAccessBlock:
    Type: "Custom::S3PublicAccessBlock"
    Properties:
      BlockPublicAcls: !Ref BlockPublicAcls
      BlockPublicPolicy: !Ref BlockPublicPolicy
      IgnorePublicAcls: !Ref IgnorePublicAcls
      RestrictPublicBuckets: !Ref RestrictPublicBuckets
      ServiceToken: !GetAtt S3PublicAccessBlockLambda.Arn