[アップデート]AWS Control Towerのランディングゾーン バージョン3.3がリリース!よりセキュアになりました!

2023.12.16

あしざわです。

AWS Control Towerのランディングゾーン バージョン3.3がリリースされました!

AWS公式ドキュメントのDocument Historyにて更新が確認できました。

このブログでは、本アップデートの内容や旧バージョンとの差分について紹介します。

アップデートの4行まとめ

  • S3バケットのバケットポリシーおよびSNSトピックのアクセスポリシーのチューニング(aws:SourceOrgIDによる制限の追加)
  • リージョン拒否コントロールのチューニング(aws-marketplace:*の削除とquicksight:DescribeAccountSubscriptionの追加)
  • CloudTrail設定を担うCloudFormationテンプレートのリファクタリング(一部プロパティへの!Ref AWS::NoValueの適用)
  • 更新の対象は、Control Tower管理アカウントおよび監査アカウント(Auditアカウント, LogArchveアカウント)のみ

概要

ランディングゾーンとは?

AWSのベストプラクティスに基づいて構成したアカウントをスケーラブルに展開していくための仕組みの総称です。

AWS Control Tower(以下 Control Towerと略します)はランディングゾーンの仕組みを実装する1つの方法で、ランディングゾーンに必要な機能を簡単にセットアップできます。

参考: AWS再入門2020 AWS Control Tower編 | DevelopersIO

今回のアップデートについて

今回新しくリリースされたランディングゾーン3.3でのアップデート内容は、以下の4つです。

  • S3バケット(aws-controltower-logs-xxxxxx)のバケットポリシーの更新
  • SNSトピック(aws-controltower-AllConfigNotifications)のアクセスポリシーのアップデート
  • リージョン拒否コントロールのアップデート
  • CloudFormationスタック(AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER)のアップデート

アップデート内容の詳細については 公式ドキュメントのRelease notesにて確認できます。原文を確認したい方はこちらからお願いします。

January 2023 - Present - AWS Control Tower

本日時点では、日本語のドキュメントが更新されていないため、英語の原文を日本語に機械翻訳したものがこちらです。

AWS Control Tower landing zone version 3.3

2023年12月14日

(AWS Control Tower landingのバージョン3.3へのアップデートが必要です。詳細については、ランディングゾーンの更新を参照してください)。

AWS Control Tower AuditアカウントのS3バケットポリシーの更新

AWS Control Tower がアカウントにデプロイする Amazon S3 Audit バケットポリシーを変更し、書き込み許可には aws:SourceOrgID 条件を満たす必要があります。このリリースでは、AWSサービスは、リクエストが組織または組織単位(OU)から発信された場合にのみ、リソースにアクセスできます。

aws:SourceOrgID条件キーを使用し、S3バケットポリシーのcondition要素で組織IDに値を設定することができます。この条件により、CloudTrailは組織内のアカウントに代わってログのみをS3バケットに書き込むことができます。

既存のワークロードの機能に影響を与えることなく、潜在的なセキュリティの脆弱性を修正するためにこの変更を行いました。更新されたポリシーを表示するには、監査アカウントのAmazon S3バケットポリシーを参照してください。

新しい条件キーの詳細については、IAMドキュメントと "Use scalable controls for AWS services accessing your resources "と題されたIAMブログポストを参照してください。

AWS Config SNSトピックのポリシーの更新

AWS Config SNSトピックのポリシーに、新しいaws:SourceOrgID条件キーを追加しました。更新されたポリシーは、AWS Config SNSトピックのポリシーを参照してください。

ランディングゾーンRegion Denyコントロールの更新

  • discovery-marketplace:を削除。このアクションは、aws-marketplace:*免除によってカバーされます。
  • quicksight:DescribeAccountSubscriptionを追加しました。

AWS CloudFormationテンプレートの更新

BASELINE-CLOUDTRAIL-MASTER というスタック用の AWS CloudFormation テンプレートを更新し、AWS KMS 暗号化が使用されていない場合にドリフトが表示されないようにしました。

それぞれどのように変わったのか 以降の検証にて確認してみましょう。

ランディングゾーンを更新してみた(3.2 → 3.3)

まずはランディングゾーンを更新してみましょう。

検証環境のControl Tower ランディングゾーンの設定は以下です。

  • バージョン: 3.2
  • 管理対象リージョン: 東京、バージニア北部
  • その他: リージョン拒否設定を有効化済み

AWS Control Towerのマネジメントコンソールから、ランディングゾーン設定 → バージョンと遷移し、バージョン番号: 3.3にチェックをいれ、更新をクリックしましょう。

そのまま画面に沿って進めてください。ステップ1、ステップ2の要素は今回のアップデートに関係ないため、そのままでOKです。

ステップ3の画面下部に以下の記述がありました。内容を確認して、ランディングゾーンの更新をクリックしてください。

(3.3) aws:SourceOrgID AWS IAM グローバル条件キーをサポートするために、SNS と S3 のリソースベースのポリシーが更新されました。リージョン拒否コントロールの更新が含まれています。

ランディングゾーンの更新に少し時間がかかるため、しばらく(30分〜50分程度)待ちます。

ランディングゾーンの更新が完了しました。

更新内容の確認

リリースノートから更新箇所がおおよそ特定できたため、事前に取得した更新前のポリシードキュメント等と比較しながら更新内容を確認します。

以下のフォーマットで紹介します。

  • 対象リソースについて
  • 更新内容
  • 更新のメリット
  • 具体的な更新箇所

1. S3バケット(aws-controltower-logs-xxxxxx)のバケットポリシーの更新

対象リソースについて

S3バケット(aws-controltower-logs-xxxxxx)は、Control Towerで管理するCloudTrailやConfigなどの監査・セキュリティログを保存する役割を持っており、Control Towerのログアーカイブ(LogArchive)アカウントに作成されます。

更新内容

AWS CloudTrailおよびAWS Configをサービスプリンシパル(送信元)とするs3:PutObjectのアクションを許可する項目で、aws:SourceOrgIDを制限するCondition句が追加されています。

更新のメリット

これまではCondition等で送信元の制限を厳密に行なっていなかったため、Control Towerのバケット名を知ることができれば、Organizations組織外のアカウントであってもCloudTrailやConfigのログをControl TowerのS3へ配信が権限上可能でした。

今回の更新によって、CloudTrailおよびConfigを送信元(Pirincipal)とするs3:PutObjectアクションがaws:SourceOrgIDで指定したOrgnaizationsの組織ID(例: o-1a2b3c4d5)が同じアカウントのみに制限されました。

つまり、悪意のあるユーザーによる組織IDが異なる外部の関係のないアカウントからのログ配信を抑止できます。

※S3バケットの名称がaws-controltower-logs-(AWS アカウントID)-(ホームリージョン)となっており、AWSアカウントIDさえわかればS3バケット名を推測できるため

具体的な更新箇所

バケットポリシーは以下のように変更されました(ハイライトの箇所が更新点です)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSSLRequestsOnly",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx",
                "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx/*"
            ],
            "Condition": {
                "Bool": {
                    "aws:SecureTransport": "false"
                }
            }
        },
        {
            "Sid": "AWSBucketPermissionsCheck",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "cloudtrail.amazonaws.com",
                    "config.amazonaws.com"
                ]
            },
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx"
        },
        {
            "Sid": "AWSConfigBucketExistenceCheck",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "cloudtrail.amazonaws.com",
                    "config.amazonaws.com"
                ]
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx"
        },
        {
            "Sid": "AWSBucketDeliveryForConfig",
            "Effect": "Allow",
            "Principal": {
                "Service": "config.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx/o-1a2b3c4d5/AWSLogs/*/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceOrgID": "o-1a2b3c4d5"
                }
            }
        },
        {
            "Sid": "AWSBucketDeliveryForOrganizationTrail",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": [
                "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx/o-1a2b3c4d5/AWSLogs/123456789012/*",
                "arn:aws:s3:::aws-controltower-logs-123456789012-xxxxxx/o-1a2b3c4d5/AWSLogs/o-1a2b3c4d5/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:SourceOrgID": "o-1a2b3c4d5"
                }
            }
        }
    ]
}

2. SNSトピック(aws-controltower-AllConfigNotifications)のアクセスポリシーのアップデート

対象リソースについて

aws-controltower-AllConfigNotificationsは、Control Towerで管理されるAWS Configで検知されたコンプライアンス・非準拠・変更に関する通知を受信するSNSトピックです。このリソースはControl Towerの監査(Audit)アカウントに作成されます。

参考: 監査アカウントの SNS によるコンプライアンス通知 - AWS Control Tower

SNSトピックのアクセスポリシーは、S3のバケットポリシーやIAMポリシーなどと同じく、そのSNSトピックに対してアクションを実行できる権限の設定です。

更新内容

AWS CloudTrailおよびAWS Configをサービスプリンシパル(送信元)とするsns:Publishのアクションを許可する項目で、aws:SourceOrgIDを制限するCondition句が追加されています。

更新のメリット

1の更新内容と重複しますが、これまではSNSトピックに対するアクセスポリシーにOrganizationsの組織IDによる制限がなかったため、権限上異なるOrganizations組織のAWSアカウントからのsns:Publishが実行可能になっていました。

これまでの状態が是正され、許可されるのは同じ組織IDのアカウントからのアクションのみなので、よりセキュアになっています。

SNSトピックのARNも、rn:aws:sns:ap-northeast-1:123456789012:aws-controltower-AllConfigNotificationsという形式で、AWSアカウントIDがわかれば推測できるARNです。

具体的な更新箇所

アクセスポリシーは以下のように変更されました(ハイライトの箇所が更新点です)

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "AWSSNSPolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "config.amazonaws.com",
                    "cloudtrail.amazonaws.com"
                ]
            },
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:ap-northeast-1:123456789012:aws-controltower-AllConfigNotifications",
            "Condition": {
                "StringEquals": {
                    "aws:SourceOrgID": "o-1a2b3c4d5"
                }
            }
        }
    ]
}

3.リージョン拒否コントロールのアップデート

対象リソースについて

リージョン拒否コントロールはControl Towerの予防的統制を実装する予防コントロールの一種で、SCPで実装されています。

参考: [アップデート]AWS Control Towerで管理外リージョンを強制的に利用できなくするガードレールが追加されました #reinvent | DevelopersIO

設定するとControl Towerが管理対象としていないリージョン以外のAPIアクションを全て禁止できます。

通常のSCPとは異なりControl Towerがマネージドに管理しているためユーザー側で変更することができず、OU単位ではなくランディングゾーン全体にのみ影響します。

本件とは関係ないですが、直近のアップデートでOU単位でリージョン制限できるコントロールが追加されています。

参考: [アップデート] AWS Control Tower に新たに 65 個のコントロールが追加されて OU 単位でのリージョン制限もできるようになりました #AWSreInvent | DevelopersIO

更新内容

今回の更新によるSCPのポリシードキュメントの差分は以下です。

  • discovery-marketplace:の削除。このアクションは、aws-marketplace:*免除によってカバーされます。
  • quicksight:DescribeAccountSubscriptionの追加

更新のメリット

更新箇所の前者はポリシードキュメントの記述の無駄を省くもので内容自体はそこまで変わっていないようです。

後者はこれまで意図せず禁止できていなかったAPIアクションquicksight:DescribeAccountSubscriptionの追加です。非管理リージョンのQuickSightのサブスクリプション情報を確認できないようになっています。

最近追加されたOU単位のリージョン拒否コントロールのポリシードキュメントにはこれらの更新内容が反映されているようです。

参考: AWS Control Tower における OU に対してリージョン制限を実施するコントロールの例外設定を試してみた | DevelopersIO

具体的な更新箇所

リージョン拒否コントロール(SCP)のポリシードキュメントは以下のように変更されました(ハイライトの箇所が更新点です)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringNotEquals": {
                    "aws:RequestedRegion": [
                        "ap-northeast-1",
                        "us-east-1"
                    ]
                },
                "ArnNotLike": {
                    "aws:PrincipalARN": [
                        "arn:aws:iam::*:role/AWSControlTowerExecution"
                    ]
                }
            },
            "Resource": "*",
            "Effect": "Deny",
            "NotAction": [
                "a4b:*",
                "access-analyzer:*",
                "account:*",
                "acm:*",
                "activate:*",
                "artifact:*",
                "aws-marketplace-management:*",
                "aws-marketplace:*",
                "aws-portal:*",
                "billing:*",
                "billingconductor:*",
                "budgets:*",
                "ce:*",
                "chatbot:*",
                "chime:*",
                "cloudfront:*",
                "cloudtrail:LookupEvents",
                "compute-optimizer:*",
                "config:*",
                "consoleapp:*",
                "consolidatedbilling:*",
                "cur:*",
                "datapipeline:GetAccountLimits",
                "devicefarm:*",
                "directconnect:*",
                "ec2:DescribeRegions",
                "ec2:DescribeTransitGateways",
                "ec2:DescribeVpnGateways",
                "ecr-public:*",
                "fms:*",
                "freetier:*",
                "globalaccelerator:*",
                "health:*",
                "iam:*",
                "importexport:*",
                "invoicing:*",
                "iq:*",
                "kms:*",
                "license-manager:ListReceivedLicenses",
                "lightsail:Get*",
                "mobileanalytics:*",
                "networkmanager:*",
                "notifications-contacts:*",
                "notifications:*",
                "organizations:*",
                "payments:*",
                "pricing:*",
                "quicksight:DescribeAccountSubscription",
                "resource-explorer-2:*",
                "route53-recovery-cluster:*",
                "route53-recovery-control-config:*",
                "route53-recovery-readiness:*",
                "route53:*",
                "route53domains:*",
                "s3:CreateMultiRegionAccessPoint",
                "s3:DeleteMultiRegionAccessPoint",
                "s3:DescribeMultiRegionAccessPointOperation",
                "s3:GetAccountPublicAccessBlock",
                "s3:GetBucketLocation",
                "s3:GetBucketPolicyStatus",
                "s3:GetBucketPublicAccessBlock",
                "s3:GetMultiRegionAccessPoint",
                "s3:GetMultiRegionAccessPointPolicy",
                "s3:GetMultiRegionAccessPointPolicyStatus",
                "s3:GetStorageLensConfiguration",
                "s3:GetStorageLensDashboard",
                "s3:ListAllMyBuckets",
                "s3:ListMultiRegionAccessPoints",
                "s3:ListStorageLensConfigurations",
                "s3:PutAccountPublicAccessBlock",
                "s3:PutMultiRegionAccessPointPolicy",
                "savingsplans:*",
                "shield:*",
                "sso:*",
                "sts:*",
                "support:*",
                "supportapp:*",
                "supportplans:*",
                "sustainability:*",
                "tag:GetResources",
                "tax:*",
                "trustedadvisor:*",
                "vendor-insights:ListEntitledSecurityProfiles",
                "waf-regional:*",
                "waf:*",
                "wafv2:*"
            ],
            "Sid": "GRREGIONDENY"
        }
    ]
}

4. CloudFormationスタック(AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER)のアップデート

対象リソースについて

AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER は、Control Towerの管理アカウントに作成されるCloudFormationスタックです。

CLOUDTRAILとスタック名にある通り、Control TowerのCloudTrailに関する設定を行うリソースです。

更新内容

CloudFormationテンプレートに以下の変更が適用されました。

  • Resource > Trail > Properties > KMSKeyId''(ブランク)!Ref AWS::NoValueへ変更

わかりにくいのでCFnテンプレートを引用しますが、以下の''!Ref AWS::NoValueに変更されています。

KMSKeyId: !If
  - IsUsingKmsKey
        - !Ref KMSKeyArn
        - ''

更新のメリット

IsUsingKmsKeyは、Condition句で以下のように定義されています。

IsUsingKmsKey: !Not [!Equals [!Ref KMSKeyArn, 'NONE']]

つまり、KMSKeyArnのパラメータが'NONE'と等しくない(ブランクではない)場合にtrueを返します。

逆にKMSKeyArnのパラメータが'NONE'と等しい(ブランクである)場合にtrueを返します。

以下のように!Ref AWS::NoValueと!If関数を組み合わせると指定するIsUsingKmsKeyがFalseだったとき、同じレベルにあるプロパティがテンプレート上から動的に削除されます。

KMSKeyId: !If
  - IsUsingKmsKey
  - !Ref KMSKeyArn
  - !Ref AWS::NoValue

つまり、IsUsingKmsKeyがFalseだった時(= !Ref KMSKeyArn が空の時)にKMSKeyIdのプロパティ自体がなくなる、ということです。

従来のテンプレートだとKMSの暗号化を指定していない時にも、KMSKeyIdがブランクで指定されてしまいます。オプションの項目なので必須ではありませんが、従来の形式は無駄な記述なように見えます。

CloudTrailのPropertiesの他項目、CloudWatchLogsLogGroupArnCloudWatchLogsRoleArnでは以前から同じ記法が使われており、KMSKeyIdもそれに倣って変更されたようです。

具体的な更新箇所

細かい更新箇所は前述していますが、CloudFormationテンプレート全体から見る更新差分は以下です(ハイライトの箇所が更新点です)

AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS CloudTrail

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'

  EnableLogFileValidation:
    Type: String
    Default: 'true'
    Description: Indicates whether CloudTrail validates the integrity of log files.
    AllowedValues:
      - 'true'
      - 'false'

  IncludeGlobalEvents:
    Type: String
    Default: 'true'
    Description: Indicates whether the trail is publishing events from global services, such as IAM, to the log files.
    AllowedValues:
      - 'true'
      - 'false'

  MultiRegion:
    Type: String
    Default: 'true'
    Description: Indicates whether the CloudTrail trail is created in the region in which you create the stack (false) or in all regions (true).
    AllowedValues:
      - 'true'
      - 'false'

  OrganizationTrail:
    Type: String
    Default: 'true'
    Description: Indicates whether the Organization Trail is enabled (true) or disabled (false).
    AllowedValues:
      - 'true'
      - 'false'

  AllConfigTopicName:
    Type: String
    Default: ''
    Description: All Configuration Notification SNS Topic in Security Account that AWS Config delivers notifications to.

  SecurityAccountId:
    Type: 'String'
    Description: AWS Account Id of the Security account.

  AuditBucketName:
    Type: String
    Default: ''
    Description: Audit Bucket name from the Log Archive Account

  PublishToCloudWatchLogs:
    Type: String
    Default: 'true'
    Description: Indicates whether notifications are published to CloudWatch Logs.
    AllowedValues:
      - 'true'
      - 'false'

  LogsRetentionInDays:
    Description: 'Specifies the number of days you want to retain CloudTrail log events in the CloudWatch Logs.'
    Type: Number
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]

  AWSLogsS3KeyPrefix:
    Type: 'String'
    Description: 'Organization ID to use as the S3 Key prefix for storing the audit logs'

  KMSKeyArn:
    Type: 'String'
    Description: 'KMS key ARN for enabling SSE.'

  IsLogging:
    Type: 'String'
    Default: 'true'
    Description: Indicates whether or not Logs are being delivered by CloudTrail
    AllowedValues:
      - 'true'
      - 'false'

Conditions:
  IsMultiRegion: !Equals
    - !Ref MultiRegion
    - 'true'

  IsPublishToCloudWatchLogs: !Equals
    - !Ref PublishToCloudWatchLogs
    - 'true'

  IsUsingKmsKey: !Not [!Equals [!Ref KMSKeyArn, 'NONE']]

Resources:
  Trail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: !Sub ${ManagedResourcePrefix}-BaselineCloudTrail
      S3BucketName: !Ref AuditBucketName
      S3KeyPrefix: !Ref AWSLogsS3KeyPrefix
      SnsTopicName: !Sub arn:${AWS::Partition}:sns:${AWS::Region}:${SecurityAccountId}:${AllConfigTopicName}
      IsLogging: !Ref IsLogging
      EnableLogFileValidation: !Ref EnableLogFileValidation
      IncludeGlobalServiceEvents: !If
        - IsMultiRegion
        - True
        - !Ref IncludeGlobalEvents
      IsMultiRegionTrail: !Ref MultiRegion
      IsOrganizationTrail: !Ref OrganizationTrail
      KMSKeyId: !If
        - IsUsingKmsKey
        - !Ref KMSKeyArn
        - !Ref AWS::NoValue
      CloudWatchLogsLogGroupArn: !If
        - IsPublishToCloudWatchLogs
        - !GetAtt TrailLogGroup.Arn
        - !Ref AWS::NoValue
      CloudWatchLogsRoleArn: !If
        - IsPublishToCloudWatchLogs
        - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/service-role/AWSControlTowerCloudTrailRole
        - !Ref AWS::NoValue

  TrailLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Condition: IsPublishToCloudWatchLogs
    Properties:
      LogGroupName: !Sub ${ManagedResourcePrefix}/CloudTrailLogs
      RetentionInDays: !Ref LogsRetentionInDays

Outputs:
  BaselineCloudTrail:
    Description: Baseline CloudTrail
    Value: !GetAtt 'Trail.Arn'
  CloudWatchLogsGroupArn:
    Description: CloudWatch Log Group ARN for Baseline CloudTrail
    Value: !GetAtt 'TrailLogGroup.Arn'
  CloudWatchLogsGroupName:
    Description: CloudWatch Log Group Name for Baseline CloudTrail
    Value: !Ref TrailLogGroup

さいごに

今回はアップデートによって利用可能になったAWS Control Tower ランディングゾーンバージョン3.3のアップデート内容について詳しく確認しました。

バージョン3.3のアップデートについては、すべて監査アカウントや管理アカウントに関する設定チューニングやリファクタリングが主となっています。

特にaws:SourceOrgIDによるS3バケットポリシーの更新は、マルチアカウント運用をする上で必須クラスの制限なので、ぜひアップデートをご検討ください。

Control Towerのアップデートは大体半年に1回の頻度で来ています。

直近のアップデート内容に大きなものはそこまでありませんが、アップデートに置いていかれると良いタイミングで最新のアップデートの恩恵を受けられません。

アップデートの検討を継続的に実施し、可能な限り最新に近いバージョンでの運用を心がけるようにしましょう。

以上です。最後までお読みいただきありがとうございます。