oracle/kubernetes-vault-kms-plugin を試す

# はじめに

今日は、[oracle/kubernetes-vault-kms-plugin](https://github.com/oracle/kubernetes-vault-kms-plugin) という k8s の KMS Plugin を試してみます。この Oracle 社が作って OSS で公開している KMS Plugin は、[HashiCorp Vault](https://www.vaultproject.io/) が持つ KMS（Key Management Service）の機能と連携して、k8s の シークレットの暗号化を実現します。k8s のシークレットを暗号化する方法は、他にもいくつかありますが、KMS を使う方法は、エンベロープ暗号化を導入できるという点で優れています。

エンベロープ暗号化というのは、暗号化の手法の一つです。わたしは、この優れた手法を、[KubeCon + CloudNativeCon China 2018](https://www.lfasiallc.com/events/kubecon-cloudnativecon-china-2018/) の [『Turtles All the Way Down: Securely Managing Kubernetes Secrets With Secrets - Maya Kaczorowski & Alexandr Tcherniakhovski, Google』](https://kccncchina2018english.sched.com/event/FuKP/turtles-all-the-way-down-securely-managing-kubernetes-secrets-with-secrets-maya-kaczorowski-alexandr-tcherniakhovski-google)（[PDF資料](https://schd.ws/hosted_files/kccncchina2018english/6a/Turtles%20all%20the%20way%20down%20-%20Securely%20managing%20Kubernetes%20Secrets.pdf)）というセッションで知りました。すでに世界中で使われているものと思います。

エンベロープ暗号化では、2種類の鍵を使います。一つは、データを暗号化するための鍵です。これを Data Encryption Key（DEK）と呼びます。DEK は、データ毎に異なるものを作り、ローテーションはしません。もう一つは、鍵を暗号化するための鍵です。これを Key Encryption Key（KEK）と呼びます。KEK は、DEK の暗号化に使います。KEK は、KMS などで安全に管理して、定期的にローテーションします。

エンベロープ暗号化を使うと鍵の管理がとても楽になります。簡単に言えば、KEK だけしっかりと管理すれば、それで OK です。DEK は、暗号化してしまうので、データとセットで保存できます。DEK は、ローテーションもしないので、事実上、管理不要です。DEK が変わらないことで、データの再暗号化も不要というメリットも付いてきます。今回、[oracle/kubernetes-vault-kms-plugin](https://github.com/oracle/kubernetes-vault-kms-plugin) を試すことで、エンベロープ暗号化も体験できます。

# 検証環境

![image.png](https://qiita-image-store.s3.amazonaws.com/0/96933/b2b590b0-ce7e-8137-f166-dd066f02e47b.png)

上図は、今回の検証環境です。k8s は、シングル Master 構成です。Vault と etcd は、 Master ノードとは別の VM で動かしました。Master ノード内の kubernetes-vault-kms-plugin プロセスが、kube-apiserver や Vault と連携します。kubectl の実行は、手元の Mac から行いました。

## Vault

Vault は、開発モードで動かしました。開発モードの Vault は、アンシールされた状態で起動し、インメモリで稼働します。Vault の操作は、Vault が起動している VM から行いました。Vault を KMS として使うために、[Transit Secrets Engine](https://www.vaultproject.io/docs/secrets/transit/index.html) を有効にしました。

```bash
# Vault の起動
$ vault server -dev -dev-listen-address="${IP}:8200" -dev-root-token-id="${TOKEN}"

# Transit Secrets Engine の有効化
$ export VAULT_ADDR="http://${IP}:8200"
$ vault secrets enable transit
Success! Enabled the transit secrets engine at: transit/
```

## kubernetes-vault-kms-plugin

kubernetes-vault-kms-plugin は、k8s Master ノード上で、以下の設定で動かしました。

```vault-plugin.yaml
keyNames:
  - kube-secret-enc-key
transitPath: /transit
addr: http://"VaultのIPアドレス":8200
token: "Vault のルートトークン"
```

`keyNames:`には、kube-secret-enc-key をセットしました。これが KEK になります。KEK は Vault に保存されます。`token:` には、Vault 起動時に設定したルートトークンをセットしました。これで、Vault にアクセスします。

```bash
# kubernetes-vault-kms-plugin の起動
$ kubernetes-vault-kms-plugin \
  -socketFile='/PATH/TO/socketfile.sock' -vaultConfig="$HOME/vault-plugin.yaml"
```

kubernetes-vault-kms-plugin は、まだリリースバージョンがないため、バイナリには、master ブランチからビルドしたものを使いました。

kube-apiserver は、`-socketFile=` で指定した UNIX ドメインソケットを使って、kubernetes-vault-kms-plugin と gRPC で通信します。この UNIX ドメインソケット（socketfile.sock）は、kube-apiserver が読めるパスに置く必要があります。kube-apiserver をコンテナで起動している場合は、注意が必要です。

## kube-apiserver

kube-apiserver は、KMS Plugin による Secret の暗号化を有効にするために、以下のオプションを追加して起動しました。

```bash
--experimental-encryption-provider-config=/PATH/TO/encryption-config.yaml
```

オプションに指定する設定ファイルは、以下の内容で作成しました。このファイルは、kube-apiserver が起動時に読み込むため、kube-apiserver が読めるパスに置きます。kube-apiserver をコンテナで起動する場合は、注意が必要です。


```encryption-config.yaml
kind: EncryptionConfig
apiVersion: v1
resources:
 - resources:
   - secrets
   providers:
   - kms:
       name: myVault
       endpoint: unix:///PATH/TO/socketfile.sock
       cachesize: 0
   - identity: {}  
```

`endpoint:`には、kubernetes-vault-kms-plugin の UNIX ドメインソケット のパスをセットしました。`kms:`の`name:`は、myVault としました。

`cachesize:`には、0 をセットしました。kube-apiserver は、KMS Plugin を使ってデコードした、平文の DEKを、`cachesize:`の個数分キャッシュします。キャッシュは動作を分かりにくくするので、無効にしようと思い、0 をセットしました。しかし、[ソースコード](https://github.com/kubernetes/kubernetes/blob/v1.12.2/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope.go#L64L66)を見る限り、`cachesize: 0`だと Default の1000 が適用されるようです。残念。まあ、リスタートをかませればいいか。

# 検証

KMS Plugin の有効化の前後（暗号化あり・暗号化なし）で、それぞれ、Secret を作って検証します。最初に kubectl で Secret を参照できるか確認します。それから、etcdctl で etcd のデータを確認します。暗号化ありの場合は、etcd のデータの Secret 部分は、暗号化されているはずです。

| 暗号化 | Secret | Key | Value |
| --- | --- | --- | --- |
| なし | secret0 | password | Merry Christmas! |
| あり | secret3 | password | Merry Christmas! |

secret1 と secret2 がないのは、作業上の都合です。気にしないで下さい。

## 暗号化なし

```bash
# secret0 を作成
$ kubectl create secret generic secret0 --from-literal=password='Merry Christmas!'
secret/secret0 created

# kubectl で secret0 を参照
$ kubectl get secret secret0 -o yaml |grep password: |cut -d":" -f2 |base64 --decode; echo
Merry Christmas!

# etcdctl で secret0 を参照
$ etcdctl get /registry/secrets/default/secret0 |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 73 65 63 72 65 74  |s/default/secret|
00000020  30 0a 6b 38 73 00 0a 0c  0a 02 76 31 12 06 53 65  |0.k8s.....v1..Se|
00000030  63 72 65 74 12 74 0a 4c  0a 07 73 65 63 72 65 74  |cret.t.L..secret|
00000040  30 12 00 1a 07 64 65 66  61 75 6c 74 22 00 2a 24  |0....default".*$|
00000050  32 30 62 39 37 36 31 37  2d 66 39 31 37 2d 31 31  |20b97617-f917-11|
00000060  65 38 2d 62 38 61 32 2d  66 61 31 36 33 65 34 38  |e8-b8a2-fa163e48|
00000070  62 63 35 33 32 00 38 00  42 08 08 ec e4 a2 e0 05  |bc532.8.B.......|
00000080  10 00 7a 00 12 1c 0a 08  70 61 73 73 77 6f 72 64  |..z.....password|
00000090  12 10 4d 65 72 72 79 20  43 68 72 69 73 74 6d 61  |..Merry Christma|
000000a0  73 21 1a 06 4f 70 61 71  75 65 1a 00 22 00 0a     |s!..Opaque.."..|
000000af
```

暗号化なしの場合は、etcd の中に、平文でしっかりと、**Merry Christmas!** の文字があります。

## 暗号化あり

```bash
# secret3 を作成
$ kubectl create secret generic secret3 --from-literal=password='Merry Christmas!'
secret/secret3 created

# kubectl で secret3 を参照
$ kubectl get secret secret3 -o yaml |grep password: |cut -d":" -f2 |base64 --decode; echo
Merry Christmas!

# etcdctl で secret3 を参照
$ etcdctl get /registry/secrets/default/secret3 |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 73 65 63 72 65 74  |s/default/secret|
00000020  33 0a 6b 38 73 3a 65 6e  63 3a 6b 6d 73 3a 76 31  |3.k8s:enc:kms:v1|
00000030  3a 6d 79 56 61 75 6c 74  3a 00 67 6b 75 62 65 2d  |:myVault:.gkube-|
00000040  73 65 63 72 65 74 2d 65  6e 63 2d 6b 65 79 3a 76  |secret-enc-key:v|
00000050  31 3a 33 6e 69 76 73 61  6f 65 61 43 35 6e 54 69  |1:3nivsaoeaC5nTi|
00000060  46 39 70 79 43 74 4a 32  44 50 51 37 6c 6b 42 5a  |F9pyCtJ2DPQ7lkBZ|
00000070  63 4c 56 38 69 44 67 79  4c 36 42 4c 35 6b 41 41  |cLV8iDgyL6BL5kAA|
00000080  56 62 76 46 69 6f 41 30  65 75 79 56 53 41 52 2f  |VbvFioA0euyVSAR/|
00000090  74 75 47 39 7a 63 57 71  43 38 50 61 62 69 45 72  |tuG9zcWqC8PabiEr|
000000a0  49 68 c2 10 69 6b e8 7b  69 8a b9 10 c9 77 29 bd  |Ih..ik.{i....w).|
000000b0  0c f2 61 a9 e1 12 e9 9d  d1 47 0b 05 e5 03 d0 be  |..a......G......|
000000c0  18 85 35 f9 34 62 0e dc  00 01 a6 db 97 ee 60 ab  |..5.4b........`.|
000000d0  59 64 4c a6 99 60 37 62  5e 6a ac 28 8f 0d 79 1e  |YdL..`7b^j.(..y.|
000000e0  12 1b b9 5b 62 78 5e c2  c0 b7 51 4c 78 1b b8 e3  |...[bx^...QLx...|
000000f0  b7 9b 90 6b a1 39 7a b0  f5 a1 e0 1a 90 c2 78 f8  |...k.9z.......x.|
00000100  34 c8 74 f5 90 05 b3 5c  e0 3b fe 29 d6 04 6d ed  |4.t....\.;.)..m.|
00000110  12 2f 73 3a 55 1f 8c 5d  d7 5c f8 11 45 02 2a 77  |./s:U..].\..E.*w|
00000120  24 61 fc 9a 9f 44 62 08  15 ce 82 c8 1e 8c 5e ce  |$a...Db.......^.|
00000130  0c 31 f5 f5 3e e8 46 81  9e fe bd 1c 83 c0 1b af  |.1..>.F.........|
00000140  66 dd 0a                                          |f..|
00000143
```

暗号化ありの場合は、etcd の中に、**Merry Christmas!** や **password** といった文字列は見当たりません。Key と Value の両方が、しっかりと暗号化されています。やったね。

## 解析

せっかくなので、暗号化ありの場合の etcd のデータを解析してみます。

```
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 73 65 63 72 65 74  |s/default/secret|
00000020  33 0a 6b 38 73 3a 65 6e  63 3a 6b 6d 73 3a 76 31  |3.k8s:enc:kms:v1|
00000030  3a 6d 79 56 61 75 6c 74  3a 00 67 6b 75 62 65 2d  |:myVault:.gkube-|
00000040  73 65 63 72 65 74 2d 65  6e 63 2d 6b 65 79 3a 76  |secret-enc-key:v|
00000050  31 3a 33 6e 69 76 73 61  6f 65 61 43 35 6e 54 69  |1:3nivsaoeaC5nTi|
~
00000130  0c 31 f5 f5 3e e8 46 81  9e fe bd 1c 83 c0 1b af  |.1..>.F.........|
00000140  66 dd 0a                                          |f..|
```

3行目2バイト目の `0a(\n)` を挟んで、etcd の Key と Value になっています。Value は、`3a(:)` 区切りになっていて、`0a(\n)`で終わっています。

- Key: **/registry/secrets/default/secret3**
- Value: **k8s:enc:kms:v1:myVault:.gkube-secret-enc-key:3niv〜..f.**

| カラム | 値 |
| --- | --- |
| 1 | k8s |
| 2 | enc |
| 3 | kms |
| 4 | v1 |
| 5 | myVault |
| 6 | .gkube-secret-enc-key |
| 7 | v1 |
| 8 | 3niv〜..f. |

1-5番目のカラムには、`encryption-config.yaml` の内容が反映されています。6番目のカラムには、KEK 名が入っています。`.g(00 67)`という奇妙なデータがくっ付いていますが、何だかわかりません。7番目のカラムは、`kube-secret-enc-key` のバージョンでしょう。最後の8番目のカラムは、暗号化後のデータのようです。

```
00000080  56 62 76 46 69 6f 41 30  65 75 79 56 53 41 52 2f  |VbvFioA0euyVSAR/|
00000090  74 75 47 39 7a 63 57 71  43 38 50 61 62 69 45 72  |tuG9zcWqC8PabiEr|
000000a0  49 68 c2 10 69 6b e8 7b  69 8a b9 10 c9 77 29 bd  |Ih..ik.{i....w).|
000000b0  0c f2 61 a9 e1 12 e9 9d  d1 47 0b 05 e5 03 d0 be  |..a......G......|
```

8カラム目のデータをよーく見ると、途中から、ASCII 文字コードが激減しています。このことから、8カラム目のデータは、異なるアルゴリズムで暗号化した2種類のデータが結合されたものだと予想できます。どっちがどっちだか分かりませんが、DEK と Secret がくっ付いているのだと思います。

## KEK のローテーション

エンベロープ暗号化の醍醐味は、KEK のローテーションにあります。せっかくなので、体験しておきます。今回の KEK である、kube-secret-enc-key は、Vault に保存されているはずです。

```bash
# Transit Keys を確認
$ vault list transit/keys
Keys
----
kube-secret-enc-key

# kube-secret-enc-key を確認
$ vault kv get transit/keys/kube-secret-enc-key
============= Data =============
Key                       Value
---                       -----
allow_plaintext_backup    false
deletion_allowed          false
derived                   false
exportable                false
keys                      map[1:1544086570]
latest_version            1
min_available_version     0
min_decryption_version    1
min_encryption_version    0
name                      kube-secret-enc-key
supports_decryption       true
supports_derivation       true
supports_encryption       true
supports_signing          false
type                      aes256-gcm96
```

やはり、Vault にありました。暗号化アルゴリズムは、aes256-gcm96 とあります。

ローテートして、その後も、secret3 を参照できるか確認してみます。secret3 のデコード後の DEK が kube-apiserver のキャッシュに残っているかもしれないので、念のため、参照前に kube-apiserver をリスタートしておきます。

```bash
# kube-secret-enc-key をローテート
$ vault write -force transit/keys/kube-secret-enc-key/rotate
Success! Data written to: transit/keys/kube-secret-enc-key/rotate

# kube-secret-enc-key を確認
$ vault kv get transit/keys/kube-secret-enc-key |grep version
latest_version            2
min_available_version     0
min_decryption_version    1
min_encryption_version    0

# kube-apiserver をリスタート
$ sudo systemctl restart kube-apiserver

# kubectl で secret3 を参照
$ kubectl get secret secret3 -o yaml |grep password: |cut -d":" -f2 |base64 --decode; echo
Merry Christmas!
```

ローテートしても、secret3 を参照できました。kube-secret-enc-key の latest_version は 2 に上がりました。min_decryption_version という項目は 1 のままです。

もう一回ローテートします。

```bash
# kube-secret-enc-key をローテート
$ vault write -force transit/keys/kube-secret-enc-key/rotate
Success! Data written to: transit/keys/kube-secret-enc-key/rotate

# kube-secret-enc-key を確認
$ vault kv get transit/keys/kube-secret-enc-key |grep version
latest_version            3
min_available_version     0
min_decryption_version    1
min_encryption_version    0

# kube-apiserver をリスタート
$ sudo systemctl restart kube-apiserver

# kubectl で secret3 を参照
$ kubectl get secret secret3 -o yaml |grep password: |cut -d":" -f2 |base64 --decode; echo
Merry Christmas!
```

再度ローテートしても、secret3 を参照できました。kube-secret-enc-key の latest_version は 3 に上がりましたが、min_decryption_version は 1 のままです。

secret3 の DEK は、バージョン1の kube-secret-enc-key で暗号化しました。kube-apiserver をリスタートしているので、DEK のキャッシュはないはずです。それでも secret3 を読めているということは、Vault が、バージョン1 の kube-secret-enc-key を、消さずに、まだ持っているということだと思います。

Vault の [Transit Secrets Engine](https://www.vaultproject.io/docs/secrets/transit/index.html) には、[Update Key Configuration](https://www.vaultproject.io/api/secret/transit/index.html#update-key-configuration) や [Trim Key](https://www.vaultproject.io/api/secret/transit/index.html#trim-key) といった API が用意されています。これらの API を使うと、min_decryption_version を操作したり、古い Transit Key を削除したりできるようです。

# まとめ

今日は、[oracle/kubernetes-vault-kms-plugin](https://github.com/oracle/kubernetes-vault-kms-plugin) を試しました。k8s の Secret が、しっかりと暗号化されていることを確認できたと同時に、エンベロープ暗号化という、優れた暗号化の手法を体験することができました。

# 参考

- [Encrypting Secret Data at Rest (k8s-v1.12)](https://v1-12.docs.kubernetes.io/docs/tasks/administer-cluster/encrypt-data/)
- [Using a KMS provider for data encryption (k8s-v1.12)](https://v1-12.docs.kubernetes.io/docs/tasks/administer-cluster/kms-provider/)

---

このエントリは、弊社 Z Lab のメンバーによる [Z Lab Advent Calendar 2018](https://qiita.com/advent-calendar/2018/zlab) の11日目として業務時間中に書きました。12日目は @TakanariKo の担当です。