ACS 事業部の埜下です。
前回、プレビュー公開されたばかりの Vault Secrets Operator を使って Kubernetes のシークレット管理について投稿しました。
こちらの記事で、Vault Secrets Operator は以下の 3 種類のシークレットを扱えると説明しました。
- 静的シークレット (Static Secret)
- 動的シークレット (Dynamic Secret)
- PKI シークレット
前回は「静的シークレット」を確認したので、今回は「動的シークレット」を対象とした検証をおこないました。
はじめに
動的シークレットとは
「動的シークレット」は、HashiCorp Vault (以下、Vault) で利用できるシークレット管理方法の 1 つで、Azure のサービスプリンシパルや SQL Server のログインといった認証情報を一時的に作成してくれます。
Vault に認証情報などのデータを登録する「静的シークレット」とは異なり、「動的シークレット」はアプリケーションからリクエストがあったタイミングでデータが作成されます。
また、「動的シークレット」で作成された認証情報には有効期間 (TTL) が設定されており、アプリケーションから使われなくなると認証情報は削除されるため、セキュリティリスクを下げることができます。
下図のように、あらかじめ管理者が Vault にシークレットエンジンやロールを設定しておくことで、アプリケーションからのリクエストに応じてデータベース等に認証情報が作成されます。
検証内容
今回の検証では、Vault から SQL Server に対して一時的な認証情報を作成するような「動的シークレット」を設定します。
なぜ SQL Server かと言うと、筆者が一番馴染みのある RDBMS だからです! 最近(最近でもないか)の SQL Server は Kubernetes でも動くんですよ、すごいですねー。
また、Vault に対して「動的シークレット」へのリクエストはアプリケーション自身ではなく、Vault Secrets Operator からリクエストするようにします。
そして、今回使用するコンポーネントはすべて Kubernetes 上で動かします。 なぜ Kubernetes オンリーかと言うと、筆者がやってみたかったからです!
というわけで、各コンポーネントの繋がりと「動的シークレット」を使う流れは下図のようになります。
- Operator が
VaultDynamicSecret
カスタムリソースの内容を確認 - Operator から Vault に対して SQL Server の動的シークレットをリクエスト
- Vault が SQL Server に一時的な認証情報を作成
- Vault から Operator に SQL Server 認証情報を応答
- Operator が SQL Server 認証情報を Kubernetes シークレットに同期
- Operator がアプリケーションをローリングアップデートして Kubernetes シークレットを反映
- アプリケーションが Kubernetes シークレットを使用して SQL Server に接続
- 今回ここは実施しません
前提条件
今回紹介する手順では以下のコマンドを使用しています。 手順を実行される場合は、これらのコマンドが実行可能な環境をご準備ください。
- Vault CLI
- kubectl
- Helm
- sqlcmd
また、Kubernetes 環境をご準備ください。 今回の検証で使った環境は AKS になりますが、特に AKS 特有の設定はありません。
StorageClass などは AKS デフォルトのものを使っていたりしますので、適宜各自の環境に合わせて読み替えてください。
環境構築
SQL Server
Vault から一時的な認証情報を作成するターゲットとなる SQL Server を Kubernetes にデプロイします。
sa ユーザのパスワードは kubernetes シークレット mssql
として作成しておきます。
また、PersistentVolumeClaim で指定する StorageClass については各自の環境に合わせて変更してください。
$ kubectl create ns mssql $ export SQL_USERNAME=sa $ export SQL_PASSWORD=<sa_password> $ kubectl create secret generic mssql --from-literal=MSSQL_SA_PASSWORD="$SQL_PASSWORD" -n mssql $ cat <<EOF | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: mssql namespace: mssql spec: replicas: 1 selector: matchLabels: app: mssql template: metadata: labels: app: mssql spec: terminationGracePeriodSeconds: 30 hostname: mssqlinst securityContext: fsGroup: 10001 containers: - name: mssql image: mcr.microsoft.com/mssql/server:2022-latest ports: - containerPort: 1433 env: - name: MSSQL_PID value: "Developer" - name: ACCEPT_EULA value: "Y" - name: MSSQL_SA_PASSWORD valueFrom: secretKeyRef: name: mssql key: MSSQL_SA_PASSWORD volumeMounts: - name: mssqldb mountPath: /var/opt/mssql volumes: - name: mssqldb persistentVolumeClaim: claimName: mssql-data --- apiVersion: v1 kind: Service metadata: name: mssql namespace: mssql spec: selector: app: mssql ports: - protocol: TCP port: 1433 targetPort: 1433 type: LoadBalancer --- kind: PersistentVolumeClaim apiVersion: v1 metadata: name: mssql-data namespace: mssql annotations: volume.beta.kubernetes.io/storage-class: default spec: accessModes: - ReadWriteOnce resources: requests: storage: 8Gi EOF
SQL Server をデプロイできたら、動作確認で使用するデータベースとテーブルを作成します。
今回は mssql
Service を Type: LoadBalancer として作成したので、グローバル IP アドレス経由で SQL Server に接続します。
$ export SQL_ADDR=`kubectl get svc mssql -n mssql --no-headers | awk '{print $4}'` $ cat <<EOF | sqlcmd -U $SQL_USERNAME -P $SQL_PASSWORD -S $SQL_ADDR USE [master]; GO CREATE DATABASE myapp; GO USE [myapp]; GO CREATE TABLE location (street varchar(20), city varchar(20), state varchar(20)); GO INSERT INTO location (street, city, state) VALUES ('main', 'anytown', 'california'); EOF
データベースを作成できたらテーブルを参照できるか確認します。
$ sqlcmd -U $SQL_USERNAME -P $SQL_PASSWORD -S $SQL_ADDR -Q "SELECT street FROM myapp.dbo.location;"
以下のように出力されたらデータベースの作成に成功しています。
street -------------------- main (1 rows affected)
SQL Server の準備は以上です。
Vault Secrets Operator
前回と同じく、Helm を使って Vault Secrets Operator をデプロイします。
HashiCorp のリポジトリを追加して、バージョンに 0.1.0-beta
を指定します。
$ helm repo add hashicorp https://helm.releases.hashicorp.com $ helm install vault-secrets-operator hashicorp/vault-secrets-operator --version 0.1.0-beta --create-namespace -n vault-secrets-operator
Pod が起動していることを確認できたら Vault Secrets Operator のデプロイは完了です。
$ kubectl get pods -n vault-secrets-operator NAME READY STATUS RESTARTS AGE vault-secrets-operator-controller-manager-6845f97f96-km7s5 2/2 Running 0 58s
Vault
前回の記事では Vault はマネージドサービスである「HCP Vault」を使用しました。 今回は Helm を使って Kubernetes 上にデプロイします。
Helm を使って Vault をデプロイする際、いくつか変数を設定しておきます。
injector.enabled
は、Vault Sidecar Agent Injector をデプロイ有無に関する変数です。
今回は Vault Secrets Operator を使用するため、Vault Sidecar Agent Injector がデプロイされないように false
を設定します。
ui.enabled
と ui.serviceType
は Kubernetes のロードバランサ経由で Vault UI にアクセスするための設定です。
$ helm install vault hashicorp/vault --create-namespace -n vault \ --set 'injector.enabled=false' \ --set 'ui.enabled=true' \ --set 'ui.serviceType="LoadBalancer"'
その他の変数については以下を参照してください。
Vault のデプロイが完了しても Unseal していないので Pod は READY 0/1
のままです。
$ kubectl get pods -n vault NAME READY STATUS RESTARTS AGE vault-0 0/1 Running 0 66s
Vault の Pod で vault operator init
を実行して、初期化と Unseal をおこないます。
その際、Unseal Key と Initial Root Token が出力されるので控えておいてください。
$ kubectl exec -it vault-0 -n vault -- vault operator init $ kubectl exec -it vault-0 -n vault -- vault operator unseal <Unseal Key 1> $ kubectl exec -it vault-0 -n vault -- vault operator unseal <Unseal Key 2> $ kubectl exec -it vault-0 -n vault -- vault operator unseal <Unseal Key 3>
Unseal が完了すると Pod が READY 1/1
になります。
$ kubectl get pods -n vault NAME READY STATUS RESTARTS AGE vault-0 1/1 Running 0 3m29s
以上で Vault のデプロイは完了です。
次に、Vault Secrets Operator と動的シークレットのための設定をおこなっていきます。 設定する項目は以下の 3 つです。
- シークレットエンジン
- ポリシー
- Kubernetes 認証
Vault CLI で Kubernetes 上の Vault にアクセスするために、グローバル IP アドレスを確認して環境変数に設定しておきます。
$ export VAULT_ADDR=`kubectl get svc vault-ui -n vault --no-headers | awk '{print $4}'`
シークレットエンジン
SQL Server 用にデータベースシークレットエンジンを有効化します。
$ vault secrets enable database
SQL Server 用プラグイン mssql-database-plugin
を使うようにシークレットエンジンを設定します。
その際、readonly
というロールの使用を許可します。
今回は Vault から SQL Server への接続に sa ユーザを使用していますが、本番運用時には適切なユーザを使用してください。
$ vault write database/config/mssql \ plugin_name=mssql-database-plugin \ connection_url=sqlserver://{{username}}:{{password}}@$SQL_ADDR \ allowed_roles="readonly" \ username=$SQL_USERNAME \ password=$SQL_PASSWORD
上で許可した readonly
ロールを作成します。
ロールには Vault が認証情報を作成する際のクエリを記述します。
また、今回は有効期限切れによる動作を確認するため、TTL を 3 分として default_ttl
に設定しています。
$ vault write database/roles/readonly \ db_name=mssql \ creation_statements="USE [myapp]; \ CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; \ CREATE USER [{{name}}] FOR LOGIN [{{name}}]; \ EXEC sp_addrolemember db_datareader, [{{name}}];" \ default_ttl=3m \ max_ttl=24h
ポリシー
ポリシーにはデータベースシークレットエンジンの情報を読み取る権限を記述します。
$ cat <<EOF | vault policy write dynamic-policy - path "database/*" { capabilities = ["read"] } EOF
このポリシーを次に設定する Kubernetes 認証と紐づけます。
Kubernetes 認証
Vault Secrets Operator から利用する Kubernetes 認証を設定します。
はじめに、Kubernetes 認証を有効化します。
$ vault auth enable kubernetes
今回は Vault を Kubernetes 上で動かしているので、Kubernetes の Token Review API の宛先として default
namespace のサービスを使うように Kubernetes 認証を設定します。
$ KUBERNETES_SERVICE_HOST=`kubectl get svc -n default --no-headers | awk '{print $3}'` $ KUBERNETES_SERVICE_PORT=`kubectl get svc -n default --no-headers | awk '{print $5}' | awk -F "/" '{print $1}'` $ vault write auth/kubernetes/config kubernetes_host=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT
最後に、デモアプリケーションが使用するロールを作成します。 このロールには先ほど作成したポリシーを紐づけています。
$ vault write auth/kubernetes/role/dynamic-role \ bound_service_account_names=dynamic-secrets-demo-app \ bound_service_account_namespaces=dynamic-secrets-demo \ policies=dynamic-policy \ ttl=1h
以上で、検証用の環境構築は完了です。
動作確認
やっと準備ができたので、動作確認をしてみましょう。 まずはデモ用のアプリケーションをデプロイします。
ここでは、NGINX コンテナを使った Pod に空っぽのシークレットをマウントさせています。
この Pod は dynamic-secrets-demo-app
というサービスアカウントを使用します。
$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Namespace metadata: name: dynamic-secrets-demo --- apiVersion: apps/v1 kind: Deployment metadata: name: dynamic-secrets-demo-app namespace: dynamic-secrets-demo spec: replicas: 1 selector: matchLabels: app: dynamic-secrets-demo-app template: metadata: labels: app: dynamic-secrets-demo-app spec: serviceAccountName: dynamic-secrets-demo-app containers: - name: nginx image: nginx volumeMounts: - name: secrets mountPath: "/etc/secrets" readOnly: true volumes: - name: secrets secret: secretName: dynamic-secret optional: false --- apiVersion: v1 kind: ServiceAccount metadata: name: dynamic-secrets-demo-app namespace: dynamic-secrets-demo --- apiVersion: v1 kind: Secret metadata: name: dynamic-secret namespace: dynamic-secrets-demo type: Opaque EOF
この状態では dynamic-secret
は空っぽのままです。
$ kubectl get secrets -n dynamic-secrets-demo NAME TYPE DATA AGE dynamic-secret Opaque 0 5m13s
次に以下のカスタムリソースをデプロイします。
- VaultConnection
- VaultAuth
- VaultDynamicSecret
$ cat <<EOF | kubectl apply -f - apiVersion: secrets.hashicorp.com/v1alpha1 kind: VaultConnection metadata: name: vaultconnection namespace: dynamic-secrets-demo spec: address: http://vault.vault.svc.cluster.local:8200 skipTLSVerify: true --- apiVersion: secrets.hashicorp.com/v1alpha1 kind: VaultAuth metadata: name: vaultauth namespace: dynamic-secrets-demo spec: vaultConnectionRef: vaultconnection method: kubernetes mount: kubernetes kubernetes: role: dynamic-role serviceAccount: dynamic-secrets-demo-app --- apiVersion: secrets.hashicorp.com/v1alpha1 kind: VaultDynamicSecret metadata: name: vaultdynamicsecret namespace: dynamic-secrets-demo spec: vaultAuthRef: vaultauth mount: database role: readonly rolloutRestartTargets: - kind: Deployment name: dynamic-secrets-demo-app destination: name: dynamic-secret EOF
マニフェストだけだと Vault とどうやって紐づけているのか分かりづらいので、図にしてみました。
VaultConnection
で Vault のアドレス、VaultAuth
でどの認証方式を使用するか、VaultDynamicSecret
でシークレットエンジンと Kubernetes 内のアプリケーションとシークレットを指定しています。
ややこしいのが VaultAuth
の spec.kubernetes.role
で、Kubernetes 内の Role リソースかと思いきやシークレットエンジン内のロールを指定するのです。
どうしてこうなった。
ここまでデプロイしたところで、Vault Secrets Operator がカスタムリソースを検出して、Vault が SQL Server に一時的な認証情報を作成してくれます。
下図は SSMS で SQL Server の認証情報を確認したスクリーンショットです。
v-kubernetes-dynamic-s-readonly-(snip)
という名前の認証情報が作成されていることが確認できました。
この認証情報はデータベースシークレットエンジンのロール readonly
で指定したクエリで作成されているため、myapp
データベースの db_datareader
ロールを持っています。
# 再掲 $ vault write database/roles/readonly \ db_name=mssql \ creation_statements="USE [myapp]; \ CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; \ CREATE USER [{{name}}] FOR LOGIN [{{name}}]; \ EXEC sp_addrolemember db_datareader, [{{name}}];" \ ←ここ default_ttl=3m \ max_ttl=24h
Vault からリース情報を確認すると 1 件ありました。
$ vault list sys/leases/lookup/database/creds/readonly Keys ---- W6gaZimXWEfDYKrrZNyxJUQj
Vault UI でも同じリース情報を確認することができます。
そして、VaultDynamicSecret
の spec.destination
で指定した Kubernetes シークレット内にユーザ名とパスワードが格納されて、デモアプリケーションがローリングアップデートされてマウントしているシークレットが更新されます。
デモアプリケーションに SQL Server の認証情報がマウントされていることを確認します。
$ kubectl exec -it `kubectl get pods -l app=dynamic-secrets-demo-app -n dynamic-secrets-demo --no-headers | awk '{print $1}'` -n dynamic-secrets-demo -- cat /etc/secrets/username && echo v-kubernetes-dynamic-s-readonly-5qaBXy0HQyvQlmiEUehr-1683695441
このように、Vault Secrets Operator から Vault の「動的シークレット」を使って SQL Server に認証情報を作成し、アプリケーションにも認証情報を反映させることができました。
最後に、VaultDynamicSecfet
リソースを削除してみます。
$ kubectl delete vaultdynamicsecret vaultdynamicsecret -n dynamic-secrets-demo
リソースを削除した直後は Kubernetes シークレットにも SQL Server 認証情報にも変化はありません。
しかし、リース情報に記載されている有効期限 (TTL) を過ぎると、SQL Server の認証情報は Vault によって削除されます。
このように、VaultDynamicSecfet
リソースがなくなると「動的シークレット」の機能で認証情報が削除されます。
言い換えれば、VaultDynamicSecfet
リソースが残り続けていれば TTL が過ぎても認証情報は削除されません。
なぜかというと、Vault Secrets Operator がリースを更新して TTL を延ばしているからです!
Vault Secrets Operator には以下のログが出力されていました。
2023-05-10T05:10:41Z DEBUG events Secret synced, lease_id=database/creds/readonly/W6gaZimXWEfDYKrrZNyxJUQj, horizon=2m38.297936881s {"type": "Normal", "object": {"kind":"VaultDynamicSecret","namespace":"dynamic-secrets-demo","name":"vaultdynamicsecret","uid":"e1c1fcf2-ad3b-4803-b71a-ac45eae3ac5e","apiVersion":"secrets.hashicorp.com/v1alpha1","resourceVersion":"33334"}, "reason": "SecretSynced"} 2023-05-10T05:13:19Z DEBUG events Renewed lease, lease_id=database/creds/readonly/W6gaZimXWEfDYKrrZNyxJUQj, horizon=2m30.712625808s {"type": "Normal", "object": {"kind":"VaultDynamicSecret","namespace":"dynamic-secrets-demo","name":"vaultdynamicsecret","uid":"e1c1fcf2-ad3b-4803-b71a-ac45eae3ac5e","apiVersion":"secrets.hashicorp.com/v1alpha1","resourceVersion":"33987"}, "reason": "SecretLeaseRenewal"}
1 行目は VaultDynamicSecret
リソース作成直後、2 行目は TTL が過ぎる前の更新処理のログです。
Vault Secrets Operator を使った「動的シークレット」では VaultDynamicSecret
リソースの有無でアプリケーションが認証情報を使っているかどうか判断しているのですね。
おわりに
前回に引き続き、Vault Secrets Operator を検証してみました。
今回は「動的シークレット」という機能を使ってみましたが、とても便利そうな機能ですね! 本番運用するに当たっては細かな設計は必要そうですが、ぜひ皆様も試してみてください。
ACS事業部のご紹介
私達 ACS 事業部は Azure・AKS などのクラウドネイティブ技術を活用した内製化のご支援をしております。
また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。