🏓

Amazon Machine Images (AMI)をTrivyでイメージスキャン、を自動で実行するシステム

2022/12/15に公開

はじめに

AWS re:Invent 2022の興奮の中、Trivyのアップデートも発表されました。

https://blog.aquasec.com/trivy-now-scans-amazon-machine-images-amis

Amazon Machine Images (AMI)を対象にしたイメージスキャン!?うおおおお、我慢できねえ!早く試したいぜ。

ちなみに本リリース後の作者のTwitterをみるに、この時期に合わせるために大変だった様子が垣間見えます。

https://twitter.com/knqyf263/status/1598553769612025857

この努力に報いるためには私もハードコアにやらなくてはいけません。という訳で、アドベントカレンダー2週間前ですが、予定を変更して本記事を書くことにします。

おさらい - 機能把握

※再掲

https://blog.aquasec.com/trivy-now-scans-amazon-machine-images-amis

このブログがすべてなのですが、機能を簡単に説明すると

  • VMイメージやファイルシステムボリュームのスキャンを実施するためのtrivy vmコマンドが追加された。
    • ローカルだけではなく、AWSのAMI(VMイメージ)とEBS(FileSystem)のIDを指定してスキャンできる。
  • AWSユーザ的に見たとき、AMIからEC2インスタンスを起動してEBSをマウントするのに比べ、以下の点が嬉しい。
    • スキャン対象のEC2インスタンス、VPCに関連するアクセス制御の考慮が不要。
      • 特定のIAM Actionで完結するのがメリット。これがデメリットになる環境もあるのかもしれませんが。
    • スキャン用の環境が構築不要ということは、リソース起動時の費用やリソースのライフサイクルの考慮からも解放される。

という感じです。CIに組み込むのも簡単そうですね。

動作確認

Blogのチュートリアルでも雰囲気はわかるとはいえ、感覚をつかむために一度動かしてみます。

※各種リソースは検証用に使い捨てているので、id等のマスクは割愛します。

参考:ドキュメント

公式のドキュメントはこちら。
https://aquasecurity.github.io/trivy/v0.35/docs/vm/aws/

環境

  • 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

参考

https://aquasecurity.github.io/trivy/v0.35/getting-started/installation/#install-script

検証

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の作成が完了したら、というイベントを拾う仕組みを考える部分で頭を使うかな……と想定していたが、調べるとそのままの公式ドキュメントが出てきた。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/monitor-ami-events.html

これを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.ImageIdami:<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が良さそうなのかな。

    https://github.com/cdklabs/cdk-docker-image-deployment

  • イベントのマルチリージョン、マルチアカウント対応

    • 最近はEventBridgeで標準対応している。

まとめ

というわけでTrivyを実践投入するまでに頻出のアーキテクチャ、サンプルの構築資材は示せたかなと思ってます。Trivyあまり使えてなかったけど、これからはガンガン使っていきたいですね。それでは。

Discussion