APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Vault Secrets Operator で HashiCorp Vault の動的シークレットを体験

ACS 事業部の埜下です。

前回、プレビュー公開されたばかりの Vault Secrets Operator を使って Kubernetes のシークレット管理について投稿しました。

techblog.ap-com.co.jp

こちらの記事で、Vault Secrets Operator は以下の 3 種類のシークレットを扱えると説明しました。

  • 静的シークレット (Static Secret)
  • 動的シークレット (Dynamic Secret)
  • PKI シークレット

前回は「静的シークレット」を確認したので、今回は「動的シークレット」を対象とした検証をおこないました。

はじめに

動的シークレットとは

「動的シークレット」は、HashiCorp Vault (以下、Vault) で利用できるシークレット管理方法の 1 つで、Azure のサービスプリンシパルや SQL Server のログインといった認証情報を一時的に作成してくれます。

Vault に認証情報などのデータを登録する「静的シークレット」とは異なり、「動的シークレット」はアプリケーションからリクエストがあったタイミングでデータが作成されます。

また、「動的シークレット」で作成された認証情報には有効期間 (TTL) が設定されており、アプリケーションから使われなくなると認証情報は削除されるため、セキュリティリスクを下げることができます。

下図のように、あらかじめ管理者が Vault にシークレットエンジンやロールを設定しておくことで、アプリケーションからのリクエストに応じてデータベース等に認証情報が作成されます。

引用: https://developer.hashicorp.com/vault/tutorials/db-credentials/database-secrets

検証内容

今回の検証では、Vault から SQL Server に対して一時的な認証情報を作成するような「動的シークレット」を設定します。

なぜ SQL Server かと言うと、筆者が一番馴染みのある RDBMS だからです! 最近(最近でもないか)の SQL Server は Kubernetes でも動くんですよ、すごいですねー。

また、Vault に対して「動的シークレット」へのリクエストはアプリケーション自身ではなく、Vault Secrets Operator からリクエストするようにします。

そして、今回使用するコンポーネントはすべて Kubernetes 上で動かします。 なぜ Kubernetes オンリーかと言うと、筆者がやってみたかったからです!

というわけで、各コンポーネントの繋がりと「動的シークレット」を使う流れは下図のようになります。

  1. Operator が VaultDynamicSecret カスタムリソースの内容を確認
  2. Operator から Vault に対して SQL Server の動的シークレットをリクエスト
  3. Vault が SQL Server に一時的な認証情報を作成
  4. Vault から Operator に SQL Server 認証情報を応答
  5. Operator が SQL Server 認証情報を Kubernetes シークレットに同期
  6. Operator がアプリケーションをローリングアップデートして Kubernetes シークレットを反映
  7. アプリケーションが Kubernetes シークレットを使用して SQL Server に接続
    • 今回ここは実施しません

前提条件

今回紹介する手順では以下のコマンドを使用しています。 手順を実行される場合は、これらのコマンドが実行可能な環境をご準備ください。

また、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.enabledui.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"'

その他の変数については以下を参照してください。

github.com

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 内のアプリケーションとシークレットを指定しています。

ややこしいのが VaultAuthspec.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 でも同じリース情報を確認することができます。

そして、VaultDynamicSecretspec.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 などのクラウドネイティブ技術を活用した内製化のご支援をしております。

www.ap-com.co.jp

また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。

www.ap-com.co.jp

本記事の投稿者: 埜下 太一
AKS/ACA をメインにインフラ系のご支援を担当しています。
個人ブログ