Sealed Secrets(とKMS)を使ってk8s上の機密情報を管理する

# はじめに

皆さんは、Kubernetes上の機密情報をどうやって管理しているでしょうか？

一応、k8sにはSecretというリソースが存在しますが、これは所詮base64エンコードされているだけなので、リポジトリ管理するとなるとセキュリティ的によろしくありません。

そのため、ネット上にはいくつかの暗号化の仕組みが存在しますが、その中でも今回はGitOpsを提唱するWeaveworksが紹介している[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets)というOSSについて書いていきたいと思います。

（他にも[kubesec](https://github.com/shyiko/kubesec)や[Kubernetes External Secrets](https://github.com/godaddy/kubernetes-external-secrets)などがあり、特に後者はAWSとの相性が良さそうなので、EKSを使っている方は検討してみると良いかもしれません）

# 環境

|使用|バージョン|
|:-:|:-:|
|Kubernetes|1.14|
|Sealed Secrets|0.9.7|

# Sealed Secretsで機密情報を管理する

## Sealed Secretsとは

Sealed Secretsとは、公開鍵暗号方式を利用した暗号化の仕組みで、ユーザーがSecretリソースをSealed Secretsから提供された公開鍵で暗号化しておけば、k8s上で秘密鍵を持ったSealed Secretsのコントローラが復号化してくれます。
これにより、リポジトリ上に平文を保存しなくてよくなるため、Secretリソースのみを使うのに比べてセキュアとなります。

![life-of-a-64c71420.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253997/baa4fdaf-a66d-6ef8-7769-9f04d1db85a9.png)
公式参照: [Sealed Secrets: Protecting your passwords before they reach Kubernetes](https://engineering.bitnami.com/articles/sealed-secrets.html)

GitOpsでArgo CDなどのpull型デプロイツールを使っている際には特におすすめで、自前の環境でもこの組み合わせを利用しています。

## Sealed Secretsの導入

Sealed Secretsの導入に必要なスクリプトは以下の通りです。

```sh
# Step1: ドキュメント参考
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.9.7/controller.yaml


# Step2: キーペアが生成される必要があるため、podが立ち上がるまで待機
kubectl wait pod -n kube-system -l name=sealed-secrets-controller --for=condition=Ready


# Step3: 鍵を出力するので、ディレクトリを作っておく
mkdir -p ${鍵のディレクトリ}


# Step4: マニフェストファイルごとキーペアのバックアップ or 置き換え
if [ ! -e ${鍵のディレクトリ}/${キーペア} ]; then
  # キーペアが存在しないなら、新たにバックアップする
  kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > ${鍵のディレクトリ}/${キーペア}
  yq r ${鍵のディレクトリ}/${キーペア} 'items[0].data."tls.crt"' | base64 --decode > ${鍵のディレクトリ}/${公開鍵}
else
  # キーペアが存在するなら、その鍵に置き換える
  kubectl replace --force -f ${鍵のディレクトリ}/${キーペア}
  kubectl delete pod -n kube-system -l name=sealed-secrets-controller
fi
```

一つずつ見ていきましょう。

### Step1: リソースの適応

```sh
# Step1: ドキュメント参考
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.9.7/controller.yaml
```

まずはSealed Secretsのドキュメントを参考に、リソースを適応していきます。
基本的に、クラスタ側の設定は本来これで終了です。

ただ、これ以外にSealed Secretsで使われる秘密鍵をバックアップしておく必要があります。
でないと、何らかの理由でクラスタが破棄され、再度クラスタを生成しSealed Secretsを適応した場合に、秘密鍵も生成し直されるため、復号化ができなくなってしまうからです。

これ以降は、バックアップがなければ、マニフェストファイルごとキーペアのバックアップを行い、すでに存在するのであれば、そのバックアップ情報に置き換える処理になっています。

### Step2: Podが立ち上がるまで待機

```sh
# Step2: キーペアが生成される必要があるため、podが立ち上がるまで待機
kubectl wait pod -n kube-system -l name=sealed-secrets-controller --for=condition=Ready
```

上記の通り、以降はキーペアのバックアップや置き換えを行いますが、それには当然キーペアが生成されている必要があるため、Sealed Secretsのコントローラが立ち上がるまで待機しなければいけません。

そこで、ここでは`kubectl wait`を使い、PodのconditionがReadyになるまで待機しています。
このwaitは、こういったシチュエーションでとても便利ですね。

### Step3: 鍵を出力する用にディレクトリ作成

```sh
# Step3: 鍵を出力するので、ディレクトリを作っておく
mkdir -p ${鍵のディレクトリ}
```

これは人によっては不要です。
もし鍵の出力先で、存在しないディレクトリの可能性があるなら、このコマンドを叩いておきましょう。

### Step4: マニフェストファイルごとキーペアのバックアップ or 置き換え

```sh
# Step4: マニフェストファイルごとキーペアのバックアップ or 置き換え
if [ ! -e ${鍵のディレクトリ}/${キーペア} ]; then
  # キーペアが存在しないなら、新たにバックアップする
  kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > ${鍵のディレクトリ}/${キーペア}
  yq r ${鍵のディレクトリ}/${キーペア} 'items[0].data."tls.crt"' | base64 --decode > ${鍵のディレクトリ}/${公開鍵}
else
  # キーペアが存在するなら、その鍵に置き換える
  kubectl replace --force -f ${鍵のディレクトリ}/${キーペア}
  kubectl delete pod -n kube-system -l name=sealed-secrets-controller
fi
```

マニフェストファイルごとキーペアのバックアップ or 置き換えを行います。

#### キーペアのバックアップ

```sh
# キーペアが存在しないなら、新たにバックアップする
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > ${鍵のディレクトリ}/${キーペア}
yq r ${鍵のディレクトリ}/${キーペア} 'items[0].data."tls.crt"' | base64 --decode > ${鍵のディレクトリ}/${公開鍵}
```
キーペアのバックアップでは、まずSealed Secretsが生成したキーペアをマニフェストファイルごと出力します。
そのあと、実際にSecretリソースを暗号化するにあたって必要となる公開鍵を一緒に出力しています。

公開鍵の取得には、jqのyaml版ラッパーであるyqを使用しているので、yqが入っていない方はインストールしてください。
（ちなみに、後述のkubesealを使えば、`kubeseal --fetch-cert > ${鍵のディレクトリ}/${公開鍵}`でも取得できます）

#### キーペアの置き換え
```sh
# キーペアが存在するなら、その鍵に置き換える
kubectl replace --force -f ${鍵のディレクトリ}/${キーペア}
kubectl delete pod -n kube-system -l name=sealed-secrets-controller
```

キーペアの置き換えでは、まず`kubectl replace`で丸ごとリソースを置き換えます。
そのあと、置き換えを反映するためにはコントローラを再起動する必要があるので、`kubectl delete`します。
（詳しくは[ドキュメント参考](https://github.com/bitnami-labs/sealed-secrets#how-can-i-do-a-backup-of-my-sealedsecrets)）

これでSealed Secretsの設定は完了です。

## 公開鍵を使って暗号化する

### kubesealの導入

Secretの暗号化など、Sealed Secretsに関する操作をする場合、公式から提供されているkubesealを使用することがオススメです。
導入方法は、

```sh
brew install kubeseal
```

でOKです。

### Secretリソースの作成

暗号化する対象となるSecretリソースを作成します。

yamlで直接書こうとすると、いちいちbase64エンコーディングするのが面倒なため、`kubectl create --dry-run`を活用すると、簡単に作成することができます。
（ただし、passwordなどの扱いには注意してください）

```sh
kubectl create secret generic mysecret \
  --from-literal username=xxxx \
  --from-literal password=xxxx \
  -o yaml --dry-run > ${Secretリソース}
```

### 暗号化する

上記で生成されたSecretリソースを、Sealed Secrets導入時に保存した公開鍵を使い暗号化します。

```sh
kubeseal --format=yaml --cert=${鍵のディレクトリ}/${公開鍵} < ${Secretリソース} > ${暗号化されたSealedSecretリソース}
```

すると、以下のようなusernameとpasswordが暗号化されたSealedSecretリソースが生成されます。

```yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: mysecret
  namespace: default
spec:
  encryptedData:
    password: xxxxxxxxxxxxxxx
    username: xxxxxxxxxxxxxxx
```

これを、Sealed Secretsが導入されたクラスタに対して`kubectl apply`してやれば、コントローラが復号化し、k8s上ではSecretリソースと同じように扱うことができます。
（リポジトリにはSecretリソースではなく、このSealedSecretリソースを保存します）

# Sealed Secretsの秘密鍵をAWS KMSで暗号化する

勘の良い方ならお気づきでしょうが、この方法だと、結局バックアップされたキーペアが平文でリポジトリに保存されることになるので、本末転倒です。

そこでAWS KMSを使い、バックアップを暗号化します。

## AWS KMSとは

AWS KMSとは、AWS上でマスターキーと呼ばれる鍵が管理され、ユーザーはそれを使って暗号化できるサービスです。

基本的に、`aws kms encrypt`の1コマンドで暗号化できるため非常に便利ですが、この方法ではサイズが4KBまでといくつか制限があります。
その制限に引っかかってしまう場合には、データキーを使う方式が有効です。

データキーを使う方式とは、暗号化したいデータをopensslなどを使い暗号化し、その際に使用したpasswordをKMSで管理する仕組みです。
このpasswordで使われるものが、データキーと呼ばれています。

今回の例でも、このデータキーの方式で暗号化を行なっていきます。

## 暗号化するスクリプト

```diff

# Step1: ドキュメント参考
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.9.7/controller.yaml


# Step2: 鍵が生成される必要があるため、podが立ち上がるまで待機
kubectl wait pod -n kube-system -l name=sealed-secrets-controller --for=condition=Ready


# Step3: 鍵を出力するので、ディレクトリを作っておく
mkdir -p ${鍵のディレクトリ}


# Step4: マニフェストファイルごとキーペアのバックアップ or 置き換え
if [ ! -e ${鍵のディレクトリ}/${キーペア} ]; then
  # キーペアが存在しないなら、新たに生成する
  kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > private-key
  yq r private-key 'items[0].data."tls.crt"' | base64 --decode > ${鍵のディレクトリ}/${公開鍵}
+ data_key_json=$(aws kms generate-data-key \
+   --key-id ${KMS上のkey id} \
+   --key-spec AES_256 \
+   --output json)
+ openssl enc -e -aes-256-ecb -in private-key -out ${鍵のディレクトリ}/${キーペア} -pass pass:$(echo $data_key_json | jq -r '.Plaintext')
+ rm -f private-key
+ echo $data_key_json | jq -r '.CiphertextBlob' | base64 --decode > ${鍵のディレクトリ}/${データキー}
else
  # キーペアが存在するなら、その鍵に置き換える
+ data_key=$(aws kms decrypt \
+   --ciphertext-blob fileb://${鍵のディレクトリ}/${データキー} \
+   --query 'Plaintext' \
+   --output text)
+ openssl enc -d -aes-256-ecb -in ${鍵のディレクトリ}/${キーペア} -out private-key -pass pass:${data_key}
  kubectl replace --force -f private-key
  kubectl delete pod -n kube-system -l name=sealed-secrets-controller
+ rm -f private-key
fi
```

先ほどのスクリプトにKMSでの暗号化を加えたものになります。

### バックアップでの暗号化

```diff
+ data_key_json=$(aws kms generate-data-key \
+   --key-id ${KMS上のkey id} \
+   --key-spec AES_256 \
+   --output json)
+ openssl enc -e -aes-256-ecb -in private-key -out ${鍵のディレクトリ}/${キーペア} -pass pass:$(echo $data_key_json | jq -r '.Plaintext')
+ rm -f private-key
+ echo $data_key_json | jq -r '.CiphertextBlob' | base64 --decode > ${鍵のディレクトリ}/${データキー}
```
まず、バックアップの際には、`aws kms generate-data-key`を使いデータキーを作成します。
すると、

```json
{
    "Plaintext": "xxxxxxxxxxxxxxx",
    "KeyId": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxxxxx",
    "CiphertextBlob": "xxxxxxxxxxxxxxx"
}
```

こちらのjsonが結果として出力されます。
このうち、Plaintextがデータキーの平文で、CiphertextBlobがそれの暗号化しbase64エンコードしたものになります。

そのため、opensslのpasswordとしてはPlaintextを使用し、リポジトリに保存する際はCiphertextBlobを使用します。

### 置き換えでの復号化

```diff
+ data_key=$(aws kms decrypt \
+   --ciphertext-blob fileb://${鍵のディレクトリ}/${データキー} \
+   --query 'Plaintext' \
+   --output text)
+ openssl enc -d -aes-256-ecb -in ${鍵のディレクトリ}/${キーペア} -out private-key -pass pass:${data_key}
```
次に、置き換えの際には、`aws kms decrypt --ciphertext-blob`でデータキーを復号化します。

ちなみに、暗号化する際に`--key-id`が一緒に含まれているようになっているため、復号化の際にコマンドの引数として`--key-id`を指定しなくても良いようになっています。
（ありがたい）



この結果、リポジトリに平文が保存されなくなったので、比較的安全な構成になりました。

# まとめ

Sealed Secretsの導入方法と、運用していくにあたってのバックアップ方法を共有しました。

Sealed SecretsはGitOpsを実践する上で相性が良く、とても気に入っています。
皆さんも、k8sでの機密情報管理で迷われましたら、一度お試しください。

GitHub: [@yukiarrr](https://github.com/yukiarrr)
Twitter: [@yukiarrr](https://twitter.com/yukiarrr)

# 参考リンク

- [SecretをGitHubに登録したくないのでSealedSecretを使う](https://qiita.com/sotoiwa/items/3cd9ba7b7175ce9c71be)