MF KESSAI SRE(兼CTO)の @shinofara です。
いきなりですが、皆さんはどの様に秘密情報の管理と配布を行っていますか?
https://tech.mfkessai.co.jp/2017/07/1/ を公開した2017/07/31時点では、Google Kubernetes Engine(GKE)を中心としたシンプルなアーキテクチャでした。
当初はKubernetes Secretだけですので特に問題は無かったのですが、2年半と時間が立つことで目的に応じて利用するサービスも増えた事で Google App Engine(GAE/Se and GAE/Flex)
,Cloud Function(CF)
,Cloud Run
などのうえで、複数のアプリケーションが動く状態になってます。
今回の記事では、複数のアプリケーションが異なる環境で動く状態となり大変になってきた秘密情報管理と配布方法をふりかえりたいと思います。 そしてどういう管理配布方法に落ち着いたのかをご紹介させていただければと思っております。
とても長くなりますが、お付き合いいただければと思います。
創業(2017/03)から先日まで
2017年3月に創業してから最初の頃は、前述した通り利用していたサービスはGKEだけでしたので、Kubernetes Secretだけで特に問題は発生しませんでした。 ですがサービスの成長が進みGAE、CF、そしてRunと利用するサービスが増えてきた事で、それぞれの異なるデプロイ方法に合わせて配布方法も複雑化しました。 一度書き出してみたいと思います。
Google Kubernetes Engine(GKE)
秘密情報は特性上コンテナに埋め込んでアップロードしない方針ですので、初期の頃はDeleteしてからApplyして作り直す運用をしていました。 途中からKustomizeを利用してSuffixをつけて古いSecretは残して、どんどん新しいSecretを作っていく運用方法に変更しました。 Kubernetes Secretは公式を参考にしています。
$ kubectl delete secret SECRET_NAME
$ kubectl apply -f secret/secret.yml
Google App Engine(GAE)
公式の情報を参考にして、Source Repositoryで管理してる秘密情報を、GAE DEPLOYするタイミングで一緒にデプロイしています。
$ gcloud cp secret/secret.yml ./
$ gcloud app deploy
Cloud Function(CF)
公式の情報を参考にして、CircleCIなどで Source Repositoryから取得して、GAE DEPLOYするタイミングで一緒にデプロイしています。
$ gcloud cp secret/secret.yml ./
$ gcloud app deploy
Cloud Run(Run)
Cloud Runは一度GCRにPushしてから gcloudコマンドでデプロイ処理を行うため、Secretfileを取得してデプロイという事ができません。 もしできるとしたらenvに埋め込むとかになりますが、弊社で定めている秘密情報管理と配布に関するポリシー的に反するため、その方法は採用できませんでした。
そして弊社が採用した手段は、GCSにKMSで暗号化したSecret.ymlを、Cloud Run起動時にGetして複合するという方法でした。
$ gcloud kms encrypt —plaintext-file secrret.yml -chiper-file secret.enc.yml
$ gsuitl cp secret.enc.yml gs://your-secret-bucket/secret.enc.yml
bucket := client.Bucket("your-secret-bucket")
object := bucket.Object("secret.enc.yml")
reader, err := object.NewReader(ctx)
kmsCli := kms.NewCloudKMS("your-gcp-project-id")
encrypted, err_ := ioutil.ReadAll(reader)
decrypted, err := kmsCli.NewEncryptor(
ctx,
kms.GenerateKMSKey("global", "your-keyring", "your-key"),
).Decrypt(encrypted)
結局どういう状況だったのか
- Cloud Source Repositoryで管理している秘密情報を、GKEのSecret更新時に取得してApplyしていた
- 途中からKustomize化したので、Delete/ApplyからSuffixをつけて管理する形に移行
- CircleCIやCloud BuilderでSource Repositoryをcloneして取得して、GAEやCFのデプロイ対象に含めてデプロイ
- Source Repositoryで管理しているが、KMS暗号化した秘密情報をGCSにUpload
- そもそも1つのSource Repositoryに複数環境(本番、サンドボックス、ステージング)毎のSecretをまとめて管理している状態だったため、参照・更新権限を持っているメンバーが限られていた
一言で言えばSource Repositoryという場所で管理できているが、編集者が限られてしまっていたこと、配布方法が複雑になってしまっていた。カオス!
そして2019年7月からは
現在どういう形に落ち着いたかといいますと、HashicorpのVaultを採用しました。
YouTube
や、公式で紹介されていたドキュメントを参考に https://cloud.google.com/solutions/using-vault-for-secret-management?hl=ja
なぜHashi Corp Vaultなのか
今回、秘密情報管理の仕組みを考えるにあたって、必要な要件は下記になります。
- Cloud Function, GKEコンテナ等に設定されたService Account単位で認証を行う事ができる
- Service Account毎に取得できる情報を制限できる
- 開発者毎にアクセスできる情報レベルや、参照権限、書き込み権限をある程度柔軟に設定できる
- GCPマネージドであることもしくは、最低限用意されたイメージを置くだけで運用できること(運用コストが安いこと)
- 秘密情報のバージョン管理ができること
https://cloud.google.com/solutions/using-vault-for-secret-management?hl=ja こちらに書かれている内容は以下です。
- Service Accountの秘密鍵を使って署名を行い(JWT)、Vault側で認証が可能
- 認証結果毎にアクセスできる情報もPolicyを作れる
- GCSをDataStorageとすることでHigh Availability対応可能
- Datasotreに置かれる情報はKMSで暗号化済み
- Githubログインで開発者の識別が可能
どういう設計で運用しているか
Hashicorp公式でVaultのDockerImageが公開されているので、こちらを利用しています。 https://hub.docker.com/_/vault
Vaultを中心とした運用方法
弊社ではGCPのプロジェクトを本番環境やステージング環境という環境毎に分けているかつ、社内の様々なプロジェクトが利用するプラットフォームとしての、Infra Projectありました。
企業によっては1プロジェクト内でNamespaceとかPrefixを使うか、もしくは複数サービス作れるものはサービス分離したりするなどして、複数環境を構築してると思います。 弊社ではこのような形になっています。
という事もあり、本番プロジェクトのGKEやRunなどから、InfraプロジェクトのVaultにアクセスする形となります。 そのため、Service Account周りは少し複雑な設定が必要になります。
Service Accountに関連した流れは以下になります。
実際に設定する一部の例
App1のみ参照許可する。
# cat app1.hcl
path "+/data/app1" {
capabilities = ["read"]
}
本番環境のみ許可する。
# cat prod.hcl
path "sandbox/*" {
capabilities = ["deny"]
}
path "staging/*" {
capabilities = ["deny"]
}
path "development/*" {
capabilities = ["deny"]
}
VaultのPolicyを登録する。
vault write -tls-skip-verify sys/policy/app1/allow policy=@policy/app1.hcl
vault write -tls-skip-verify sys/policy/production/allow policy=@policy/prod.hcl
ポリシーをService Accountに割り当てる(Roleを作る)
$ vault write auth/gcp/role/production-app1-read \
type="iam" \
project_id="your-production-project-id" \
policies="production/allow,app1/read" \
bound_service_accounts="your-production-app1-sa@your-production-project-id.iam.gserviceaccount.com"
このようにして特定のService Accountは本番のApp1しか参照できないように設定してます。 この時project_idはService Account発行元のGCP ProjectIDにしておかないと、前述した鍵情報取得が行えず認証エラーとなります。
結果どう?
現在まだ移行途中なため、まだまだゴールは遠いですが、Vault時代はシンプルで使いやすく今までの管理方法よりとても楽になりました。 またService Account単位のアクセス権設定(RoleとPolicyもわかりやすい)が容易に行える為、今後開発者が増えても適切な形でスケールできそうです。