Google Cloudとコンテナの継続的なセキュリティ
本文の内容は、2020年4月16日にSysdigのVíctor Jiménezが投稿したブログ(https://sysdig.com/blog/image-scanning-admission-controller/)を元に日本語に翻訳・再構成した内容となっております。
この投稿では、アドミッションコントローラーにおけるイメージスキャンを使用して、ワークロードがクラスターにスケジュールされる直前に、コンテナイメージをオンデマンドでスキャンする方法について説明します。
すべてのランタイムワークロードがスキャンされ、重大な脆弱性がないことを確認することは簡単な作業ではありません。クラスターで実行する前に、スキャンポリシーに合格しないポッドをブロックする方法を見ていきましょう。
SysdigのOPAイメージスキャナーは、Sysdig SecureエンタープライズレベルのイメージスキャナーとOPAポリシーベースのrego言語の機能を組み合わせてスキャン結果とアドミッションコンテキストを評価し、アドミッションの決定において卓越した柔軟性を実現します。
イメージスキャナーは、コンテナイメージの内容を分析し、Common Vulnerabilities and Exposures(CVE)を検索することで機能します。通常はNational Vulnerability Database(NVD)などのデータベース、および設定の誤り、不正行為などを検索します。スキャナーには、レジストリからイメージをプルするか、CI/CDパイプラインでのビルド直後にインラインでスキャンします。スキャン結果は、見つかった問題のリストを含むレポートであり、脆弱性の重要度や特定のプラクティスのブロックなど、いくつかのポリシーを定義することでさらに評価できます。
すでにイメージスキャナーを使用している場合は、いくつかの制限に気づいているかもしれません。スキャンされたイメージのセットを明示的に設定する必要があるため、クラスターで実行されてしまうイメージや、まったく実行されないイメージをスキャンしてしまう可能性があります。さらに、イメージスキャナーがスキャンするイメージに関する情報は限られています。レジストリ、イメージ名、タグのみです。このような狭いコンテキストでは、より高度な決定を下すことは不可能です。
より寛容なルールを備えた「dev」namespace、そして、信頼できるレジストリからのイメージのみを許可し、脆弱性を許可しないような非常に制限された本番環境のnamespaceが必要でしょうか?
イメージスキャンは、次の方法でトリガーできます。
クラスター環境での最終目標は、スキャンの結果と追加情報に基づいてイメージをデプロイできるかどうかを知ることです。一般的なイメージスキャンの使用例には次のものがあります。
Kubernetesの拡張性を使用してアドミッションコントローラでイメージスキャンを実行し、結果を評価することで、事前の制限に対処します。スキャンは、クラスターにデプロイしようとしているすべてのイメージに対して必要なときにだけトリガーされるため、使用されないイメージに時間を費やす必要も、クラスターにデプロイされているイメージを見逃す事もありません。また、アドミッションの決定は、イメージの名前とタグだけに依存するのではなく、namespace、ポッドメタデータなどを含むアドミッションレビューからの追加のコンテキストにも依存します。
さらに、OPAと強力なrego言語を使用して、アドミッションポリシールールを定義し、スキャン結果をアドミッションコンテキストとともに評価することにより、十分な情報に基づいた決定を行い、最も一般的なイメージスキャンのユースケースを解決するための優れた柔軟性を提供します。
オープンポリシーエージェント(OPA)は、オープンソースの汎用ポリシーエンジンで、regoと呼ばれる高水準の宣言型言語を使用しています。OPAの背後にある重要なアイデアの1つは、意思決定をポリシーエンフォースメントから切り離すことです。これにより、ポリシーをソフトウェアとは別にコードとして定義および配布できます。アプリケーションがポリシーを決定する必要があるときはいつでも、関連するデータを使用してOPAにクエリを実行し、入力に対してルールを評価できるようにします。
OPAは単なる評価エンジンですが、Kubernetes Admission Controllerのような他のツールを補完して、アドミッションに関するポリシーを適用出来ることに着目してください。OPAゲートキーパーは、テンプレートと制約を定義するためにネイティブKubernetes CRDを使用する拡張可能なKubernetesポリシーコントローラーとして実装されています。
Kubernetesアドミッションコントローラー+ Sysdigイメージスキャナー+ OPAエンジンの組み合わせにより、イメージスキャンをアドミッションの決定から切り離し、regoで単純なルールまたは高度なルールを定義し、スキャン結果レポートとKubernetesコンテキストの両方でそれらを評価する方法を実現します。
Kubernetesアドミッションコントローラは、オブジェクトが作成される前にKubernetes API呼び出しをインターセプトするコードの一部であり、さまざまなコントローラがオブジェクトを検証したり、オブジェクトを変更したりできます。
Sysdig OPAイメージスキャナーは、動的アドミッションコントローラープラグインとして登録され、クラスター内のポッドの作成または更新をインターセプトします。ポッドリソースがインターセプトされると、コントローラーはポッド内のすべてのコンテナに対していくつかのステップを実行していきます。
ルールのOPA評価で、いずれかのコンテナのイメージを拒否する必要があると判断された場合、ポッドの作成/更新は拒否されます。
スキャンレポートがまだ利用できない場合は、大きなイメージをスキャンするのに数分かかる場合があるため、OPAルールで状態を検出して評価できます。スキャン結果は「report_not_available」として報告され、ポッドを検証するか拒否するかを決定できます。
アドミッションコントローラーが原因でポッドが拒否されると、通常のKubernetes再試行メカニズムが適用され、エクスポネンシャル・バックオフメカニズムに従ってしばらくしてから作成が再試行されます。
不思議に思うかもしれませんが、代わりにイメージダイジェストを使用するようにポッド仕様を変更しますか? これについては今後の投稿で取り上げますが、基本的にはTOCTOU(Time-of-check Time-of-use)の脅威にさらされているタグを保持します。
イメージダイジェストを使用するようにポッドを変更すると、この問題は完全に防止されます。これにより、将来どのようなスケジュールイベントが発生しても、スキャンされた同じイメージがクラスターにデプロイされます。元のイメージタグを取得する場合に備えて、イメージとタグの名前はポッド内の注釈として保持されます。
OPA rego言語を使用して定義されたルールの例がREADME.mdファイルにいくつかありますが、それがどのように機能するか、およびいくつかの使用例を分析してみましょう。
OPA評価エンジンは、ScanReportおよびAdmissionRequestリクエストを含む入力オブジェクトを、この入力オブジェクトのフィールドとして受け取ります。AdmissionRequest構造の詳細については、公式のgoドキュメントとアドミッションコントローラリファレンスをご覧ください。これらはOPAで評価用に使用できます。
JSONのサンプルScanReportを見てみましょう(簡潔にするために一部が削除されています)。
{ "ImageAndTag": "nginx:1.17.9", "Status": "accepted", "InnerReport": { "detail": { "policy": { ... ], "name": "Default Sysdig policy bundle", "policies": [ { "comment": "System default policy", "id": "default", "name": "DefaultPolicy", "rules": [ { "action": "WARN", "gate": "dockerfile", "id": "rule_1FlJOnK9qdRSRcTNrfz3IUZXbou", "params": [ { "name": "instruction", "value": "HEALTHCHECK" }, { "name": "check", "value": "not_exists" } ], "trigger": "instruction" }, ... ], "version": "1_0" }, ... ], "version": "1_0", "whitelisted_images": [], "whitelists": [ { "comment": "Default global whitelist", "id": "global", "items": [], "name": "Global Whitelist", "version": "1_0" } ] }, "result": { "bundle": { ... }, "created_at": 1582633822, "evaluation_problems": [], "final_action": "warn", "final_action_reason": "policy_evaluation", "image_id": "c7460dfcab502275e9c842588df406444069c00a48d9a995619c243079a4c2f7", "last_modified": 1582633822, "matched_blacklisted_images_rule": false, "matched_mapping_rule": { "id": "mapping_1CI5tw3zxNL9b344sSsXBfth3dW", "image": { "type": "tag", "value": "*" }, "name": "default", "policy_ids": [ "default" ], "registry": "*", "repository": "*", "whitelist_ids": [ "global" ] }, "matched_whitelisted_images_rule": false, "result": { "c7460dfcab502275e9c842588df406444069c00a48d9a995619c243079a4c2f7": { "result": { "final_action": "warn", "header": [ "Image_Id", "Repo_Tag", "Trigger_Id", "Gate", "Trigger", "Check_Output", "Gate_Action", "Whitelisted", "Policy_Id" ], "row_count": 17, "rows": [ [ "c7460dfcab502275e9c842588df406444069c00a48d9a995619c243079a4c2f7", "docker.io/nginx:1.17.7", "41cb7cdf04850e33a11f80c42bf660b3", "dockerfile", "instruction", "Dockerfile directive 'HEALTHCHECK' not found, matching condition 'not_exists' check", "warn", false, "default" ], [ "c7460dfcab502275e9c842588df406444069c00a48d9a995619c243079a4c2f7", "docker.io/nginx:1.17.7", "1571e70ee221127984dcf585a56d4cff", "dockerfile", "instruction", "Dockerfile directive 'USER' not found, matching condition 'not_exists' check", "warn", false, "default" ], ... ] } }, "policy_data": [], "policy_name": "", "whitelist_data": [], "whitelist_names": [] }, "tag": "docker.io/nginx:1.17.7", "user_id": "tenant_1TqQxfrhMuzrTAkZ5X7smleHiRe" } }, "last_evaluation": "2020-02-25T12:30:22Z", "policyId": "default", "status": "pass" }
}
そこにはたくさんの情報があります! ただし、通常は、イメージスキャナーが入力するドキュメントのルートの[Status]フィールドに焦点を当てることができます:
スキャンの結果と適用されたポリシーに応じて、「scan_failed」はエラーが発生してスキャンを実行できなかったことを示し、「report_not_available」はスキャンがまだ完了していないことを示します。検出されたポリシー情報や特定の脆弱性や不正行為など、高度なユースケースのOPAルールでも評価できるInnerReportフィールド内には多くの詳細があります。
アドミッションコントローラは、スキャンタスクがまだ存在しない場合は作成し、イメージダイジェストを取得して、スキャンの結果に対してその特定のイメージダイジェストをクエリします。次に、AdmissionRequestとScanReportを入力オブジェクトの対応するフィールドに割り当て、既存のルールを評価します。deny_image [msg]ルールのいずれかがtrueと評価された場合、イメージの承認を拒否します(ログにメッセージが表示されます)。
したがって、すべてをまとめるために、イメージスキャナーが受け入れるイメージのみを許可するルールを記述できます。
allow_image { input.ScanReport.Status == "accepted" } deny_image[msg] { not allow_image msg := sprintf("Denying images that are not accepted. Status: %s", [input.ScanReport.Status])
}
または、承認済みまたは進行中のイメージを許可します:
allow_image { input.ScanReport.Status == "accepted" } allow_image { input.ScanReport.Status == "report_not_available" } deny_image[msg] { not allow_image msg := sprintf("Denying images that are not accepted or in progress. Status: %s", [input.ScanReport.Status])
}
AdmissionRequestからの基準を含める場合は、イメージスキャナーポリシーによって受け入れられるイメージを許可するか、次のようにして「dev」namespaceで何かを許可することができます。
allow_image { input.ScanReport.Status == "accepted" } allow_image { input.AdmissionRequest.object.metadata.namespace == "dev" } deny_image["Denying images otherwise"] { not allow_image
}
前の例では、AdmissionRequestのobject.metadata.namespace属性を使用しました。
JSONでの完全なAdmissionRequestオブジェクトの例は次のとおりです:
{ "uid": "6870143b-55da-40be-b42f-3fc64799bd5d", "kind": { "group": "", "version": "v1", "kind": "Pod" }, "resource": { "group": "", "version": "v1", "resource": "pods" }, "requestKind": { "group": "", "version": "v1", "kind": "Pod" }, "requestResource": { "group": "", "version": "v1", "resource": "pods" }, "name": "test-dep-76758659b5-4qq69", "namespace": "default", "operation": "CREATE", "userInfo": { "username": "system:serviceaccount:kube-system:replicaset-controller", "uid": "3745a732-c159-4a58-8a8e-61b07e27573b", "groups": [ "system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated" ] }, "object": { "kind": "Pod", "apiVersion": "v1", "metadata": { "name": "test-dep-76758659b5-4qq69", "generateName": "test-dep-76758659b5-", "namespace": "default", "uid": "aac24f4d-d63f-4ab5-b95b-56cd57579071", "creationTimestamp": "2020-02-24T19:15:01Z", "labels": { "app": "test-dep", "pod-template-hash": "76758659b5" }, "ownerReferences": [ { "apiVersion": "apps/v1", "kind": "ReplicaSet", "name": "test-dep-76758659b5", "uid": "4c925465-d86a-447b-a6d0-e5a7cefba425", "controller": true, "blockOwnerDeletion": true } ] }, "spec": { "volumes": [ { "name": "default-token-pnrxp", "secret": { "secretName": "default-token-pnrxp" } } ], "containers": [ { "name": "nginx", "image": "nginx:1.17.9", "resources": {}, "volumeMounts": [ { "name": "default-token-pnrxp", "readOnly": true, "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" } ], "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "imagePullPolicy": "Always" } ], "restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "dnsPolicy": "ClusterFirst", "serviceAccountName": "default", "serviceAccount": "default", "securityContext": {}, "schedulerName": "default-scheduler", "tolerations": [ { "key": "node.kubernetes.io/not-ready", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 }, { "key": "node.kubernetes.io/unreachable", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 } ], "priority": 0, "enableServiceLinks": true }, "status": { "phase": "Pending", "qosClass": "BestEffort" } }, "oldObject": null, "dryRun": false, "options": { "kind": "CreateOptions", "apiVersion": "meta.k8s.io/v1" }
}
したがって、volumeMounts、restartPolicy、securityContext、または同様の属性などを確認し、これらの値に応じてルールを定義することもできます。
ScanReportおよびAdmissionRequestフィールドとは別に、2つの追加フィールドが入力オブジェクトに追加されます:
完全なスキャンレポートとすべてのAdmissionReview情報を評価に使用できるため、非常に高度で強力なアドミッション基準を作成できます。
このアドミッションコントローラーをクラスタにデプロイする最も簡単な方法は、リポジトリで利用可能なhelmチャートを使用することです。アドミッションコントローラは、相互TLS認証を使用してKubernetes aggregated APIサーバーとして登録され、ポッドリソースの作成または更新をインターセプトするための動的アドミッションコントロールWebhookを登録します。
values.yamlファイルの設定をカスタマイズし、namespaceを作成して、Helm 3を使用してデプロイするだけです:
$ kubectl create ns sysdig-image-scanner
$ helm install -n sysdig-image-scanner sysdig-image-scanner .
数秒後、このチャートは以下を含む必要なすべてのコンポーネントをデプロイします:
values.yamlのデフォルト設定はほとんどの場合に問題ありませんが、少なくとも、以下を指定する必要があります:
value verboseLogをtrueに設定すると、OPAエンジンは、入力データ(AdmissionReviewおよびScanReport)や評価されるルールなどの追加情報を出力ログに含めます。これは、情報をコピーしてRego Playgroundでテストすることにより、ルールの問題のデバッグに役立ちます:
values.yamlには、イメージとスキャンレポートを評価するためのデフォルトアクションを設定できるscanRulesセクションと、customRulesセクションがあります。
scanRules: # If set to "true", a default set of rules will be generated from this YAML values. # Otherwise, no rules will be generated, and only "customRules" below will apply autoGenerate: true # Default admission policy to apply: [accept | reject | scan-result] defaultPolicy: scan-result # What should we do if the Scan Result is not yet available (scan in progress): [accept | reject] reportPending: reject # What should we do if the Scan has failed (wrong credentials, misconfiguration, etc.): [accept | reject] scanFailed: reject alwaysAccept: [] # These 2 registries will always be rejected unless alwaysReject: - "bad-registry.com/" - "malware-registry.io/" alwaysScanResult: [] byNamespace: {} # ns-dev: # # By default, images will be accepted in this NS regardless of the scan result # defaultPolicy: accept # ns-prod: # # All images rejected by default in this namespace # defaultPolicy: reject # # Images from "my-trusted-registry.com/" will be always accepted # alwasyAccept: # - "my-trusted-registry.com/" # ns-playground: # defaultPolicy: accept # alwaysReject: [] # Define a set of custom rego rules. If scanRules.autoGenerate is true, # these customRules are appended to the set of generated rules. # Otherwise, these customRules are the only rules definition, customRules: | ###### Begin: Custom rules ###### my_example_rule { # Some conditions... false } other_rule { # Some other conditions... true } deny_image["This is a custom deny message"] { my_example_rule other_rule }
###### End: Custom rules ######
デフォルト(scanRules.autoGenerate:trueを使用)では、このセクションで定義された設定を使用するOPAルールのセットを含むConfigmapが作成されます。この場合、スキャンレポートが利用可能になるまでイメージは拒否され、イメージスキャナーは「accepted」と報告します。プレフィックスによってレジストリをホワイトリストまたはブラックリストに登録したり、ネームスペースごとに例外を作成したりできます。namespaceの設定が指定されていない場合は、最上位で定義されているデフォルトのオプションが使用されます。
customRulesセクションで、追加のカスタムルールをraw文字列として追加できます。ルールを完全にカスタマイズする場合は、scanRules.autoGenerateを「false」に設定して、独自のルールを記述するだけです。リポジトリでデフォルトのOPAルールセットを確認できます。
次のようなscanRules設定でイメージスキャナーをデプロイした後:
scanRules: autoGenerate: true defaultPolicy: scan-result reportPending: reject scanFailed: reject alwaysAccept: [] alwaysReject: [] alwaysScanResult: [] byNamespace: dev:
defaultPolicy: accept
"default" namespaceにデプロイを作成してみましょう。
$ kubectl create deployment test-dep --image=airadier/test:bad
deployment.apps/test-dep created
したがって、「defaultPolicy:scan-result」が適用されます。 イメージスキャナーポッドのログでエラーの詳細を確認できます。
I0402 11:56:31.291171 1 mutationhook.go:31] [mutation-server] mutating Pod admission request I0402 11:56:31.291240 1 evaluate.go:28] [admission-server] Admission review 969345db-8e45-44ae-b373-24a190112580 - evaluating admission of pod '<Not yet generated>' I0402 11:56:31.301618 1 admissionevaluatorimpl.go:35] Checking container 'test' image 'airadier/test:bad' I0402 11:56:31.301822 1 client.go:149] [Anchore] Sending POST request to https://api.sysdigcloud.com/api/scanning/v1/anchore/images, with params map[tag:airadier/test:bad] I0402 11:56:33.551378 1 client.go:131] [Anchore] Added image to Anchore Engine: airadier/test:bad I0402 11:56:33.551627 1 client.go:149] [Anchore] Sending GET request to https://api.sysdigcloud.com/api/scanning/v1/anchore/images/sha256:b3787bd182d60ee3bd8d0bb53064e7eaa1073b817c31769dba3822895f9254d6/check?tag=airadier/test:bad&history=false&detail=true, with params map[] I0402 11:56:34.093250 1 evaluate.go:38] [admission-server] Admission review 969345db-8e45-44ae-b373-24a190112580 - finished evaluating admission of pod 'test-dep-6f78bb8cc4-*' I0402 11:56:34.093285 1 evaluate.go:46] [admission-server] Admission review 969345db-8e45-44ae-b373-24a190112580 - pod 'test-dep-6f78bb8cc4-*' rejected. Reasons: image 'airadier/test:bad' for container 'test' failed policy check Error: Image denied by OPA rules: - Image rejected by scan-result
I0402 11:56:34.093297 1 mutationhook.go:63] [mutation-server] Patching container image: airadier/test:bad -> airadier/test@sha256:b3787bd182d60ee3bd8d0bb53064e7eaa1073b817c31769dba3822895f9254d6
ただし、「dev」namespaceにデプロイしようとすると、ルールが「dev」namespaceのポッドの受け入れのdefaultPolicyを定義するため、成功します。
I0402 11:57:46.087798 1 evaluate.go:38] [admission-server] Admission review 7643d522-9055-4471-9358-de6993ad7e77 - finished evaluating admission of pod 'test-dep-6f78bb8cc4-*' I0402 11:57:46.087816 1 evaluate.go:41] [admission-server] Admission review 7643d522-9055-4471-9358-de6993ad7e77 - pod 'test-dep-6f78bb8cc4-*' accepted
I0402 11:57:46.087825 1 mutationhook.go:63] [mutation-server] Patching container image: airadier/test:bad -> airadier/test@sha256:b3787bd182d60ee3bd8d0bb53064e7eaa1073b817c31769dba3822895f9254d6
OPAベースのイメージスキャナーアドミッションコントローラーを使用すると、クラスターにデプロイされたすべてのイメージをスキャンして検証し、タグの変更があったとしても重大な脆弱性がないことを確認できます。スキャナーポリシーをはるかに超える評価ルールを定義する強力で柔軟な方法を提供し、高度にカスタマイズされたルールを適用するためのアドミッションレビュー仕様で強化されたコンテキストを提供します。
レジストリとクラスターにデプロイされるイメージを信頼できるかどうかが100%わからない場合は(おそらく誰もそうではありません)、アドミッションコントローラーをインストールして、ポッドの作成中にSysdig Secureですべてのイメージをスキャンすることにより、多くのセキュリティの脅威を防ぐことができます。
クラスターでイメージが実行されたら、ウェビナーでFalcoを使用してクラスターの異常なアクティビティを検出する方法も学びましょう!