Terraform でAWS 環境にDockle + git-secrets を組み込んだCIを構築する

# はじめに

コンテナセキュリティのガイドライン [NIST SP800-190(アプリケーションコンテナセキュリティガイド)](https://www.ipa.go.jp/files/000085279.pdf) では、イメージのリスクの1つとして **「イメージの設定の不備」** について言及されています。
さらに、NIST SP800-190 では、このリスクへの対策として、 **「セキュアな設定のベストプラクティスへの準拠を検証および実施するためのツールとプロセスを採用すること」** を推奨しています。

本記事では、この **「イメージの設定の不備」** へのリスク対策ツールとして、**Dockle** と**git-secrets** を採用し、このツール群を組み込んだCI パイプライン(ビルドのプロセス)をTerraform でAWS 環境に構築する方法を記載しています。

コンテナイメージのセキュリティ対策の一例として参考になれば幸いです。

# Terraform で構築する全体構成図
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/23896c1d-a8f5-6136-dfc5-fb47bc6193d9.png"　 width=80%>

# 構成の概要
1. Dockerfile を含むコードをCodeCommit にプッシュするとCodePipeline が実行されます。
2. CodeBuild 内で、**git-secrets** がリポジトリをスキャンします。
3. **git-secrets** がAWSのアクセスキー等の秘密情報を検出した場合は、CodePipeline を停止します。(CI を停止)
4. 検出されなかった場合は、次のCodeBuild 内で**Dockle** がイメージをスキャンします。
5. その結果、FATAL レベルのアラートを検出した場合は、イメージをECR にはプッシュせず、CIを停止させます。
6. スキャンした結果は、アーティファクトストアに出力され、確認できるようにしています。
7. イメージに FATAL レベルのアラートを検出しなかった場合は、安全なイメージとしてECR にプッシュしています。

### Dockle とは

https://github.com/goodwithtech/dockle#readme

引用：
> 特徴
コンテナの脆弱性を検出する
ベストプラクティスのDockerfileの構築を支援
簡単な使い方
画像名のみを指定してください
クイックスタートと一般的な例を参照してください
CISベンチマークサポート
高精度
DevSecOps
Travis CI、CircleCI、JenkinsなどのCIに適しています。
### Dockle によるチェック項目ついて

Dockle では、[CIS BenchmarksのDockerに関する項目(Container Images and Build File)](https://www.cisecurity.org/benchmark/docker)と[Dockerfileのベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)についてチェックしてくれます。

各チェック項目の詳細は、下記になります。

https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md

### git-secrets とは

https://github.com/awslabs/git-secrets#readme

引用：
>パスワードやその他の機密情報をgitリポジトリにコミットできないようにします。

git-secrets は開発者の端末側で設定することが一般的ですが、ここでは、commit されたリポジトリを再スキャンする処理をCI に組み込むことで、ガードレールの第2層目として機能させることを目的としています。（開発者の設定忘れといったケースはよくありますので。）

# Terraform のコードと構成
https://github.com/okubo-t/aws-tf-dockle-secrets-scan-ci.git

```terminal
$ tree aws-tf-dockle-secrets-scan-ci 
aws-tf-dockle-secrets-scan-ci
├── main.tf
└── modules
    └── codepipeline
        ├── buildspec_dockle_check.yml
        ├── buildspec_secrets_check.yml
        ├── cloudwatch_event.tf
        ├── cloudwatch_logs.tf
        ├── codebuild.tf
        ├── codecommit.tf
        ├── codepipeline.tf
        ├── ecr.tf
        ├── iam.tf
        ├── provider.tf
        ├── s3.tf
        └── variables.tf

2 directories, 13 files
```

# 使い方
## Terraform の動作確認環境

```terraform
$ terraform -version
Terraform v1.1.9
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v4.17.0
```

## 変数の設定
main.tf に下記の変数を設定します。
|変数名|説明|
|:---|:---|
|profile|各AWSのプロファイル名|
|region|リージョン|
|prefix|各AWSのリソースに付与するプレフィックス|
|env|環境を識別するプレフィックス|
|repository_name|CodeCommit のリポジトリ名|
|branch_name|ブランチ名|
|ecr_name|ECR のリポジトリ名|

```terraform:main.tf
module "codepipeline" {
  source = "./modules/codepipeline"

  Account = {
    profile = "YOUR AWS ACCOUNT PROFILE NAME"
    region  = "ap-northeast-1"
  }

  # Project Prefix
  prefix = "prefix"

  # Environment Prefix
  env = "test"

  # Codecommit Repository Name
  repository_name = "dockle-secrets-scan-ci"
  # Branch Name
  branch_name = "master"

  # ECR Repository Name
  ecr_name = "dockle-secrets-scan-ci"
}
```

## Terraform の実行
```terraform
$ terraform init
$ terraform plan
$ terraform apply
```

## テスト用の秘密情報のファイルをCodeCommit にプッシュ
git-secrets の動作確認として、ダミーの秘密情報が含まれるファイルをCodeCommit にプッシュします。

```text:dummy_secrets
[default]
aws_access_key_id=AKIAIOSFODNN7TEST123
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCTESTKEY123

password = "p@ssword123"
```

## CodePipeline > CodeCommit > CodeBuild の実行
CodeCommit の変更イベントを検知してCodePipeline > CodeBuild が実行されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/619221db-63a0-6035-7332-80d94d3b7cc9.png"　 width=60%>

## CodeBuildでgit-secrets によるリポジトリのスキャン
git-secrets のリポジトリのスキャンによって、秘密情報が検出され、ビルドのステータスが失敗となり、CodePipeline が失敗し、停止されます。

**ビルドログ**
![02_secrets_check_build_log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/c1afa5f2-0b62-8968-49f6-58468cf3adaa.png)

**フェーズ詳細**
![03_build_phase_fail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/6f6d0115-4e18-1452-9464-ac30eb1f2950.png)

## Dockerfile をCodeCommit にプッシュ
続いて、Dockle の動作確認として、設定の不備を含むテスト用のDockerfileとファイル をCodeCommit にプッシュします。

```terminal
$ tree dockle-secrets-scan-ci 
dockle-secrets-scan-ci
├── Dockerfile
└── index.html
```
```Dockerfile:Dockerfile
FROM nginx:alpine
ADD index.html /usr/share/nginx/html/
```

```html:index.html
Dockle Check CIS-DI-0009
```
## CodePipeline > CodeCommit > CodeBuild の実行
CodeCommit の変更イベントを検知してCodePipeline > CodeBuild が実行されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/e3fb07d1-e648-6a50-8bc8-0ede92324e2b.png"　 width=60%>

## CodeBuildでDockle による脆弱性スキャン
Dockle のコンテナイメージのスキャンによって、FATAL レベルのアラートを検出した場合、ビルドのステータスが失敗となり、CodePipeline は停止されます。

**ビルドログ**
![06_secrets_check_build_log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/a4ce469e-850c-89fd-688e-733bc5fe3767.png)

**フェーズ詳細**
![07_build_phase_fail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/e6e119d0-28ad-2aad-5081-3473e6f93c1a.png)

## スキャン結果の確認方法
[ビルドの詳細]の[アーティファクトのアップロード場所]から、アーティファクト(Dockle のスキャン結果)をダウンロードして確認します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/8a159156-22d1-aa03-82de-f4b7391dcc61.png"　 width=80%>

**Dockle のスキャン結果**
```json:results.json
{
  "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/dockle-secrets-scan-ci:de01562",
  "summary": {
    "fatal": 1,
    "warn": 1,
    "info": 2,
    "skip": 0,
    "pass": 12
  },
  "details": [
    {
      "code": "CIS-DI-0009",
      "title": "Use COPY instead of ADD in Dockerfile",
      "level": "FATAL",
      "alerts": [
        "Use COPY : ADD index.html /usr/share/nginx/html/ # buildkit"
      ]
    },
    {
      "code": "CIS-DI-0001",
      "title": "Create a user for the container",
      "level": "WARN",
      "alerts": [
        "Last user should not be root"
      ]
    },
    {
      "code": "CIS-DI-0005",
      "title": "Enable Content trust for Docker",
      "level": "INFO",
      "alerts": [
        "export DOCKER_CONTENT_TRUST=1 before docker pull/build"
      ]
    },
    {
      "code": "CIS-DI-0006",
      "title": "Add HEALTHCHECK instruction to the container image",
      "level": "INFO",
      "alerts": [
        "not found HEALTHCHECK statement"
      ]
    }
  ]
}
```
# 補足: 各CodeBuild のビルドの仕様について

## git-secrets によるスキャンのビルド仕様
git-secrets をインストールしてから、CodeCommit のリポジトリをクローンして、そのディレクトリに対してスキャンしています。秘密情報を検出した場合は、CodeBuild を失敗としています。

```yaml:buildspec_secrets_check.yml
version: 0.2

phases:
  install:
    commands:
      - echo git-secrets Install...
      - git clone https://github.com/awslabs/git-secrets /opt/git-secrets
      - echo Install completed on `date`
  build:
    commands:
      - echo git-secrets Scan started on `date`
      - cd /opt/git-secrets
      - make install
      - cd ~
      - git config --global credential.helper '!aws codecommit credential-helper $@'
      - git config --global credential.UseHttpPath true
      - git clone ${CODE_REPO_URL}
      - cd ${CODE_REPO_NAME}
      - git secrets --install
      - git secrets --register-aws
      - git secrets --add 'password\s*=\s*.+'
      - cat .git/config
      - git secrets --scan -r .
```

## Dockle によるスキャンのビルド仕様
Dockle をインストールして、イメージをスキャンし、結果をJSONフォーマットで出力しています。FATAL レベルのアラートを検出した場合、CodeBuild を失敗としています。

参考： Common Examples

https://github.com/goodwithtech/dockle#common-examples

```yaml:buildspec_dockle_check.yml
version: 0.2

env:
  variables:
    DOCKER_BUILDKIT: "1"

phases:
  install:
    runtime-versions:
      docker: 19
    commands:
      - echo Dockle Install...
      #- DOCKLE_VERSION=0.4.5
      - |
        DOCKLE_VERSION=$(
        curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \
        grep '"tag_name":' | \
        sed -E 's/.*"v([^"]+)".*/\1/' \
        )
      - rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.rpm
      - echo Install completed on `date`
      - dockle --version
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - echo $AWS_DEFAULT_REGION
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
      - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t ${REPOSITORY_URI}:${IMAGE_TAG} .
      - echo Build completed on `date`
      - echo Dockle Scan started on `date`
      - dockle --format json -o results.json --exit-code 1 --exit-level "FATAL" ${REPOSITORY_URI}:${IMAGE_TAG}

  post_build:
    commands:
      - echo Dockle Scan completed on `date`
      - cat results.json
      - |
        if [ $CODEBUILD_BUILD_SUCCEEDING == "1" ]; then
          docker push ${REPOSITORY_URI}:${IMAGE_TAG} ;
          printf "[{\"name\":\"${IMAGE_REPO_NAME}\",\"imageUri\":\"%s\"}]" $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json ;
          echo Build stage successfully completed on `date`
        fi
artifacts:
  files:
    - imagedefinitions.json
    - results.json
```
# さいごに

NIST SP800-190 では、イメージのリスクの1つとして **「イメージの脆弱性」** についても言及されているのですが、この **「イメージの脆弱性」** へのリスク対策ツールとしては、例として、**Trivy** があります。

このTrivy も合わせてCI　に組み込んで運用するとコンテナイメージのセキュリティ対策が推進されるかと思います。

参考：

https://qiita.com/okubot55/items/985781d1eef4894f862c