Edited at
Z LabDay 11

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


はじめに

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

エンベロープ暗号化というのは、暗号化の手法の一つです。わたしは、この優れた手法を、KubeCon + CloudNativeCon China 2018『Turtles All the Way Down: Securely Managing Kubernetes Secrets With Secrets - Maya Kaczorowski & Alexandr Tcherniakhovski, Google』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 を試すことで、エンベロープ暗号化も体験できます。


検証環境

image.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 を有効にしました。

# 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 にアクセスします。

# 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 の暗号化を有効にするために、以下のオプションを追加して起動しました。

--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 をセットしました。しかし、ソースコードを見る限り、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 がないのは、作業上の都合です。気にしないで下さい。


暗号化なし

# 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! の文字があります。


暗号化あり

# 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 に保存されているはずです。

# 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 をリスタートしておきます。

# 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 のままです。

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

# 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 には、Update Key ConfigurationTrim Key といった API が用意されています。これらの API を使うと、min_decryption_version を操作したり、古い Transit Key を削除したりできるようです。


まとめ

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


参考


このエントリは、弊社 Z Lab のメンバーによる Z Lab Advent Calendar 2018 の11日目として業務時間中に書きました。12日目は @TakanariKo の担当です。