Google Secret Manager と External Secrets Operator を連携して GKE で secret を自動生成 #external-secrets #GCP #GKE #SecretManager
はじめに
KubernetesでSecret manifestを作成する際に、Secretのデータはただ単にbase64でエンコードしているだけなので、Kubernetes APIサーバにアクセスしてSecretリソースを閲覧できる権限があれば、誰でもSecretのデータをbase64でデコードして、データの中身を見ることができてしまいます。そのため、Secretデータなどの秘匿情報を安全に管理する方法を別途ユーザー自身で検討する必要があります。
本Blogでは、そのSecretデータなどの秘匿情報を安全に管理する一つの手段として、Secret管理コンポーネントとなる External Secrets Operator を利用して、Google CloudのSecret Management Systemとなる Secret Manager からSecretを取得し、Google Kubernetes Engine(GKE)で安全に秘匿情報を利用する方法について検証しましたので、ざっくりと紹介します。
External Secrets Operatorとは
概要
公式HPでは以下のように紹介されています。
The External Secrets Operator extends Kubernetes with Custom Resources, which define where secrets live and how to synchronize them. The controller fetches secrets from an external API and creates Kubernetes secrets. If the secret from the external API changes, the controller will reconcile the state in the cluster and update the secrets accordingly.
https://external-secrets.io/latest/introduction/overview/
KubernetesにExternal Secrets Operator(以降、ESO)をデプロイすることで、AWS Secrets ManagerやGoogle Secret Manager、Azure Key VaultのようなSecret Management Systemから秘匿情報を自動取得して、Kubernetes Cluster内で自動的にSecretリソースを作成することができます。Secret Management System側の秘匿情報に変更があった場合は、ESO(controller)がその変更を自動検知して、Cluster内のSecretリソースも自動更新します(reconciliation loop)
ESOのCustom Resorceでは主に、Secret Management Systemにどのようにアクセスするかを定義する SecretStore
リソースと、どのようなデータを取得するかを定義する ExternalSecrets
リソースの2種類が存在します。また、SecretStore
リソースの関連で、cluster-wideな(全てのnamespaceに適用される)SecretStore
として、ClusterSecretStore
リソースも存在します。
Google Secret Manager と External Secrets Operator の連携イメージ
今回のGoogle Cloudにおける検証ベースになりますが、ESOの各Custom ResourceとGoogle Secret Managerの連携イメージは以下のようになります。検証ではcluster-wideなClusterSecretStore
リソースを利用する前提です。最初にESOがデプロイされると、常にESOはExternalSecretリソースをreconciliation loopで自動更新している状態になります。Custom ResourceであるExternalSecretsリソースが作成されると、ExternalSecretリソースで指定したClusterSecretStoreリソースを参照し、ClusterSecretStoreリソースに設定されているアクセス先(Google Cloud)の認証情報や、取得するsecret manager secretの情報をもとに、external API をインスタンス化します(①)。external API がインスタンス化されるとESOはexternal APIからsecret情報を抽出し、ExternalSecretリソースで定義された「作成するsecretのnameやdata.keyなど」の情報もとに、実際にCluster内でsecretを自動生成します(②)。ESOは常にexternal APIと自動生成したsecret valuesが同期しているかどうか、チェックします。
[検証] Google Secret Manager と External Secrets Operator を連携して GKE で secret を自動生成
それでは、前述の連携イメージをもとに、実際にESOをデプロイしてsecretが自動生成されるか確認していきます。
前提
- Kubernetes Engine API / Secret Manager API 有効化済み
- Workload Identityが有効化された GKE Cluster を作成済み
- CloudShellで作業実施
Google Secret Manager に 認証情報(version) を登録
事前準備として、ESOが取得するシークレットをGoogle Secret Managerであらかじめ作成します。
- Google Cloud コンソール画面のナビゲーションメニューで、[セキュリティ] > [Secret Manager] から、シークレットを新たに作成します。シークレットの名前、値は検証用として以下のように設定しました。
- 名前:test-secret
- シークレットの値:test-password
GKE に External Secrets Operator をインストール
GKE ClusterにESOをインストールしていきます。基本的な流れは公式HPに記載の手順に沿いますが、今回のproviderはGoogle Cloudになるので、SecretStoreの記載方法やその他Google Cloud特有の設定については、こちらのリファレンスを参考に進めます。
- Cloud Shell を利用して、インストール先のGKE Clusterに接続します。
$ gcloud container clusters get-credentials <cluster_name> --zone <zone_name> --project <project_name>
~~~
Fetching cluster endpoint and auth data.
kubeconfig entry generated for <cluster_name>.
- インストールはHelm Chartを利用します。Helm Chart repositoryにexternal-secrets repoを追加して、Clusterにインストールします。
$ helm repo add external-secrets https://charts.external-secrets.io
~~~
"external-secrets" has been added to your repositories
$ helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace
~~~
NAME: external-secrets
LAST DEPLOYED: Mon Oct 23 08:53:13 2023
NAMESPACE: external-secrets
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
external-secrets has been deployed successfully!
In order to begin using ExternalSecrets, you will need to set up a SecretStore
or ClusterSecretStore resource (for example, by creating a 'vault' SecretStore).
More information on the different types of SecretStores and how to configure them
can be found in our Github: https://github.com/external-secrets/external-secrets
インストールが完了すると、ESOに関連するdeploymentなどがデプロイされます。
$ kubectl get all -n external-secrets
NAME READY STATUS RESTARTS AGE
pod/external-secrets-969fb45c8-mqvk8 1/1 Running 0 19h
pod/external-secrets-cert-controller-757f9d86d4-5tlzt 1/1 Running 0 19h
pod/external-secrets-webhook-8468995c6f-lz6m5 1/1 Running 0 19h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/external-secrets-webhook ClusterIP 10.116.3.210 <none> 443/TCP 20h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/external-secrets 1/1 1 1 20h
deployment.apps/external-secrets-cert-controller 1/1 1 1 20h
deployment.apps/external-secrets-webhook 1/1 1 1 20h
NAME DESIRED CURRENT READY AGE
replicaset.apps/external-secrets-969fb45c8 1 1 1 20h
replicaset.apps/external-secrets-cert-controller-757f9d86d4 1 1 1 20h
replicaset.apps/external-secrets-webhook-8468995c6f 1 1 1 20h
Service Accountの作成/Workload Identityの設定
GKE ClusterにインストールしたESOが、Google Secret Managerで作成したシークレットにアクセスするためのGoogle Service Account(以降、GSA)やKubernetes Service Account(以降、KSA)作成と、Workload Identityの設定を実施します。ESOのGoogle Cloudへの認証は、GSAのアクセスキーを発行して利用する方法もありますが、アクセスキー流出などのリスクもあり、よりセキュアなWorkload Identityを利用する方法を実践していきます。
- secretを自動生成するKubrenetes namespace を作成します。検証では「team1」というnamespaceを作成します。
$ kubectl create ns team1
namespace/team1 created
- namespace「team1」に紐づくKSAを作成します。このKSAはWorkload Identityの設定により、最終的にGSA「external-secrets-team1@<project_name>.iam.gserviceaccount.com」に紐づけて、Google Cloudの認証を行う想定です。
$ vi serviceaccount.yaml
~~~
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-secrets-team1
namespace: team1
annotations:
iam.gke.io/gcp-service-account: external-secrets-team1@<project_name>.iam.gserviceaccount.com
$ kubectl apply -f serviceaccount.yaml
serviceaccount/external-secrets-team1 created
- 次に、CloudShellで以下gcloudコマンドを実行してGSAを作成します。
$ gcloud iam service-accounts create external-secrets-team1 \
--project=<project_name>
~~~
Created service account [external-secrets].
- 作成したGSAに、Google Secret Managerのシークレットへのアクセスに必要なロール
roles/secretmanager.secretAccessor
を付与します。
$ gcloud projects add-iam-policy-binding <project_name> \
--member "serviceAccount:external-secrets-team1@<project_name>.iam.gserviceaccount.com" \
--role "roles/secretmanager.secretAccessor"
- また、GCP アクセストークンの生成に必要なロール
roles/iam.serviceAccountTokenCreator
を付与します。
$ gcloud projects add-iam-policy-binding <project_name> \
--member "serviceAccount:external-secrets-team1@<project_name>.iam.gserviceaccount.com" \
--role "roles/iam.serviceAccountTokenCreator"
- 権限
iam.serviceAccounts.getIamPolicy
だけを含むカスタムロールgetIamPolicy
を作成して、GSAに付与します。権限
はgcloudコマンドでWorkloadIdentity設定時に必要となる権限です。iam.serviceAccounts.getIamPolicy
$ gcloud iam roles create getIamPolicy --project=<project_name> \
--title=getIamPolicy \
--permissions="iam.serviceAccounts.getIamPolicy" --stage=GA
$ gcloud projects add-iam-policy-binding <project_name> \
--member "serviceAccount:external-secrets-team1@<project_name>.iam.gserviceaccount.com" \
--role "projects/<project_name>/roles/getIamPolicy"
- 作成したGSAとKSAのIAM ポリシーバインディングを実施します。
$ gcloud iam service-accounts add-iam-policy-binding external-secrets-team1@<project_name>.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:<project_name>.svc.id.goog[team1/external-secrets-team1]" \
--project <project_name>
- KSAに、作成したGSAのメールアドレスでアノテーションを付与します。
$ kubectl annotate serviceaccount external-secrets \
--namespace external-secrets \
iam.gke.io/gcp-service-account=external-secrets-team1@<project_name>.iam.gserviceaccount.com
External Secrets / Cluster Secret Store リソースデプロイ
ESOを動作されるための準備が整ったので、ExternalSecretリソースとClusterSecretStoreリソースを作成して、実際にsecretが自動生成されるか確認します。
- 以下のClusterSecretStoreを用意して、デプロイします。
$ vi clustersecretstore.yaml
~~~
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: test-secretstore
spec:
provider:
gcpsm:
projectID: <project_name>
auth:
workloadIdentity:
clusterLocation: asia-northeast1-b
clusterName: <cluster_name>
clusterProjectID: <project_name>
serviceAccountRef:
name: external-secrets-team1
namespace: team1
$ kubectl apply -f clustersecretstore.yaml
~~~
clustersecretstore.external-secrets.io/test-secretstore created
- 以下のExternalSecretsを用意して、デプロイします。
$ vi externalsecret.yaml
~~~
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: test-externalsecret
namespace: team1
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: test-secretstore
target:
name: test-secret
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: test-secret
version: latest
$ kubectl apply -f externalsecret.yaml
externalsecret.external-secrets.io/test-externalsecret created
- 問題無くリソースがデプロイされると、「team1」namespaceにsecretが自動生成されているのが確認できます。
$ kubectl get secret -n team1
NAME TYPE DATA AGE
test-secret Opaque 1 96m
- secret の value は base64 でエンコードされているので、以下のようにデコードして実際のvalueを確認することができます。
$ kubectl get secret test-secret -n team1 -o jsonpath='{.data.password}' | base64 -d
test-password
まとめ
簡単ですが Google Secret Manager と External Secrets Operator を連携して GKE で secret を自動生成する方法について紹介しました。実際のESO利用イメージとしては、namespace毎にチームを分け、ExternalSecretsリソースやSecretStoreリソースを各チームで管理させる(他のチームのリソースを触れないようにする)適切なscopingを実施したいケースが多いと思いますので、機会があればそちらも検証/紹介したいと思います。