Patch Managerベースラインの作成から定期レポート送信までをCloudFormationで作成してみた

2021.06.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

AWSでは、Systems Manager Patch Managerを使うことで、EC2インスタンスへのセキュリティパッチの管理や更新の自動化を行うことが可能です。
ただし、パッチマネージャーの導入を検討する際には、ベースラインをどうするか、パッチ承認をどうするか、インストールのタイミングは、など慎重に検討しなければいけない要素が多いと思います。

そこで、まずはすべてのパッチを対象としたベースラインでスキャンだけしておき、スキャンした結果を週次など定期的にレポートする程度のスモールスタートな方法から始めることにしました。
その上で定期的にレポートを確認し、判断のうえオンデマンドでパッチ適用やベースラインの見直しを行っていきます。

定期スキャン

定期スキャンは、パッチベースラインを作成し、パッチグループとの紐づけを行います。
専用のタグをEC2に付与することでインスタンスをパッチグループ配下で管理することができるようになります。

そしてメンテナンスウィンドウを使って、定期的なスキャンをスケジューリングします。

ベースライン適用はスキャンのみか、スキャン&インストールを選択できます。
今回はスキャンのみなので承認日数は0日にしています。
もしインストールまで行う場合はより承認日数を考えた運用設計が必要ですね。

パッチマネージャーのスキャン状況および結果は次のようにマネジメントコンソール上でレポートとして表示されます。

パッチマネージャー内のレポート内容をS3へエクスポートしつつSNSでの通知を行うことが可能です。
次項ではそのあたりを設定します。

定期レポート

レポート機能は以下の記事でも紹介されています。割と最近のアップデートです。

レポートはスケジュール実行が可能です。

実体としては、EventBridgeでSSM Automationを定期実行しています。

レポートはS3バケットにCSVファイルが出力されます。

Index,Instance ID,Instance name,Instance IP,Platform name,Platform version,SSM Agent version,Patch baseline,Patch group,Compliance status,Compliance severity,Noncompliant Critical severity patch count,Noncompliant High severity patch count,Noncompliant Medium severity patch count,Noncompliant Low severity patch count,Noncompliant Informational severity patch count,Noncompliant Unspecified severity patch count
0,i-002bd55d99254ce3f,,172.31.2.45,Microsoft Windows Server 2019 Datacenter,10.0.17763,3.0.529.0,pb-0bbd976a5e8553e9e,iwapatch,NON_COMPLIANT,UNSPECIFIED,0,0,0,0,0,4

通知にはAmazonSNSを使用しており、Eメールで以下の通知内容が送信されます。

Patch Summary was successfully exported as a CSV file. The file,iwarepo.csv, is located in the following Amazon S3 bucket: iwasa-report-123456789012-patch-report-bucket

CloudFormationテンプレート

定期スキャンと定期レポートでテンプレートを分けています。
SecurityHubなどで管理する場合は定期スキャンのみ使うなどを想定しています。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  PatchGroupName:
    Type: String
    Description: patch group name
  PatchScanSchedule:
    Type: String
    Default: "cron(0 0 3 ? * * *)"
    Description: "patch scan schedule cron expression (JST)"
  PatchTargetOperatingSystem:
    Type: String
    Default: WINDOWS
    AllowedValues: 
      - AMAZON_LINUX
      - AMAZON_LINUX_2
      - CENTOS
      - DEBIAN
      - MACOS
      - ORACLE_LINUX
      - REDHAT_ENTERPRISE_LINUX
      - SUSE
      - UBUNTU
      - WINDOWS
# Metadata:
Description: ""
Resources:
  SSMPatchBaseline:
    Type: "AWS::SSM::PatchBaseline"
    Properties:
      Name: !Sub ${AWS::StackName}-fullscan-patchbaseline
      OperatingSystem: !Ref PatchTargetOperatingSystem
      ApprovalRules:
        PatchRules:
          - ApproveAfterDays: 0
            PatchFilterGroup:
              PatchFilters:
                - Key: PATCH_SET
                  Values: 
                    - OS
                - Key: PRODUCT
                  Values:
                    - "*"
                - Key: CLASSIFICATION
                  Values:
                    - "*"
                - Key: MSRC_SEVERITY
                  Values:
                    - "*"
            ComplianceLevel: UNSPECIFIED
            EnableNonSecurity: false
      ApprovedPatchesComplianceLevel: "UNSPECIFIED"
      ApprovedPatchesEnableNonSecurity: false
      RejectedPatchesAction: "ALLOW_AS_DEPENDENCY"
      PatchGroups: 
        - !Ref PatchGroupName

  SSMMaintenanceWindow:
    Type: "AWS::SSM::MaintenanceWindow"
    Properties:
      Name: !Sub ${AWS::StackName}-maintenancewindow
      Schedule: !Ref PatchScanSchedule
      ScheduleTimezone: "Asia/Tokyo"
      Duration: 1
      Cutoff: 0
      AllowUnassociatedTargets: true

  SSMMaintenanceWindowTask:
    Type: "AWS::SSM::MaintenanceWindowTask"
    Properties:
      Name: !Sub ${AWS::StackName}-maintenancewindow-task
      WindowId: !Ref SSMMaintenanceWindow
      Targets: 
        - 
          Key: "WindowTargetIds"
          Values: 
            - !Ref SSMMaintenanceWindowTarget
      TaskArn: "AWS-RunPatchBaseline"
      ServiceRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ssm.amazonaws.com/AWSServiceRoleForAmazonSSM"
      TaskType: "RUN_COMMAND"
      TaskParameters: {}
      Priority: 1
      MaxConcurrency: "50"
      MaxErrors: "0"
      TaskInvocationParameters: 
        MaintenanceWindowRunCommandParameters: 
          Parameters: 
            Operation: 
              - "Scan"
            SnapshotId: 
              - "{{WINDOW_EXECUTION_ID}}"
          TimeoutSeconds: 600

  SSMMaintenanceWindowTarget:
    Type: "AWS::SSM::MaintenanceWindowTarget"
    Properties:
      Name: !Sub ${AWS::StackName}-patch-target
      WindowId: !Ref SSMMaintenanceWindow
      ResourceType: "INSTANCE"
      Targets: 
        - 
          Key: "tag:Patch Group"
          Values: 
            - !Ref PatchGroupName
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EMailAddress: 
    Type: String
    Description: Specifies your E-Mail for Patch Scan Report.
  ScheduleExpression:
    Type: String
    Default: "cron(10 18 ? * * *)"
    Description: "cron/rate expression by UTC"
  ReportName:
    Type: String
    Description: ""
Description: "The scheduling expression that determines when and how often the rule runs"
Resources:
  S3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-patch-report-bucket
      BucketEncryption: 
          ServerSideEncryptionConfiguration: 
            - 
              ServerSideEncryptionByDefault: 
                  SSEAlgorithm: "AES256"
              BucketKeyEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration: 
          Rules: 
            - 
              Id: "auto-delete"
              Status: "Enabled"
              ExpirationInDays: 400

  SNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      DisplayName: !Sub ${AWS::StackName}-patch-report-topic
      Subscription:
      - Endpoint: !Ref EMailAddress
        Protocol: email
      TopicName: !Sub ${AWS::StackName}-patch-report-topic

  SNSTopicPolicy:
    Type: "AWS::SNS::TopicPolicy"
    Properties:
      Topics: 
        - !Ref SNSTopic
      PolicyDocument:
        Version: 2012-10-17
        Statement: 
        - Effect: Allow
          Principal:
            AWS: "*"
          Action:
          - SNS:GetTopicAttributes
          - SNS:SetTopicAttributes
          - SNS:AddPermission
          - SNS:RemovePermission
          - SNS:DeleteTopic
          - SNS:Subscribe
          - SNS:ListSubscriptionsByTopic
          - SNS:Publish
          - SNS:Receive
          Resource: !Ref SNSTopic
          Condition:
            StringEquals: 
              AWS:SourceOwner: !Ref AWS::AccountId

  EventsRule:
    Type: "AWS::Events::Rule"
    Properties:
      Name: !Sub AWS-SystemsManager-PatchManager-PatchReport-${AWS::StackName}
      Description: "Schedule recurring patch reporting"
      ScheduleExpression: !Ref ScheduleExpression
      State: "ENABLED"
      EventBusName: "default"
      Targets: 
        - Id: !Sub ${AWS::StackName}-patch-report-target
          Arn: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/AWS-ExportPatchReportToS3:$DEFAULT"
          RoleArn: !GetAtt PatchAutomationRole.Arn
          Input: !Sub | 
            {
              "assumeRole": [
                "${PatchExportRole.Arn}"
              ],
              "reportName": [
                "${ReportName}"
              ],
              "s3BucketName": [
                "${S3Bucket}"
              ],
              "targets": [
                "instanceids=*"
              ],
              "snsTopicArn": [
                "${SNSTopic}"
              ]
            }

  PatchExportRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${AWS::StackName}-patch-summary-export-role
      AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ssm.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
      MaxSessionDuration: 3600
      ManagedPolicyArns: 
        - !Ref PatchExportPolicy
      Description: "Service role for lambda to execute csv export of patch reports"

  PatchAutomationRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${AWS::StackName}-patch-automation-role
      AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
      MaxSessionDuration: 3600
      ManagedPolicyArns: 
        - "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole"
        - !Ref PatchAutomationPolicy
      Description: "Service role for event bridge to call ssm automation on a schedule"

  PatchAutomationPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub ${AWS::StackName}-patch-automation-policy
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": "ssm:StartAutomationExecution",
              "Effect": "Allow",
              "Resource": [
                "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/AWS-ExportPatchReportToS3:$DEFAULT"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "iam:PassRole"
              ],
              "Resource": "${PatchExportRole.Arn}",
              "Condition": {
                "StringLikeIfExists": {
                  "iam:PassedToService": "ssm.amazonaws.com"
                }
              }
            }
          ]
        }

  PatchExportPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub ${AWS::StackName}-patch-summary-export-policy
      PolicyDocument: |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "s3:PutObject"
              ],
              "Resource": [
                "*"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "s3:GetBucketAcl"
              ],
              "Resource": [
                "*"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "sns:Publish"
              ],
              "Resource": [
                "*"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "ssm:DescribeInstancePatchStates",
                "ssm:DescribeInstancePatches",
                "ssm:ListComplianceItems",
                "ssm:ListResourceComplianceSummaries",
                "ssm:DescribeInstanceInformation",
                "ssm:GetInventory",
                "ec2:DescribeInstances"
              ],
              "Resource": [
                "*"
              ]
            }
          ]
        }

使い方

CloudFormationを実行します。
その後EC2インスタンスにタグを付与します。(キー:Patch Group)
TagEditorなど使うと複数インスタンスへの一括設定ができますので便利です。

注意点など

マネジメントコンソールからレポートのスケジュール機能を作成した場合、必要なロールが自動生成されますが、現時点ではどうやらポリシーが不足したロールが生成されるようでレポートのスケジュールを作成しただけだと定期レポートは動作しません。
よって今回はマネージドポリシーを追加しています。

さいごに

スモールスタートで運用しながらベースラインの見直しをすることを想定しているため、事前定義済みパッチは使いません。

これらの操作はすべてマネジメントコンソールから設定が可能ですが、スモールスタート用に流用しやすくしたかったのでCloudFormationテンプレートにしました。
テンプレートはGitHubリポジトリにも置いておきます。