アクセス制限されたECR上のイメージに対して脆弱性スキャンを行い結果をAmazon SNSで通知する

この記事を書いたメンバー:

Daiki Handa

アクセス制限されたECR上のイメージに対して脆弱性スキャンを行い結果をAmazon SNSで通知する

目次

はじめに

こんにちは、半田(@handy)です。

AWSでECSやEKSといったコンテナサービスを利用されている方は、コンテナイメージのレジストリサービスであるECRも使われていることが多いのではないでしょうか。

本番環境でECRを利用する場合、リポジトリへのアクセス制限やイメージの脆弱性スキャンも併せて設定すると思いますが、アクセス制限されているECRに対しての脆弱性スキャン(ECR拡張スキャン)を行う際にちょっとした追加設定が必要でしたので、今回はそれを実装してみました。

また、ECRだけだと物足りなかったので、ECRの構築と脆弱性スキャン結果の通知の仕組みを1つのCloudformationで実装しています。


前提条件

以下の2つの手順が既に実行されていることを前提とします。

  • Inspectorが有効化されている(手順)
  • ECRの拡張スキャンが有効化されている(手順)


構成図

構成としては以下のようなイメージです。

コンテナイメージがECRにPushされたらInspectorが自動で脆弱性スキャンを実施して、スキャン完了を検知したらEventBridge+SNSで管理者に結果をメール通知するようになっています。



作成したCloudformation

実際に作成したCloudformationは以下になります。固定パラメータは特にないので、そのままコピー&ペーストして使っていただくことが可能です。

Service Catalogにも設定できるように実装していますが、SNSとEventBridgeを別のCloudformationに分けて複数のECRリポジトリに対して共通で利用するという設定でも良いと思います。(運用を考えると寧ろそちらのほうが良い)

 AWSTemplateFormatVersion: "2010-09-09"
Description: Create ECR Repository
Parameters:
  RepositoryName:
    Type: String
  EcrApiVpcEndpointId:
    Type: String
  EcrDkrVpcEndpointId:
    Type: String
  MyIP:
    Type: String
  EMail:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z]+'

Resources:
  SnsTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: !Sub EcrScanResult_${RepositoryName}
      TopicName: !Sub EcrScanResult_${RepositoryName}
      Subscription:
        - Endpoint: !Ref EMail
          Protocol: email
  SnsTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Version: '2008-10-17'
        Statement:
        - Sid: "default_statement_ID"
          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
        - Sid: !Sub AWSEvents_${EcrScanEventRule}
          Effect: Allow
          Principal:
            Service: events.amazonaws.com
          Action: sns:Publish
          Resource: !Ref SnsTopic
      Topics:
        - !Ref SnsTopic
  EcrScanEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ecrscan-${RepositoryName}-rule
      Description: "ECR Scan Result Notification Rules"
      EventPattern:
        {
          "source": [
            "aws.inspector2"
          ],
          "detail-type": [
            "Inspector2 Scan"
          ],
          "detail": {
            "repository-name": [
              !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${EcrRepository}"
            ]
          }
        }
      Targets:
        - Arn: !Ref SnsTopic
          Id: "ecr-scan-result"
          InputTransformer:
            InputPathsMap:
              account: $.account
              time: $.time
              region: $.region
              resources: $.resources
              scan_status: $.detail.scan-status
              repository_name: $.detail.repository-name
              finding_critical: $.detail.finding-severity-counts.CRITICAL
              finding_high: $.detail.finding-severity-counts.HIGH
              finding_medium: $.detail.finding-severity-counts.MEDIUM
              finding_total: $.detail.finding-severity-counts.TOTAL
              image_digest: $.detail.image-digest
              image_tags: $.detail.image-tags
            InputTemplate: |
              "コンテナイメージの脆弱性スキャンが行われました。"
              ""
              "スキャン時間:<time>"
              "スキャンステータス:<scan_status>"
              "AWSアカウント:<account>"
              "AWSリージョン:<region>"
              "リポジトリ名:<repository_name>"
              "イメージタグ:<image_tags>"
              "ダイジェスト:<image_digest>"
              ""
              "脆弱性結果"
              "CRITICAL:<finding_critical>"
              "HIGH:<finding_high>"
              "MEDIUM:<finding_medium>"
              "TOTAL:<finding_total>"
              ""
              "詳細は以下のURLから確認してください。"
              "https://<region>.console.aws.amazon.com/inspector/v2/home?region=<region>#/findings?by=repository"
  EcrRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref RepositoryName
      ImageTagMutability: MUTABLE
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Sid: Deny
            Effect: Deny
            Principal: "*"
            Action:
              - "ecr:*"
            Condition:
              StringNotEquals:
                aws:CalledVia:
                  - cloudformation.amazonaws.com
                aws:sourceVpce:
                  - !Ref EcrApiVpcEndpointId
                  - !Ref EcrDkrVpcEndpointId
              ArnNotEquals:
                aws:PrincipalArn:
                  - !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/inspector2.amazonaws.com/AWSServiceRoleForAmazonInspector2"
              NotIpAddress:
                aws:SourceIp: !Ref MyIP


Cloudformation解説

Parameters

パラメータでは、大きく分けて以下の4つ設定するようにしました。

  • リポジトリ名
  • VPCEndpointID
  • アクセスを許可するIPアドレス(サブネットマスク含む)
  • 脆弱性結果を通知するメールアドレス

IPアドレス部分は記事を書く関係で自身のIPアドレスを指定するようにしていますが、社内ネットワークのIPアドレス範囲やProxyを利用している環境であればProxyサーバのIPアドレスを指定する等も可能です。

本記事では書いてませんが、デフォルト値を設定して必要な場合のみ上書き指定するといった実装も可能です。

 Parameters:
  RepositoryName:
    Type: String
  EcrApiVpcEndpointId:
    Type: String
  EcrDkrVpcEndpointId:
    Type: String
  MyIP:
    Type: String
  EMail:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z]+'


Resources  - SnsTopic/SnsTopicPolicy

SNS部分の実装では単純にSNSトピックを作成しているだけなのですが、1つだけ特筆する点としてSNSトピックのアクセスポリシーに以下の許可設定を追加し、EventBridgeからのSNS Publishを許可させる必要があります。

コンソールからEventBridgeを作成した時は自動的にポリシーに追加されるため気づきにくいですが、Cloudformationから作成する際は忘れないように設定が必要です。

         - Sid: !Sub AWSEvents_${EcrScanEventRule}
          Effect: Allow
          Principal:
            Service: events.amazonaws.com
          Action: sns:Publish
          Resource: !Ref SnsTopic


Resources  - EcrScanEventRule

以下の設定でInspectorの脆弱性スキャンイベントをキャッチして、指定したターゲット(今回はSNSトピック)に流すことができます。

本記事ではリポジトリ名まで指定するようにしていますが、sourceとdetail-typeだけ指定することで特定のAWSアカウント内の全リポジトリのイベントをキャッチすることもできます。

       EventPattern:
        {
          "source": [
            "aws.inspector2"
          ],
          "detail-type": [
            "Inspector2 Scan"
          ],
          "detail": {
            "repository-name": [
              !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${EcrRepository}"
            ]
          }
        }


Targetの記述ではインプットされるイベント情報からパラメータを取得・変数にマッピングする設定と通知メールの本文に記載するテンプレートの設定を行っています。

Cloudformationの仕様の関係でInputTemplateの文字列は""で囲む必要があります。

       Targets:
        - Arn: !Ref SnsTopic
          Id: "ecr-scan-result"
          InputTransformer:
            InputPathsMap:
              account: $.account
              time: $.time
              region: $.region
              resources: $.resources
              scan_status: $.detail.scan-status
              repository_name: $.detail.repository-name
~省略~
            InputTemplate: |
              "コンテナイメージの脆弱性スキャンが行われました。"
              ""
              "スキャン時間:<time>"
              "スキャンステータス:<scan_status>"
              "AWSアカウント:<account>"
              "AWSリージョン:<region>"
              "リポジトリ名:<repository_name>"
              "イメージタグ:<image_tags>"
              "ダイジェスト:<image_digest>"


Resources  - EcrRepository

最後にECRリポジトリを作成している箇所ですが、ここで説明しておきたいのはCondition 内の以下の記載になります。

               ArnNotEquals:
                aws:PrincipalArn:
                  - !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/inspector2.amazonaws.com/AWSServiceRoleForAmazonInspector2"

この条件でInspectorのサービスリンクロールからのアクセスをDeny対象から除外しておかないとInspectorからECRリポジトリがアクセスができずに脆弱性スキャンが正常に行われません。

こちらのドキュメントにも記述がありますが、サービスリンクロールとはその名の通り「サービスにリンクされたロール」のことで、そのサービスのみ引き受けることができるIAMロールになります。

※過去に挙動を検証した時の記事はQiitaの方に投稿しています。


Cloudformation実行

それでは以下のコマンドで作成したCloudformationを実行してみます。

 $ aws cloudformation deploy --template-file ecr.yaml --stack-name ecr-testapp-stack --parameter-overrides RepositoryName="test-app" EcrApiVpcEndpointId="vpce-xxxxxxxxxxxxxx" EcrDkrVpcEndpointId="vpce-yyyyyyyyyyyyyyy" MyIP="XXX.XXX.XXX.XXX/32" EMail="tanaka.taro@xxx.com"


Cloudformationコンソールからリソースが構築されていることが確認できます。


SNSからConfirm subscriptionメールが届いているのでConfirmしておきます。


ECRリポジトリのリソースベースポリシーを確認すると想定通りDenyポリシーが設定されていることがわかります。


イメージPush&脆弱性結果確認

実際にイメージをPushして脆弱性スキャン結果がメールで通知されるところまでを確認してみます。ベースイメージはできるだけ脆弱性が検知されるものが良かったので、nginxの1.17.1にしました。

 $ cat Dockerfile
FROM nginx:1.17.1


ECRにPush可能な端末から以下のコマンドを実行してイメージをPushします。

 $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 12345678912.dkr.ecr.ap-northeast-1.amazonaws.com
 $ docker build -t test-app .
 $ docker tag test-app:latest 12345678912.dkr.ecr.ap-northeast-1.amazonaws.com/test-app:latest
 $ docker push 12345678912.dkr.ecr.ap-northeast-1.amazonaws.com/test-app:latest


リポジトリの脆弱性欄の「調査結果を表示」を押下するとInspectorで行われた脆弱性スキャンの結果が確認できます。


脆弱性結果はInspectorからも確認することができます。


SNSで設定したメールアドレスを確認すると脆弱性結果が本文に記載されたメールが届いていました。CloudformationのInputTemplateで設定した内容がそのまま記載されており、パラメータも想定通り設定されていました。

※Step Functions等を利用するとメール件名の変更や本文の""の削除ができるようですが今回は割愛します。


注意点

今回は以下のイベントパターンで脆弱性スキャン完了を検知して重要度レベルごとの件数を通知しましたが、こちらの記載によると重要度レベルが存在しない場合は件数パラメータが返されないようなので注意が必要です。

※例:CRITICALレベルの結果が含まれていない場合はCRITICALの件数は返されない

 {  "source": ["aws.inspector2"],  "detail-type": ["Inspector2 Scan"]


また、件数ではなくイメージスキャン結果自体を通知したいという場合は以下のイベントパターンを設定することで同じようにメール通知が可能です。

※試しに設定してみたところイメージスキャン結果分メールが届いたので実装時は別途検討が必要です

 {  "source": ["aws.inspector2"],  "detail-type": ["Inspector2 Finding"] }


おわりに

ECRでアクセス制限を行った時に何故かInspectorの脆弱性スキャンが行われず、色々調べた結果サービスリンクロールの除外設定が必要だったということがわかったので、今回はそれをお伝えしたくて書いてみました。

脆弱性結果通知は以前から実装してみたかったのでやってみましたが、EventBridge+SNS周りの実装で想定外に詰まったりしたので良い勉強になりました。特にEventBridgeはもっと使い込んでみたいと思う面白いサービスだったので、またタイミングがあれば記事にしてみたいと思います。

この記事がどなたかの参考になれば幸いです。



カテゴリー
タグ

この記事を書いたメンバー

SAPシステムや基幹システムのクラウド移行・構築・保守、
DXに関して
お気軽にご相談ください

03-6260-6240 (受付時間 平日9:30〜18:00)