Kubernetes のセキュリティ周りを調べていたので、そのメモです。
間違いや追加の情報があれば @mrtc0 までお願いします。

Kubernetes Version

ServiceAccount Token の Auto Mount を無効する

ServiceAccount の token や証明書は Pod 内の /var/run/secrets/kubernetes.io/serviceaccounts/ 配下に自動でマウントされるようになっています。
もし Pod が侵害された場合は Kubernetes API を操作されることになります。

root@ubuntu:/var/run/secrets/kubernetes.io/serviceaccount# pwd
/var/run/secrets/kubernetes.io/serviceaccount
root@ubuntu:/var/run/secrets/kubernetes.io/serviceaccount# ls
ca.crt  namespace  token

root@ubuntu:/var/run/secrets/kubernetes.io/serviceaccount# KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
root@ubuntu:/var/run/secrets/kubernetes.io/serviceaccount# curl -sSk -H "Authorization: Bearer $KUBE_TOKEN" \
https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_PORT_443_TCP_PORT/api/v1/namespaces/default/pods/$HOSTNAME
{
  "kind": "Pod",
  "apiVersion": "v1",
  "metadata": {
    "name": "ubuntu",
    "namespace": "default",
    "selfLink": "/api/v1/namespaces/default/pods/ubuntu",
    ...

これを Pod 単位で防ぐには spec に automountServiceAccountToken: false を追加します。

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  serviceAccountName: build-robot
  automountServiceAccountToken: false
  ...

クラスタから制御したい場合は ServiceAccount に automountServiceAccountToken: false を追加します。
もし、Pod から token を使いたい場合は Service Account Token Volume Projection で明示的にマウントするという手段があります。

kubelet の引数に次の3つの引数を加えます。

    - --service-account-issuer=api
    - --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
    - --service-account-api-audiences=api

ServiceAccount には automountServiceAccountToken: false を追加します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
automountServiceAccountToken: false
...

この時点で token は自動マウントされなくなりました。
もし Pod から token を利用したい場合は、次のように Projected Volume で token をマウントします。

kind: Pod
apiVersion: v1
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: token
  serviceAccountName: default
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 600
          audience: api

expirationSeconds は token の有効期限です。これは10分以上でないといけないようです。
token が漏洩した場合のケースを考えて10分にしておくといいでしょう。
ちなみにマウントされている token は自動で refresh されるので、その点は気にしなくて良いです。

API を誰でも叩ける状態にしない

kube-apiserver / kubelet の引数に --anonymous-auth=true--authorization-mode=AlwaysAllow を付与している場合、Pod から master / node へで任意コード実行が可能です。

$ # 172.16.20.100 = kubernetes master IP address
root@ubuntu:/# curl -k https://172.16.20.100:10250/run/kube-system/kube-apiserver-k8s-master/kube-apiserver -X POST -d "cmd=whoami"
root
root@ubuntu:/# curl -k https://172.16.20.100:10250/run/kube-system/kube-apiserver-k8s-master/kube-apiserver -X POST -d "cmd=uname -a"
Linux k8s-master 4.15.0-29-generic #31-Ubuntu SMP Tue Jul 17 15:39:52 UTC 2018 x86_64 GNU/Linux
root@ubuntu:/# curl -k https://172.16.20.100:10250/run/kube-system/kube-apiserver-k8s-master/kube-apiserver -X POST -d "cmd=ls /etc/kubernetes/pki"
apiserver-etcd-client.crt
apiserver-etcd-client.key
apiserver-kubelet-client.crt
apiserver-kubelet-client.key
apiserver.crt
apiserver.key
ca.crt
ca.key
etcd
front-proxy-ca.crt
front-proxy-ca.key
front-proxy-client.crt
front-proxy-client.key
sa.key
sa.pub
$ # apiserver-kubelet-client.crt と apiserver-kubelet-client.key を取得する
$ curl -iv -k --cert apiserver-kubelet-client.crt --key apisever-kubelet-client.key https://127.0.0.1:6443/healthz
HTTP/2 200
< content-type: text/plain; charset=utf-8
content-type: text/plain; charset=utf-8
< x-content-type-options: nosniff
x-content-type-options: nosniff
< content-length: 2
content-length: 2
< date: Fri, 19 Jul 2019 06:56:35 GMT
date: Fri, 19 Jul 2019 06:56:35 GMT

<
* Connection #0 to host 127.0.0.1 left intact
ok

これを防ぐためには次の点対策を行います。

AlwaysPullImages を利用する

Kubernetes はコンテナイメージをキャッシュしています。
そのため、古いイメージが利用されるのを防ぐために Admission Controller に AlwaysPullImages を指定します。
すると Pod の imagePullPolicy が Always に強制的に変更されます。

PodSecurityPolicy を利用する

Pod からの権限昇格を防ぐために Security Policy を適用します。
これは特権コンテナを許可するかやホスト上のファイルのマウントの制御などができます。

特権コンテナや過剰に権限を与えたコンテナの場合、どのような影響が生じるかは以下のスライドを参照してください。

また、ホスト上のファイルのマウントに制限を施していない場合、次のように node の root を取得することが可能です。

apiVersion: v1
kind: Pod
metadata:
  name: noderootpod
  labels:
spec:
  hostNetwork: true
  hostPID: true
  hostIPC: true
  containers:
  - name: noderootpod
    image: busybox
    securityContext:
      privileged: true
    volumeMounts:
    - mountPath: /host
      name: noderoot
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 30; done;" ]
  volumes:
  - name: noderoot
    hostPath:
      path: /
$ kubectl exec -it noderootpod sh
# chroot /host
root@k8s-node-1:/# id
uid=0(root) gid=0(root) groups=0(root),10(uucp)
root@k8s-node-1:/# uname -a
Linux k8s-node-1 4.4.0-140-generic #166-Ubuntu SMP Wed Nov 14 20:09:47 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

初めての PodSecurityPolicy として次のようなものが適用しやすいでしょう。

apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'runtime/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
  privileged: false
  hostPID: false
  hostIPC: false
  hostNetwork: false
  allowedHostPaths: []
  allowedCapabilities: []
  requiredDropCapabilities: [
    "AUDIT_WRITE",
    "CHOWN",
    "DAC_OVERRIDE",
    "DAC_READ_SEARCH",
    "FOWNER",
    "FSETID",
    "KILL",
    "MKNOD",
    "NET_RAW",
    "SETFCAP",
    "SETGID",
    "SETPCAP",
    "SETUID",
    "SYS_CHROOT",
    "SYS_PTRACE",
    "SYS_RAWIO",
    "SYS_RESOURCE",
    "SYSLOG"
  ]
  forbiddenSysctls: ['*']
  seLinux:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  volumes: ['secret', 'projected', 'configMap'] 

上記はあくまで一個人の考えです。環境によって Policy は異なると思います。

この PodSecurityPolicy を ClusterRole に紐づけることで機能します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: member
rules:
- apiGroups: ["extensions"]
  resources: ["podsecuritypolicies"]
  resourceNames: ["psp"]
  verbs: ["use"]

PodSecurityPolicy が利用できない場合は AdmissionController で DenyEscalatingExecSecurityContextDeny だけでも有効化しておくと良いでしょう。

EventRateLimit を設定する

Admission Contoller で EventRateLimit を設定します。
これにより API へのイベントリクエストを制御し、可用性を確保します。

aduit ログを取得する

いつ誰が何をどのように操作したかを記録します。
詳細は https://blog.ssrf.in/audit-log-for-kubernetes/ を参照してください。

(結構ログが多いので何を記録する必要があるか、という見極めが大変ですが…)

etcd へのアクセスを制御する

etcd への書き込み可能 = master で root になるのと同義であるため、Pod からの接続は拒否、接続には証明書認証を使うようにしましょう。

etcd の暗号化を行う

etcd は平分でデータを保持しているため、暗号化するとより堅牢でしょう。

その他のログ

eBPF を使ってコンテナでどのようなコマンドが実行されたか、どのようなファイルがオープンされたかの記録も必要になるケースもあると思います。
その場合、bcc を使って簡易的にツールを作ることもできますし、 sysdigfalco などの利用も良いでしょう。

アプリケーションレイヤでの対策

GKE の場合、以下のエンドポイントにアクセスすることで Kubernetes へのアクセスに必要な証明書等を取得できます。
そのため SSRF 等の脆弱性があると、インスタンスへのアクセス等を許してしまいます。

$ curl http://metadata.google.internal/computeMetadata/v1beta1/instance/attributes/kube-env?alt=json
"ALLOCATE_NODE_CIDRS: \"true\"\nCA_CERT: XXXXXX...

ちなみにこの攻撃例は Shopify で実際にありました。
詳細は https://blog.ssrf.in/post/example-of-attack-on-gce-and-gke-instance-using-ssrf-vulnerability/ を参照してください。


Kubernetes のセキュリティをチェックするためのツール

これらを自動で、あるいは継続的にチェックするためのツールがあるのでメモ。

kube-bench

CIS Kubernetes Benchmark に基づいてクラスタのセキュリティ監査を行う

kubeaudit

現在動いている Pod に対してセキュリティ監査を行う

kubesec

Pod の yaml から脆弱な設定がないかを確認する。kubeaudit にも同様の機能はあるので、kubeaudit で良さそう。
ところで kubesec は https://github.com/shyiko/kubesec もあるのがつらい。

kube-hunter

Job で Pod を動かして実際にクラスタに攻撃できるかをチェックしてくれる。