Security
etcd
kubernetes

KubernetesのSecretは本当に安全か

この記事はKubernetes Advent Calendar 12日目の記事になります。

Overview

KubernetesのPodにパスワードやトークンを指定する際にはSecretを使います。(ってゆうか使ってください)
コンテナ内に秘密情報を入れたままDockerHub等にアップロードしてしまい大変な事態に・・・ってことには注意しましょう。

さて、この記事の本題ですが"KubernetesのSecretは本当に安全か"です。
確かにKubernetes上に機能として存在しますが、本当に安全性は確保されているのでしょうか?
また、もしノードが攻撃された場合、どの程度までSecretのデータを守れるのでしょうか?

この記事では公式ドキュメントを参考に現在のSecretについて紹介できればと思います。
※著者はまだまだ未熟者ゆえ間違い等もあるかと思いますが、温かい目&お手柔らかなコメントでお願いします:slight_smile:

前段(Secretとは)

参考サイトのほうが詳しいので詳細はそちらにお譲りします。
概要だけ述べますと、次のような感じになります。

  • 作成したsecretはKubernetesのetcd上にplain-text形式で保存される
  • Nodeではpodのtmpfs上に保存される
  • Node Authorization機能(v1.7〜)により、Node は割り当てられた Pod が参照する Secret 以外にはアクセスできない。

まとめると、生データはetcd上にある。Masterにはない。NodeはSecretを参照するPodがいるNodeのtmpfs上にのみ存在する(ディスク上にデータは保存されない)。通信はすべてhttps(etcd/Master/Node間)。
※いずれもちゃんと設定した場合

・・・なかなか安全そうです。

参考:

公式docs(https://kubernetes.io/docs/concepts/configuration/secret/)
Secretとは(https://ubiteku.oinker.me/2017/03/01/kubernetes-secrets/)
Node Authorization(https://qiita.com/tkusumi/items/f6a4f9150aa77d8f9822)

Secretの安全性

しかし、Secretがetcd上にplain-text形式で保存されるのは気になりますね。
公式docs上にも安全性のためにはetcdのアクセス制御をきちんと設定しろとありますが、もう少し安全にできないのでしょうか?

Kubernetes 1.7よりetcdを暗号化する機能が開発されました。
この機能を使うと、より安全にSecretを運用できそうです。
※2017年12月(v1.8)現在、まだalpha機能です。
公式docs(https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/)

etcdの中身の確認方法

ではここから実機検証です。
まず、etcdの暗号化機能を使用する前に、何もしないとパスワード等が生データのまま保存されているのか確認しておきましょう。

確認用Kubernetesクラスタの作成

vagrantとkubeadmでサクッと確認用のKubernetesクラスタを作りましょう。
※本当に動作確認のためだけですので読者さまの環境で動作しなくてもごめんなさい(てか多分VMを再起動させると動作しない←)

vagrant box add centos7 https://github.com/holms/vagrant-centos7-box/releases/download/7.1.1503.001/CentOS-7.1.1503-x86_64-netboot.box
vagrant up (※Vagrantfileは下記参照)
Vagrantfile.
Vagrant.configure("2") do |config|
  config.vm.box = "centos7"
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "2048"
  end
end

※sshでVMにアクセスしてからの手順

[vagrant@localhost ~]# sudo su -
[root@localhost ~]# yum update -y && yum install -y docker etcd
[root@localhost ~]# swapoff -a
[root@localhost ~]# systemctl stop firewalld && systemctl disable firewalld
[root@localhost ~]# sysctl -w net.bridge.bridge-nf-call-iptables=1
[root@localhost ~]# cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://yum.kubernetes.io/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
        https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

[root@localhost ~]# yum install -y kubelet kubeadm kubectl

[root@localhost ~]# systemctl enable docker && systemctl start docker
[root@localhost ~]# systemctl enable kubelet

[root@localhost ~]# systemctl enable etcd && systemctl start etcd

[root@localhost ~]# cat <<EOF > etcd-conf.yaml
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  advertiseAddress: 192.168.33.10
etcd:
  endpoints:
  - http://127.0.0.1:2379
networking:
  podSubnet: 10.244.0.0/16
EOF

[root@localhost ~]# kubeadm init --config=etcd-conf.yaml

[root@localhost ~]# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml --kubeconfig=/etc/kubernetes/admin.conf
[root@localhost ~]# kubectl taint node localhost.localdomain node-role.kubernetes.io/master --kubeconfig=/etc/kubernetes/admin.conf

Secretデータの作成

続いてクラスタにサンプルのSecretを作成します。
秘密文章は何でもいいのですが、今回はpassword: "ilovejapan"にします。

echo "ilovejapan" | base64
vi secret.yaml
  ※yamlファイルは以下を参照
kubectl create -f secret.yaml
secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  password: aWxvdmVqYXBhbgo=

etcd上のデータの確認

etcd上のデータを確認する方法として2つの方法があります。
(1) バイナリを直接見る

hexdump -C /var/lib/etcd/default.etcd/member/snap/db | grep "ilovejapan"
000dc2b0  6f 72 64 12 0b 69 6c 6f  76 65 6a 61 70 61 6e 0a  |ord..ilovejapan.|

(2) etcdctlコマンドで見る

ETCDCTL_API=3 etcdctl get --prefix /registry/secrets/default/mysecret | hexdump -C
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6d 79 73 65 63 72  |s/default/mysecr|
00000020  65 74 0a 6b 38 73 00 0a  0c 0a 02 76 31 12 06 53  |et.k8s.....v1..S|
00000030  65 63 72 65 74 12 70 0a  4d 0a 08 6d 79 73 65 63  |ecret.p.M..mysec|
00000040  72 65 74 12 00 1a 07 64  65 66 61 75 6c 74 22 00  |ret....default".|
00000050  2a 24 32 32 39 35 65 31  39 63 2d 64 34 32 38 2d  |*$2295e19c-d428-|
00000060  31 31 65 37 2d 61 33 32  35 2d 30 38 30 30 32 37  |11e7-a325-080027|
00000070  36 62 35 37 38 38 32 00  38 00 42 08 08 ea f8 f4  |6b57882.8.B.....|
00000080  d0 05 10 00 7a 00 12 17  0a 08 70 61 73 73 77 6f  |....z.....passwo|
00000090  72 64 12 0b 69 6c 6f 76  65 6a 61 70 61 6e 0a 1a  |rd..ilovejapan..|
000000a0  06 4f 70 61 71 75 65 1a  00 22 00 0a              |.Opaque.."..|
000000ac

etcdの暗号化

etcd上の任意のディレクトリ(※)を暗号化する機能を使ってみましょう。
この機能は大雑把に言うとyaml形式で暗号化方式と共通かぎ暗号方式の秘密鍵を書いて、apiserverに読み込ませる感じです。
※: etcd v3からディレクトリという概念はなくなったので厳密にいうとディレクトリではない
etcdの暗号化機能を使用するためにはkube-apiserverに--experimental-encryption-provider-config="configファイル"を指定して起動します。
kubeadmの場合はconfigファイルをapiserverに渡すためにHostPathもマウントしておきましょう。
今回パスワードは公式サイトに倣って以下のコマンドで作成しています。

head -c 32 /dev/urandom | base64

yamlはこんな感じです。

/etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    scheduler.alpha.kubernetes.io/critical-pod: ""
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --experimental-encryption-provider-config=/etc/kubernetes/enc-conf.yaml
    - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
    - --secure-port=6443
    - --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
    - --admission-control=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,ResourceQuota
    - --requestheader-allowed-names=front-proxy-client
    - --service-cluster-ip-range=10.96.0.0/12
    - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
    - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
    - --allow-privileged=true
    - --advertise-address=192.168.33.10
    - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
    - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
    - --enable-bootstrap-token-auth=true
    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
    - --requestheader-username-headers=X-Remote-User
    - --requestheader-group-headers=X-Remote-Group
    - --requestheader-extra-headers-prefix=X-Remote-Extra-
    - --insecure-port=0
    - --service-account-key-file=/etc/kubernetes/pki/sa.pub
    - --authorization-mode=Node,RBAC
    - --etcd-servers=http://127.0.0.1:2379
    image: gcr.io/google_containers/kube-apiserver-amd64:v1.8.4
    livenessProbe:
      failureThreshold: 8
      httpGet:
        host: 127.0.0.1
        path: /healthz
        port: 6443
        scheme: HTTPS
      initialDelaySeconds: 15
      timeoutSeconds: 15
    name: kube-apiserver
    resources:
      requests:
        cpu: 250m
    volumeMounts:
    - mountPath: /etc/kubernetes
      name: k8s-conf
      readOnly: true
    - mountPath: /etc/kubernetes/pki
      name: k8s-certs
      readOnly: true
    - mountPath: /etc/ssl/certs
      name: ca-certs
      readOnly: true
    - mountPath: /etc/pki
      name: ca-certs-etc-pki
      readOnly: true
  hostNetwork: true
  volumes:
  - hostPath:
      path: /etc/kubernetes
      type: DirectoryOrCreate
    name: k8s-conf
  - hostPath:
      path: /etc/kubernetes/pki
      type: DirectoryOrCreate
    name: k8s-certs
  - hostPath:
      path: /etc/ssl/certs
      type: DirectoryOrCreate
    name: ca-certs
  - hostPath:
      path: /etc/pki
      type: DirectoryOrCreate
    name: ca-certs-etc-pki
status: {}
/etc/kubernetes/enc-conf.yaml
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: cSNmGdfrr/IZeYN9npkl3F7/ScQcer2We9VxmM4X5ww=
    - identity: {}

その後kube-apiserverを再起動させます。

systemctl restart kubelet

先ほどと同様にetcdの中身を確認してみると・・・

ETCDCTL_API=3 etcdctl get --prefix /registry/secrets/default/mysecret | hexdump -C
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6d 79 73 65 63 72  |s/default/mysecr|
00000020  65 74 0a 6b 38 73 3a 65  6e 63 3a 61 65 73 63 62  |et.k8s:enc:aescb|
00000030  63 3a 76 31 3a 6b 65 79  31 3a 55 55 5d 34 1e 5d  |c:v1:key1:UU]4.]|
00000040  72 67 0e be ef 16 5b d3  0c 08 36 42 2f 7c 92 1b  |rg....[...6B/|..|
00000050  70 29 1f 67 0b 79 9c 29  2d 4c 1c ef 8a 7b 66 2b  |p).g.y.)-L...{f+|
00000060  0a a8 8a 1e 5e 72 e5 6a  50 52 62 1e 8a 80 58 6d  |....^r.jPRb...Xm|
00000070  99 5c d3 21 50 a1 08 14  b5 be 7d a1 1f 73 78 22  |.\.!P.....}..sx"|
00000080  7d c4 33 b5 eb 8a f1 dd  32 e2 c3 cc 1f 64 e0 f1  |}.3.....2....d..|
00000090  6d aa 49 56 31 93 79 19  cf 53 dc 0b db a7 60 8b  |m.IV1.y..S....`.|
000000a0  a7 ad ad 33 2a 5d 3a 1b  e2 15 91 9d d5 e3 c5 ea  |...3*]:.........|
000000b0  fc 6b d4 21 33 a0 2e 80  3f 1d a8 0b f5 b4 98 98  |.k.!3...?.......|
000000c0  1c 2a 00 1d d7 e6 55 f9  88 35 c2 c6 a3 b3 20 63  |.*....U..5.... c|
000000d0  e8 2e eb af 1f bc 6b 92  d8 72 0a                 |......k..r.|
000000db

無事パスワードは見えなくなりました。

総評

KubernetesのSecretは機能が豊富でちゃんと設定すれば安全そうです。
ただKubernetesはSecretだけにとどまらずちゃんと設定するのが大変なのですが・・・
もうちょっと簡単に安全なクラスタを設計できれば万々歳ですね