ソフトウェア パッケージからソフトウェア ライブラリ、オペレーティング システム、インフラストラクチャに至るまで、脆弱性はさまざまなところで頻繁に発見されます。ソフトウェアに脆弱性がないかどうかを確認し、脆弱性があれば分類のうえ対応の優先順位を付け、パッチを適用する作業を繰り返すプロセスを、脆弱性管理といいます。現代のテクノロジー スタックに安全性と安定性を確保するうえでは、メンテナンスと更新を繰り返すこの脆弱性管理のプロセスが欠かせません。

脆弱性管理は、セキュリティ コンプライアンスの重要な要素の 1 つとなっています。実際、CircleCI では過去数年の間に FedRAMP certificationSOC 2 Type II compliance の 2 つの認証を取得しましたが、そのどちらにおいても、脆弱性管理のプロセスの詳細を監査人に説明する必要がありました。

脆弱性管理は、セキュリティの基盤であって絶対に欠かすことのできない業務ですが、同時に繰り返しの多い退屈なものでもあります。しかし、そのように繰り返しが多いからこそ、自動化や最適化が活きる可能性が高いというのも事実です。監査人を満足させられるだけの厳格なプロセスを構築しつつ、セキュリティ チームやエンジニアリング組織に大きな負担が生じないようにできるかどうか。このことは、私たちにとって興味深い課題でした。

また、やや個人的な話になりますが、この課題に取り組むなかで、DevSecOps に対する Docker の強みが腑に落ちたという気もしています。

この記事では、私たちが策定し、監査人が満足し、さらにはチーム内の CI/CD、Docker、Kubernetes の使い方にも合致した脆弱性管理プロセスをご紹介します。

Docker ベースのテクノロジー スタックを使ったパッチ作業

まず、CircleCI のテクノロジー スタックは Docker ベースです。これが従来のサーバーベースのアーキテクチャとどのような点で違っており、そのことが DevOps や開発にどのような影響を与えているのかについては、既にさまざまな記事が取り上げている内容なので、ここでは深く立ち入りません。ここで言及しておきたいのは、Docker ベースのアーキテクチャであることがセキュリティ、この記事との関連で言うなら脆弱性管理にも影響を及ぼしているという点です。

Docker イメージは変更が不可能であり、各イメージが固有のインフラストラクチャ パッケージとライブラリを備えています。そのため、従来のような “パッチをサーバーに適用する” という意味で Docker イメージに “パッチを適用” することはできません。Docker イメージの場合には、依存関係を更新した新しいイメージを作成してデプロイし、古いイメージを廃止するという方法を使うことになります。

その影響の 1 つが、イメージのオーナーや SRE チームが把握し、処理できなければならないデプロイメントが増えるという点です。私たちには継続的デプロイメントのための安定的なパイプラインが既にあり、それを活用できたため、デプロイメントが増えても、業務に対する影響は最小限にとどまりました。

また、Docker を使うと、メンテナンス コストのうち、パッチ作業にかかる部分を従来よりも広くエンジニアリング組織に分散できます。従来のサーバーベースの環境では、管理者または SRE がサーバーのメンテナンスやパッチのデプロイを担当しています。これに対して CircleCI では、Docker イメージのメンテナンスは開発チームの責任範囲です。そのため、イメージにセキュリティ パッチを適用する作業も、開発チームが担当しています。開発チームによっては、自分たちが使用する Docker イメージのセキュリティ関連のメンテナンス作業に慣れることが必要になる可能性も考えられます。

さらに、コンプライアンス分野の専門家や監査人と呼ばれる人たちの多くは、Docker ベースのテクノロジー スタックのしくみや、それが脆弱性管理に対して及ぼす影響を深く理解できていません。この種のツールがパッチ適用のプロセスにどのような影響を与えるかについて、そのような人たちに理解してもらうための時間を確保しておくことが重要です。

計画を立てる

脆弱性スキャナーをインストールしただけで何かが守られるということはありません。他のセキュリティ ツールと同じく、そのメリットを享受できるように、ツールをシステムに適応させる作業が必要になります。

私たちの場合、このプロセスを始めた時点で思い描いていた計画は次のようなものでした。

  1. 問題が実際にどのようなかたちで存在するかを把握し、優先度の高いパッチを提供する。
  2. Docker イメージのオーナーになっているチームを対象として、これまでよりも緊密なフィードバック ループを開始する。
  3. チケット作成まわりの自動化に使うツールや自動化の実現方法、それに続いてレポート生成のあり方を再検討する。
  4. チームがパッチ作業のプロセスを最適化および調整するうえで有益な方策を探る。

今回の取り組みを貫く原則を表現するなら、”エンジニアリング チームに対して、同チームが日々使っているツールやインターフェイスを使ってフィードバックを提供する” ということです。Slack、リマインダー、メール、ミーティング、レポートなどを使ったアプローチでは作業に対する集中が途切れてしまうので、ツールやインターフェイスを変えない方が格段に効率的なのです。

万人に共通のスタート地点: スプレッドシート

最初に必要な作業は、プロジェクトの規模と範囲に関する情報を入手したうえで、状況の把握に役立つよう、生のデータを並べ替えたり、フィルタリングしたりできるようにすることでした。

そこで、多くのプロジェクトと同じく、私たちもスプレッドシートを出発点としました。

まずは .csv をスプレッドシートにエクスポートし、概要表示のための式と、データのクリーンアップと照合のためのシンプルな AppScript コードをいくつか追加しました。このとき目標としていたのは、実際のシステムで稼働している各種のイメージとコンテナにはどのようなものがあり、そこにどのような脆弱性があって、パッチ作業の現行の流れがどうなっているかなどの状況を把握することでした。つまり、チケットを作成したり、開発チームに話をしたりする前に、現状の把握を試みたのです。正式なオーナーがいるサービスと、そうでないサービスはそれぞれ何か? ツールから流れてくるデータの形式はどのようなものか? そういったデータを、まずはスプレッドシートを見て人力で確認することが重要でした。実際に自動化したときに、脆弱性スキャナーから提示されるデータを理解することにつながるからです。

この段階でさまざまな詳細を確認した結果、データが抱える矛盾や複雑さを、一部ではあるものの把握することができました。また、重要な情報として、正式なオーナー チームがいないイメージやコンテナの数も確認できました。

次に行ったのは、チケットの割り当てです。私たちはまず、重大な脆弱性を抱えるイメージに注力することにしました。最初のチケットは手作業で作成し、チームに割り当てました。一部のイメージについてはオーナーの情報がなかったので、知識と経験を基に推測してチケットを割り当て、そのチケットが後から別のチームに転送されているかどうかを確認しました。

チケットを受信したチームの多くは、他のチームからチケットが送られてくることに慣れておらず、ましてやセキュリティ チームからチケットが送られてくることなどほとんどありませんでした。ですから、初めは抵抗もありました。開発チームは過去に Docker イメージのメンテナンスに対応したことがなく、その作業にかかる時間を懸念していたようでした。そのため、私たちはチームが最初のパッチをデプロイする作業に協力し、そのなかでパッチ作業の重要性や、パッチ作業が今後もっと日常的なことになるという点を説明しました。

さて、この段階で私たちがほかに会話をすることになった相手として、監査人が挙げられます。最初のポリシー草案の運用を始めてみると、パッチ作業の期間が短すぎて無理があるらしいことがわかりました (監査人はこの問題に気がついていたものの、特に反対意見を出すようなことはありませんでした)。開発チームがとても付いてこられなかったのです。私たちは、過度に厳しいポリシーに頻繁に例外を設けるようなやり方よりも、合理的な範囲のポリシーを用意し、それで運用を始める方が良いと判断しました。そこで、監査人と連携してポリシーを現実的なものに更新する作業を実施しました。この作業は、自分たちのプロセスの成熟度を把握できたという意味で、私たちにとって大きな学びとなりました。期間を短くするのは、プロセスが反復可能なものになった後で良いのです。

この段階で学んだのは、完璧を追い求めないということでした。特定のチームの状況がわからない、チケットがたらい回しにされている、特定のパッチに技術的な問題があるなどといった場合でも、気にしすぎないことです。自分が対応できるパッチやチームから、自分のペースで対応を進めましょう。

CI/CD で作業を分割し、自動化する

最初の段階でできることが終わったので、次の段階に移りました。今回最も大きな変化となったのはこの点です。具体的には、サービスに定期的にパッチが適用されるようになりました。ちなみに、いち開発者として Docker の利点がはっきりと感じられたのもここでした。

従来のサーバーベースのインフラストラクチャでは、テスト環境と本番環境とが明確に分かれています。そのため、パッチ作業をそれぞれ別個に実施しなければならず、特定のパッチが問題を引き起こすかどうかの確認にかかる手間が非常に大きくならざるをえません。

Docker イメージなら、デプロイメントの単位が明確に決まっており、その中でソフトウェアの依存関係がまとまっているので、CI/CD パイプラインの中でのスキャン作業がシンプルになります。また、ソフトウェアの依存関係を含めたテストを自動化し、既存のテストを流用してパッチの試行やバリデーションをすばやく実施することも可能になります。

脆弱性のパッチ作業には毎月多大な手間がかかっていたので、その作業を CI/CD パイプラインに組み込めば、負担を軽くできるのではないかと思いました。パッチ作業を、ソフトウェアをリリースする流れに組み込んでしまうわけです。

自分のチームでも同じやり方を採用してみようと思っている方は、最初のうちは痛みを伴うという点にご注意ください。チームで優れた CI/CD を採用していたとしても、ビルド パイプラインにステップを追加したせいで合格するはずのビルドが壊れてしまうような事態が起これば、そのたびに不満を感じるメンバーが出てきます。そのような場合には、ビルドが無事テストに合格できるよう、そのメンバーをしっかりサポートしなければなりません。大きな問題ではありませんが、心の準備をしておくようにしましょう。しばらくパッチ作業を実施していなかったシステムの場合には、脆弱性の解消に他よりも大幅に時間がかかることが予想されます。初回のパッチ作業を実施する際には、チームやマネージャーとの調整を忘れないでください。また、さまざまなグループのサービスに展開することは大切ですが、初回のパッチ作業では、特定のサービスについて一時的に例外措置を設けたり、短期的に適用を除外したりする選択肢も考慮するようにしましょう。

この段階で重要な原則は、開発プロセスにスピーディなフィードバック ループを取り入れることによって、別個の作業を大量に追加することなく、パッチ作業を継続的な活動に組み込むということです。これにより、各チームによるプロセスの順守が容易になります。

本番環境のスキャンを自動化する

CI/CD の導入は画期的ではあるものの、それ単体では十分な脆弱性管理とは言えません。新しいデプロイメントに起因して発生した脆弱性を検知するうえでは非常に有益であるのに対し、既存のソフトウェアで随時発見される脆弱性には対応できないからです。全部のテストに合格し、ある日めでたく本番環境へとリリースしたコードに、次の日になって脆弱性が見つかることもあります。CI/CD だけではパズルのピースの半分にすぎません。残り半分を占める要素が、本番環境のスキャンです。

言い方を変えると、チームとの接点で効率化を図るという点では、CI/CD の導入ほど良いものはありません。しかし、脆弱性というものの全容を考えてみると、本番環境のスキャンが必要になってくるのです。

そこで、私たちはスプレッドシートを再度確認しました。

もっとも、それほど長く時間を掛けたわけではありません。スプレッドシートは、データの概要を掴むのには便利であるものの、チケットの生成や、脆弱性管理の状況に関するレポートの作成を始めようと思うと、スプレッドシートで快適に済ませられるレベルを超えた分析が必要になるからです。私たちは、セキュリティ チームが手作業で行っていた業務のうち、最も大きな割合を占めていた部分を自動化するインテグレーション パイプラインを構築しました。具体的には、本番環境の脆弱性スキャナーからデータを取り込み、チケットに入力できるようにしました。既に書いたとおり、セキュリティ チームとエンジニアリング チームとのやり取りには、エンジニアリング チームが日々使っているツールを使用するのが非常に効果的です。そのため、私たちのプログラムがデータをチケットに変換できるようにすることは不可欠でした。

しかし、これは単純なコネクタで実現できるような話ではありませんでした。脆弱性スキャナーからデータを取り込んでチケットを作成するまでのプロセスには、細かく複雑な要素がいくつもあり、プログラムを組む必要があったのです。今回の自動化は、Clojure で実装することにしました。これには、CircleCI で開発に採用している主要言語であったという理由のほか、技術に関する意思決定を下すにあたり、メンテナンスの側面を考慮する必要があったという事情があります。つまり、社内の他の人間がツールのメンテナンスに対応できるという点を考慮した結果です。

本番環境の脆弱性スキャナーからデータを取り込み、変換したうえでチケットを作成するという流れを見ると、統合は一見ごく単純に思えてきます。しかし、実際はそれほど簡単ではありません。

この統合には検討が必要な課題が多数ありました。

まず、既存のチケットを更新し、未解決のチケットが重複しないようにする必要がありました。 この点については、Jira に脆弱性管理 ID を追加し、イメージやチケットの ID のマッピングを追跡できるようにしました。

次に、時間の経過に伴って入力元と出力先の両方の API に大きな変更が生じるという問題がありました。 この問題には、統合した (Jira とスキャン ツールの) API の間に抽象化レイヤーを構築することによって対応しました。

さらに、サービスでは、多種多様なディストリビューション、ライブラリ、ツールのすべてに対応できるよう、何か整合性を確保したかたちでデータを正規化する必要がありました。 具体的に必要な作業は、深刻度に “moderate” (中程度) と “medium” (中) が混在している状況の解消 (難易度: 易) から、ベンダーによってそれぞれ異なる “修正版” の形式の統一 (難易度: 高) まで、さまざまでした。

この問題に対しては、データ正規化のためのレイヤーを設けて対応することにしました。このレイヤーで、深刻度の表記統一、サブパッケージの CVE (共通脆弱性識別子) の重複排除、および修正版を表すさまざまな文字列の形式解析を実施するようにしたのです。このほか、パッチの適用が必要なバージョンがどれであるかについて、チケットの詳細の中でチームに要望を明確に伝えられるように、パッケージ情報の正規化も実施しました。

Jira でエンジニアリング プロジェクトやエンジニアリング チームが変わった場合にどうやってチケットを正しく割り当てるかという問題もありました。 これについては、正規表現や部分一致に対応した柔軟な設定を作成し、チームにチケットを割り当てられるようにして対応しました。チームに対するチケット割り当てに関しては、ほかにも重要なことがあります。それは、設定に合致するチームが複数または 0 件だった場合にエラーが出るようにしておくと良い、ということです。エラーが出るようにしておけば、追加の設定が必要な場合にセキュリティ チームがそのことを認識できるようになるからです。

以上の作業には大いに苦労しましたが、結果的にはシンプルなコマンド ライン アプリケーションが完成し、最新情報を読み取り、詳細なチケットを生成してチームに割り当てるまでの処理をわずか数分で完了できるようになりました。

この段階で重要な原則は、自動化に取り組むことにより、繰り返し発生し、間違いが起こりやすい作業からセキュリティ チームを解放するということです。チームに対するチケットの割り当てが多少変わったり、オーナー不明のコンテナが発生したりする頻度はかなりのものなので、柔軟なチーム設定などの工夫が、毎週の作業時間の短縮につながります。

本番環境に関するレポート

セキュリティに関する業務では、単に正しいことをするだけでは十分ではありません。正しいことをしているという事実を利害関係者に示すことができるという点が重要です。目標は、あらゆる人 (経営陣、チームの管理者、監査人) のニーズをそれ 1 つで満たすことができるレポートを作成することです。そのようなレポートを作成できれば、業務負担が軽くなるばかりでなく、物事の状況について全員の認識を揃えることができます。

Docker の脆弱性管理は従来のパッチ作業とは異なるため、監査人に私たちの業務の割り当て方法を理解してもらうと共に、私たちが生成したレポートを活用できるようになってもらうために、監査人と連携して作業する必要がありました。従来であれば、システム管理チームがパッチ作業を担当していることが多く、監査人にとって馴染みのある体制もそのようなものでした。そのため、(私たちの場合ですが) 組織内の各エンジニアリング チームがパッチ作業を実施しているという点について、監査人に説明するための時間が必要でした。何度かやり取りを重ね、レポートにいくつか詳細を追加した結果、FedRAMP の監査人と社内の利害関係者のどちらのニーズにも対応した形式のレポートができあがりました。

この段階で重要な原則は、レポートのあり方をよく検討し、利害関係者全員に満足してもらえるような形式を導き出したうえで、レポートを自動的に生成するという点です。

パッチ作業の最適化

パッチ作業の最適化については、個々のチームとの連携が重要になります。大切なのは連携であって、強権的に指示することではありませんのでご注意ください。目標は、既にあるワークフローにパッチ作業をうまく組み込めるよう、チームをサポートすることです (新しいワークフローを作るのではありません)。エンジニアリング チームのプロセスについてはエンジニアリング チームの方がよく知っているのですから、その最適化に関しても、セキュリティ チームでは思いつきもしなかったアイデアを出してくれる可能性が高いという点を忘れないようにしましょう。

さて、この段階で生産性の観点から特に有益だったテクニックが 3 つあります。結合テストの自動化、ベース イメージの共有、ソフト バージョン指定です。

  1. CI パイプラインには一般に自動的な結合テストが組み込まれているものですが、結合テストの自動化には、パッチの適用作業をすばやく、かつ自信を持って進めることができるようになるというメリットがあります。一連の結合テストのなかでパッチを自動的に実行し、そのパッチが原因となって引き起こされる問題を検出できるので、”パッチを適用して確認してみる” タイプのアプローチの大幅な省力化が実現します。
  2. ベースとなる Docker イメージを共有すると、インフラストラクチャやソフトウェアの要件がよく似ているサービスを、共通のイメージに基づいて構築できるようになります。さまざまなサービスに共通する更新をベース イメージの中で実施したら、あとは全サービスに展開するだけなので、どのパッチ作業でも最初のステップで最新のイメージが共有され、ほとんどの開発チームがそのイメージに更新できます。
  3. ソフト バージョン指定については、パッケージのメジャー バージョンのみ、またはメジャー バージョンとマイナー バージョンの両方を指定するだけで、あとは、スケジュール設定された Docker イメージのビルド処理の一環としてパッケージ マネージャー経由で自動的に最新のバージョンに更新できるようにしました。このビルド処理を週 1 で実行することにすれば、パッチ作業のうち人間が介在する部分の多くを削ることができるというわけです。このようなかたちで最新バージョンへの更新処理を自動化することには、業務上のリスクがあります。そのため、このレベルの自動化を始めるときは、結合テストを入念に実施すると共に、まず重要性が比較的低いサービスで試してみることが大切です。

この段階で重要な原則は、パッチを適用する作業の負荷を軽減することです。脆弱性を詳細に分析し、対応の優先順位を決めることは、セキュリティ チームにとっては多大な時間と労力を要する作業です。また、重大な脆弱性であるのに誤って環境に関係がないと判断してしまうリスクが大きい点も問題です。パッチ作業のフローがシンプルでわかりやすいものであれば、チームがパッチをシンプルに適用でき、多くの作業が円滑に進むようになります。

脆弱性管理のヒント

(あるいは、監査人、Docker、CI/CD システムとの付き合い方)

  1. エンジニアリング チームとのやり取りには、チームが既に採用しているツールやプロセスを使うこと。
  2. 最初から理想を追いすぎないこと。
  3. パッチ適用の作業を現行の CI/CD に組み込むこと。
  4. 脆弱性管理の自動化に精力的に取り組むこと。
  5. 作成するレポートは 1 種類だけに絞り、そこにあらゆる利害関係者が必要とする情報を網羅すること。
  6. パッチの適用は、シンプルでわかりやすいものにすること。

最後に

脆弱性管理を導入および展開するときは、早い段階で見つかった懸念にばかりとらわれ、”目の前の火を一刻も早く消さなければ” といったような態度に陥りがちです。しかし、脆弱性管理とは、パッチを 1 回適用して終わるものでもなければ、1 件の重大な脆弱性に対応すれば済むものでもありません。また、1 つのチームやイメージのみで完結するものでもありません。このことを、どうか忘れないようにしてください。重要なのは、繰り返しが可能で、かつ整合性にも配慮した自動化を模索し、エンジニアリング チームとセキュリティ チーム双方のワークフローに対する影響を最小限に抑えつつ、継続的にセキュリティを強化していくことにほかなりません。

Tips for vulnerability management