安全なKubernetesクラスタのつくりかた 〜ポリシー編〜

こんにちは、Necoプロジェクトの池添(@zoetro)です。

今回は、安全なKubernetesクラスタを構築するために、我々がどのようなポリシーを適用しているのかを紹介したいと思います。

Kubernetesクラスタのセキュリティ対策

安全なKubernetesクラスタを構築するためには、非常にたくさんの項目について検討しなければなりません。 ざっと挙げてみただけでも以下のような項目があります。(詳細は Kubernetesの公式ガイド を参照)

  • Role-Based Access Control (RBAC)
  • ネットワークアクセスの制御(Network Policy)
  • コンテナの権限(Pod Security Policy)
  • 通信の暗号化
  • Secretの暗号化
  • 信頼できるコンテナイメージの利用
  • 安全なコンテナランタイムの利用
  • ユーザー/グループの管理
  • API ServerのAudit Log
  • KubeletのAuthorization
  • サービスメッシュによるトラフィック制御

これらの項目はそれぞれどのように設定すべきでしょうか?

それを決めるためには、どのような脅威から自分たちのシステムを守りたいのかという基本方針を明確にしておくことが重要です。 例えば基本方針には関係のないセキュリティ対策を実施しても、コストやオーバーヘッドが増えるだけで意味がありません。

今回は Pod Security Policy と Network Policy そして Open Policy Agent に焦点を当てたいと思います。 これらのポリシーを設定することでコンテナができることを制限することができます。 それにより悪意のあるユーザーの攻撃や通常のユーザーの誤りによってコンテナが管理者の意図しない危険なことをしようとした場合に、被害を最小限に食い止めることが可能になります。

ポリシー適用例

本記事では、我々が適用しているポリシーの一例を紹介します。 なお、ここで紹介している例はサイボウズの環境に適したポリシーなので、あくまでも参考として見ていただければと思います。

Pod Security Policy (PSP)

Pod Security Policyとは、 Kubernetes クラスタ上で動作する Pod に与える権限を制御するための機能です。 例えば、以下のような制限を与えることができます。

  • privilegedコンテナの利用禁止
  • Linux Capability による制限
  • 利用可能な Volume の種類を制限
  • hostNetworkの利用禁止
  • rootでのコンテナの実行禁止
  • ルートファイルシステムの書き込み禁止

Necoではクラスタ全体としてはほとんど何もできないようにしておいて、個々のアプリについて必要最小限の権限を与えるようにしています。

まずは、もっとも厳しい制限として下記のようなポリシーを用意します。

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  # 特権コンテナの実行を禁止
  privileged: false
  # 親プロセスよりも強い権限を許可しない
  allowPrivilegeEscalation: false
  # すべてのCapabilityをDROPする
  requiredDropCapabilities:
    - ALL
  # 利用可能なVolumeを制限
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  # ホストとネットワーク、プロセス間通信、プロセスIDの共有を許可しない
  hostNetwork: false
  hostIPC: false
  hostPID: false
  # rootでの実行を許可しない
  runAsUser:
    rule: 'MustRunAsNonRoot'
  # 任意のSELinuxのラベルを利用可能
  seLinux:
    rule: 'RunAsAny'
  # コンテナが追加可能なグループIDの範囲を指定 (rootを禁止)
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  # ボリュームに適用されるグループIDの範囲を指定 (rootを禁止)
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  # ルートファイルシステムへの書き込みを禁止
  readOnlyRootFilesystem: true

上記のポリシーを、すべてのユーザーとアカウントサービスに適用されるように ClusterRoleとClusterRoleBindingを用意します。

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: psp:restricted
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - restricted
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: default:psp:restricted
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
roleRef:
  kind: ClusterRole
  name: psp:restricted
  apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated

このポリシーは非常に厳しい制限がかかっています。 Helm Chartで公開されているマニフェストをそのまま持ってくると、多くの場合動作しないでしょう。

例えば、ネットワークプラグインを動作させるためには hostNetwork の利用が必要になりますし、 DNSサーバーを動かすためには NET_BIND_SERVICE を許可して、53番ポートで待ち受けられるようにする必要があります。

そこで、各アプリケーションごとに制限を緩めたポリシーを用意して適用していきます。 例えば、DNS Cacheのunboundには以下のようなポリシーを適用しています。

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: unbound
spec:
  allowPrivilegeEscalation: false
  # 利用可能なCapabilityを追加
  allowedCapabilities:
    - NET_BIND_SERVICE
  volumes:
    - 'configMap'
    - 'emptyDir'
  hostNetwork: false
  runAsUser:
    rule: 'RunAsAny'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'
  readOnlyRootFilesystem: true

Network Policy

Network Policyは、Pod間の通信や Podと他のネットワークエンドポイントとの通信の可否を制御するための仕組みです。 なお、Network Policyの機能を利用するためには何らかのネットワークプラグインが必要となります。

我々は Calico Network Policy を利用しています*1

Calico Network Policy のカスタムリソースは、Kubernetes 標準のNetwork Policyと比較してより高度な機能を提供しています。 例えば、orderフィールドを利用してネットワークポリシーの適用順序を制御することができます。

Network Policyでは、Podに入ってくるトラフィックを制御するためのIngressと、Podから出ていくトラフィックを制御するためのEgressという2種類の設定があります。

Ingress

NecoのIngress ルールでは、基本的にデータセンター内の通信であれば許可し、データーセンター外からの通信は拒否しています。

Ingress Rule

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: ingress-cluster-allow
spec:
  order: 9000.0
  types:
    - Ingress
  ingress:
    - action: Allow
      source:
        nets:
          - 10.0.0.0/8
---
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: ingress-all-deny
spec:
  order: 10000.0
  types:
    - Ingress
  ingress:
    - action: Log
    - action: Deny

Ingress という namespace を用意し、インターネット経由でアクセスされる Pod を配置します。 L7ロードバランサーである contour には下記のようなポリシーを適用し、インターネットからのアクセスを許可します。

apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: ingress-contour
  namespace: ingress
spec:
  order: 1000.0
  selector: app.kubernetes.io/name == 'contour'
  types:
    - Ingress
  ingress:
    - action: Allow
      protocol: TCP
      destination:
        ports:
          - 8080
          - 8443

なお、他のnamespaceからのアクセスを遮断したいケースもあるでしょう。 そういう場合は、各namespaceごとに通信を受け入れない設定を適用します。

Egress

一方の Egress ルールでは、Pod から Node へのアクセスを禁止していますが、それ以外は基本的に許可しています。

Egress Rule

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: egress-node-deny
spec:
  order: 1000.0
  types:
    - Egress
  egress:
    - action: Deny
      destination:
        nets:
          - 10.69.0.0/16
---
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: egress-all-allow
spec:
  order: 10000.0
  types:
    - Egress
  egress:
    - action: Allow

ただし、Nodeへのアクセスであっても、kube-apiserver(6443ポート)やetcd(2379ポート), DNS Cache(53ポート)については、下記のようにアクセスを許可します。

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: egress-controlplane-allow
spec:
  order: 500.0
  types:
    - Egress
  egress:
    - action: Allow
      protocol: UDP
      destination:
        nets:
          - 10.69.0.0/16
        ports:
          - 53
    - action: Allow
      protocol: TCP
      destination:
        nets:
          - 10.69.0.0/16
        ports:
          - 53
          - 2379
          - 6443

また、インターネットにアクセス可能なPodを配置するために Internet-Egress という namespace を用意し、 Web Proxy である squid や DNS キャッシュサーバである unbound を配置しています。 この namespace から他の namespace やホストへの通信を禁止しています。

Open Policy Agent

RBACとPodSecurityPolicy, NetworkPolicyを適用することで、Podに対して様々な制限をかけることができます。 しかし、これらのポリシーだけでは以下のような細かな制限をおこなうことはできません。

  • コンテナイメージを取得するレジストリを制限したい
  • system namespaceへのアクセスを制限したい
  • 一部のクラスタリソースの操作を許可したい
  • すべてのPodにResource Limitの付与を強制したい

そこで我々はOpenPolicyAgentを利用して、より柔軟なポリシーを適用しています。

www.openpolicyagent.org

OpenPolicyAgentでは、Regoという独自の言語でポリシーを記述することができます。

例えば先程紹介したNetworkPolicyでは、orderフィールドでポリシーの適用順序を指定していました。 開発者が適用順序の早いポリシーを作成できてしまうと、本来適用したいポリシーが無視されてしまいます。

そこで以下のようなルールを用意し、orderが1,000以下のNetworkPolicyの作成や更新を禁止することで、重要なNetworkPolicyを必ず適用することが可能となります。

package kubernetes.admission

operations = {"CREATE", "UPDATE"}
system_namespaces = {"kube-system", "ingress", "internet-egress"}

deny[msg] {
    input.request.kind.kind == "NetworkPolicy"
    input.request.kind.group == "crd.projectcalico.org"
    operations[input.request.operation]
    not system_namespaces[input.request.namespace]
    input.request.object.spec.order <= 1000
    msg := "cannot create/update non-system NetworkPolicy with order <= 1000"
}

OpenPolicyAgent の活用方法については、機会があれば別途紹介したいと思います。

まとめ

Kubernetesクラスタをセキュアにするためのポリシーとして、 Pod Security Policy, Network Policy, OpenPolicyAgentの適用例を紹介しました。

なお、このポリシーを適用していればどんな環境でもセキュアで安心というわけではありません。 本記事の例を参考に、それぞれの環境に適したポリシーを適用していただければと思います。

また、Kubernetesにデプロイされているアプリケーションが増えれば増えるほど、 あとからポリシーを適用するのは非常に大変になります。 ポリシーを適用したいと考えているのであれば、早い段階での適用をおすすめします。

*1:Necoでは自作のネットワークプラグインcoilを利用していますが、coilはネットワークポリシーを実装していないため。