Kubernetes(EKS)で秘密情報をどう扱うか
まとめ
- 秘密情報をEKS上で楽に管理したい。
- これまでは秘密情報を暗号化したファイルを使って注入してた
- 変数ごとに何が変更されたかを確認するのが面倒
- AWS KMSで暗号化してたのでAWS CLIのインストールのためにPythonを入れるのが微妙
- KubernetesのSecretをそのまま使うのは避けたい
- Base64してるだけで暗号化してない
- k8sの権限の強い人からは見えてしまう
- 環境変数の値だけを暗号化してPod内で複合
- リポジトリに読める形でコミットできる
- 追加・変更されたか否かのレベルでなら確認できる
- Pod以外は複合できないためマルチテナントなk8sだと権限管理が楽
- shushならバイナリ一個入れるだけですむ
- GKEに移っても実装できそうな規模なのでなんとかなりそう。
- リポジトリに読める形でコミットできる
- 欠点もある
- バイナリを一個置く必要がある
- entrypoint/commandの変更が必須
- 起動時に暗号化した数だけEMSにリクエストが飛ぶ
- Pod数が少ないなら課金は問題なさそう
- 起動時間も数秒伸びるけど無視できるレベル
- Kubernetesの権限管理が大変じゃないならkubesecが楽そう
- コンテナは一切気にしなくて良い
- Kubernetesの中にも
背景と問題
Cookieの暗号化鍵やAccess Tokenといった秘密情報をEKS上で扱いやすい形で管理したいです。
これらは本番や開発環境といった環境ごとに変化する値のため、Twelve-factor app12 で言われているようにアプリケーションの外部に持たせたいです。
また、コンテナではIMAGE IMMUTABILITY PRINCIPLE(IIP、イメージ不変性の原則)3が重視されるため、設定をコンテナの外に外部化すべき要求がより強いです。そのため、環境変数や設定ファイルなどを利用してコンテナ外から秘密情報を切り替え、開発環境や本番環境等で同一のイメージを利用する方法が欲しくなります。
このとき、秘密情報をそのままリポジトリにコミットすることは基本的に避け、暗号化を行い実行時に復号化するべきです。 そのため、設定を外部化することに加え復号化をどのように行うかが重要になります。
その他に以下のように解決したいことがありますが、全てを解決できなくても構いません。
- 複合する範囲を狭める
- 複数の関係ないプロジェクトがEKS上で動きうるので、複合したデータへのアクセスは可能な限り狭めたい
- 設定の管理
- 値の追加・変更がレビュー時などに容易にわかるように
- できればソースコードと同一にして、反映タイミングの統一、ロールバック時に戻しやすく
- 依存関係の簡素化
- アプリケーションには暗号化の有無を意識させない
- コンテナからも暗号化の有無を意識させない
- 複合をコンテナ内でやるにしても依存関係は極力少なく4
- AWSへの依存は可
- どうせ別のクラウドに動かす際は大幅な変更が必要
- 秘密情報のセット部分の修正は大変ではない
解決方法
具体的にどうやるかについては、大きく分けてコンテナ内で解決する方法、Kubernetesで解決する方法、両方使って解決する方法の3つの方法が考えられます。
コンテナ内で解決する方法
例えば.envやアプリが読み込む設定ファイルを暗号化して保存しておき、それを実行前に復号化してロードする方法です。
どこから、何を読み込むかといった情報を外部から与えることで、IIPを保ちつつ複数の環境に対応可能です。
AWS KMSに暗号化鍵があれば、AWS CLIを使って以下のようにファイルの暗号・復号化ができます。
aws kms decrypt --ciphertext-blob fileb://$ENC_FILE --query Plaintext --output text | base64 --decode > $DEC_FILE
aws kms encrypt --key-id $KEY_ID --plaintext fileb://$DEC_FILE --query CiphertextBlob --output text | base64 --decode > $ENC_FILE
これで暗号化したファイルをコンテナ内に入れておき、どのファイルを複合するかは秘密情報ではないため環境変数で設定すれば、一つのイメージで複数の環境にできます5。特にEKSのPodは任意のIAM Roleを紐付けられるため6、特定のPodでのみ複合可能な暗号化ファイルが作成でき、権限管理も容易です。
また、以下のようにAWS S3を活用することで、よりエレガントに解決可能です。
https://speakerdeck.com/joker1007/ling-he-shi-dai-falserailsyun-yong?slide=19
他にもAWS Secrets Managerから取ってくるなどコンテナ内のコマンドで解決する方法の派生型はいくつかあります。
これらの方法はECS等でも可能でかなり汎用性が高いですが、いくつかの問題点がありました。
-
変数の管理が困難
設定ファイルを暗号化して保存するため、基本的に中身は複合しないと読めません。
そのため特定の環境だけ設定し忘れ、変更し忘れといったことが起きやすいです。
また、外部に置いた場合はコードの管理とは別になるため、ある機能で新たに環境変数を設定して開発していたが、本番に出す際にそこには設定されていない…みたいなことは起きやすいです。 -
AWS CLIを入れるのが面倒
現在はgolangでアプリケーションを書いているため顕著ですが、本来なら不要なAWS CLIをコンテナに入れないといけず、そのためにPythonのインストールが必要になるためかなり面倒になります。
特にベースとなるDockerイメージに軽量なものを使っていると顕著です。
ただし、AWS CLI v2はバイナリ単体で動くようになったため、この問題はかなり融和されました。
特に前者の問題はけっこう面倒であり、良い方法が無いかと考え次の2つの方法を検討しました。
KubernetesのSecretで解決する方法
KubernetesにはSecretと呼ばれる秘密情報を管理するための機能が存在します。
https://kubernetes.io/docs/concepts/configuration/secret/
Secretに保存したデータは環境変数としてPodに設定を行うことが出来るため、 Secretに秘密情報を保存することで外部から設定を行うことができます。 また、前述の方法と違いコンテナで復号化を行う必要がないため、 PodのIAM Roleの制御やコンテナ内の依存関係が大幅に簡素になります。
kustomizeを利用してScertetを精製しているような場合、アプリケーションのデプロイとSecretの更新を同時に行うことができる、ロールバックもしやすいなど、Kubernetesの利点を最大限活用できます。
しかし、Secretにはいくつかの注意点があり7、主に次の2点から利用できませんでした。
- 通常ではBase64エンコードしただけなので簡単に値が読める
- etcdの中に平文が入る
- Secretへのアクセスを適切に制御しなければならない
- 複数の独立したプロジェクトがEKS上で動くので相互に読めるのは避けたい
- Kubernetesの世界でのきめ細かい制御はしたくない
- PodのRoleにリソース制御を行うので、そこに寄せたい
- 設定した値の管理が面倒
- Secretのマニフェストファイルの扱いに困る
- 暗号化してバージョン管理にコミットしたい
なお、この方法にはSecretを手元で暗号化しておく、KMSからSecretを作るなど様々な派生系があります。
-
EKSのetcdの暗号化8
- etcdを暗号化するため、保存されているデータは安全に管理される
- Pros
- Kubernetes上で暗号化を意識しなくていい
- k8sの管理者に細かい権限管理が不要であればとても便利
- 相互に見せたくない場合はこれだけだと不十分
- 他の手法と組み合わせることもできるので、とりあえずやって他の方法と組み合わせることも可能
- Kubernetes上で暗号化を意識しなくていい
- Cons
- Secretの内部的な格納方法が変わるだけなので基本は変わらない
- Secretへのアクセスが出来るなら平文が取得できる
- 秘密情報のバージョン管理に関する問題は解決しない
- Secretの内部的な格納方法が変わるだけなので基本は変わらない
-
Sealed Secrets
- https://github.com/bitnami-labs/sealed-secrets
- 暗号化されたSecretを表すSealedSecretというリソースと、複合するControllerを追加
- SealedSecretをapplyすると、Controllerが複合してSecretにする
- Pros
- リポジトリにはSealedSecretを保存すれば良いのでバージョン管理可能
- 値のみが暗号化されるため環境変数名は確認できる
- Podは暗号化を意識する必要がない
- クラスタ上では通常のSecretになるので汎用性がある
- Cons
- クラスタ上の管理は通常のSecret同様
- 暗号化する鍵ファイルの運用
- Controllerをクラスタ上に置くので若干コストがかかる
- https://github.com/bitnami-labs/sealed-secrets
-
kubesec
- https://github.com/shyiko/kubesec
- Secretの値だけを暗号・複合化する
- Sealed Secretsと違いSecretリソースを扱う
- 複合したSecretをapplyして利用する
kubesec decrypt secret.enc.yml | kubectl apply -f -
- ローカルやリポジトリのSecretマニフェストのみを暗号化するイメージ
- Pros
- リポジトリには暗号化したSecretを保存すれば良いのでバージョン管理可能
- 値のみが暗号化されるため環境変数名は確認できる
- Podは暗号化を意識する必要がない
- クラスタ上では通常のSecretになるので汎用性がある
- AWS KMS/Cloud KMS対応
- Cons
- クラスタ上の管理は通常のSecret同様
- kubesecしたSecretとそうでないものを適切に使い分ける必要がある
- https://github.com/shyiko/kubesec
-
Kubernetes External Secrets
- https://github.com/godaddy/kubernetes-external-secrets
- 外部のSecretManagerの値をSecretとして利用する
- SecretManagerの値を示すExternalSecretsというリソースの追加
- 値を読み出してSecretにするController
- AWS/GCP/Alibaba等複数のSecretManagerに対応
- Pros
- SecretManagerの機能を活用できる
- ローテーションやバージョン管理
- 削除保護等
- リポジトリにExternalSecretをコミットできる
- 秘密情報そのものは入ってない
- 参照のみ
- 暗号化・復号化をKubernetesは考えなくて良い
- Controllerが全て行う
- SecretManagerの機能を活用できる
- Cons
- クラスタ上の管理は通常のSecret同様
- SecretManager側で適切に秘密情報を管理しなければならない
- 管理するところが増える…
- 外部のSecretManagerの値をSecretとして利用する
- https://github.com/godaddy/kubernetes-external-secrets
両方を利用して解決する方法
環境変数の値を暗号化する
値を暗号化した環境変数をSecretを使って外から設定し、コンテナ内で複合する方法です。
具体的には秘密情報を暗号化し、ENCRYPT_ACCESS_TOKEN
のようなPrefixを持った名前でSecretに設定します。
このSecretをPodに環境変数として設定し、コンテナ内で前述のPrefixを持つ環境変数の値を複合してPrefixのないACCESS_TOKEN
のような環境変数に設定し直します。
AWS CLIを利用したシェルスクリプトや、実装済みのバイナリとしてshushというものがあります。
- Pros
- 鍵の管理をPodのIAM Roleに寄せられる
- 暗号化した変数はバージョン管理可能
- 変数の変更の有無は複合せずとも確認可能
- コンテナ内のプロセスでしか複合されない
- Cons
- コンテナに追加のバイナリインストールが必要
- shushなら1個ですむとはいえ依存関係が増えはする
- 復号化の前処理も実行時に必要になる
- kubectl execコマンドを利用した際に環境変数のロードを手動でやる必要がある
- 暗号化した鍵の数だけKMSにアクセスが行く
- 数が多いと料金がかかる
- Pod数が多くないなら料金は十分安い
- KMSの料金はわりと安い
- 起動時間が鍵の個数によって伸びていく
- AWS内の通信なので微々たるものだけど…
- 並列に複合すればほとんど気にならないはず
- KMSのアクセス制限
- 短時間に大量にアクセスすると制限に引っかかる可能性がある
- https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/limits.html
- 数が多いと料金がかかる
- コンテナに追加のバイナリインストールが必要
設定ファイルを暗号化して直前に復号化する
暗号化したファイルをSecretにしておき、initContainerで複合する方法です。 環境変数の場合、initContainerから変更はできない(はず)ですが、 ファイルであればマウントしたものを変更可能なため、アプリケーションのコンテナからは独立して複合することができます。
具体的にはkamsuというプロダクトがあります。
https://github.com/Soluto/kamus
なお、今回は環境変数で注入する前提だったため選択肢からは外しました。
どれが良いのか
コンテナ内で解決する方法、Kubernetesで解決する方法、両方を利用する方法の3つの方法を上げましたが、どれが良いかは何を担保したいかといった要件で大きく変わります。理想的には暗号化した値をSecretに入れ、Podにわたす直前に復号化するような仕組みがあるのが最高ですが、現状見つけられなかったので今回はshushを採用しようと考えています。
一つのEKSクラスタ上に相互に見えてはいけない情報が載る可能性がある、設定の状態をバージョン管理システムで確認したいという点に比重を置き、これをshushは綺麗に解決してくれます。コンテナの依存関係は増えますがバイナリ一つなため影響は小さく、KMSへのアクセス回数はまだPodが少ないので問題にならない、起動時間の増加も微々たるものだったので前述の2つを解決できるなら問題ないと考えました。
暗号化した設定ファイルという形を取ってよいのであればkamsは依存関係がかなりスッキリさせられるため良さそうです。 また,強い権限がクラスター管理者に絞られて全部見えていい、権限の分類が少ないなど、 細かい権限管理を行うことが問題にならない場合はKubernetesで解決する方法が最も楽です。 その中でも、kubesecは導入・運用も簡単に始めることができるため、これを使うのが良さそうです。
-
https://cloud.google.com/solutions/twelve-factor-app-development-on-gcp#3_configuration ↩︎
-
https://www.redhat.com/cms/managed-files/cl-cloud-native-container-design-whitepaper-f8808kc-201710-a4-ja.pdf ↩︎
-
goのコンテナにAWS CLI v1のためにPythonを入れるとか避けたい ↩︎
-
厳密には設定外部化ではないが、暗号化した全環境分のファイルを入れておけば追加の情報は外に無いのでギリギリ目を瞑っていいかなというところ ↩︎
-
厳密にはPodを実行するServiceAccountに紐付けます
https://aws.amazon.com/jp/about-aws/whats-new/2019/09/amazon-eks-adds-support-to-assign-iam-permissions-to-kubernetes-service-accounts/ ↩︎ -
https://kubernetes.io/docs/concepts/configuration/secret/#risks ↩︎