Amazon Machine Images (AMI)をTrivyでイメージスキャン、を自動で実行するシステム
はじめに
AWS re:Invent 2022の興奮の中、Trivyのアップデートも発表されました。
Amazon Machine Images (AMI)を対象にしたイメージスキャン!?うおおおお、我慢できねえ!早く試したいぜ。
ちなみに本リリース後の作者のTwitterをみるに、この時期に合わせるために大変だった様子が垣間見えます。
この努力に報いるためには私もハードコアにやらなくてはいけません。という訳で、アドベントカレンダー2週間前ですが、予定を変更して本記事を書くことにします。
おさらい - 機能把握
※再掲
このブログがすべてなのですが、機能を簡単に説明すると
- VMイメージやファイルシステムボリュームのスキャンを実施するための
trivy vm
コマンドが追加された。- ローカルだけではなく、AWSのAMI(VMイメージ)とEBS(FileSystem)のIDを指定してスキャンできる。
- AWSユーザ的に見たとき、AMIからEC2インスタンスを起動してEBSをマウントするのに比べ、以下の点が嬉しい。
- スキャン対象のEC2インスタンス、VPCに関連するアクセス制御の考慮が不要。
- 特定のIAM Actionで完結するのがメリット。これがデメリットになる環境もあるのかもしれませんが。
- スキャン用の環境が構築不要ということは、リソース起動時の費用やリソースのライフサイクルの考慮からも解放される。
- スキャン対象のEC2インスタンス、VPCに関連するアクセス制御の考慮が不要。
という感じです。CIに組み込むのも簡単そうですね。
動作確認
Blogのチュートリアルでも雰囲気はわかるとはいえ、感覚をつかむために一度動かしてみます。
※各種リソースは検証用に使い捨てているので、id等のマスクは割愛します。
参考:ドキュメント
公式のドキュメントはこちら。
環境
- AWS Cloud9 (AmazonLinux 2)
- Trivy(v0.35)
インストール
実は最初はDockerで試したのですが、IAM Roleの情報を上手く渡せずに権限不足でスキャンが失敗したので、Trivyをホストに入れ直した経緯があります。
sudo rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v0.35.0/trivy_0.35.0_Linux-64bit.rpm
trivy --version
Version: 0.35.0
Vulnerability DB:
Version: 2
UpdatedAt: 2022-12-03 06:08:29.768300931 +0000 UTC
NextUpdate: 2022-12-03 12:08:29.768300731 +0000 UTC
DownloadedAt: 2022-12-03 06:16:34.38559892 +0000 UTC
参考
検証
EBSのスナップショットにスキャンすると10分程度でスキャンできた。結構クリティカルが出ているが、AWS Credentialがあちこちに残っていたのが原因の様子。
trivy vm ebs:snap-0f8541144390c0edb
2022-12-03T06:16:32.183Z INFO Need to update DB
2022-12-03T06:16:32.183Z INFO DB Repository: ghcr.io/aquasecurity/trivy-db
2022-12-03T06:16:32.183Z INFO Downloading DB...
35.46 MiB / 35.46 MiB [------------------------------------------------------------------------------------------------] 100.00% 20.06 MiB p/s 2.0s
2022-12-03T06:16:34.396Z INFO Vulnerability scanning is enabled
2022-12-03T06:16:34.396Z INFO Secret scanning is enabled
2022-12-03T06:16:34.396Z INFO If your scanning is slow, please try '--security-checks vuln' to disable secret scanning
2022-12-03T06:16:34.396Z INFO Please see also https://aquasecurity.github.io/trivy/v0.35/docs/secret/scanning/#recommendation for faster secret detection
2022-12-03T06:26:13.146Z WARN Partition error: filesystem walk error: fs.Walk error: read directory /var/lib/docker/overlay2/f0295cf8ebe0f023c6d7dcc9d6b966037ffb3e7b37cd8bc453f31eedd25d7820/diff/go/pkg/mod/golang.org/x/text@v0.3.3/secure/precis: failed to list directory entries inode: 16598985: failed to list entries: failed to parse dir2 block: failed to read block: failed to read: operation error EBS: GetSnapshotBlock, https response error StatusCode: 403, RequestID: 2f39c614-18aa-4cfa-a429-694d31bb9b79, api error ExpiredTokenException: The security token included in the request is expired
2022-12-03T06:26:23.529Z INFO Detected OS: amazon
2022-12-03T06:26:23.535Z INFO Detecting Amazon Linux vulnerabilities...
2022-12-03T06:26:23.663Z INFO Number of language-specific files: 0
snap-0f8541144390c0edb (amazon 2 (Karoo))
Total: 48 (UNKNOWN: 0, LOW: 0, MEDIUM: 12, HIGH: 16, CRITICAL: 20)
やりたいこと
-
AMI作成時に作成イベントを拾って、自動でTrivy Scanを走らせたい。
-
想定するアーキテクチャはLambdaやStepFunctionsは使わないシンプル構成。
- 費用はFargate(+NAT Gateway)程度ですんだらうれしい。
- コマンドの出力結果の連携は間に合わなかったので、標準出力(CloudWatch Logs)で良しとする。
CDKで実装
CDK v2.54.0で検証しています。
1. EventBridge(Trigger: Create an AMI)
AMIの作成が完了したら、というイベントを拾う仕組みを考える部分で頭を使うかな……と想定していたが、調べるとそのままの公式ドキュメントが出てきた。
これをCDKのコードで表現すると、こうなる。
const eventCreateAmisucceed: EventPattern = {
source: ["aws.ec2"],
detailType: ["EC2 AMI State Change"],
detail: {"State": ["available"]}
}
const amiCreateRule = new Rule(this, 'rule', {
eventPattern: eventCreateAmisucceed,
});
2. タスク定義(VPC, ECS, IAM Role)
- Trivyコンテナを動かすFargate定義のリソースは目安である。頻度が高く、費用を気にするほどでもないのならば、もう少しリソース割り当てを大きくしてよいのかも。
- 参考程度に、筆者環境ではメモリが2GB程度だとOut of memoryが出力された。
- Fargateタスクに割り当てるRoleには、公式サイトを参考に必要なIAMポリシーを割り当てる。
https://aquasecurity.github.io/trivy/v0.35/docs/vm/aws/
const fargateTaskDef = new FargateTaskDefinition(this, 'TaskDef', {
memoryLimitMiB: 4096,
cpu: 512,
});
fargateTaskDef.taskRole.addToPrincipalPolicy(
new PolicyStatement({
actions: [
'ec2:DescribeImages',
'ebs:ListSnapshotBlocks',
'ebs:GetSnapshotBlock',
],
effect: Effect.ALLOW,
resources: ['*']
})
)
3. コンテナ定義
- コンテナイメージはECR Publicから取得している。
-
containerName
は次の項で指定するため、固定としている。- この辺はもうすこし綺麗に表現できるのかもしれない。
- 実行コマンドのうち固定する部分は、Entrypointを書き換えて表現している。
- 実運用では
--format
やら--output
などのオプションを加えることが想定されるので、必要に応じて書き換える。
- 実運用では
const registry = "public.ecr.aws/aquasecurity/trivy:0.35.0"
const containerName = "TrivyContainer"
const trivyEntryPoint: string[] = 'trivy vm'.split(' ')
fargateTaskDef.addContainer(containerName, {
image: ContainerImage.fromRegistry(registry),
logging: new AwsLogDriver({
streamPrefix: "trivyScanCreatedAmi"
}),
entryPoint: trivyEntryPoint,
containerName
});
4. Event to Task
- Eventから渡ってきた
detail.ImageId
をami:<ImageId>
形式としてTrivyコマンドの引数に渡したいので InputTransformerを使っている。 - 本質ではないためVPC, ECS Clusterは通信が疎通する最低限のセッティングとしてある。実際にビジネスで利用する場合は別途設計してほしい。
const vpc = new Vpc(this, "vpc", {})
const cluster = new Cluster(this, "cluster", { vpc })
const scanCommand: ContainerOverride = {
containerName,
command: [`ami:${EventField.fromPath("InputTransformer")}`],
}
const ecsTaskTarget = new EcsTask({
cluster,
taskDefinition: fargateTaskDef,
platformVersion: FargatePlatformVersion.VERSION1_4,
containerOverrides: [scanCommand],
});
amiCreateRule.addTarget(ecsTaskTarget)
完成
こんなコードで環境を作ると、想定通りAMI作成イベントをキャッチして、気づくとCloudWatch Logsに標準出力が記録されている。ワイルドな運用だけど、とりあえずは便利。
差分はimport周りくらいだが、コピーして動くサンプルも掲載しておきます。
CDK Sample
import { Stack, StackProps} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam';
import { EventField, EventPattern, Rule} from 'aws-cdk-lib/aws-events';
import { EcsTask, ContainerOverride } from 'aws-cdk-lib/aws-events-targets';
import {
AwsLogDriver,
Cluster,
ContainerImage,
FargateTaskDefinition,
FargatePlatformVersion
} from 'aws-cdk-lib/aws-ecs';
export class TrivyVmscanStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const eventCreateAmisucceed: EventPattern = {
source: ["aws.ec2"],
detailType: ["EC2 AMI State Change"],
detail: {"State": ["available"]}
}
const amiCreateRule = new Rule(this, 'rule', {
eventPattern: eventCreateAmisucceed,
});
const fargateTaskDef = new FargateTaskDefinition(this, 'TaskDef', {
memoryLimitMiB: 4096,
cpu: 512,
});
fargateTaskDef.taskRole.addToPrincipalPolicy(
new PolicyStatement({
actions: [
'ec2:DescribeImages',
'ebs:ListSnapshotBlocks',
'ebs:GetSnapshotBlock',
],
effect: Effect.ALLOW,
resources: ['*']
})
)
const registry = "public.ecr.aws/aquasecurity/trivy:0.35.0"
const containerName = "TrivyContainer"
const trivyEntryPoint: string[] = 'trivy vm'.split(' ')
fargateTaskDef.addContainer(containerName, {
image: ContainerImage.fromRegistry(registry),
logging: new AwsLogDriver({
streamPrefix: "trivyScanCreatedAmi"
}),
entryPoint: trivyEntryPoint,
containerName
});
const vpc = new Vpc(this, "vpc", {})
const cluster = new Cluster(this, "cluster", { vpc })
const scanCommand: ContainerOverride = {
containerName,
command: [`ami:${EventField.fromPath("$.detail.ImageId")}`],
}
const ecsTaskTarget = new EcsTask({
cluster,
taskDefinition: fargateTaskDef,
platformVersion: FargatePlatformVersion.VERSION1_4,
containerOverrides: [scanCommand],
});
amiCreateRule.addTarget(ecsTaskTarget)
}
}
Next Step
このサンプルでも最低限は動くのですが、もう少し凝りたい人向けのアイデアを残しておく。
-
Fargate → Lambdaへの移行
- メモリ消費量を考慮してFargateで着手したが、実はLambda運用でも機能したかも。
-
出力結果の外部登録
- AWSコマンド入りのコンテナイメージはこの記事が参考になりそう。
https://developers.wonderpla.net/entry/2022/08/08/110031
- CDKでのコンテナイメージ管理には独特のつらさがありましたが、最近は
cdklabs/cdk-docker-image-deployment
が良さそうなのかな。
-
イベントのマルチリージョン、マルチアカウント対応
- 最近はEventBridgeで標準対応している。
まとめ
というわけでTrivyを実践投入するまでに頻出のアーキテクチャ、サンプルの構築資材は示せたかなと思ってます。Trivyあまり使えてなかったけど、これからはガンガン使っていきたいですね。それでは。
Discussion