投稿日:
更新日:

OPA で実現する Kubernetes のポリシ制御

Authors

目次

overview.png

はじめに

Kubernetes はコンテナ化されたアプリケーションのデプロイ、スケーリング、管理を自動化するための強力なプラットフォームエンジンです。 マニフェストを用いてリソースの種類や制御パラメータを組み合わせることで、アプリケーションを柔軟にデプロイ・管理できますが、運用面での統制やセキュリティの確保が課題となっています。 組織全体で一貫したポリシを確立し、セキュリティ要件を設けることは、運用の安全性を確保する上で非常に重要になります。

また、インフラ・プラットフォームサイドのエンジニアが継続的に介入せずとも、開発者が自立して安全にアプリケーションを開発できるように舗装することは Platform Engineering の文脈でも必要になってきます。

今回のブログでは、ポリシ整備ツールのデファクトスタンダートとなっている OPA(Open Policy Agent) および Gatekeeper を取り上げ、Kubernetes に対するデプロイメントを制御する仕組みについて紹介したいと思います。

ポリシ制御が必要な背景

Kubernetes を用いてサービスを運用していると、規模が拡大するにつれ、例えば以下のような課題が生じてきます。

オペレーション制御

本番環境(Production)と開発環境(Development)のクラスタが分離されている場合、アプリケーションの開発者が誤って開発環境にデプロイするはずのリソースを本番環境へ適用してしまう可能性があります。

例えば、許可されていないレジストリの Docker イメージを使用したり、Pod に割り当てるリソースを過剰に設定していたりすると、サプライチェーン攻撃 や、ノードのリソースを喰い潰すといった、運用上のリスクが生じます。

また、サービス規模が大きければ大きいほど、リソースの設定状況をクラスタの管理者が追従するのは困難になります。

セキュリティ管理の複雑化

Kubernetes のリソース(Namespace、Pod、Service 等)は自由度が高いため、適切なアクセス制御やセキュリティポリシを設定しない場合、意図しないリソース変更やセキュリティリスクを招く可能性があります。

例えば、不適切な RBAC の設定や、特権コンテナの使用等です。 特権コンテナ(PrivilegedContainer)の利用を許可していると、Pod 内のコンテナからホストマシンへの操作が可能となり、意図せずクラスタを破壊しかねないオペレーションが実行できてしまいます。

運用標準化の困難性

マイクロサービスの普及により、多くのチームが独立した Kubernetes クラスタを使用することが一般的になっています。 この場合、運用方法のばらつきが問題となり、効率的かつ安全な環境を保つのが困難になります。

コンプライアンス要件への対応

金融や医療等、特定の業界では、例えば、データ暗号化や監査ログの保持等、法規制や内部ルールに準拠することが必要です。 これらを運用フローに確実に組み込むためには、ポリシによる機械的な検証と制御が欠かせません。

これらの課題は一般に認証・認可の仕組みを使用して制御・抑止することも可能です。

$ kubectl describe clusterrole view
Name:         view
Labels:       kubernetes.io/bootstrapping=rbac-defaults
              rbac.authorization.k8s.io/aggregate-to-edit=true
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources                                              Non-Resource URLs  Resource Names  Verbs
  ---------                                              -----------------  --------------  -----
  bindings                                               []                 []              [get list watch]
  configmaps                                             []                 []              [get list watch]
  endpoints                                              []                 []              [get list watch]
  ...                                                    ...                ...             ...

一方、RBAC のみでは対応できないケースも多数存在し、独自のカスタムポリシを Kubernetes デフォルトの認証・認可機構で実現するのは非常に困難です。

  • コンテナレジストリの制限
  • 特定のメタデータ(labels, annotations) 付与の強制
  • リソース Request / Limit の設定必須化 etc...

OPA:Open Policy Agent

open-policy-agent.png

OPA(Open Policy Agent)は CNCF Graduated Projects として OSS 化された汎用ポリシエンジンです。 また、ポリシエンジンとは Admission Controller と呼ばれるポリシ定義専用の Kubernetes カスタムコントローラを用いて、kube-apiserver に対する API リクエスト時に Webhook を介してオペレーションを制御する仕組みで、言わば Kubernetes に対してガードレールを提供するためのコンポーネントです。

ポリシを記載したマニフェストを カスタムリソース として事前にクラスタに適用しておくことで、不適切なリソースをデプロイしようとした際に、ポリシの内容に一致するかを確認し、必要なアクション(オペレーションの可否等)を実行します。

open-policy-agent-architecture.png

従来、Admission Controller は Pod の権限管理のために Pod Security Policies(PSP)というリソースで用意されていましたが、Kubernetes v1.25 で Removal となり、廃止・削除されたため、最新版のクラスタでは利用することができません。

PSP が廃止された背景は こちら を参照ください。

psp-removal.png

PSP の後継として、Pod Security Admission(PSA)がリリースされましたが、OPA 自体は Kubernetes ネイティブの機能ではなく、あくまで Third-party 製の Admission Controller となっています。

OPA は Go 言語 で実装されています。

Gatekeeper

Gatekeeper は OPA をベースとしたポリシエンジンです。 Gatekeeper では、OPA を使用するために必要なポリシを Rego というプログラミング言語で記述します。

Rego 自体は、Kubernetes だけでなく、様々なプラットフォームで汎用的に利用することができます。 Gatekeeper は、Rego を利用した Kubernetes 特化のポリシエンジンとなっています。

policy-driven-operation.png

Gatekeeper も OPA 同様に Go 言語 で実装されています。

Rego

Rego はポリシを定義するプログラミング言語で、OPA Gatekeeper ポリシエンジンを通じて実行されます。 PrologDatalog の着想を得て改良され、入出力にフォーカスした関数型・宣言型(Functional)の言語となっています。 そのため、普段利用するような手続き型(Procedural)のパラダイムが利用できず、癖のあるプログラミング言語となっています。

Rego は Kubernetes マニフェストの他に、Terraform によるクラウドリソース(IAM ロールや VPC のネットワーク設定)の構成検証やサービスメッシュ、API 管理ツールをポリシベースで制御する際にも利用できます。

ポリシ記述例

package my_policy

// ルールブロックを定義
allow {
    input.user == "alice" // ブロック内の評価式が全て成立する場合 "allow" に true が格納される。
}

// ルールブロックを定義(同一ルール名を複数定義可できるが、異なる評価結果は重複してはならない)
allow {
    //(必要に応じて)変数の定義が可能
    allowed_roles := [
        "admin",
        "developer",
    ]
    input.role == allow_roles[_] // [_] はリスト内の全てのリソースを示す。この場合、一つでも式を満たす要素があれば成立する。
}

このポリシに対して、以下のような JSON を入力すると、

{
  "user": "bob",
  "role": "admin"
}

上のポリシは非成立、下のポリシは成立となります。

Go 言語との比較

  • Rego(Functional 言語)
default allow = false

allow = true {
    input.size != 0
    input.size < data.limit
}
  • Go(Procedural 言語)
func allow() bool {
    if input.size == 0 {
        return false
    }
    if input.size >= data.limit {
        return false
    }
    return true
}

パッと見ただけでも、Rego の方がルールを直感的に記述することができます。 そのため、特定の条件に合致するかどうかを判断するような、ポリシ評価や認可に特化したユースケースで非常に有用です。

一方、Go は汎用的にビジネスロジックを記述できますが、今回のようなケースではコードが複雑化する上、矛盾したルールブロックを見つけにくいというデメリットもあります。

Policy as Code

一般に、ポリシをコードで表現・管理するアプローチは Policy as Code と呼ばれ、履歴・承認管理、テスト可能性、再現性、継続的デプロイ等のメリットがあります。 Rego を利用することで、Policy as Code を実現することができます。

カスタムリソース

Gatekeeper でポリシを扱うためには、以下の 2 種類の CRD(Custom Resource Definitions) を定義します。

  • ConstraintTemplate
    • 使用されるパラメータやポリシのテンプレートを定義
    • Rego で適用するポリシを記述
  • Constraint
    • 定義した ConstraintTemplate を元に作成する実際のポリシ
    • ポリシを反映させたい Kubernetets リソースの種別や名前空間を指定

ConstraintTemplate

ConstraintTemplate では、Kubernetes リソースの特定フィールドを評価するポリシを Rego で記述します。

  • 例:Pod の特権コンテナ(PrivilegedContainer)の利用を禁止するポリシ
package privileged_container

import data.lib.core
import data.lib.pods
import data.lib.ignore

policyID := "privileged-container"

violation[msg] {
    c := pods.containers[_]
    container_is_privileged(c)
    not ignore.is_ignore_container(c, input.parameters)

    msg = core.format_with_id(
        sprintf("apiVersion: %v, kind: %v, name: %v, container: %v; container runs as privileged", [
            core.apiVersion, core.kind, core.name, c.name
        ]),
        policyID
    )
}

container_is_privileged(c) {
    c.securityContext.privileged
}

Rego から ConstraintTemplate(CRD)を作成する際は Konstraint という Third-party 製のツールを用いると、ポリシを共通化・一元管理できます。

  • Konstraint
    • Rego ファイルを元に Gatekeeper 用のマニフェストを自動生成するツール
    • Conftest のプラグインと Rego をマニフェストに変換する CLI から構成
konstraint-manage-flow.png
$ konstraint create policies
### Privileged コンテナのデプロイを禁止するポリシ例
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  creationTimestamp: null
  name: privilegedcontainer
spec:
  crd:
    spec:
      names:
        kind: PrivilegedContainer
      validation:
        openAPIV3Schema:
          properties:
            ignoreContainersAnnotationKey:
              type: string
            ignoreContainersName:
              items:
                type: string
              type: array
  targets:
    - libs:
        - |-
          # 省略
      rego: |-
        package privileged_container

        import data.lib.core
        import data.lib.pods
        import data.lib.ignore

        policyID := "privileged-container"

        violation[msg] {
            c := pods.containers[_]
            container_is_privileged(c)
            not ignore.is_ignore_container(c, input.parameters)

            msg = core.format_with_id(
                sprintf("apiVersion: %v, kind: %v, name: %v, container: %v; container runs as privileged", [
                    core.apiVersion, core.kind, core.name, c.name
                ]),
                policyID
            )
        }

        container_is_privileged(c) {
            c.securityContext.privileged
        }
      target: admission.k8s.gatekeeper.sh
status: {}

Constraint

ConstraintTemplate を元に Constraint を作成します。 Constraint は ConstraintTemplate の metadata.namekind に指定します。

なお、Constraint は名前を自由に付与することができます。 ConstraintTemplate で定義したポリシをどのリソースやネームスペースに適応させるのかや、ConstraintTemplate に渡すパラメータ等、Gatekeeper コントローラの制約リソースを具体的に定義します。

enforcementAction は [deny, dryrun, warn] から選択します。 これらのパラメータをそれぞれポリシ違反を見つけた際に、Gatekeeper がどのようなアクションを取るのかを定義します。

アクション挙動
deny(デフォルトの挙動)
違反がある場合、リソースの作成や更新を実行させない
dryrun違反がある場合でも単にログを記録するだけでアクションはブロックしない
バックグラウンドでログを生成する
warn違反がある場合は、リソース作成時等の承認タイミングで、即座に違反に関する情報をユーザに提示する
アクションはブロックしない(フォワグラウンドで即座に警告する)
### 上の ConstraintTemplate を全ての Pod に対して適用する例
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: PrivilegedContainer
metadata:
  name: deny-privileged-container
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']

また、--disable-enforcementaction-validation=true をマニフェストに記述することで、適用アクションの検証を無効にすることもできます。

ポリシの種類

Validate Rule

Kubernetes リソースがデプロイされる前に、そのリソースが定義したポリシに準拠しているかを検証します。 Validate Rule を使うには、ConstraintTemplate と Constraint を使用する必要があり、ConstraintTemplate を事前にクラスタ上にデプロイします。 その後 Constraint を適応することで、ポリシによる制御を実現します。 ポリシの内容は Rego で柔軟に定義することができます。

Mutate Rule

Mutate Rule は、Rego ではなく、YAML で定義します。 Validate Rule とは別の以下の 4 つの CRD を定義して、Mutate を扱います。

  • AssignMetadata:ソースのメタデータ・セクションへの変更を定義
  • Assign:メタデータ・セクション以外の変更
  • ModifySet:コンテナの引数等、リストにエントリの追加・削除が可能
  • AssignImage:イメージ文字列の構成要素に対する変更を定義

ポリシの検証

Gatekeeper は Gator CLI でポリシの検証を行うことができます。

ポリシをテストするには、Suite(CRD)を定義してテストケースを記述します。 Suite には、ConstraintTemplate と Constraint、テスト対象のマニフェスト、期待する結果の 4 つの項目を書きます。

kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests:
  - name: deny-privileged-container
    template: template.yaml ## 1. ConstraintTemplate
    constraint: constraint.yaml ## 2. Constraint
    cases:
      - name: deny-privileged-container
        object: testdata/privileged-container.yaml ## 3. テスト対象のマニフェスト
        assertions: ## 4. 期待する結果
          - violations: 1

gator verify コマンドで作成した Suite ファイルを指定して、ポリシの E2E テストを実行します。

$ gator verify suite.yaml

gator test コマンドを使用することで、任意のマニフェストの検証を実行することもできます。

$ cat my-manifest.yaml | gator test --filename=template-and-constraints/

例えば、以下のような特権コンテナを使用する Pod をデプロイしようとすると、deny-privileged-container ポリシにより、作成が拒否されることが分かります。

apiVersion: v1
kind: Pod
metadata:
  name: denied-pod
  namespace: restricted-namespace
spec:
  containers:
    - name: denied-container
      image: nginx:latest
      securityContext:
        privileged: true ## 特権昇格を許可
$ cat ./gatekeeper/privileged-container/constraint/testdata/denied-pod.yaml | gator test \
    --filename=gatekeeper/privileged-container/constraint/constraint.yaml \
    --filename=gatekeeper/privileged-container/template/template.yaml
v1/Pod restricted-namespace/denied-pod: ["deny-privileged-container"] Message: "privileged-container: apiVersion: v1, kind: Pod, name: denied-pod, container: denied-container; container runs as privileged"

v1/Pod restricted-namespace/denied-pod: ["deny-privileged-container"] Message: "privileged-container: apiVersion: v1, kind: Pod, name: denied-pod, container: denied-container; container runs as privileged"

実際にポリシが適用されているクラスタにデプロイしようとすると、Gatekeeper の Admission Webhook によりリソースの作成がブロックされます。

Error from server (Forbidden): error when creating "./gatekeeper/privileged-container/constraint/testdata/denied-pod.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [deny-privileged-container] privileged-container: apiVersion: v1, kind: Pod, name: denied-pod, container: denied-container; container runs as privileged

admission webhook "validation.gatekeeper.sh" denied the request: [deny-privileged-container] privileged-container: apiVersion: v1, kind: Pod, name: denied-pod, container: denied-container; container runs as privileged

まとめ

今回は OPA(Open Policy Agent)を用いた Kubernetes のポリシ制御について紹介しました。

Kubernetes はその柔軟さ故に、運用規模が拡大すると統制・管理が複雑化してきます。 アプリケーション開発者の運用効率と安全性を確保するためにも、組織全体で一貫したポリシを確立し、ガードレールを設けることが重要になります。 Gatekeeper を使用することで、クラスタに一貫したポリシを適用することができるため、運用の安全性やセキュリティの向上が期待できます。

Gatekeeper によるポリシ運用には学習コストやパフォーマンスへの注意も必要なので、サービスの成熟度や開発体制を鑑みて徐々に制約を追加すると良いと思います。 また、頻繁に使用されるポリシについては、コミュニティで ライブラリ も用意してくれています。


使用したコードは以下のリポジトリにまとめています。

参考・引用