こんにちは!KEELチームの花塚です。
最近一番驚いたことは、OPA Gatekeeperの「OPA」を「オーパ」と発音するらしいということです。
さて今回は、OPA GatekeeperやConftestなどを用いてKubernetesのセキュリティ面を強化した話です。
以前からチームメンバー全員がセキュリティに気を配っているものの、今まで対策していることが妥当なのか、考慮漏れはないだろうかということを定期的に確認する機会がありませんでした。
闇雲に対策せず一度自分たちの対策を見直し、継続的にセキュリティを向上していける仕組み作りの過程をお伝えできればと思います。
目次
解決したかった課題
改めてセキュリティ対策を進める上で解決したい課題がありました。
それは「マニフェストの監査が自動化できていない」ことです。
KEELはマルチテナント構成をとっており、各サービスのマニフェストを一つのクラスタにデプロイします。
人力で各サービスのマニフェストのチェックすることは、不適切な設定・悪意のある設定の見落としなどが発生する可能性がありましたし、そのようなマニフェストをデプロイできない仕組みが必要でした。
結論としてOPA GatekeeperとConftestを採用しており、この後に詳しく紹介します。
OPA Gatekeeperとは
解決したい課題でも触れたように、結論としてはOPA Gatekeeperを採用しました。ここでは簡単にOPA Gatekeeperについて紹介します。
OPA Gatekeeper(以降、Gatekeeper)は、KubernetesのAdmission Controllerとして動作し、アンチパターンなマニフェストのデプロイの禁止などを可能にするカスタムコントローラーです。 github.com
以下の例では、Constraint
とConstraintTemplate
のCustom ResourceをデプロイすることでhostPID: true
であるPodのデプロイを禁止することができます。
package deny_host_pid violation[msg] { input.review.object.spec.hostPID msg := sprintf("Sharing the host pid is not allowed: %v", [input.review.object.metadata.name]) }
上記の例でも分かる通り、Gatekeeperは、内部でポリシーエンジンであるOpen Policy Agentを使用しているためポリシーの記述にRegoを使用します。
強い権限を用いているマニフェストのデプロイを禁止するなど、セキュリティ面でも活用できますが、IstioのVirtualService
やGateway
の衝突を防ぐなど、さまざまなユースケースを実現することができます。
余談ですが、Regoを使うことで汎用的にポリシーを記述することができる一方、「学習コストが高そう」 、「少し苦手」という方がいるかもしれません。
最近ではRegoの学習への課題を解決するために、ポリシーをyamlで記述できるKubernetes NativeなKyvernoが開発されています。
Pod Security Policyの廃止
Kubernetesクラスタのセキュリティ面を強化していく意思決定をした際、まず最初に解決策として候補に上がったのがPod Security Policyです。
PodSecurityPolicy
を使うことで、Privileged containerのデプロイなど、セキュリティ的に好ましくない行動を制限することができます。
しかしPodSecurityPolicy
は、Kubernetes v1.21から非推奨となり、v1.25からは廃止となることが発表されました。
先ほど紹介したようにGatekeeperは、さまざまなポリシーを記述することができます。PodSecurityPolicy
でできることは、多くのことがGatekeeperでも実現可能です。
実際にGatekeeperのlibraryでは、PodSecurityPolicy
をRegoで記述したサンプルコードが置かれています。
KEELでは、以前からGatekeeperを使っていることや廃止されることを考慮して、PodSecurityPolicy
の代わりとなるものをRegoで記述する意思決定を行いました。
また、Gatekeeperと似たような用途として使えるConftestというツールがあります。
Gatekeeperは実際にクラスタ内でリソースが違反していないかを動的にチェックしますが、一方Conftestは、Kubernetesのマニフェスト(マニフェスト以外にも対応しています)が違反していないかを静的にチェックします。
ConftestをCIに組み込むことで、デプロイ前にマニフェストがポリシーに違反してないかを確かめることが可能です。
ConftestもGatekeeperと同様にポリシーをRegoで記述するため、Gatekeeperとセットで採用しています。
ちなみにPodSecurityPolicy
は廃止されるようですが、今後はPodSecurity admissionがPodSecurityPolicy
の代替となるようです。
Kubernetesへの脅威
Kubernetesに限らずセキュリティ対策を行う場合は、まずは「脅威を知る」ということが重要になります。
最初はKubernetesのセキュリティの全体感を、以下のような記事を読んで把握することをおすすめします。
- クラウドネイティブセキュリティの概要 | Kubernetes
- Matrix - Enterprise | MITRE ATT&CK®
- Threat matrix for Kubernetes
- NIST SP 800-190
今回Kubernetesのセキュリティ対策を考える上で参考にしたのが、Kubernetesを対象とした以下の脅威モデリングのレポートです。
このレポートは2020年1月に作成されているので少しだけ古いですが、一般的なKuberentesクラスタを対象とした主なAttack Vectorを把握することが可能です。
またAttack Treeもレポートに含まれており、ある攻撃を達成するための大まかな道筋を把握することもできます。
例えば以下の「Malicious Codeの実行」に関するAttack Treeでは、Privileged containerを利用したステップなどが必要であるということが分かります。
このように攻撃の目的を達成する道筋を把握することで、「Privileged containerのデプロイを禁止する」など具体的な対策を考案することができます。
AWSやGCPなどのクラウド上でKubernetesを運用している場合は、「使用している権限(IAM Userなど)でどこまで悪用できるのか」を考慮するなど、自分たちの環境と照らし合わせて対策を考えることが大切です。
このように簡単に脅威を洗い出すだけでも、対策への妥当性を説明できることやコストに見合った対策を優先して取り組めると思います。
本番環境に導入するまで
さて、ここからはセキュリティ対策を本番環境へ導入するまでの過程について書いていきたいと思います。
GatekeeperとConftestで使用するRegoを同期させる
ConftestとGatekeeperで同じ内容のポリシーを記述する場合でも、以下の理由からRegoを併用することができません。
- 少しだけ文法に差異がある
- Gatekeeperでは
ConstraintTemplate
に直接Regoが埋め込まれる
GatekeeperとConftestのRegoが別々で管理されるということは、どちらか一方を変更した場合、もう一方のRegoも変更しなければなりません。それは不便ですよね。
そこでKEELチームでは、GatekeeperとConftestのRegoを併用するためにkonstraintを導入しました。
ここでは詳しく説明しませんが、konstraintのlibraryを使用することで、GatekeeperとConftestのRegoの差異を吸収してくれます。
以下がkonstraint用に書き換えたポリシーです。core
やpod
ライブラリをimportしています。
package deny_host_pid import data.lib.core import data.lib.pods policyID := "P1000" violation[msg] { pod_has_hostpid msg := core.format_with_id(sprintf("%s/%s: Sharing the host pid is not allowed", [core.kind, core.name]), policyID) } pod_has_hostpid { pods.pod.spec.hostPID }
ポリシーを記述した後に、以下のコマンドを実行すればConstraint
とConstraintTemplate
が生成されます。
$ konstraint create <policy_dir>.
現在はkonstraintとGithub Actionsを併用することでRegoのファイルを更新しただけで、自動でConstraint
, ConstraintTemplate
のファイルを生成・更新するようにしています。
ConstraintとConstraintTemplateをクラスタへ適用する
Constraint
とConstraintTemplate
を何も考えずにクラスタに適用してしまうと、意図していないリソースまでも影響を受ける可能性があります。
enforcementAction: dryrun
を使用し、一定期間様子を見ながら適用することをおすすめします。
また、Gatekeeperではデフォルトでメトリクスを公開しているため、違反しているConstraint
数も知ることが可能です。どのConstraint
が違反しているかは、今のところメトリクスからは判別できないようです。
Gatekeeper v3.4.0からは、enforcementAction
にwarn
が追加されたそうなので、本番環境でのConstraint
の使い分けがいろいろとできそうです。
GatekeeperとConftestのテスト
本番に適用することを考えると、記述したRegoのテストについても考慮しなければなりません。 セキュリティに関する機能のテストはとても重要です。いつの間にか攻撃し放題になっていたら嫌ですよね。
まずは記述したRegoのテストについてですが、先ほど説明したようにkonstraintを使用することで、RegoをConftestとGatekeeperで同期させています。 そのため片方のRegoが正しく動くということを証明できれば、もう一方のRegoも同様に正しく動くということが言えそうです。
Conftestの場合、cliでverify
コマンドを使用すると、xxx_test.regoというファイルのテストケースを実行してくれます。
$ cat policy/privileged-container/src_test.rego package privileged_container test_privileged_true { input := {"securityContext": {"privileged": true}} container_is_privileged(input) } test_privileged_false { input := {"securityContext": {"privileged": false}} not container_is_privileged(input) } $ conftest verify 2 tests, 2 passed, 0 warnings, 0 failures
またKEELでは、e2eテストを定期的に実施しています。
以下のようにGatekeeperのテストケースを書いておけば、意図しないデプロイが防がれているかを定期的に確認することができます。
Describe("Gatekeeper", func() { Context("when try to deploy deployments", func() { f := framework.NewFramework(basename, framework.Options{ClientQPS: -1}, nil) f.SkipNamespaceCreation = false BeforeEach(func() { gatekeeperTester.cs = f.ClientSet gatekeeperTester.namespace = f.Namespace.Name }) It("should be successful to deploy a normal deployment", func() { gatekeeperTester.createNormalDeployment() }) It("should be failed to deploy a violated deployment", func() { gatekeeperTester.createViolatedDeployment() }) }) })
CIの形骸化を防ぐ
CIにおけるセキュリティ等のベストプラクティス(今回だとConftestのポリシー)を提供する場合に考慮すべきことは、「CIの形骸化」です。 Linterなどを導入してみたはいいが、結局無視している状態になることは少なくないと思います。
KEELではCIを形骸化させないために、CIのテストに失敗したら必ず対応する方針にしています。
これは言い方を変えると、「必ず対応すべきポリシーやテストのみを提供する」ということになります。 「できれば対策をしたほうがいい」ようなものは提供せず、あくまでガードレールのような開発する上で必ず対策しておきたいものを提供しています。
また、各種Linterの出力をrunbookに変換できるrunbook-translator
というソフトウェアも開発しています。
各Linterの出力を、reviewdogのerrorformatを用いてParseして変換することにより、開発者に何をしてほしいのかを具体的に指示します。
errrorformatを使うことで、Linterの特定のエラーコードは無視するなども可能です。
おわりに
いかがでしたでしょうか。Kubernetesにおけるセキュリティ対策を、考案から本番環境に導入するまでの流れを紹介しました。
今回の取り組みは、脅威を洗い出して対策を考案したこともあり、間違いなくクラスタのセキュリティを向上させたと思います。
また対策に必要な工程の一部を自動化させたことで、今後も継続してセキュリティ対策に取り組めると思います。
まだまだセキュリティ関連でもやることがありますので、これからも取り組みを発信していきます。
それではまた次回のブログで!