はじめに
皆さんは、Kubernetes上の機密情報をどうやって管理しているでしょうか?
一応、k8sにはSecretというリソースが存在しますが、これは所詮base64エンコードされているだけなので、リポジトリ管理するとなるとセキュリティ的によろしくありません。
そのため、ネット上にはいくつかの暗号化の仕組みが存在しますが、その中でも今回はGitOpsを提唱するWeaveworksが紹介しているSealed SecretsというOSSについて書いていきたいと思います。
(他にもkubesecや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リソースのみを使うのに比べてセキュアとなります。
公式参照: Sealed Secrets: Protecting your passwords before they reach Kubernetes
GitOpsでArgo CDなどのpull型デプロイツールを使っている際には特におすすめで、自前の環境でもこの組み合わせを利用しています。
Sealed Secretsの導入
Sealed Secretsの導入に必要なスクリプトは以下の通りです。
# 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: リソースの適応
# 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が立ち上がるまで待機
# 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: 鍵を出力する用にディレクトリ作成
# Step3: 鍵を出力するので、ディレクトリを作っておく
mkdir -p ${鍵のディレクトリ}
これは人によっては不要です。
もし鍵の出力先で、存在しないディレクトリの可能性があるなら、このコマンドを叩いておきましょう。
Step4: マニフェストファイルごとキーペアのバックアップ or 置き換え
# 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 置き換えを行います。
キーペアのバックアップ
# キーペアが存在しないなら、新たにバックアップする
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 > ${鍵のディレクトリ}/${公開鍵}
でも取得できます)
キーペアの置き換え
# キーペアが存在するなら、その鍵に置き換える
kubectl replace --force -f ${鍵のディレクトリ}/${キーペア}
kubectl delete pod -n kube-system -l name=sealed-secrets-controller
キーペアの置き換えでは、まずkubectl replace
で丸ごとリソースを置き換えます。
そのあと、置き換えを反映するためにはコントローラを再起動する必要があるので、kubectl delete
します。
(詳しくはドキュメント参考)
これでSealed Secretsの設定は完了です。
公開鍵を使って暗号化する
kubesealの導入
Secretの暗号化など、Sealed Secretsに関する操作をする場合、公式から提供されているkubesealを使用することがオススメです。
導入方法は、
brew install kubeseal
でOKです。
Secretリソースの作成
暗号化する対象となるSecretリソースを作成します。
yamlで直接書こうとすると、いちいちbase64エンコーディングするのが面倒なため、kubectl create --dry-run
を活用すると、簡単に作成することができます。
(ただし、passwordなどの扱いには注意してください)
kubectl create secret generic mysecret \
--from-literal username=xxxx \
--from-literal password=xxxx \
-o yaml --dry-run > ${Secretリソース}
暗号化する
上記で生成されたSecretリソースを、Sealed Secrets導入時に保存した公開鍵を使い暗号化します。
kubeseal --format=yaml --cert=${鍵のディレクトリ}/${公開鍵} < ${Secretリソース} > ${暗号化されたSealedSecretリソース}
すると、以下のようなusernameとpasswordが暗号化されたSealedSecretリソースが生成されます。
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で使われるものが、データキーと呼ばれています。
今回の例でも、このデータキーの方式で暗号化を行なっていきます。
暗号化するスクリプト
# 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での暗号化を加えたものになります。
バックアップでの暗号化
+ 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
を使いデータキーを作成します。
すると、
{
"Plaintext": "xxxxxxxxxxxxxxx",
"KeyId": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxxxxx",
"CiphertextBlob": "xxxxxxxxxxxxxxx"
}
こちらのjsonが結果として出力されます。
このうち、Plaintextがデータキーの平文で、CiphertextBlobがそれの暗号化しbase64エンコードしたものになります。
そのため、opensslのpasswordとしてはPlaintextを使用し、リポジトリに保存する際はCiphertextBlobを使用します。
置き換えでの復号化
+ 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
Twitter: @yukiarrr