目次
はじめに
こんにちは、半田(@handy)です。
AWSでECSやEKSといったコンテナサービスを利用されている方は、コンテナイメージのレジストリサービスであるECRも使われていることが多いのではないでしょうか。
本番環境でECRを利用する場合、リポジトリへのアクセス制限やイメージの脆弱性スキャンも併せて設定すると思いますが、アクセス制限されているECRに対しての脆弱性スキャン(ECR拡張スキャン)を行う際にちょっとした追加設定が必要でしたので、今回はそれを実装してみました。
また、ECRだけだと物足りなかったので、ECRの構築と脆弱性スキャン結果の通知の仕組みを1つのCloudformationで実装しています。
前提条件
以下の2つの手順が既に実行されていることを前提とします。
構成図
構成としては以下のようなイメージです。
コンテナイメージが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はもっと使い込んでみたいと思う面白いサービスだったので、またタイミングがあれば記事にしてみたいと思います。
この記事がどなたかの参考になれば幸いです。
- カテゴリー