AWSセキュリティを強化!CloudFormationテンプレートでまとめてデプロイ

AWS

2024.1.17

Topics

1. はじめに

AWSのセキュリティを強化したいと考えている方向けの内容となります。
CloudFormation StackSetsを使えば、AWSのセキュリティ設定を一括で簡単にデプロイできます。
このブログでは、AWS CloudFormation StackSetsを使用して、組織全体のセキュリティを一気に強化する方法を紹介します。
この方法は、AWSアカウント間で一貫性を保ちながらセキュリティ設定を効率良くデプロイするための手段です。

【AWS入門】AWS CloudFormationとは? 初心者のための使い方・構築手順

まずはじめに、この記事にて提供するCloudFormationテンプレートの全体像をお見せします。

このテンプレートでは以下のリソースを作成・設定します。

・AWS CloudTrailの設定
・S3バケットのデプロイとそのポリシー設定
・AWS Configの設定と配信チャンネル
・IAMロール(AWS Config用、Lambda実行用)
・KMSキーの作成
・Lambda関数とそのトリガー
・デフォルトVPCおよび関連リソースの削除
・EBSのデフォルト暗号化
・S3バケットのブロックパブリックアクセスの有効化

※詳細な情報やテンプレートは後述します。

2. AWS CloudFormation StackSetsについて

AWS CloudFormationは、AWSの各種サービスを組み合わせてインフラをコードとして管理するサービスです。これにより、ユーザーはインフラストラクチャをテキストファイル(テンプレート)として定義し、そのテンプレートを使用してリソースを自動的に作成し、設定することができます。この自動化されたプロセスにより、ユーザーは手動での設定ミスを減らし、一貫性と再現性を確保することができます。

一方、AWS StackSetsはCloudFormationをさらにスケールアップするためのサービスです。StackSetsを使用すると、複数のAWSアカウントやリージョンにわたってCloudFormationスタックを作成、更新、削除することができます。これにより、組織全体での設定の一貫性を保つと同時に、管理の一元化が可能となります。

つまり、CloudFormationとStackSetsを組み合わせることで、組織全体のセキュリティ設定を一元的に管理し、自動化することが可能になります。これにより、セキュリティの一貫性を確保しながら、管理の手間を大幅に削減することができます。

StackSets の概念

次のセクションでは、具体的なCloudFormationテンプレートとその詳細について説明します。

3. セキュリティ強化のためのテンプレート設計

AWS CloudFormationを使用して組織全体のセキュリティを強化するには、いくつかのAWSリソースと設定をテンプレートに組み込むことが有用です。以下に具体的な例を示します。

・IAM Roles/Policies:IAMポリシーを使用して、ユーザー、グループ、またはサービスにAWSリソースへの最小限のアクセスを許可することが重要です。たとえば、特定のS3バケットへのアクセスをIAMロールに割り当て、そのロールを特定のサービスやユーザーに適用するなどがあります。
・Security Groups:Security Groupを適切に設定することで、インバウンドとアウトバウンドのネットワークトラフィックを管理できます。特定のポートへのアクセスを制限したり、特定のIPアドレスのみからのアクセスを許可したりすることができます。
・VPCとSubnetの構成:デフォルトのVPCではなく、カスタムVPCを使用することで、ネットワークのセキュリティを強化できます。特にプライベートサブネットとパブリックサブネットの適切な分離は、セキュリティを向上させます。
・AWS Config:AWS Configを使用すると、AWSリソースの設定履歴を追跡し、設定の変更を監視できます。不適切な設定変更を検出するとアラートを発することも可能です。
・AWS CloudTrail:AWS CloudTrailを使用すると、AWSアカウントのアクティビティを監視し、ログを保存できます。これにより、セキュリティインシデントの調査時に役立つ情報を取得できます。
・AWS Secrets Manager:AWS Secrets Managerを使用すると、データベースや他のサービスの資格情報を安全に保存し、管理することができます。
・AWS WAF & Shield:AWS WAFを使用すると、Webアプリケーションのセキュリティを強化し、一般的なウェブ攻撃から保護することができます。また、AWS ShieldはDDoS攻撃から保護します。

AWS のセキュリティ、アイデンティティ、コンプライアンス

これらの設定とリソースは、AWS CloudFormationテンプレートに組み込むことができ、セキュリティのベストプラクティスを一貫して適用するのに役立ちます。ただし、これらの設定を適用する際は、組織の特定の要件と制約を考慮することが重要です。

肝心なテンプレートはこのような感じになっています。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  TrailName:
    Type: String
  BucketName:
    Type: String
  RoleName:
    Type: String
  FunctionName:
    Type: String

Resources:
  # CloudTrail設定
  SettingTrail:
    Type: 'AWS::CloudTrail::Trail'
    DeletionPolicy: Retain
    Properties:
      EnableLogFileValidation: true
      IncludeGlobalServiceEvents: true
      IsLogging: true
      IsMultiRegionTrail: true
      KMSKeyId: !Ref EncryptionKMSKey
      S3BucketName: !Ref SubjectBucket
      TrailName: !Ref TrailName

  # S3バケットデプロイ
  SubjectBucket:
    Type: 'AWS::S3::Bucket'
    DeletionPolicy: Retain
    Properties:
      BucketName: !Join ["-", [!Ref BucketName, !Ref "AWS::AccountId", !Ref "AWS::Region"]]
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              KMSMasterKeyID: !Ref EncryptionKMSKey
              SSEAlgorithm: aws:kms
      LifecycleConfiguration:
        Rules:
          - ExpirationInDays: 1095
            Status: Enabled

  # S3バケットポリシー
  S3BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    DeletionPolicy: Retain
    Properties:
      Bucket: !Ref SubjectBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AWSCloudTrailAclCheck"
            Effect: "Allow"
            Principal:
              Service: "cloudtrail.amazonaws.com"
            Action: "s3:GetBucketAcl"
            Resource: !Join ["", ["arn:aws:s3:::", !Ref BucketName, "-", !Ref "AWS::AccountId", "-", !Ref "AWS::Region"]]
          - Sid: "AWSCloudTrailWrite"
            Effect: "Allow"
            Principal:
              Service: "cloudtrail.amazonaws.com"
            Action: "s3:PutObject"
            Resource: !Join ["", ["arn:aws:s3:::", !Ref BucketName, "-", !Ref "AWS::AccountId", "-", !Ref "AWS::Region", "/AWSLogs/", {"Ref":"AWS::AccountId"}, "/*"]]
            Condition:
              StringEquals:
                "s3:x-amz-acl": "bucket-owner-full-control"

  # config設定
  SettingConfig:
    Type: 'AWS::Config::ConfigurationRecorder'
    DeletionPolicy: Retain
    Properties:
      RecordingGroup:
        AllSupported: true
        IncludeGlobalResourceTypes: true
      RoleARN: !GetAtt 'ConfigRole.Arn'

  # config配信チャンネル
  ConfigDeliveryChannel:
    Type: 'AWS::Config::DeliveryChannel'
    DeletionPolicy: Retain
    Properties:
      S3BucketName: !Ref SubjectBucket
      ConfigSnapshotDeliveryProperties: 
        DeliveryFrequency: Six_Hours

  # Config用ロール
  ConfigRole:
    Type: 'AWS::IAM::Role'
    DeletionPolicy: Retain
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: config.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ConfigPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'config:*'
                  - 's3:PutObject'
                  - 's3:GetBucketAcl'
                  - 's3:GetBucketLocation'
                  - 's3:ListBucket'
                Resource: '*'

  # kms
  EncryptionKMSKey:
    Type: 'AWS::KMS::Key'
    DeletionPolicy: Retain
    Properties:
      Description: My KMS Key for CloudTrail and Config
      KeyPolicy:
        Version: '2012-10-17'
        Id: key-default-1
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow CloudTrail to use key
            Effect: Allow
            Principal:
              Service: "cloudtrail.amazonaws.com"
            Action:
              - 'kms:GenerateDataKey*'
              - 'kms:Decrypt'
            Resource: '*'

  # Lambda実行用ロール
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ["-", [!Ref RoleName, !Ref "AWS::Region"]]
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: SecuritySettingLambdaPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "ec2:Describe*"
                  - "ec2:DetachInternetGateway"
                  - "ec2:DeleteInternetGateway"
                  - "ec2:DeleteVpcEndpoints"
                  - "ec2:DeleteRouteTable"
                  - "ec2:DeleteNetworkAcl"
                  - "ec2:DeleteVpcPeeringConnection"
                  - "ec2:DeleteSecurityGroup"
                  - "ec2:DeleteSubnet"
                  - "ec2:DeleteVpc"
                  - "ec2:TerminateInstances"
                  - "ec2:DeleteNetworkInterface"
                  - "ec2:EnableEbsEncryptionByDefault"
                Resource: "*"

              - Effect: Allow
                Action: 
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"

              - Effect: Allow
                Action: "s3:PutBucketPublicAccessBlock"
                Resource: !Join ["", ["arn:aws:s3:::", !Ref BucketName, "-", !Ref "AWS::AccountId", "-", !Ref "AWS::Region"]]

  # Lambda関数
  LambdaFunction:
    Type: AWS::Lambda::Function
    DependsOn: SubjectBucket
    Properties:
      FunctionName: !Ref FunctionName
      Description: Lambda for Security Settings
      Role: !GetAtt "LambdaExecutionRole.Arn"
      Runtime: "python3.9"
      Handler: index.lambda_handler
      Timeout: 120
      Environment:
        Variables:
          BucketName: !Join ["-", [!Ref BucketName, !Ref "AWS::AccountId", !Ref "AWS::Region"]]
      Code:
        ZipFile: |
          import boto3
          import json
          import logging
          import os
          import cfnresponse

          BUCKET_NAME = os.environ['BucketName']
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def delete_default_vpc(default_vpc):
              # vpcに関連付けられているすべてのインターネットゲートウェイを解除して削除する
              for gw in default_vpc.internet_gateways.all():
                  default_vpc.detach_internet_gateway(InternetGatewayId=gw.id)
                  gw.delete()
              # すべてのルートテーブルの関連付けを削除する
              for rt in default_vpc.route_tables.all():
                  for rta in rt.associations:
                      if not rta.main:
                          rta.delete()
              # サブネットを削除する
              for subnet in default_vpc.subnets.all():
                  subnet.delete()
              # 最後にvpcを削除する
              try:
                  default_vpc.delete()
                  logger.info('デフォルトVPCを削除しました')
              except Exception as e:
                  logger.error('デフォルトVPCを削除できませんでした: %s', str(e))

          def lambda_handler(event, context):
              if event['RequestType'] == 'Delete':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                  return

              ec2_client = boto3.client('ec2')
              s3_client = boto3.client('s3')
              region = os.environ['AWS_REGION']
              ec2 = boto3.resource('ec2', region_name=region)
              ec2_client_region = boto3.client('ec2', region_name=region)

              try:    
                  # デフォルトのEBS暗号化を有効にする
                  ec2_client_region.enable_ebs_encryption_by_default()
                  logger.info("リージョンにてEBSのデフォルト暗号化を有効にしました %s", region)
              except Exception as e:
                  logger.error("リージョンにてEBSのデフォルト暗号化を有効にするときにエラーが発生しました %s: %s", region, str(e))
              try:
                  # デフォルトVPCの削除
                  response = ec2_client_region.describe_vpcs(Filters=[{'Name': 'isDefault', 'Values': ['true']}])
                  vpcs = response.get('Vpcs', [])
                  if vpcs:
                      default_vpc_id = vpcs[0].get('VpcId', '')
                      if default_vpc_id:
                          default_vpc = ec2.Vpc(default_vpc_id)
                          logger.info("%s 内のデフォルトVPC( %s )を削除中です...", default_vpc_id, region)
                          delete_default_vpc(default_vpc)
                          logger.info("%s 内のデフォルトVPCを削除しました", region)
                      else:
                          logger.info("リージョン %s のデフォルトVPCはVpcIdが見つかりません", region)
                  else:
                      logger.info("リージョン %s にデフォルトVPCは存在しません", region)
              except Exception as e:
                  logger.error("リージョン %s でデフォルトVPCの削除中にエラーが発生しました: %s", region, str(e))
                  raise e
              try:
                  # アカウントのパブリックアクセスのブロック設定を有効にする
                  s3_client.put_public_access_block(
                      Bucket=BUCKET_NAME,
                      PublicAccessBlockConfiguration={
                          'BlockPublicAcls': True,
                          'IgnorePublicAcls': True,
                          'BlockPublicPolicy': True,
                          'RestrictPublicBuckets': True
                      }
                  )
                  logger.info("S3パブリックアクセスのブロック設定を有効にしました")
              except Exception as e:
                  logger.error("S3パブリックアクセスのブロック設定を有効にするときにエラーが発生しました: %s", str(e))
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
                  return
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

  # Lambdaトリガー
  CustomResource:
    Type: Custom::LambdaTrigger
    DependsOn: LambdaFunction
    Properties:
      ServiceToken: !GetAtt LambdaFunction.Arn
      dummyProperty: dummy

 

以下今回のテンプレートで作成・設定するリソースの詳細です。

■AWS Lambda:特定のセキュリティ対策を実行するためのLambda関数を定義しています。
このLambda関数は以下の操作を行います。

■デフォルトVPCの削除
AWSアカウントを作成すると、各リージョンにデフォルトVPCが自動的に作成されます。このVPCは、プライベートとパブリックの両方のサブネットを持ち、リソースが間違って公開される可能性があります。デフォルトVPCを削除すると、AWSリソースをデプロイするために明示的にVPCを作成する必要があり、それによりセキュリティと可視性が向上します。

■デフォルトセキュリティグループの設定
デフォルトのセキュリティグループは、全てのルールを削除し、使用しないように設定します。これは、デフォルトのセキュリティグループがパラメータを省略した際に意図せずにアタッチされるなど、不用意な公開を引き起こす可能性があるためです。

■デフォルトルートテーブル
AWSのデフォルトルートテーブルは、特に設定がない場合にネットワークトラフィックがルーティングされる方法を決定します。デフォルトルートテーブルを変更することは、VPC内のネットワークトラフィックフローに重大な影響を及ぼし得ます。したがって、あらかじめ具体的なルートテーブルを設定し、ネットワークセキュリティとネットワークエンドポイントの制御を強化することが重要です。

■デフォルトインターネットゲートウェイ
VPCのデフォルトインターネットゲートウェイは、VPCとインターネット間の通信を許可します。これを削除または変更すると、VPC内のリソースとインターネットとの通信に影響を及ぼします。インターネットゲートウェイの設定は、インターネット上のリソースへのアクセスが必要な場合と、そうでない場合で異なります。

■暗号化
CloudFormationテンプレートは、EBSボリュームやS3バケットなどのリソースにデフォルトで暗号化を適用します。これにより、ストレージに格納されるデータの安全性が高まります。

■S3バケットのブロックパブリックアクセス設定
S3バケットについて、全ての公開アクセスをブロックする設定を適用します。これは、誤ってデータを公開するリスクを軽減するためです。

 

他設定リソース
以下はその他特定のセキュリティ対策を実行するための設定を定義しています。

S3バケット
CloudTrailのログやConfigの設定履歴などを保存するためのバケットを設定しています。このバケットはバケットポリシーによりCloudTrailからのログ保存を許可し、KMSキーで暗号化され、3年後にオブジェクトが削除されるライフサイクル設定が施されています。

AWS KMS
CloudTrailログとConfigのスナップショットを暗号化するためのKMSキーを作成しています。このキーはCloudTrailが使用でき、IAMユーザーが全ての操作を許可されています。

AWS CloudTrail
設定されたCloudTrailは、AWSアカウントのAPI呼び出し履歴を取得し、アクティビティ監視やリソースの変更履歴確認、セキュリティ分析、コンプライアンス監査などに利用します。このCloudTrailはログファイル検証を有効にし、全リージョン対応、全サービスイベントを含む設定になっています。

AWS Config
リソースの設定変更を監視し、評価するために使用します。全てのリソースタイプを対象とし、6時間ごとに設定スナップショットがS3バケットに保存されるよう設定しています。

CloudTrailのS3バケットへのログ配信
AWS CloudTrailは、AWSアカウントのAPIコール履歴を提供します。ログはS3バケットに保存され、監査、セキュリティ分析、コンプライアンスチェックなどに使用できます。この設定は、AWSリソースの操作についての透明性と可視性を提供します。

Configの配信チャンネル
AWS Configは、AWSリソースの設定変更を追跡し、その変更をS3バケットやSNSトピックに配信します。この配信チャンネルの設定により、リソースの設定変更に対する通知が可能になり、異常行動の早期発見やコンプライアンスチェックが容易になります。

IAM Role
ConfigとLambda関数が適切な権限を持つためのロールが定義されています。これらのロールは必要なアクションを行うために特定の権限を持っています。

これらの機能を適用することで、アカウント全体のセキュリティが向上し、リソースの操作と設定についての可視性と監視が強化されます。ただし、これらの設定はAWSアカウントの運用に影響を及ぼすため、その影響を理解し、必要に応じて設定を調整することが重要です。
また、このテンプレートに追加的なセキュリティ対策として、セキュリティグループやネットワークACLの構成、IAMポリシーの設定、GuardDutyやAWS Security Hubの有効化などを考慮できます。これらは組織全体のセキュリティをさらに強化するための措置となります。※それらのリソースはCloudFormationテンプレートでも実装ができ、今後順次追加更新していく予定です。

 

4. StackSetsを使用して実際にテンプレートをデプロイ

AWS CloudFormation StackSetsを使用すれば、AWSのアカウント全体、または特定のAWSオーガニゼーションに対してCloudFormationテンプレートをデプロイすることが可能です。
CloudFormation Stackでもデプロイ可能ですが、今回は複数リージョンに対して実施したい為にStackSetsを使用します。
以下に、具体的な手順を示します。

ステップ1【AWS Management Consoleにログイン】

AWS Management Consoleにサインインし、CloudFormationサービスを開きます。

ステップ2【StackSetsの作成】

CloudFormationダッシュボードにて、左側のナビゲーションパネルから”StackSets”を選択し、その後”StackSet の作成”をクリックします。

ステップ3【テンプレートのアップロード】

“StackSet の作成”画面で、”テンプレートファイルの準備完了”を選択し、その後”テンプレートファイルのアップロード”を選択します。それから、先ほど用意したCloudFormationテンプレートファイルをアップロードします。

ステップ4【StackSetの詳細設定】

次に”StackSet 名”に名前を入力し、テンプレートで定義した各パラメーターの値を設定します。※s3バケット名は小文字でなければいけません

ステップ5【設定オプションの指定】

“StackSet オプションの設定”画面にて、必要に応じてタグやパーミッションを設定します。※特になければデフォルトでOK

ステップ6【展開設定】

「新しいスタックのデプロイ」を選択します。これにより、新たにOUにアカウントが追加された際に自動的にスタックインスタンスがデプロイされます。
次に、「組織単位 (OU) へのデプロイ」を選択し、対象OUのOU IDを入力します。(OU ID例:ou-xxxx-xxxxxxxx)
※組織全体へデプロイする場合は「組織へのデプロイ」を、特定のOUのみにデプロイする場合は「組織単位 (OU) へのデプロイ」を選択します。この選択により、アカウントが追加または削除されるときに、それらが自動的にデプロイまたは取り下げられる対象範囲を設定できます。

「リージョン」のドロップダウンから、スタックセットをデプロイしたいAWSリージョンを選択します。一つ以上のリージョンを選択できます。
※ほかの設定値はデフォルトでOKです

ステップ7【レビューと作成】

最後に設定内容を確認し、確認チェックボックスにチェックを入れ、”送信”ボタンを押すことでStackSetの作成が開始されます。

ステップ8【OUにメンバーを追加】

AWS Organizationsのダッシュボードから、StackSets作成時に指定した既存のOUに新しいアカウントを追加します。新しいアカウントがOUに追加されると、それらのアカウントに対してStackSetで定義したテンプレートが自動的に適用され、リソースがデプロイされます。

※ステップ9【メンバーのOUからの離脱とリソースのロールバック】

ステップ8にて本セキュリティ設定が全てデプロイされている為、ステップ9は実行しなくても設定には影響がございません。
こちらを行うとメンバー(アカウント)がOUから離脱したときに、自動的にそのメンバーにデプロイされたリソースをロールバック(削除または更新)されます。ただし、リソースがロールバックされるかどうかは、そのリソースのDeletionPolicy設定によります。
今回紹介したテンプレートには、Lambda及びLambda用Role以外には、DeletionPolicy設定(Retain)をしているので、離脱したとしても設定した各種リソースはロールバックされることはありません。
メンバーがOUから離脱すると、そのメンバーにデプロイされているリソースについてCloudFormationテンプレートにDeletionPolicyに「Retain」が設定されているか否かにより、そのポリシーに従ってリソースがロールバックされます。※DeletionPolicyが 「Retain」 に設定されているリソースは、メンバーがOUから離脱した後もロールバックされずに保持されます。

DeletionPolicy 属性

5. まとめ

このブログを通じて、AWS CloudFormation StackSetsを用いて組織全体のセキュリティを向上させるための手法を紹介しました。皆さんの環境でも試されるきっかけになればと思います。

StackSetsを活用することで、手動で行っていたセキュリティ設定作業を一元管理・自動化することが可能になります。

ただし、本テンプレートはあくまでも一例であり、全ての組織にそのまま適用できるわけではありません。セキュリティグループやネットワークACLの設定、IAMポリシーの設定など、組織の状況に応じて追加的なセキュリティ対策を考慮する必要があります。これらを組み合わせることで、組織全体のセキュリティをさらに強化することが可能です。

他にも、強固なセキュリティを確保するためには、以下の追加的なセキュリティ対策を考慮することを推奨します。

  • セキュリティグループやネットワークACLの構成
  • IAMポリシーの設定
  • GuardDutyやAWS Security Hubの有効化

AWS Solutions Architectが オススメする “これだけは抑えておきたいセキュリティ”

これらは組織全体のセキュリティをさらに強化するための措置となります。再度の話となりますが、これらのリソースもテンプレートで実装可能です。
テンプレートの使い方や最適なセキュリティ対策については、今後も記事にしていこうと思いますので、是非続きの記事もご覧ください。

re

NHN テコラスのAWSリセールサービス「C-Chorus」の運用を担当しています。 Lambdaでいろいろ自動化されるのが好きです。 投稿する内容や意見は個人的な見解も含まれます

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら