パイプラインにおける品質ゲートの配置場所は、スキャン内容よりも重要である
コミットをプッシュする。パイプラインが起動する。待つ。さらに待つ。15分後、コードが実際には使っていないライブラリの低重要度の脆弱性が原因でパイプラインが失敗する。修正して再度プッシュし、また15分待つ。
これは、すべての品質ゲートをパイプラインの同じ場所に配置した場合の代償である。別の方法も同様に frustrating だ。すべてのスキャンをパイプラインの最後、本番環境の直前で実行する。コードはビルド、単体テスト、結合テスト、ステージングを通過する。その後、最初のコミットから設定ファイルにハードコードされたシークレットが存在していたために失敗する。数秒で捕捉できたはずのものに、何時間ものパイプライン時間を浪費したことになる。
各ゲートの位置は、開発者がフィードバックを得る速度と、何かが失敗したときに無駄になる時間と計算リソースの2つを決定する。これを正しく行うことは、速度とセキュリティのどちらかを選ぶことではない。両方が連携するようにチェックを順序付けることである。
高速で軽量なものを先に、重いものを後に
基本原則は単純だ。高速で軽量なチェックはパイプラインの早い段階で実行する。より多くのコンテキストを必要とする重いチェックは後で実行する。しかし、これはスキャンを単に2つのグループに分割することではない。各タイプのスキャンには、最も少ない摩擦で最大の価値を提供する自然な場所がある。
以下の図は、各ゲートを推奨されるパイプラインステージにマッピングしたものである。
シークレットスキャン:ビルドの前に実行
シークレットは、何かがビルドされる前に検出されるべきである。シークレットがコンテナイメージやアーティファクトに焼き付けられてしまうと、それを削除するのははるかに困難になる。イメージはすでにレジストリにプッシュされ、他のシステムにプルされ、環境にデプロイされている可能性がある。イメージを削除しても、シークレットがどこかにキャッシュされたりログに記録されたりしている可能性がある。
シークレットスキャンは、コードチェックアウト直後、ビルドステップの前に実行する。パイプラインがハードコードされたAPIキーやデータベースパスワードを発見した場合、開発者は即座にフィードバックを得る。ファイルを修正し、再度プッシュすると、無駄になっていたはずのビルドを待つことなくパイプラインが再開される。
依存関係スキャン:アーティファクト作成の前
依存関係スキャンは、プロジェクトが取り込むライブラリをチェックする。これには依存関係マニフェストファイルが必要であり、これはチェックアウト直後に利用可能である。このスキャンの自然な場所は、チェックアウト後、アーティファクトがビルドされる前である。
新しく追加されたライブラリに深刻な脆弱性がある場合、パイプラインは早期に失敗する。開発者はビルド、単体テスト、結合テストを待つ必要がない。依存関係を修正して再度プッシュする。これが早期フィードバックの本質である。修正が安価な問題については、迅速に失敗させることだ。
一部の依存関係スキャナーはビルド前に実行できるほど高速である。他のものは遅い。スキャナーに数分かかる場合は、メインブランチへのすべてのコミットと、フィーチャーブランチのプルリクエストでのみ実行することを検討する。これにより、日常業務ではフィードバックを高速に保ちながら、問題が本番環境に到達する前に捕捉できる。
コンテナイメージスキャン:ビルド後、レジストリの前
コンテナイメージスキャンは異なる。イメージが存在するまでスキャンできない。適切な場所は、イメージがビルドされた後、レジストリにプッシュされたり、どの環境でも使用されたりする前である。
イメージに脆弱性が含まれている場合、パイプラインはここで停止する。イメージはステージングや本番環境に到達しない。これは重要である。なぜなら、イメージが一度レジストリに入ると、他のパイプラインやチームがそれをプルする可能性があるからだ。この時点でパイプラインを停止することで、脆弱なイメージが拡散するのを防ぐ。
トレードオフは、イメージスキャンに時間がかかることだ。チームが1日に多くのコミットをプッシュする場合、すべてのコミットで完全なイメージスキャンを実行すると、パイプラインが大幅に遅くなる可能性がある。一般的なアプローチの1つは、すべてのコミットでクイックスキャンを実行し、メインブランチへのマージ時に完全スキャンを実行することである。別の方法は、スキャン結果をキャッシュし、ベースイメージまたは依存関係が変更された場合にのみ再スキャンすることである。
IaCスキャンとポリシーチェック:2つの場所、2つの目的
Infrastructure as Codeのスキャンとポリシーチェックは、2つの異なる時点で実行でき、それぞれ異なる目的を果たす。
まず、インフラストラクチャコードが作成されたときに実行する。これにより、開発者は設定に取り組んでいる最中に迅速なフィードバックを得られる。セキュリティグループのルールが寛大すぎる、ストレージバケットがパブリックアクセス可能であるといったことを知るために、完全なパイプライン実行を待つ必要はない。
次に、設定が環境に適用される前に再度実行する。これがコンプライアンスゲートである。開発者が以前の警告を無視したとしても、インフラストラクチャの変更が有効になる前に、パイプラインがポリシーを強制する。
最初のゲートは開発者の利便性のためである。2番目はコンプライアンスの確実性のためである。両方とも必要だが、同じチェックを実行する必要はない。初期のゲートは軽量なチェックを実行し、後のゲートは完全なポリシースイートを実行できる。
避けるべきこと:最後に1つの大きなゲート
最悪のパターンは、すべてのスキャンをパイプラインの最後にある単一のブロックに配置することである。これにより、あらゆるタイプの問題に対して長いフィードバックループが発生する。欠落したシークレット、脆弱な依存関係、誤設定されたIaCファイル、コンテナの脆弱性がすべて同時に報告され、開発者はビルド、単体テスト、結合テスト、ステージングを待った後になる。
このパターンはパイプラインを脆弱にもする。1つの遅いスキャンが他のすべてをブロックする。スキャンが失敗した場合、開発者は問題を修正し、すでに通過したすべてのステップを含むパイプライン全体をもう一度待つ必要がある。
各ステージが明確な責任を持つように、ゲートをパイプライン全体に分散させる。初期のステージは安価な問題を迅速に捕捉する。後のステージは高価な問題が本番環境に到達する前に捕捉する。
スキャンのコストを考慮する
一部のスキャンは高価である。完全な依存関係データベースルックアップ、深いコンテナ分析、包括的なポリシー評価には数分かかり、かなりの計算リソースを消費する可能性がある。これらをすべてのブランチのすべてのコミットで実行することは無駄である。
解決策はスキャンをスキップすることではない。戦略的に配置することである。高価なスキャンはメインブランチ、またはメインブランチをターゲットとするプルリクエストでのみ実行する。フィーチャーブランチでは、シークレットスキャン、クイック依存関係スキャン、構文検証などの高速なチェックのみを実行する。これにより、日常業務ではパイプラインを高速に保ちながら、コードが本番環境に到達する前に品質を強制できる。
実践的なチェックリスト
- シークレットスキャンはビルド前、チェックアウト直後に実行する。
- 依存関係スキャンはアーティファクト作成前、マニフェストファイルを使用して実行する。
- コンテナイメージスキャンはビルド後、レジストリプッシュ前に実行する。
- IaCスキャンは開発中と環境適用前の2つの時点で実行する。
- 高価なスキャンはメインブランチまたはマージターゲットのみで実行する。
- 高速なスキャンはすべてのブランチのすべてのコミットで実行する。
まとめ
適切に配置されたゲートは、問題が安価に修正できる早期に捕捉する。不適切に配置されたゲートは、時間と計算リソースが無駄になった後、遅れて問題を捕捉する。目標は、すべてをあらゆる場所でスキャンすることではない。各スキャンを、それが捕捉するように設計された問題に対して最も速いフィードバックを提供する場所に配置することである。配置を正しく行うと、開発者は迅速な成果を得られ、コンプライアンスは保証を得られ、パイプラインは誰も迂回したくないほど高速に保たれる。