kakakakakku blog

Weekly Tech Blog : Keep on Learning 👍

Kubernetes と OPA を組み合わせて Deployment の spec.replicas に対して「最低個数」を制限する

前回の記事では Kubernetes と OPA (Open Policy Agent) を組み合わせて「ラベル強制」を試した.OPA を使ってできることをより把握するために,今回は Deployment の spec.replicas に対して「最低個数」を制限できるようにする.なお OPA のインストール手順などは前回の記事を参照してもらえればと!Kubernetes v1.20 を使う.

kakakakakku.hatenablog.com

CRD k8sminreplicas.constraints.gatekeeper.sh を追加する

さっそく「レプリカ最低個数」のために必要な設定を適用する.まず,ConstraintTemplate からレプリカ数を判定する CRD として K8sMinReplicas を作る.強制するためには Rego を使って実装をする必要があり,今回は以下の k8sminreplicas.yaml を使う.簡単に言うと,制限する parameters.min とマニフェストの spec.replicas を比較している.

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8sminreplicas
spec:
  crd:
    spec:
      names:
        kind: K8sMinReplicas
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            min:
              type: integer
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sminreplicas
        violation[{"msg": msg, "details": {"missing_replicas": missing}}] {
          provided := input.review.object.spec.replicas
          required := input.parameters.min
          missing := required - provided
          missing > 0
          msg := sprintf("you must provide %v more replicas", [missing])
        }

kubectl apply コマンドで k8sminreplicas.yaml を適用すると,新しく CRD として k8sminreplicas.constraints.gatekeeper.sh が追加された.準備 OK👌

$ kubectl apply -f k8sminreplicas.yaml
constrainttemplate.templates.gatekeeper.sh/k8sminreplicas created

$ kubectl get crds
NAME                                                 CREATED AT
configs.config.gatekeeper.sh                         2021-08-02T05:00:00Z
constraintpodstatuses.status.gatekeeper.sh           2021-08-02T05:00:00Z
constrainttemplatepodstatuses.status.gatekeeper.sh   2021-08-02T05:00:00Z
constrainttemplates.templates.gatekeeper.sh          2021-08-02T05:00:00Z
k8sminreplicas.constraints.gatekeeper.sh             2021-08-02T05:10:00Z

Deploymet で「レプリカ最低個数」を試す

Deploymet で「レプリカ最低個数」の制限を試す.以下のように K8sMinReplicas でマニフェストを作る.Rego で実装した通り parameters.min に最低個数 2 を設定している.よって spec.replicas = 1 だとエラーになる.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sMinReplicas
metadata:
  name: deployment-must-have-min-replicas
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
  parameters:
    min: 2

実際に deployment-must-have-min-replicas.yaml を適用する.

$ kubectl apply -f deployment-must-have-min-replicas.yaml
k8sminreplicas.constraints.gatekeeper.sh/deployment-must-have-min-replicas created

さっそく以下の2種類のマニフェストを適用する.

  • 🆖 my-nginx1.yaml ... spec.replicas = 1
  • 🆗 my-nginx2.yaml ... spec.replicas = 2

📁 my-nginx1.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-nginx
  template:
    metadata:
      labels:
        app: my-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21-alpine

📁 my-nginx2.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-nginx
  template:
    metadata:
      labels:
        app: my-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21-alpine

結果として my-nginx1.yaml は「レプリカ最低個数」の制限でエラーになる.violation に設定した you must provide 1 more replicas というエラーも出ている.

$ kubectl apply -f my-nginx1.yaml
Error from server ([deployment-must-have-min-replicas] you must provide 1 more replicas): error when creating "nginx.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [deployment-must-have-min-replicas] you must provide 1 more replicas

$ kubectl apply -f my-nginx2.yaml
deployment.apps/my-nginx created

まとめ

前回に引き続き OPA (Open Policy Agent) を試した.Deployment の spec.replicas に対して「最低個数」を制限できるようにした.実践的に使えそうなポリシーだと思う.現状まだ Rego をスラスラと書くことができず,サンプルなどをコピーしているため,より複雑なポリシーなどを書けるように勉強を続けていくぞ!

Kubernetes と OPA を組み合わせて Namespace と Pod の「ラベル強制」を試した

今回は Kubernetes と OPA (Open Policy Agent) を組み合わせて「ラベル強制」を試す.

OPA Gatekeeper をインストールする

まず,Kubernetes クラスターに OPA Gatekeeper をインストールする.今回は OPA のドキュメントに載っている kubectl apply コマンドを使う.他にも Helm を使うこともできる.

open-policy-agent.github.io

kubectl apply コマンドでインストールをすると,多くの Kubernetes オブジェクトが適用される.重要なのは CRD (Custom Resource Definition) で,以下の4種類の CRD が適用されている.

  • configs.config.gatekeeper.sh(kind: Config)
  • constraintpodstatuses.status.gatekeeper.sh(kind: ConstraintPodStatus)
  • constrainttemplatepodstatuses.status.gatekeeper.sh(kind: ConstraintTemplatePodStatus)
  • constrainttemplates.templates.gatekeeper.sh(kind: ConstraintTemplate)
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.5/deploy/gatekeeper.yaml
namespace/gatekeeper-system created
resourcequota/gatekeeper-critical-pods created
customresourcedefinition.apiextensions.k8s.io/configs.config.gatekeeper.sh created
customresourcedefinition.apiextensions.k8s.io/constraintpodstatuses.status.gatekeeper.sh created
customresourcedefinition.apiextensions.k8s.io/constrainttemplatepodstatuses.status.gatekeeper.sh created
customresourcedefinition.apiextensions.k8s.io/constrainttemplates.templates.gatekeeper.sh created
serviceaccount/gatekeeper-admin created
podsecuritypolicy.policy/gatekeeper-admin created
role.rbac.authorization.k8s.io/gatekeeper-manager-role created
clusterrole.rbac.authorization.k8s.io/gatekeeper-manager-role created
rolebinding.rbac.authorization.k8s.io/gatekeeper-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/gatekeeper-manager-rolebinding created
secret/gatekeeper-webhook-server-cert created
service/gatekeeper-webhook-service created
deployment.apps/gatekeeper-audit created
deployment.apps/gatekeeper-controller-manager created
poddisruptionbudget.policy/gatekeeper-controller-manager created
validatingwebhookconfiguration.admissionregistration.k8s.io/gatekeeper-validating-webhook-configuration created

$ kubectl get crds
NAME                                                 CREATED AT
configs.config.gatekeeper.sh                         2021-07-05T07:00:00Z
constraintpodstatuses.status.gatekeeper.sh           2021-07-05T07:00:00Z
constrainttemplatepodstatuses.status.gatekeeper.sh   2021-07-05T07:00:00Z
constrainttemplates.templates.gatekeeper.sh          2021-07-05T07:00:00Z

CRD k8srequiredlabels.constraints.gatekeeper.sh を追加する

さっそく「ラベル強制」のために必要な設定を適用する.まず,ConstraintTemplate から「ラベル強制」をする CRD として K8sRequiredLabels を作る.強制するためには Rego を使って実装をする必要があるけど,詳細は割愛して,今回は OPA の GitHub リポジトリに公開されているサンプルとして,k8srequiredlabels_template.yaml をそのまま使う.

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }

kubectl apply コマンドで k8srequiredlabels_template.yaml を適用すると,新しく CRD として k8srequiredlabels.constraints.gatekeeper.sh が追加された.

$ kubectl apply -f k8srequiredlabels_template.yaml

$ kubectl get crds
NAME                                                 CREATED AT
configs.config.gatekeeper.sh                         2021-07-05T00:00:00Z
constraintpodstatuses.status.gatekeeper.sh           2021-07-05T00:00:00Z
constrainttemplatepodstatuses.status.gatekeeper.sh   2021-07-05T00:00:00Z
constrainttemplates.templates.gatekeeper.sh          2021-07-05T00:00:00Z
k8srequiredlabels.constraints.gatekeeper.sh          2021-07-05T00:01:00Z

Namespace で「ラベル強制」を試す

まず,Namespace で「ラベル強制」を試す.以下のように K8sRequiredLabels でマニフェストを作る.ポイントは Namespace に対して app ラベルを強制するため,もし app ラベルが付いていない場合はリソースの作成に失敗する.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: namespace-app-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["app"]

実際に namespace-app-label.yaml を適用する.

$ kubectl apply -f namespace-app-label.yaml

$ kubectl get k8srequiredlabels.constraints.gatekeeper.sh 
NAME                  AGE
namespace-app-label   30s

さっそく以下の3種類のマニフェストを適用する.

  • 🆖 ns1.yaml ... app ラベルなし
  • 🆖 ns2.yaml ... app ラベルなし(env ラベルあり)
  • 🆗 ns3.yaml ... app ラベルあり(env ラベルあり)

📁 ns1.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: kakakakakku-ns

📁 ns2.yaml

apiVersion: v1
kind: Namespace
metadata:
  labels:
    env: dev
  name: kakakakakku-ns

📁 ns3.yaml

apiVersion: v1
kind: Namespace
metadata:
  labels:
    app: my-app
    env: dev
  name: kakakakakku-ns

結果として ns1.yaml と ns2.yaml は「ラベル強制」の結果エラーになる.you must provide labels: {"app"} というエラーも出ている.

$ kubectl apply -f ns1.yaml
Error from server ([namespace-app-label] you must provide labels: {"app"}): error when creating "ns1.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [namespace-app-label] you must provide labels: {"app"}

$ kubectl apply -f ns2.yaml
Error from server ([namespace-app-label] you must provide labels: {"app"}): error when creating "ns2.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [namespace-app-label] you must provide labels: {"app"}

$ kubectl apply -f ns3.yaml
namespace/kakakakakku-ns created

Pod で「ラベル強制」を試す

次に,Pod で「ラベル強制」を試す.同じく K8sRequiredLabels でマニフェストを作る.ポイントは Pod に対して app ラベルを強制するため,もし app ラベルが付いていない場合はリソースの作成に失敗する.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: pod-app-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    labels: ["app"]

実際に pod-app-label.yaml を適用する.

$ kubectl apply -f pod-app-label.yaml

$ kubectl get k8srequiredlabels.constraints.gatekeeper.sh
NAME                  AGE
namespace-app-label   15m
pod-app-label         30s

さっそく以下の3種類のマニフェストを適用する.

  • 🆖 pod1.yaml ... app ラベルなし
  • 🆖 pod2.yaml ... app ラベルなし(env ラベルあり)
  • 🆗 pod3.yaml ... app ラベルあり(env ラベルあり)

📁 pod1.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kakakakakku-pod
spec:
  containers:
    - name: nginx
      image: nginx:1.21

📁 pod2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kakakakakku-pod
  labels:
    env: dev
spec:
  containers:
    - name: nginx
      image: nginx:1.21

📁 pod3.yaml

apiVersion: v1
kind: Pod
metadata:
  name: kakakakakku-pod
  labels:
    app: my-app
    env: dev
spec:
  containers:
    - name: nginx
      image: nginx:1.21

結果として pod1.yaml と pod2.yaml は「ラベル強制」の結果エラーになる.you must provide labels: {"app"} というエラーも出ている.

$ kubectl apply -f pod1.yaml
Error from server ([pod-app-label] you must provide labels: {"app"}): error when creating "pod1.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [pod-app-label] you must provide labels: {"app"}

$ kubectl apply -f pod2.yaml
Error from server ([pod-app-label] you must provide labels: {"app"}): error when creating "pod2.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [pod-app-label] you must provide labels: {"app"}

$ kubectl apply -f pod3.yaml
pod/kakakakakku-pod created

まとめ

今回は Kubernetes と OPA (Open Policy Agent) を組み合わせて「ラベル強制」を試した.Namespace と Pod に対して,もし app ラベルが付いていない場合はリソースの作成に失敗するようにできた.引き続き OPA を試していくぞ!

docker run コマンドの --pid オプションで PID namespace を共有する

docker run コマンドで使える --pid オプションを試す.Docker では以下のドキュメントに書いてある通り,デフォルトでは PID namespace でコンテナ同士を隔離する.よって,コンテナ同士でプロセスを共有することはできず,各コンテナでは PID 1 を含む「プロセス ID」を再利用できるようになっている.今回は一歩一歩確認しながら進めていく.

docs.docker.com

PID 1 を確認する

まず,デフォルトの挙動を確認する.以下のように docker run コマンドで nginx コンテナを起動する.そして docker exec コマンドでコンテナに接続をしてプロセス一覧を確認すると,nginx プロセスが PID 1 になっていることがわかる.なお,Alpine Linux なので ps コマンドを実行できるようにしておく必要がある.

$ docker run --rm -d -it nginx:1.21
9313d4a4fc73cf2a173569d3f5f76595169c1577b242b9cf7b019afe32785df0

$ docker exec -it 9313d4a4fc73 /bin/sh

# apt update
# apt install -y procps

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 01:34 pts/0    00:00:00 nginx: master process nginx -g daemon off;
nginx       32     1  0 01:34 pts/0    00:00:00 nginx: worker process
nginx       33     1  0 01:34 pts/0    00:00:00 nginx: worker process
nginx       34     1  0 01:34 pts/0    00:00:00 nginx: worker process
nginx       35     1  0 01:34 pts/0    00:00:00 nginx: worker process
root        36     0  0 01:35 pts/1    00:00:00 /bin/sh
root       364    36  0 01:36 pts/1    00:00:00 ps -ef

次に docker run コマンドで新しく Ubuntu コンテナを起動する(今回は sleep コマンドでプロセスを常駐させておく).同じく docker exec コマンドでコンテナに接続をしてプロセス一覧を確認すると,sleep プロセスが PID 1 になっていることがわかる.よって,nginx も Ubuntu もコンテナ同士で隔離されていると言える.

$ docker run --rm -d -it ubuntu:21.10 /bin/sh -c 'sleep 300'
5c754ed1f167ece55849ac5760d902782f562b6815c2fff05dc472c1cb54bc35

$ docker exec -it 5c754ed1f167 /bin/sh

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 01:31 pts/0    00:00:00 /bin/sh -c sleep 300
root         8     1  0 01:31 pts/0    00:00:00 sleep 100
root         9     0  0 01:32 pts/1    00:00:00 /bin/sh
root        17     9  0 01:32 pts/1    00:00:00 ps -ef

--pid オプション

次に docker run コマンドの --pid オプションを試す.既に起動している nginx コンテナ(9313d4a4fc73)の PID namespace を共有した Ubuntu コンテナを起動するために --pid=container:9313d4a4fc73 のようにオプションを指定する.

$ docker run --rm -d --pid=container:9313d4a4fc73 -it ubuntu:21.10 /bin/sh -c 'sleep 300'

同じく docker exec コマンドで Ubuntu に接続をしてプロセス一覧を確認すると,nginx コンテナのプロセスを確認できるようになった.そして Ubuntu コンテナの sleep プロセスが PID 372 となり,PID 1 ではなくなっている.よって,コンテナ同士で PID namespace を共有できていると言える.

$ docker exec -it 06bbc0fb87f3 /bin/sh

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 01:34 pts/0    00:00:00 nginx: master process nginx -g daemon off;
101         32     1  0 01:34 pts/0    00:00:00 nginx: worker process
101         33     1  0 01:34 pts/0    00:00:00 nginx: worker process
101         34     1  0 01:34 pts/0    00:00:00 nginx: worker process
101         35     1  0 01:34 pts/0    00:00:00 nginx: worker process
root       372     0  0 02:03 pts/0    00:00:00 /bin/sh -c sleep 300
root       379   372  0 02:03 pts/0    00:00:00 sleep 300
root       380     0  0 02:04 pts/1    00:00:00 /bin/sh
root       386   380  0 02:04 pts/1    00:00:00 ps -ef

--pid オプションの活用例

では --pid オプションをどのように活用すれば良いかと言うと「サイドカー」のように使える.書籍「分散システムデザインパターン」の「2章 : サイドカー」で brendanburns/topz イメージを使った例が載っているので,参考にしながら試していく.

既に起動している nginx コンテナ(9313d4a4fc73)の PID namespace を共有した topz コンテナを起動する.そして http://localhost:8080/topz にリクエストを送ると,nginx コンテナのプロセス一覧とリソース使用量を確認できる.このように「プロセスのモニタリング機能」を --pid オプションで「サイドカー」として追加できるという活用に繋がる.

$ docker run --rm -d --pid=container:9313d4a4fc73 -p 8080:8080 brendanburns/topz:db0fa58 /server --addr=0.0.0.0:8080

$ curl http://localhost:8080/topz
1  0 0.21145721 nginx: master process nginx -g daemon off;
32 0 0.11457208 nginx: worker process
33 0 0.11457208 nginx: worker process
34 0 0.11457208 nginx: worker process
35 0 0.11457208 nginx: worker process
36 0 0.17175984 /server --addr=0.0.0.0:8080

Docker Compose と pid オプション

今度は Docker Compose の場合にどうなるんだろう?と気になるため PID namespace を確認する.以下のように nginx コンテナと Ubuntu コンテナを含めた docker-compose.yml を作る.

version: '3.9'
services:
  nginx:
    image: nginx:1.21
  ubuntu:
    image: ubuntu:21.10
    command: sleep 300

そして docker compose up コマンドでコンテナを起動して docker compose exec コマンドで Ubuntu コンテナに接続をしてプロセス一覧を確認すると sleep プロセスが PID 1 になっている.よって,デフォルトでは PID namespace でコンテナ同士を隔離していると言える.

なお,docker-compose.yml に pid: "host" を追加すると「ホスト側」と PID namespace を共有できる.実際に使う機会は少なそうだけど,ドキュメントに載っていた.そしてコンテナ同士での PID namespace 共有は Compose file version 2 reference のドキュメントには載っていたけど,Compose file version 3 reference では消えていた.

$ docker compose up

$ docker compose exec ubuntu /bin/sh

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 02:51 ?        00:00:00 sleep 300
root         7     0  0 02:52 pts/0    00:00:00 /bin/sh
root        14     7  0 02:52 pts/0    00:00:00 ps -ef

docs.docker.com

まとめ

docker run コマンドで使える --pid オプションを試した.デフォルトでは PID namespace でコンテナ同士を隔離するけど,--pid オプションで PID namespace を共有できるようになる.そして,書籍「分散システムデザインパターン」にも載っている通り,「サイドカー」のように活用することができる.

関連記事

書籍「分散システムデザインパターン」の書評記事は以下にある.合わせて読んでもらえると!

kakakakakku.hatenablog.com

また docker run コマンドには多くのオプションがある.今月公開した以下の記事では --security-opt オプションを紹介している.

kakakakakku.hatenablog.com

kube-apiserver で「匿名リクエスト」を無効化する : --anonymous-auth=false

Kubernetes の kube-apiserver では,デフォルトで「匿名リクエスト」機能が有効になっている(正確な条件は以下のドキュメントに載っている).今回は「匿名リクエスト」機能の動作確認と kube-apiserver のセキュリティ対策として「匿名リクエスト」機能の無効化を試す.検証環境は Kubernetes v1.20.2 とする.

kubernetes.io

「匿名リクエスト」機能

Kubernetes API にリクエストをする場合,通常は HTTP Header の Authorization に Bearer Token を付ける.以下のドキュメントを参考に Kubernetes API(今回の検証環境では https://172.17.0.12:8443/api)にリクエストをすると,うまく実行できる.

kubernetes.io

$ TOKEN=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.token}" | base64 --decode)

$ curl -s -k https://172.17.0.12:8443/api --header "Authorization: Bearer ${TOKEN}"
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "172.17.0.12:8443"
    }
  ]
}

今度は Bearer Token を付けずに Kubernetes API にリクエストをすると HTTP 403 Forbidden となる.これを「匿名リクエスト」と言う.エラーメッセージに載っている通り,RBAC としては「匿名リクエスト」を system:anonymous User として権限を確認する.もしくは system:unauthenticated Group として権限を確認する.

$ curl -s -k https://172.17.0.12:8443/api
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
  "reason": "Forbidden",
  "details": {
    
  },
  "code": 403
}

$ curl -s -k https://172.17.0.12:8443/api/v1/namespaces
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "namespaces is forbidden: User \"system:anonymous\" cannot list resource \"namespaces\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
    "kind": "namespaces"
  },
  "code": 403
}

system:anonymous User に権限を付与する

「匿名リクエスト」機能の動作確認をするために system:anonymous User に権限を付与する.以下のように Namespace に Read 権限を付与する ClusterRole と ClusterRoleBinding を作る.ClusterRoleBinding で system:anonymous User と紐付けている.

ClusterRole

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-cluster-role
rules:
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get", "list"]

ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-cluster-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: my-cluster-role
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:anonymous

もう1度 Bearer Token を付けずに Kubernetes API にリクエストをすると,今度は Namespace を確認できるようになった.ここまでで「匿名リクエスト」機能の動作確認ができた.

$ curl -s -k https://172.17.0.12:8443/api/v1/namespaces | jq .items[].metadata.name
"default"
"kube-node-lease"
"kube-public"
"kube-system"

「匿名リクエスト」機能を無効化する

とは言え,基本的には「匿名リクエスト」に権限を付与する必要はなく,kube-apiserver に --anonymous-auth=false オプションを追加すれば「匿名リクエスト」機能を無効化できる.なお「匿名リクエスト」機能の無効化は以下のように CIS Kubernetes V1.20 Benchmark でも推奨されている.

1.2.1 Ensure that the --anonymous-auth argument is set to false (Manual)

さっそく /etc/kubernetes/manifests/kube-apiserver.yaml を修正して --anonymous-auth=false を追加する.すると Static Pod によってすぐに反映される.以下には差分を抜粋して載せておく.

(中略)

spec:
  containers:
  - command:
    - kube-apiserver
+   - --anonymous-auth=false
    - --advertise-address=172.17.0.80
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC

(中略)

なお,kube-apiserver には他にも多くオプションがある.詳しくは以下のドキュメントに載っている.

kubernetes.io

もう1度 Bearer Token を付けずに Kubernetes API にリクエストをすると,今度は HTTP 401 Unauthorized となる.「匿名リクエスト」は拒否されるようになった.なるほど!

$ curl -s -k https://172.17.0.12:8443/api
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "Unauthorized",
  "reason": "Unauthorized",
  "code": 401
}

まとめ

Kubernetes の kube-apiserver では,デフォルトで「匿名リクエスト」機能が有効になっている.今回は「匿名リクエスト」機能の動作確認と kube-apiserver のセキュリティ対策として「匿名リクエスト」機能の無効化を試した.kube-apiserver の --anonymous-auth オプションは「Docker/Kubernetes開発・運用のためのセキュリティ実践ガイド」の「5.3.7 匿名ユーザの取り扱い」にも載っている.

関連記事

kakakakakku.hatenablog.com

Secret の自動マウントをオプトアウトするかどうか : automountServiceAccountToken フィールド

Kubernetes のセキュリティ対策として,Service Account と Pod に設定できる automountServiceAccountToken フィールド(Secret の自動マウントをオプトアウトするかどうか)の動作確認をした.とは言え,Service Account と Secret の関係性なども整理する必要があるため,一歩一歩進めていく.今回は Kubernetes v1.20 を検証環境とした.

kubernetes.io

Pod と Service Account

まず,Service Account を指定せずに Pod を作ると default Service Account と紐付く.確認をするために nginx:1.21 イメージを指定した以下のマニフェストを検証用に適用する.

apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
spec:
  containers:
    - name: my-nginx
      image: nginx:1.21

以下に kubectl get pods my-nginx -o yaml コマンドの結果を抜粋して載せる.serviceAccountName: default になっている通り,デフォルトでは Pod と default Service Account が紐付いている.

apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
  namespace: default
spec:
  containers:
  - image: nginx:1.21
    name: my-nginx
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-5qnst
      readOnly: true
  serviceAccountName: default
  volumes:
  - name: default-token-5qnst
    secret:
      defaultMode: 420
      secretName: default-token-5qnst

Service Account と Secret

もう1点重要なのは,上記 YAML の volumes と volumeMounts で,default-token-5qnst Secret を Pod のボリュームとしてファイルシステムにマウントしている.この default-token-5qnst Secret は default Service Account に紐付いている.以下に kubectl get serviceaccounts default -o yaml コマンドの結果を抜粋して載せる.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
secrets:
- name: default-token-5qnst

そして kubectl get secrets default-token-5qnst -o yaml コマンドで default-token-5qnst Secret の値を確認すると,Base64 でエンコードされた token を確認できる(以下は xxxxxxxxxx としてマスキングをした).

apiVersion: v1
data:
  token: xxxxxxxxxx
kind: Secret
metadata:
  name: default-token-5qnst
  namespace: default
type: kubernetes.io/service-account-token

Kubernetes API にリクエストを送る

実際に kubectl exec コマンドを使って Pod に接続をすると,ファイルシステムにマウントされた /var/run/secrets/kubernetes.io/serviceaccount/tokenファイルを確認できる.token ファイルは Base64 でデコードされた値となる(以下は yyyyyyyyyy としてマスキングをした).この token を HTTP Header に乗せると Kubernetes API にリクエストを送れるようになる.今回 default Service Account には権限を付与していないため Forbidden になっているのは期待通りと言える.

$ kubectl exec -it my-nginx -- /bin/sh

# cat /var/run/secrets/kubernetes.io/serviceaccount/token
yyyyyyyyyy

# TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# curl -s -k https://kubernetes/api/v1/namespaces/default/ --header "Authorization: Bearer ${TOKEN}"
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
  },
  "status": "Failure",
  "message": "namespaces \"default\" is forbidden: User \"system:serviceaccount:default:default\" cannot get resource \"namespaces\" in API group \"\" in the namespace \"default\"",
  "reason": "Forbidden",
  "details": {
    "name": "default",
    "kind": "namespaces"
  },
  "code": 403
}

Kubernetes API にリクエストを送る手順は以下のドキュメントを参考にした.

kubernetes.io

Pod : automountServiceAccountToken フィールド

一般的には Pod から Kubernetes API にリクエストを送る場面は少なく,不要なら automountServiceAccountToken フィールドを false にして Secret の自動マウントをオプトアウトする.例えば EKS Best Practices Guides にもベストプラクティスとして載っている.

aws.github.io

まず,Pod に automountServiceAccountToken: false を指定すると,Pod レベルで Secret の自動マウントをオプトアウトできる.以下のようにマニフェストに1行追加して,Pod を作り直す.

apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
spec:
  containers:
    - name: my-nginx
      image: nginx:1.21
  automountServiceAccountToken: false

すると volumes と volumeMounts の設定はなくなり,Secret の自動マウントはされなくなった.

$ kubectl get pod my-nginx -o yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
  namespace: default
spec:
  automountServiceAccountToken: false
  containers:
  - image: nginx:1.21
    name: my-nginx
  serviceAccountName: default

念のため /var/run/secrets/kubernetes.io/serviceaccount/token ファイルを確認したところ,存在しなかった.

$ kubectl exec -it my-nginx -- /bin/sh

# cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat: /var/run/secrets/kubernetes.io/serviceaccount/token: No such file or directory

Service Account : automountServiceAccountToken フィールド

Pod レベルではなく,Service Account レベルで automountServiceAccountToken フィールドを false にすることもできる.以下のマニフェストで新しく auto-mount-off Service Account を作る.

$ cat serviceaccount.yaml 
apiVersion: v1
kind: ServiceAccount
metadata:
  name: auto-mount-off
automountServiceAccountToken: false

次に serviceAccountName: auto-mount-off を指定した Pod を作る.今回は Pod には automountServiceAccountToken: false を指定していない.

$ cat pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
spec:
  serviceAccountName: auto-mount-off
  containers:
    - name: my-nginx
      image: nginx:1.21

以下に kubectl get pods my-nginx -o yaml コマンドの結果を抜粋して載せる.同じく volumes と volumeMounts の設定はなくなり,Secret の自動マウントはされなくなった.

apiVersion: v1
kind: Pod
metadata:
  name: my-nginx
  namespace: default
spec:
  containers:
  - image: nginx:1.21
    name: my-nginx
  serviceAccountName: auto-mount-off

なお,Service Account と Secret は紐付いているため,以下のように確認できる.

$ kubectl get serviceaccounts 
NAME             SECRETS   AGE
auto-mount-off   1         3m10s
default          1         50m

$ kubectl get secrets
NAME                         TYPE                                  DATA   AGE
auto-mount-off-token-7mhnv   kubernetes.io/service-account-token   3      3m10s
default-token-5qnst          kubernetes.io/service-account-token   3      50m

まとめ

Kubernetes のセキュリティ対策として,Service Account と Pod に設定できる automountServiceAccountToken フィールド(Secret の自動マウントをオプトアウトするかどうか)の動作確認をした.CKS (Certified Kubernetes Security Specialist) の出題範囲にも関連するトピックが含まれているし,最近読んでいる「Docker/Kubernetes開発・運用のためのセキュリティ実践ガイド」にも automountServiceAccountToken フィールドの解説が載っていたので,気になっていた!引き続き試していくぞー!