AWS×コンテナで基本的なDevSecOpsアーキテクチャをデザインしたお話

はじめに

先日、僕が担当する業務でECS/Fargate利用を前提にDevSecOpsアーキテクチャをデザインし、社内のAWS勉強会にて登壇する機会をいただきました。 本ブログでも内容をかいつまんでご紹介できればと思います。

AWSによらず、コンテナを利用されている方にとって、一つのプラクティス例としてご参考になれば幸いです。 ※コンテナ自体の説明や必要性に関する内容は省略していますm(_ _)m

そもそもDevOpsとは?

DevSecOpsの導入意義をお伝えするた前に、まず軽くDevOpsの意義をお伝えします。

※とは言え、この記事をご訪問されている方にとっては「何をいまさら...」な内容かもしれませんし、ググればDevOps自体の情報はたくさん見つかりますので、重要なポイントのみ述べることにします。

DevOpsとは、一言で述べれば、開発チームと運用チームが協力してビジネス価値を高める活動概念です。

昨今、ITはビジネス効率化の手段だけではなく、ビジネス差別化の手段の一つとなっています。 スマホネイティブなサービスが普及し、利用者に新しいユーザー体験がに届けられることができるようになりました。併せて、サービスに対する利用者からのフィードバックも受け取りやすくなっており、多数の競合サービスに引けを取らないように、サービス提供者が改善やニーズ取り込みを素早く行い、価値を提供し続けることが重要になってきています。

このような活動を継続するためには、MVP(Mimimum Viable Product)のように利用者にとって本当に価値のあるものを小さく早く届けることが重要です。

f:id:iselegant:20200726123147p:plain

ここで、ユーザーフィードバックをモニタリングする責務はサービス運用チームが担うことが多いかと思います。 また、ニーズを受け、新たな機能追加などは開発チームが行います。 継続的にサービス価値を高めるためには、以下のように運用と開発のフェーズを順番に踏んでいるわけですね。

AWSが提供するページより図を引用(DevOps とは? - DevOps と AWS | AWS)

つまり、変化に対して柔軟かつ品質の良いサービスを提供し続けるには、開発と運用がうまく協調する必要があります。 かつ、このサイクルをいかに早く回していけるか、が重要となります。

うまく協調するためには縦割りな組織構造よりはサービス・ソリューションに最適な組織作りアジャイル開発やCI/CDといったラクティス、「Git」「Docker」「AWS Codeシリーズ」等のツールなど、これら要素組み合わせることで達成を目指していきます。 DevOpsはこれらの取り組みの総称としても表現できます。

ちなみに、CI/CD視点からの切り口になってしまいますが、AWSでは年間190 million(1.9億回)もアプリケーションがデプロイ可能な環境が整っているそうです。

www.youtube.com

また、以下はAWSの最新情報から集計した2018年12月〜2020年5月のAWS月別リリース数ですが、2020年に関しては平均116件/月ものリリースがなされています。

f:id:iselegant:20200726123238p:plain

まさにDevOpsを体現しているからこそ成し遂げている結果ですね。

なぜDevSecOpsが求められるのか?

DevSecOpsとは、一言で述べるとDevOpsの取り組みにセキュリティを協調させたものです。

DevOps実践のもとで高速でサービスを追加していくと、それに応じてセキュリティ観点で脆弱な作り込みリスクも増えることになります。 また、後続の工程でセキュリティテストを実施して問題が健在化すると当然手戻りが発生します。 設計や構築などより早いタイミングで問題を検出できることが、結果的に安定なサービス提供につながるでしょう(より早い工程段階でセキュリティ対策を施すことを「シフトレフト」と呼んだりします)。

一度セキュリティインシデントが発生すると、ユーザーからの信頼が失墜し、ビジネスの根幹を揺るがすことが多いのが実情です。

もちろん、扱う情報や業界規制等によって取るべきセキュリティ対策のレベルを見直すことも重要です(個人情報を保有する or しない、クレジットカード情報や金融系取引情報を扱う or 扱わない、などで守り方の温度感も変わってきます)。 すべてのサービス層で重厚なセキュリティ対策を施すと対応に必要な工数が増えたり、運用の手間が増えてしまうため、スコープの見極めは大切です。

とは言え、メンバー全員で責務を持つことでセキュリティ意識を底上げさせ、自動化として組み込むことができれば、その手間をなるべく減らしつつ、リスクを下げることもできそうですね。 DevOpsにセキュリティ系のツールをうまく組み込み、メンバー全員で開発ライフサイクルのセキュリティに責任を持つという視点を醸成することがDevSecOpsの意義の一つなのだと考えています。

次に、プラクティスの一つであるCI/CDに対して、どのようなセキュリティツールを活用するとDevSecOpsの土台が実現できるか、という点に着目していきます。

コンテナに求められるセキュリティ対策要件

ここでは、コンテナアプリケーションで必要なセキュリティ対策について考えてみます。

結局のところ、アプリケーションに脆弱性を作り込まないように対処する点は従来のアプリケーション開発となんら変わりありません。 コンテナはベースイメージOS上にアプリケーションと必要な依存パッケージ等を一つのイメージとして閉じ込めています。つまり、ベースイメージOS自体やパッケージに対する脆弱性対応について考慮する必要があります。 また、Dockerfile等によるコンテナイメージ作成プロセスが適切に行われているかどうか、という点についても考慮する必要があります(動作に対して不要なパッケージが混入している、本来は不要であるrootユーザーでプロセス実行がされている、等)。

基本的な対策としては、まず以下の検討ポイントを挙げました。

  • イメージ作成プロセスの妥当性
  • イメージ内OS・アプリケーションの脆弱性チェック

DevSecOps実践を目指す中で、CI/CDにこれらのセキュリティ対策をどのように行っていくか、がポイントの一つになります。

※実際には、コンテナイメージ自体の保護に加えて、レジストリやCI/CDパイプライン、コンテナオーケストレーションツール等の保護なども考慮する必要があります。 また、開発ライフサイクル全体でセキュリティに必要な対応を俯瞰すると、SAST(Static Application Security Testing)、DAST(Dynamic Application Security Tesing)、IAST(Interactive Application Security Testing)など 全体の位置付けを考慮する必要がありますが、話が膨らむ上に今回のトピックから焦点がぶれてしまうので、敢えてコンテナCI/CDに組み込むセキュリティ対策という観点でお話しています。

キーファクターとなるOSS

要件に合うセキュリティ対策を実装するべく、今回はOSSを中心に以下ツールを選定しました(コスト都合かつスモールに始めたかったので、セキュリティベンダーが提供する商用ツールは一旦見送りにしました)。

f:id:iselegant:20200726123858p:plain

ここから、順番に各ツールの特徴を見ていきます。

hadolint

コンテナイメージ作成はDockerfileの記述内容に従って実行されますが、hadolintはDockerfileを静的解析することでベストプラクティスに従った流れかどうかチェックしてくれるツールです。

github.com

プログラミング言語Haskell」で開発されたDockerfileのLinterなので、hadolintと命名されているようです。

ちなみに、hadolintはShellCheck)と呼ばれるシェルスクリプトの静的解析をベースにしてチェックもしてくれます。 実際のところ、Dockerfile内はシェルスクリプトの内容を含むケースがが多く、ShellCheckベースの検証で安全ではない記述方法の検出は大変助かります。

例えば、次の様なDockerfileにてチェックを掛けると、hadolintがいくつかのダメ出しで知らせてくれます。

# Dockerfileの内容(敢えて誤った内容で記載)
FROM debian

RUN node_version = "0.10" \
  && apt-get update && apt-get -y install nodejs="$node_version"
COPY package.json usr/src//app
RUN cd /usr/src/app \
  && npm install node-static

EXPOSE 80000
CMD ["npm", "start"]

f:id:iselegant:20200726124407p:plain

上記例はhadolint側でのチェック内容を表示するために、敢えてhadolint REDOME内の内容記載をそのまま転記しているため、実際にはここまでひどく(?)指摘されることは無いと思います(たぶん)。

CI/CD内でイメージ作成の前処理として上手に組み込むことで、イメージ作成プロセスを安全に維持できそうですね。

trivy

trivyはAquaSecurity社が提供するコンテナ脆弱性スキャナーです。

github.com

日本人の福田鉄平さんが開発し、その後はAqua社側にてメンテナンス及び提供されています。 その他の脆弱性スキャナーツールと比較して軽量かつ高機能であり、インストールもスキャン方法も非常に簡単で使いやすいのが特徴です。 参考情報として、下記は英国政府の技術ブログですが、ここでもtrivyが活用されていました。

technology.blog.gov.uk

次のようにイメージを引数として実行することで、該当する脆弱性をCVE番号とともに出力してくれます。

f:id:iselegant:20200726124005p:plain

出力結果には重大度レベル(SEVERITY)も併せて表示されます。 LOW < MEDIUM < HIGH < CRITICALの順列で影響が大きくなるので、これらの結果を基に優先的に対応すべき脆弱性対象を確認することができます。

さて、イメージの脆弱性スキャン機能ですが、AWSのマネージドなコンテナレジストリであるECRでも提供しています。 なぜtrivyを採用したかというと、アプリケーションの依存パッケージまで踏み込んでスキャンしてくれるかどうかという点で優位性があるからです。 ECRのイメージスキャンはClairというOSSをベースに作成されています。 ClairはコンテナのOSパッケージレベルをスキャン対象としており、trivyと比較してスコープが異なります。

また、スキャン可能なベースイメージも異なります。具体的に、ClairはBusyboxやAlpineの最新バージョンなどのスキャンには対応していません

※参考:Docker Image Security: Static Analysis Tool Comparison - Anchore Engine vs Clair vs Trivy - a10o.net - Alfredo Pardo

加えて、Alpine Linuxの例で述べれば、Clairとtrivyは脆弱性情報の収集先情報源が異なることから、検出対象精度の数に2倍弱の違いがあるようです(詳細はGithubのtrivy REDOME.mdをご覧ください)。

Clair uses alpine-secdb. However, the purpose of this database is to make it possible to know what packages has backported fixes. As README says, it is not a complete database of all security issues in Alpine. Trivy collects vulnerability information in Alpine Linux from Alpine Linux aports repository. Then, those vulnerabilities will be saved on vuln-list. alpine-secdb has 6959 vulnerabilities (as of 2019/05/12). vuln-list has 11101 vulnerabilities related to Alpine Linux (as of 2019/05/12). There is a difference in detection accuracy because the number of vulnerabilities is nearly doubled.

以上の理由より、導入と利用が簡単かつ高精度なtrivyを採用することにしました。

Dockle

Dockleは天地知也さんが開発したコンテナイメージに含まれるセキュリティホールをチェックしてくれるツールです。 hadolintとは異なり、Dockerfileではなくイメージに対するスキャンであり、ベースOSイメージ側の設定内容に関しても踏み込んでスキャンしてくれる点が特徴です。 また、CIS(Center for Internet Security)と呼ばれる国際的なインターネットセキュリティ標準化に取り組む団体が提供しているベンチマークに対する遵守状況もチェックしてくれます。

Trivy同様、インストール及び実行が簡単で以下のように検出可能です。

f:id:iselegant:20200726124713p:plain

hadolintやTrivyとは異なる観点でのチェックを行ってくれるため、こちらも利用する方針としました。

CI/CDアーキテクチャへの落とし込み

ここまでにご紹介したDevSecOpsツールをCI/CDに組み込んでいくのですが、まずはベースとなるECS/FargateのCI/CDアーキテクチャを考えてみます。

f:id:iselegant:20200726124823p:plain

開発環境のCI/CD構成をシンプルに表現したものです。

処理の流れとしては、次の通り。

  1. 開発者がCodeCommit上にソースコードをプッシュすると、CodePipelineが起動
  2. CodeBuildにて以下の順番でビルドを実施が実施
    • アプリケーションのビルド&テスト
    • Dockerイメージのビルド
    • ECRへのDockerイメージプッシュ
  3. ビルド処理が完了するとCodeDeployに遷移
  4. CodeDeployにより、ECS/Fargate上にビルドしたコンテナイメージが展開

今回はファーストステップとしてシンプルにデザインと導入を進めたかったので、ビルドフェーズ内(CodeBuildの動作仕様を定義するbuildspec.yml内)に各処理を定義してスキャンする方針としました。

ここで、コンテナイメージビルド処理より前にhadolintによりDockerfileのチェック自体は先行で実施しておくのが望ましそうです。 イメージビルド後、影響度の大きい脆弱性が見つかったらより早い段階で検知すべきという考え方から、trivyイメージスキャンを優先しています。最後にDockleでCISベンチマークに従っているかをチェックする流れとしました。

ビルドフェーズ内の処理を図で表現すると次のようになりました。

f:id:iselegant:20200726124850p:plain

なお、開発環境ではアプリケーションの機能動作確認を優先的に行うべきであり、CRITICALな対象ではない限り、このタイミングでビルド処理を止めたくないというニーズがありました(LOWレベルの検出でアプリケーション開発が止まってしまうと、逆にアジリティを下げる原因にもなってしまう可能性があったからです)。

また、trivyで脆弱性が検出された場合、対象や対応状況を管理しておきたいと考え、セキュリティイベントの統合マネージドサービスであるSecurity Hubに集約することで実現しています。 各種ツールはJSON形式で結果を出力できるため、buildspec.yml内にてSecurity HubのASFFと呼ばれる標準的な検出結果形式に各脆弱性情報を整形し、SDKからSecurityHubにインポートすることで集約管理することができます。

また、マルチアカウント構成を考慮し、AWSアカウント毎に個別にセキュリティ情報を管理・モニタリングするのではなく、アカウント間で情報を集約できようにセキュリティ管理専用のAWSアカウントを作成し、メンバーシップ設定を実施をすることで統合しています。

f:id:iselegant:20200726125057p:plain

ここで、少しだけbuildspec.ymlの記載例に踏み込んで見たいと思います。 各buildspec内フェーズ毎の動作をコメントと併せて記載してみました。

version: 0.2

env:
  variables:
    :

phases:
  install:
    runtime-versions:
      docker: 18
    :

  pre_build:
    commands:
      # hadolintのインストールとDockerfileチェックの実行
      - docker pull hadolint/hadolint
      - docker run --rm -i hadolint/hadolint hadolint - < Dockerfile

      # 事前準備: trivyの最新バージョンをインストール
      - TRIVY_VERSION=$(curl -sS https://api.github.com/repos/aquasecurity/trivy/releases/latest  | jq -r .name | sed -e 's/v//g')
      - rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.rpm

      #事前準備: dockleの最新バージョンをインストール
      - 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
       :

  build:
    commands:
      # Dockerイメージビルドを実行 (Dockerfilne内でGoのビルド&テスト)
      - docker build -t ${APP_NAME}:${IMAGE_TAG} .
      - docker tag ${APP_NAME}:latest ${PUSH_ECR_REPO_URI}:${IMAGE_TAG}
      :

  post_build:
    commands:
      # trivyによるイメージスキャン(Security Hubへの結果格納用)
      - trivy --no-progress -f json -o trivy_results.json --exit-code 0 ${PUSH_ECR_REPO_URI}:${IMAGE_TAG}
      - (Security Hubへ結果をインポートするスクリプトを実施(trivy_results.jsonがインポート対象))
      # trivyによるイメージスキャン(CRITICALレベルの脆弱性ある場合はBuild強制終了)
      - trivy --no-progress --exit-code 1 --severity CRITICAL ${PUSH_ECR_REPO_URI}:${IMAGE_TAG}
      - exit `echo $?`

      # dockleによるイメージチェック(FATALレベルの脆弱性ある場合はBuild強制終了)
      - dockle --format json --exit-code 1 --exit-level "FATAL" ${PUSH_ECR_REPO_URI}:${IMAGE_TAG}
      - exit `echo $?`

      # ECRへビルド済&スキャン済イメージをプッシュ
      - $(aws ecr get-login --region ${AWS_REGION} --registry-ids ${AWS_ACCOUNT_ID} --no-include-email)
      - docker push ${PUSH_ECR_REPO_URI}:${IMAGE_TAG}
      :

artifacts:
  files:
    :

今回は手始めにtrivyの出力結果をSecurity Hubに登録する流れを実装しています。

上記のbuildspec.ymlでは、trivyによるイメージスキャンを2回実行しています。 この理由として、SEVELITYによらず全ての脆弱性情報をSecurity Hubに登録するため、一時的に結果をすべて出力する目的で1回目を実行しています。 SecurityHubへの登録完了後、CRITICALな対象があればビルドを中止して修正を優先すべきという方針としているため、CRITICALレベルに絞って再度実行することでリターンコードを取得しています(1回目のtrivy実行時に内部で脆弱性情報がキャッシュされているため、2回目のスキャン自体は高速です)。

実際にビルド中に脆弱性が見つかると、次のようにSecurity Hub側に自動登録されます。

f:id:iselegant:20200726125301p:plain

シンプルですが、これで脆弱性情報の管理・モニタリングまで含めたフローが完成しました。

プロダクション稼働を考慮した際の課題点

ここまででDevSecOpsを実践するアーキテクチャの一例をご紹介しました。 ただ、本番運用で利用しようとすると、まだ色々と至らない点が存在します。 ここでは、その課題点について少し向き合って見ようと思います。

アプリケーション毎に同一チェック処理を定義する必要がある

hadolintやTrivy、Dockleによるスキャンはアプリケーション毎にその流れはほぼ同じになることが見込まれます。 しかし、現状ではアプリケーション毎に別々のbuildspec.yamlに定義する作りとなっています。 これではアプリケーションの数が増えてくると定義が重複し、管理コストが増してしまいます。 状況によっては個別のCI内処理に組み込むより、共通化して切り出すほうが望ましい形になります。 この点について、先日クックパッドさんの開発者ブログにて、まさにこの問題に対処した素晴らしい内容が公開されました。

techlife.cookpad.com

LambdaやSQS活用し、非同期かつ疎結合に処理するようにアーキテクチャが設計されています。 規模拡大に伴い、このような形が望まれてくると思いますので、併せてこちらもご参考にするのが良いと思います(僕自身も参考にさせていただいています)。

CI/CD実行時にのみにセキュリティチェックがされる

現状の構成ではCI/CDが実行されたタイミングでセキュリティチェックがされます。 しかし、当然の話ですが、実際に脆弱性情報はCI/CDとは別の時間軸で日々報告されています。 IPAが運営するJVN iPediaベースの情報になりますが、1ヶ月あたり1000件以上の脆弱性が報告されています。

仮に、とあるアプリケーションの更新がしばらく実行されていないケースでは、プロダクション環境で稼働中のアプリケーションに該当する脆弱性が発見されたとしても、CI/CDのみが発見の契機であり、見逃してしまうかもしれません。

アプリケーションの重要度にもよりますが、より確実に検知を目指すのであれば定期的なチェック処理を実装する必要がありそうですね。 ※この観点はCI/CD組み込みというよりは、運用視点での日々のモニタリング活動側に含まれるかもしれません。

各環境毎にSEVELITYとどう向き合うか

今回、trivyに関してはSEVELITYがCRITICALな場合にビルド処理を異常終了する仕様にしました。 というのは、CRITICALな状態でのプロダクション環境デプロイはまず避けるべきですし、このレベルで検知されるとアプリケーション機能追加より対応を優先すべきと判断したからです。

この異常終了する判断のしきい値ですが、アプリケーションの内容によってはHIGHが適切かもしれません。

一方、開発環境では、ビジネス価値をもたらす機能追加の検証を優先したい場合があります。 そのような場合、ステージング環境で一度止めるという選択肢もケースによってはあるかもしれません。 ただし、ステージング環境の利用は総合テストフェーズで利用されるシーンも多く、シフトレフトな考え方と若干反するとも考えられます。

DevSecOpsに取り組むにあたって、アプリケーションの重要度、各環境毎の目的のバランスを見極めながら、ビルドを止めるのか、それとも後で対応するのか、といったことをCI/CD構築と併せて検討するという視点も重要な検討ポイントだと考えています。

ECRでは現状DCTに対応していない

少しDeep Diveな内容になりますが、Dockerには「Docker Content Trust(DCT)」というイメージ自体の完全性やイメージ提供者の検証を行う仕組みがあります。 これはDocker 1.8から実装された機能で、署名済みかつ検証済みのコンテナイメージしか利用させなくすることができます。

DCTの有効化は簡単で、docker pulldocker pushを行う際の環境にて、環境変数をセット(DOCKER_CONTENT_TRUST=1)することで有効になります(CodeBuildにおいては、buildspec.yml内で環境変数を定義すればDCTが有効になります)。 ちなみに「DCTが有効な状態でpull/pushがされるか?」というのは、Dockleのチェック対象となっています(さきほどDockleの出力例をご紹介しましたが、その中でINFOレベルで出力されている「CIS-DI-0005」が該当します)。

このDCT、内部的にNotaryと呼ばれるサーバー・クライアント側OSSが安全なイメージの作成と検証動作を担ってます。

github.com

docs.docker.com

しかし残念ながら、現時点ではECRはこのNotaryをサポートされておらず、ECRに検証済イメージをプッシュしようとするとエラーになってします。

そのため、現状ではDCTを無効化した上でDockleによるチェックの際「CIS-DI-0005」を対象から除外する必要があります。

f:id:iselegant:20200726125626p:plain

なお、今月開催された(AWS Cloud Containers Conference)のECRに関するセッション「Security Best Practices with Amazon ECR」内で、スピーカーのOmar Paul氏ECRのSr Product Manager)が、ECRのNotary GA目処として2021年内であることを名言しています。

github.com

ECRでのDCT利用はもう少し先になりそうですね。

その他参考情報

今回ご紹介したアーキテクチャを検討する上で参考にした情報も併せて共有しておきます。

AWSの公式ブログでもtrivyを扱った事例がいくつか紹介されていたりします。 以下はAWS Container Heroでもあり、Aqua社のVP OSSでもあるLiz Rice氏が寄稿したtrivy × CodePipelineの記事になります。

aws.amazon.com

また、以下はtrivyをSecurity Hubに取り込む際の例がサンプルコード付きで紹介されています。

aws.amazon.com

次の参考情報は、AW re:Inforce 2019 (AWS主催のセキュリティ&コンプライアンス系カンファレンス)にて実施されたコンテナ × DevSecOpsに関するワークショップの内容です。 以下のような構成をハンズオン形式で学べるので、DevSecOpsをサッと学ぶにはちょうどよいボリューム感かと思います。

※図を引用(Container DevSecOps - Integrating security into your container pipeline Overview)

さいごに

最後までお読みいただきありがとうございました。 ブログで誰かに何かを伝えようと思うと、どうも内容が盛りだくさんになってしまいがちです... 読みにくい&わかりにくい箇所があったかもしれませんが、皆さんがコンテナでDevOpsを実践する方々に少しでもコンテナ自体のセキュリティを意識するきっかけとなれば嬉しいです。

※お決まりの言葉ですが、ここでの内容はすべて筆者の私見であり、所属する組織の意見でも代表するものでもありません。また、あくまで特定の秘匿情報ではなく公開可能な情報から述べているため、その点はご留意ください。

ではでは。