2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Secrets Store CSI Driverを使って、Vaultから簡単にシークレット情報を取得しよう

Last updated at Posted at 2025-04-25

はじめに

Red Hat OpenShiftはVersion 4.18から「Secrets Store CSI Driver」がGA(General Availability, 一般提供)となりました。

このSecrets Store CSI Driverは、CSI Driverの仕組みを応用して、外部のシークレットストアに保存されている機密情報(シークレット情報)をコンテナに渡すことのできる仕組みです。

Secret Store CSI Driver Operator の一般提供開始
Secrets Store Container Storage Interface (CSI) Driver Operator である secrets-store.csi.k8s.io を使用すると、OpenShift Container Platform がエンタープライズグレードの外部シークレットストアに保存されている複数のシークレット、キー、証明書をインラインの一時ボリュームとして Pod にマウントできます。Secrets Store CSI Driver Operator は、gRPC を使用してプロバイダーと通信し、指定された外部シークレットストアからマウントコンテンツを取得します。ボリュームがアタッチされると、その中のデータがコンテナーのファイルシステムにマウントされます。Secrets Store CSI Driver Operator は、OpenShift Container Platform 4.14 でテクノロジープレビュー機能として利用可能でした。OpenShift Container Platform 4.18 では、この機能の一般提供が開始されました。

このSecrets Store CSI Driverの仕組みを使い、「Hashicorp Vault」で管理しているシークレット情報をPodにセキュアに渡してみる、そんな実験をしていきます。

Hashicorp Vaultとは

HashiCorp Vault は、API や CLI を通じて機密情報(シークレット)を安全に保存・管理・配布できるセキュリティツールです。シークレットの暗号化、動的な認証情報の発行、アクセス制御を統一的に管理できます。クラウドネイティブな環境やマルチテナント構成(※エンタープライズ版の機能)にも柔軟に対応でき、Kubernetesとの連携も可能です。

IBMによるHashicorp社の買収

IBMは2025年03月10日にHashicorp社の買収を完了しました。IBMとしては、特にVaultについてOpenShiftとの統合を計画しているらしく、今後OpenShiftとよりシームレスにつながり、便利に利用できるようになるかもしれませんね!

IBMがHashiCorp社の買収を完了し、総合的なエンドツーエンドのハイブリッドクラウド・プラットフォームを実現

HashiCorp VaultとRed Hat OpenShiftを統合することで、ハイブリッドクラウド環境全体で強力なシークレット管理とセキュリティー機能を提供します。

やることのイメージ

これからやることのイメージをざっくり描いてみました。

image.png

ちょっと分かりづらいので、それぞれの登場人物の役割や、サンプルアプリにSecret情報が渡される流れを解説します。

なにが起こるのか?

まずは、OpenShiftクラスタ内にVaultをデプロイします。その際、とある設定を行い「Vault CSI Driver」も合わせてデプロイします。このVault CSI Driverが今回の主役です。これがVaultからSecret情報を取得し、サンプルアプリの特定のパスにその情報をマウント(格納)します。Vault CSI DriverはDaemonSetとして、OpenShiftの各Computeノード(Workerノード)にデプロイされます。

なお、そのための認証や「具体的にどのSecret情報を取得するのか?」などの定義を司るのが、「Secret Provider Class」というカスタムリソースです。このSecret Provider Classは、Red Hatが提供する公式Operator「Secrets Store CSI Driver Operator」が提供するAPIを用いて作成・管理されます。

なお、Kubernetes/OpenShiftにおいて、Vaultからワークロードにシークレット情報を渡す手法は、Secrets Store CSI Driver以外にも存在しています。以前別の記事にて、VSO(Vault Secret Operator)を用いた方法を解説しました。よろしければご参照ください。
OpenShiftにHashicorp VaultをDepolyしてSingle "Secret" of Truthを実現しよう

サンプルアプリについて

今回はSecrets Store CSI Driverの挙動をわかりやすく理解するためにサンプルアプリを用意しました。このアプリはNode.jsで作成した非常に単純なアプリで、コンテナの/mnt/secrets-store/内に保存されているsecret-infoというファイルの中身(例:vaule123)を表示するだけのアプリです。

サンプルアプリのスクショ
image.png

今回使用するソースコード等の一式

今回使うするマニフェストファイルやサンプルアプリのソースコード一式は、以下の公開リポジトリに格納しています。

また、サンプルのソースコードからビルドしたコンテナイメージは以下に格納しています。

実践

それでは、早速やっていきましょう。なお、今回は「OpenShiftのバージョン:4.18.8」を利用しています。また、ROSA (Red Hat OpenShift Service on AWS, AWSのマネージドサービスとして利用できるOpenShift)を活用し、さくっと簡単にOpenShiftのクラスタを払い出しています。

無料のRed Hatアカウントさえ作れば、ROSAがすぐに試せる「Red Hat OpenShift Service on AWS Hands-on Experience」を利用しましょう。Cluster-admin権限をもったアカウントがもらえるので、自由にOperatorもインストール可能です。なお、当該環境は8時間×3回まで利用できます。詳しい利用開始までの流れは、日本語ガイドを参照ください。

Secrets Store CSI Driver Operatorをインストール

同OperatorをOperatorHubからインストールします。なお、以後の操作はすべてCluster-admin権限を持つOpenShiftユーザにて実施しています。

管理者表示の「Operator」メニューから、「OperatorHub」を選択し、検索欄に「Secrets Store CSI Driver Operator」と入力します。

image.png

これをデフォルト設定のままインストールします。カスタムリソース「Secret Provider Class」は後ほど作成しますので、このタイミングではインストールが完了すればOKです。

image.png

Vaultをインストール

さて、次はVaultをインストールします。

Namespaceの作成と設定

その前に、VaultをデプロイするためのNamespaceを作成します。OpenShiftのコンソール画面を開発者向け表示に切り替え、「すべてのプロジェクト」をクリック、「プロジェクトの作成」をクリックします。

image.png

プロジェクト名には「vault」と入力し、「作成」をクリックします。プロジェクト(Namespace)が作成できたら、OpenShiftのcluster-adminアカウントでログイン済みのCLIから以下のコマンドを適用し、Namespace「Vault」に各種ラベルを適用します。

oc label ns vault \
  security.openshift.io/scc.podSecurityLabelSync=false \
  pod-security.kubernetes.io/enforce=privileged \
  pod-security.kubernetes.io/audit=privileged \
  pod-security.kubernetes.io/warn=privileged \
  --overwrite

このコマンドにより、PSA(Pod Secrity AdmissionsとSCC(Security Context Constraints)間のラベル同期を無効(false)にします。なお、PSAはKubernetesオリジナルのPodセキュリティ設定の仕組み、SCCはOpenShift独自のそれです。また、PSAには各種モード(enforce, audit, warn)がありますが、それぞれのモードについて全てprivilegedを設定します。これにより、当該ラベルを付与されたNamespaceにデプロイされるPodは当該セキュリティ標準が適用されます。特権は特にCSI Driverを司るDaemonSetに求められる権限です。

PSAについて詳しく知りたい方はKubernetes公式ドキュメントの参照をお勧めします。また、以下の記事の説明が大変わかりやすかったです。
Pod Security AdmissionでPodの特権を制限する

Helmチャートを選択

次に「+追加」メニューから「Helmチャート」を選択します。

image.png

VaultのインストールはHashicorpが提供しているHelmチャートを利用します。検索欄に「vault」と入力します。

image.png

「Hashicorp Vault」をクリックし、「作成」をクリックします。

image.png

YAMLの編集

ここでマニフェスト編集画面(YAMLビュー)になります。このYAMLについて、以下のものに置き換えてください。

csi:
  agent:
    enabled: true
    image:
      pullPolicy: IfNotPresent
      repository: hashicorp/vault
      tag: 1.19.0
    logFormat: standard
    logLevel: info
  daemonSet:
    kubeletRootDir: /var/lib/kubelet
    providersDir: /var/run/secrets-store-csi-providers
    updateStrategy:
      type: RollingUpdate
  debug: false
  enabled: true
  hostNetwork: false
  image:
    pullPolicy: IfNotPresent
    repository: hashicorp/vault-csi-provider
    tag: 1.5.0
  livenessProbe:
    failureThreshold: 2
    initialDelaySeconds: 5
    periodSeconds: 5
    successThreshold: 1
    timeoutSeconds: 3
  logLevel: info
  readinessProbe:
    failureThreshold: 2
    initialDelaySeconds: 5
    periodSeconds: 5
    successThreshold: 1
    timeoutSeconds: 3
global:
  enabled: true
  openshift: true
  psp:
    annotations: >
      seccomp.security.alpha.kubernetes.io/allowedProfileNames:
      docker/default,runtime/default

      apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default

      seccomp.security.alpha.kubernetes.io/defaultProfileName:  runtime/default

      apparmor.security.beta.kubernetes.io/defaultProfileName:  runtime/default
    enable: false
  serverTelemetry:
    prometheusOperator: false
  tlsDisable: true
injector:
  affinity: |
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app.kubernetes.io/name: {{ template "vault.name" . }}-agent-injector
              app.kubernetes.io/instance: "{{ .Release.Name }}"
              component: webhook
          topologyKey: kubernetes.io/hostname
  agentDefaults:
    cpuLimit: 500m
    cpuRequest: 250m
    memLimit: 128Mi
    memRequest: 64Mi
    template: map
    templateConfig:
      exitOnRetryFailure: true
  agentImage:
    repository: registry.connect.redhat.com/hashicorp/vault
    tag: 1.19.0-ubi
  authPath: auth/kubernetes
  certs:
    certName: tls.crt
    keyName: tls.key
  enabled: false
  failurePolicy: Ignore
  hostNetwork: false
  image:
    pullPolicy: IfNotPresent
    repository: registry.connect.redhat.com/hashicorp/vault-k8s
    tag: 1.6.2-ubi
  leaderElector:
    enabled: true
  livenessProbe:
    failureThreshold: 2
    initialDelaySeconds: 5
    periodSeconds: 2
    successThreshold: 1
    timeoutSeconds: 5
  logFormat: standard
  logLevel: info
  metrics:
    enabled: false
  port: 8080
  readinessProbe:
    failureThreshold: 2
    initialDelaySeconds: 5
    periodSeconds: 2
    successThreshold: 1
    timeoutSeconds: 5
  replicas: 1
  revokeOnShutdown: false
  startupProbe:
    failureThreshold: 12
    initialDelaySeconds: 5
    periodSeconds: 5
    successThreshold: 1
    timeoutSeconds: 5
  webhook:
    failurePolicy: Ignore
    matchPolicy: Exact
    objectSelector: |
      matchExpressions:
      - key: app.kubernetes.io/name
        operator: NotIn
        values:
        - {{ template "vault.name" . }}-agent-injector
    timeoutSeconds: 30
server:
  affinity: |
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app.kubernetes.io/name: {{ template "vault.name" . }}
              app.kubernetes.io/instance: "{{ .Release.Name }}"
              component: server
          topologyKey: kubernetes.io/hostname
  auditStorage:
    accessMode: ReadWriteOnce
    enabled: false
    mountPath: /vault/audit
    size: 10Gi
  authDelegator:
    enabled: true
  dataStorage:
    accessMode: ReadWriteOnce
    enabled: true
    mountPath: /vault/data
    size: 10Gi
  dev:
    devRootToken: root
    enabled: false
  enabled: '-'
  enterpriseLicense:
    secretKey: license
  ha:
    config: >
      ui = true


      listener "tcp" {
        tls_disable = 1
        address = "[::]:8200"
        cluster_address = "[::]:8201"
      }

      storage "consul" {
        path = "vault"
        address = "HOST_IP:8500"
      }


      service_registration "kubernetes" {}


      # Example configuration for using auto-unseal, using Google Cloud KMS. The

      # GKMS keys must already exist, and the cluster must have a service
      account

      # that is authorized to access GCP KMS.

      #seal "gcpckms" {

      #   project     = "vault-helm-dev-246514"

      #   region      = "global"

      #   key_ring    = "vault-helm-unseal-kr"

      #   crypto_key  = "vault-helm-unseal-key"

      #}


      # Example configuration for enabling Prometheus metrics.

      # If you are using Prometheus Operator you can enable a ServiceMonitor
      resource below.

      # You may wish to enable unauthenticated metrics in the listener block
      above.

      #telemetry {

      #  prometheus_retention_time = "30s"

      #  disable_hostname = true

      #}
    disruptionBudget:
      enabled: true
    enabled: false
    raft:
      config: |
        ui = true

        listener "tcp" {
          tls_disable = 1
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          # Enable unauthenticated metrics access (necessary for Prometheus Operator)
          #telemetry {
          #  unauthenticated_metrics_access = "true"
          #}
        }

        storage "raft" {
          path = "/vault/data"
        }

        service_registration "kubernetes" {}
      enabled: false
      setNodeId: false
    replicas: 3
  hostNetwork: false
  image:
    pullPolicy: IfNotPresent
    repository: registry.connect.redhat.com/hashicorp/vault
    tag: 1.19.0-ubi
  includeConfigAnnotation: false
  ingress:
    activeService: true
    enabled: false
    hosts:
      - host: chart-example.local
    pathType: Prefix
  livenessProbe:
    enabled: false
    failureThreshold: 2
    initialDelaySeconds: 60
    path: /v1/sys/health?standbyok=true
    periodSeconds: 5
    port: 8200
    successThreshold: 1
    timeoutSeconds: 3
  networkPolicy:
    enabled: false
    ingress:
      - ports:
          - port: 8200
            protocol: TCP
          - port: 8201
            protocol: TCP
  preStopSleepSeconds: 5
  readinessProbe:
    enabled: true
    failureThreshold: 2
    initialDelaySeconds: 5
    path: /v1/sys/health?uninitcode=204
    periodSeconds: 5
    port: 8200
    successThreshold: 1
    timeoutSeconds: 3
  route:
    host: ''
    tls:
      termination: edge
      insecureEdgeTerminationPolicy: Redirect
    activeService: true
    enabled: true
  service:
    active:
      enabled: true
    enabled: true
    externalTrafficPolicy: Cluster
    instanceSelector:
      enabled: true
    port: 8200
    publishNotReadyAddresses: true
    standby:
      enabled: true
    targetPort: 8200
  serviceAccount:
    create: true
    createSecret: false
    serviceDiscovery:
      enabled: true
  shareProcessNamespace: false
  standalone:
    config: >-
      ui = true


      listener "tcp" {
        tls_disable = 1
        address = "[::]:8200"
        cluster_address = "[::]:8201"
        # Enable unauthenticated metrics access (necessary for Prometheus Operator)
        #telemetry {
        #  unauthenticated_metrics_access = "true"
        #}
      }

      storage "file" {
        path = "/vault/data"
      }


      # Example configuration for using auto-unseal, using Google Cloud KMS. The

      # GKMS keys must already exist, and the cluster must have a service
      account

      # that is authorized to access GCP KMS.

      #seal "gcpckms" {

      #   project     = "vault-helm-dev"

      #   region      = "global"

      #   key_ring    = "vault-helm-unseal-kr"

      #   crypto_key  = "vault-helm-unseal-key"

      #}


      # Example configuration for enabling Prometheus metrics in your config.

      #telemetry {

      #  prometheus_retention_time = "30s"

      #  disable_hostname = true

      #}
    enabled: '-'
  terminationGracePeriodSeconds: 10
  updateStrategyType: OnDelete
serverTelemetry:
  prometheusRules:
    enabled: false
  serviceMonitor:
    enabled: false
    interval: 30s
    scrapeTimeout: 10s
ui:
  activeVaultPodOnly: false
  enabled: false
  externalPort: 8200
  externalTrafficPolicy: Cluster
  publishNotReadyAddresses: true
  serviceType: ClusterIP
  targetPort: 8200

設定変更した箇所(4箇所)について簡単に解説します。

csi.enabled(必須)

csi:
...
- enabled: false
+ enabled: true
...

実は、VaultのHelmチャートのデフォルトでは「Vault CSI Driver」の機能が無効(false)になっています。それを有効(true)にします。

csi.daemonSet.providersDir(必須)

csi:
...
- daemonSet:
-   providersDir: /etc/kubernetes/secrets-store-csi-providers
+ daemonSet:
+   providersDir: /var/run/secrets-store-csi-providers
...

先ほど述べた通り、Vault CSI DriverはDaemonSetとして各Computeノードにデプロイされます。その際の設定を行なっています。なお、ここで値を変更した「providersDir」とは、Secrets Store CSI Driver が Vaultなどのシークレットプロバイダと通信するためのソケットファイルを探すディレクトリです。

Secret Store CSI Driver Operator の一般提供開始
Secrets Store CSI Driver Operator は、gRPC を使用してプロバイダーと通信し、指定された外部シークレットストアからマウントコンテンツを取得します。

Secrets Store CSI Driver は、Provider(Vaultなど)と gRPC で通信する必要がありますが、この通信はノード内に閉じて行われます。その際にUnixドメインソケットを通じて通信しています。そのソケットファイルが置かれるディレクトリが/var/run/secrets-store-csi-providersになります。このため、Vault CSI Provider 側のDaemonSetがこのパスにソケットをexpose(公開)している必要があるわけです。

server.route(GUIを利用する場合は必須)

Helmチャートのデフォルト設定では、OpenShiftのRouteが無効化されているため、以下のように有効化しておきます。ここでhostについては特に指定せずに空欄とし、OpenShiftに自動的に作成してもらいます。また、TLS終端(Edge)をRouteとし、HTTPアクセスは自動的にHTTPSにリダイレクトさせるものとします。

server:
...
  route:
    activeService: true
-   enabled: false
-   host: chart-example.local
-   tls:
-     termination: passthrough
+   enabled: true
+   host: ''
+   tls:
+     termination: edge
+       insecureEdgeTerminationPolicy: Redirect
...

injector.enabled(任意)

injector:
...
- enabled: true
+ enabled: false
...

最後は、インジェクタの動作を無効(false)にします。このインジェクタは、「Vault Agent方式」と呼ばれる、「Secrets Store CSI Driver方式」とは別の方式を利用する際に利用するDeploymentです。今回はインジェクタは不要なので、無効化していますが、デプロイされたとしても「Secrets Store CSI Driver方式」の挙動には影響はありません。

Vault Agent方式についての詳細は、公式サイトをご覧ください。

VaultからPodへのシークレット情報連携においてSecrets Store CSI Driverを利用する上では、上記の設定にて十分です。その他、VaultのHA機能などを利用する場合は、必要に応じて設定してください。

YAMLを差し替えたら、「作成」をクリックし、Vaultをデプロイします。

image.png

実はまだいくつか設定が必要です。このままだと、Vault CSI DriverのDaemonSetがいつまでも起動しません。

VaultおよびCSI Provider用にprivileged SCCを付与

まずは、CLIで以下のコマンドを適用します。

oc adm policy add-scc-to-user privileged -z vault -n vault
oc adm policy add-scc-to-user privileged -z vault-csi-provider -n vault

これらのコマンドは、Namespace「vault」に存在するServiceAccount(vault および vault-csi-provider)に、privileged SCC(Security Context Constraints)を付与するものです。これにより、両サービスアカウントが特権的な操作を実行できるようになります。

Vault CSI Driver は、先述の通りhostPath マウントを使用して Unix ソケット (vault.sock) を介した Secrets Store CSI Driver とのノード内通信を行うため、特権モードが必要です。
また、Vault本体(サーバー側)についても、ストレージへの書き込みやノードレベルのネットワーク設定など、OpenShiftの標準的な制約を超える操作を安定して行うために特権が求められます。

なお、Helmチャートを使用してVaultをデプロイする場合、Namespace「vault」内には、以下のサービスアカウントが自動的に作成されます。

  • vault(Vaultサーバー用)
  • vault-csi-provider(Vault CSI Provider用)

これらのServiceAccountに適切に権限を付与することで、VaultおよびVault CSI Driverが正しく動作するようになります。

image.png

明示的にVault CSI DriverのPodに特権を付与

以下のコマンドを適用し、起動しかかり中の当該Podに対して、特権を付与してあげます。

oc patch daemonset -n vault vault-csi-provider --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/securityContext", "value": {"privileged": true} }]'

これで無事、DaemonSetのデプロイが完了しました。

image.png

もし一部のComputeノードにTaintsを付与している場合は、DaemonSet側にTorelationsを設定してあげないと、そのComputeノードにDaemonSetがデプロイされないので、お気をつけください。

後からTorelations(容認)を設定する場合は、以下の手順で可能です。

  1. Daemonset「vault-csi-provider」の詳細画面の「容認」欄から「0件の容認」をクリック
    image.png

  2. 「追加する」をクリック
    image.png

  3. Taintsの内容に対応するkey-value、エフェクト等を入力し「保存」をクリック
    image.png

Podの台数が所望の台数に増えればOKです。

Vaultをセットアップ

さて、VaultのGUIにアクセスし、初期設定を行います。Routeの提供するURLをブラウザで開きます。

image.png

Unseal

Vaultは起動直後は暗号化(Sealed)されています。暗号化を解除(Unseal)するためには鍵が必要なのですが、その鍵の設定画面が最初に開きます。

image.png

  • Key shares: 払い出す鍵の数
  • Key threshold: 払い出した鍵のうち、Unsealするために必要な鍵の数

今回は簡単のため、それぞれ1つずつにしておきます。なお、VaultのPodが再起動した際等には、再び鍵によるUnsealが必要になります。鍵を無くすと初期化が必要になり、その際にはVault内のシークレット情報や設定値が消えることになります。

適当なKey sharesKey thresholdを設定したら、「Initialize」をクリックします。

image.png

「Downlaod keys」をクリックして、鍵と初期ルートトークンをダウンロードします。これらは適切に管理するようにしましょう。

image.png

今保存した鍵を使ってUnsealします。

image.png

最後に、初期ルートトークンを用いてルートユーザでログインします。

image.png

これでVaultの初期設定が完了しました。

Kubernetes認証

次にVaultと連携先のOpenShft(=Kubernetes)の認証設定を行います。VaultのGUIの左側にある「Access」メニューから「Authentication Methods」を選択します。

image.png

画面右上にある「Enable new method +」をクリックし、

image.png

「Kubernetes」を選択、

image.png

「Enable method」をクリックします。次の画面にはなにやら色々情報を入力する必要がありそうです。

image.png

今回入力が必要な情報について説明します。

Kubernetes host

値:https://kubernetes.default.svc.cluster.local:443
Kubernetesクラスタを操作するためのAPIを入力します。なお、今回はVaultはKubernetes Clusterの上で動いている為、内部ホスト名(同左)を指定することが可能です。

Kubernetes CA Certificate

値:ConfigMap「kube-root-ca.crt」の値
Kubernetes host」からAPIを叩く際に必要な認証情報です。これはKubernetesの全てのNamespaceに自動的に作成されるConfigMap「kube-root-ca.crt」のKey: ca.crtの値です。

OpenShiftの開発者向け表示において、任意のNamespaceを選択した状態で「ConfigMap」メニューから確認できます。

image.png

この一番最初の-----BEGIN CERTIFICATE-----から一番最後の-----END CERTIFICATE-----までをコピーします。

-----BEGIN CERTIFICATE-----
MIIDPDCCA...
-----END CERTIFICATE-----

Token Reviewer JWT

値:コンテナ「vault-0」/var/run/secrets/kubernetes.io/serviceaccount/tokenの中身
「Token Reviewer JWT」とは、KubernetesのServiceAccountに自動で割り当てられるトークンであり、ServiceAccountが関連づけられたPodの同パスに格納されます。 つまり、Kubernetes APIに対して当該トークンを持つServiceAccount「Vault」を確認するために使う認証情報です。

StatefulSet「Vault」の詳細画面からPod「vault-0」をクリックします。

image.png

ターミナル画面にて以下のコマンドを実行します。

cat /var/run/secrets/kubernetes.io/serviceaccount/token

すると、以下のようにToken Reviewer JWTを確認できます。

image.png

これをコピペします。

必要な値がそれぞれ入力できたら、「Save」をクリックしてKubernete認証設定を保存します。

image.png

なお、Pod「vault-0」のターミナルで以下のコマンドを実行しても同じように同認証設定が可能です。

## Vaultにログイン
vault login <root token>

## Kubernete認証を有効化
vault auth enable kubernetes

## Token Reviewer JWTを環境変数に設定
TOKEN_REVIEWER_JWT="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"

## Kubernetes CA Certificateを環境変数に設定
KUBERNETES_CA_CERTIFICATE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)"

## Kubernetes認証設定
vault write auth/kubernetes/config \
  issuer="https://kubernetes.default.svc.cluster.local" \
  token_reviewer_jwt="${TOKEN_REVIEWER_JWT}" \
  kubernetes_host="https://kubernetes.default.svc.cluster.local:443" \
  kubernetes_ca_cert="${KUBERNETES_CA_CERTIFICATE}"

ぶっちゃけコマンド操作の方が楽ですね...
なお、こちらの手順はOpenShiftのドキュメントにも記載されています。

お疲れ様でした。ここまででVaultのKubernetes認証が完了しました。

Secret Engineを有効化

続いて、VaultのSecret Engineを有効化します。GUI左の「Secret Engines」メニューを選択し、「Enable new engine」をクリックします。

image.png

Vaultはさまざまなサービスや連携先に対応したSecret Engineの種類を用意していますが、今回は最も汎用的な「Key-Valune形式(KV)」を選択します。

image.png

「Path」欄にsecretと入力し、「Enable engine」をクリックしましょう。

image.png

シークレット情報を登録

次にサンプルとなるKey-Value形式のシークレット情報を登録します。「Create secret」をクリックします。

image.png

今回は以下の通り値を登録します。

  • Path for this secret: example
  • Secret data:
    • key: testSecret1
    • value: value123

image.png

値を入力したら「Save」をクリックして保存します。

image.png

今登録したシークレット情報「example」の詳細を確認できる画面になります。ここで「API path」が後ほど必要な情報なので、覚えておいてください。

なお、この一連の操作はコマンドでも実行可能です。

## Vaultにログイン
vault login <root token>

## KVタイプのSecret Engineを有効化しシークレット情報を追加
vault kv put secret/example testSecret1=value123

あきらかにコマンドの方が楽ですね...

ポリシーを作成

次に、今作成したシークレット情報に対する参照権限を持つポリシーを作成します。GUIの左メニューから「Policies」を選択し、さらに「ACL Policies」をクリックします。

image.png

すでに2つのポリシーが作成されています。「default」はその名の通り、defaultで用意されているもの、「root」はルートトークンでログインするユーザ(ルートユーザ)のポリシーです。ここでは「Create ACL policy +」をクリックします。今回は以下の通り値を入力します。

  • Name: csi
  • Policy:
    path "secret/data/*" {
      capabilities = ["read"]
      }
    

image.png

このポリシーは、先ほど作成したKVタイプのSecret Engine「secret」内の全てのシークレットの参照権限を定義したポリシーです。「Create policy」をクリックします。

ポリシー作成もコマンドで実行可能です。

## Vaultにログイン
vault login <root token>

## ポリシー作成
vault policy write csi -<<EOF
  path "secret/data/*" {
  capabilities = ["read"]
  }
  EOF

ロールを作成

最後に、Kubernetes認証の仕組みを介して、先ほど作成したポリシーに即してシークレット情報を取得する「ロール」を作成します。再びVaultのGUIの左側にある「Access」メニューから「Authentication Methods」を選択し、先ほど設定を行った「Kubernetes」認証をクリックします。

image.png

続いて「Create role」をクリックします。

image.png

ここでは以下の通り値を設定します。

  • Name: csi
  • Bound service account names: default
  • Bound service account namespaces: sample-app
  • Generated Token's Policies: csi

このロールは「OpenShiftのNamespace「sample-app」に作成されたService Account「default」が、ポリシー「csi」に基づいてシークレット情報を取得するための「csi」という名称のロール」を意味しています。

image.png
image.png

値を入力し終えたら「Save」をクリックしてロールの作成を完了します。

ロール作成もコマンドで実行可能です。

## Vaultにログイン
vault login <root token>

## ロール作成
vault write auth/kubernetes/role/csi \
  bound_service_account_names=default \
  bound_service_account_namespaces=sample-app \
  policies=csi```

お疲れ様でした。これでVaultのセットアップとシークレット情報の登録、ポリシー&ロール作成が完了しました。次以降はいよいよコンテナにシークレット情報を渡すところまでをやっていきます。

Podにシークレット情報を連携

ここからは、先ほどセットアップしたVaultの中に作成済みのシークレット情報を、コンテナに渡すところまでをやっていきます。

サンプルアプリ用のNamespaceを作成

先ほどNamespace「vault」を作成した時と同じ要領で「sample-app」という名称のNamespaceを作成しておいてください。

image.png

SecretProviderClassを作成

次にカスタムリソース「SecretProviderClass」を作成します。OpenShiftのコンソール画面を「管理者向け表示」とし、左メニューの「Operator」から「インストール済みのOperator」をクリック、インストール済みOperator一覧から「Secrets Store CSI Driver Operator」を選択します。

image.png

「SecretProviderClass」の欄から「インスタンスの作成」をクリックします。YAMLビューに切り替え、以下のYAMLをコピペします。

kind: SecretProviderClass
apiVersion: secrets-store.csi.x-k8s.io/v1
metadata:
  name: example-vault-provider
  namespace: sample-app
spec:
  provider: vault #シークレットプロバイダ名「vault」を指定
  parameters:                               
    roleName: "csi" #Vaultからシークレット情報を取得するロールを指定
    vaultAddress: "http://vault.vault.svc.cluster.local:8200" #OpenShiftクラスタ内からアクセスできるホスト名を指定
    objects:  |
      - secretPath: "secret/data/example"
        secretKey: "testSecret1"
        objectName: "secret-info"

ここで、以下のパラメータの意味について補足します。

  • secretPath: Vault内のシークレットの格納パス(API path)を指定
  • secretKey: シークレット情報のKey
  • objectName: コンテナにマウントする際のファイル名

Vaultの内部ホスト名はServiceの詳細から確認可能です。

スクリーンショット 2025-04-23 0.16.38.png

このHostname「vault.vault.svc.cluster.local」の先頭にhttp://をつけ、末尾にPort番号「8200」をつければOKです。

YAMLをコピペしたら「作成」をクリックします。

image.png

カスタムリソース「SecretProviderClass」が作成できました。それではいよいよ、サンプルアプリをデプロイしましょう。

サンプルアプリのマニフェストを適用

以下にマニフェストを用意しました。これらをNamespace「sample-app」に適用しています。このサンプルアプリのソースコードはこちらに格納しています。このアプリは環境変数SECRET_PATHを持っており、このパスに格納されているテキストファイル(シークレット情報)の内容を画面に表示することができます。

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: vault-csi-driver-app
data:
  SECRET_PATH: '/mnt/secrets-store/secret-info'
service.yaml
kind: Service
apiVersion: v1
metadata:
  name: vault-csi-driver-app
  labels:
    app: vault-csi-driver-app
spec:
  ports:
    - name: 3000-tcp
      protocol: TCP
      port: 3000
      targetPort: 3000
  type: ClusterIP
  selector:
    app: vault-csi-driver-app
route.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: vault-csi-driver-app
  labels:
    app: vault-csi-driver-app
spec:
  to:
    kind: Service
    name: vault-csi-driver-app
  port:
    targetPort: 3000-tcp
  tls:
    termination: edge
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault-csi-driver-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault-csi-driver-app
  template:
    metadata:
      labels:
        app: vault-csi-driver-app
    spec:
      containers:
      - name: vault-csi-driver-app
        image: quay.io/rh-ee-moomura/vault-csi-driver-app:latest
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef: ##環境変数を読み込みます
            name: vault-csi-driver-app
        volumeMounts:
        - name: secrets-store-inline
          mountPath: "/mnt/secrets-store" ##シークレット情報を/mnt/secrets-storeに格納します
          readOnly: true
      volumes:
        - name: secrets-store-inline
          csi: ##ボリュームにCSIを指定します
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes: ##先ほど作成したSecretProviderClassを指定します
              secretProviderClass: "example-vault-provider"

マニフェストの適用方法はいくつかあります。OpenShiftのコンソール画面右上の「+ボタン」から、「YAMLをインポート」をクリックすると、YAML貼り付け画面が出てきます。「作成」をクリックすれば、選択されているプロジェクト(Namespace)にマニフェストが適用されます。

image.png

また、本記事で利用するマニフェストはGitlabの公開リポジトリに格納してありますので、そのURLを用いて、ocコマンドで適用することも可能です。

## ConfigMapの適用
oc apply -f https://gitlab.com/masaki-oomura/vault-csi-driver-app/-/raw/main/manifest/configmap.yaml

## Serviceの適用
oc apply -f https://gitlab.com/masaki-oomura/vault-csi-driver-app/-/raw/main/manifest/service.yaml

## Routeの適用
oc apply -f https://gitlab.com/masaki-oomura/vault-csi-driver-app/-/raw/main/manifest/route.yaml

## Deploymnetの適用
oc apply -f https://gitlab.com/masaki-oomura/vault-csi-driver-app/-/raw/main/manifest/deployment.yaml

Namespace「sample-app」を確認すると、アプリケーションがデプロイされていることを確認できます。

image.png

RouteのURLをクリックして、アプリの画面にアクセスしてみます。

image.png

このように、シークレット情報のkey「testSecret1」に対応するvalue「value123」が表示されました。
これにより、Vaultからコンテナアプリケーションに対してシークレット情報が渡されたことが確認できます。

次に、コンテナにターミナルで接続し、/mnt/secrets-store/secret-infoが存在するのか確認しておきます。Pod名「vault-csi-driver-<ランダム文字列>」をクリックし、「ターミナル」タブから以下のコマンドを入力してみます。

cat /mnt/secrets-store/secret-info

image.png

確かにシークレット情報が指定したパスにファイルとしてマウントされていることが確認できました。

Vault上のシークレット情報変更が反映されるか確認

次に、Vault上のシークレット情報を変更した場合、アプリケーションにちゃんと反映されるか?を確認します。

シークレット情報の更新

再びVaultのKV型シークレットエンジン「secret」にアクセスし、先ほど作成したシークレット情報「example」を確認します。ここで右上の「+ Create new version」をクリックします。

image.png

ここではシークレット情報の編集・更新が可能です。ここでは、key「testSecret1」のvalueを「value456」に変更してみましょう。変更できたら、「Save」をクリックし、変更を保存してください。

image.png

アプリケーションに反映

Vault上のシークレット情報が変更されたものの、アプリケーションに表示されるシークレット情報は「value123」のままのはずです。シークレット情報の変更反映は、コンテナを再作成したタイミングで反映されます。では、Deploymentをロールアウトしましょう。

image.png

OpenShiftのコンソール画面の「トポロジー表示」において、Deploymentアイコンの右側「︙」をクリックし、メニュー一覧から「ロールアウトの再開」をクリックします。

image.png

すると、コンテナが再作成され、あたらしいPodが立ち上がります。この状態でサンプルアプリの画面を更新します。

image.png

このようにVault上のシークレット情報の変更が、コンテナアプリケーションに反映されていることがわかります。

Kubernetes Secret自動作成

最後に、Vaultのシークレット情報をPodにマウントする際に、「Kubernetes Secret を自動的に作成する」というオプションを試してみましょう。

というのも、Vaultのシークレットはコンテナ内の指定パスにファイルとしてマウントされるものの、Kubernetesクラスタ内にはその情報が保存されていないためです。ユースケースによっては、KV型シークレットエンジンで管理している情報を、コンテナアプリケーションだけでなく、Kubernetes Secretとしてクラスタ内に保持したいケースもあるでしょう。

現在のKunbernetes Secretの情報一覧を確認

事前にNamespace「sample-app」に作成されているKubernetes Secret一覧を確認しておきましょう。開発者向け表示の「シークレット」メニューを選択します。ここには、OpenShiftの機能によってデフォルトで作成されたKubernetes Secretしかありません。

image.png

カスタムリソースの編集

それでは、カスタムリソース「SecretProviderClass」を編集し、Vaultからシークレット情報がコンテナに反映されるタイミングでKubernetes Secretが作成されるようにします。OpenShiftのコンソール画面を管理者向け表示に切り替え、「管理」メニューから「CustomResourceDefinitions」を選択、検索欄に「SecretProviderClass」と入力し、「SecretProviderClass」をクリックします。

image.png

次の画面で「インスタンス」タブに切り替え、先ほど作成した「example-vault-provider」をクリックし、「YAML」タブに切り替えてマニフェストを編集可能な状態にします。

以下の通り、spec.secretObjectsを追記しましょう。

...
spec:
  parameters:
    objects: |
      - secretPath: "secret/data/example"
        objectName: "secret-info"
        secretKey: "testSecret1"
    roleName: csi
    vaultAddress: 'http://vault.vault.svc.cluster.local:8200'
  provider: vault
+ secretObjects:
+   - secretName: synced-secret # 作成するKubernetes Secretの名称
+     type: Opaque # Kubernetes Secretのタイプ。OpaqueはKV形式のそれ。
+     labels:
+       app: vault-csi-driver-app # 作成するSecretに付与したいラベル
+     data:
+       - objectName: secret-info # Vault内のシークレット情報から作成されるファイル名(コンテナ内に作成されるファイル名)
+         key: gakenoueno # Kubernetes Secretを作成する際に使うKey

これでOKです。「保存」をクリックしましょう。

再度Vault上のシークレット情報を編集

先ほどの要領と同じく、再度Vault上でシークレット情報を編集します。

今度は適当にkeyを「ponyo」としてみます。

image.png

Deploymentをロールアウト

先ほどと同じくDeploymentをロールアウトし、コンテナアプリケーションにシークレット情報の変更を再反映します。

image.png

ロールアウトが完了したら、再びアプリケーションを更新します。

image.png

アプリケーションにシークレット情報の変更が再び反映されました。

Namespace「sample-app」のKubernetes Secret一覧を再確認

では、再びNamespace「sample-app」のKubernetes Secret一覧を見てみます。

image.png

お!先ほどカスタムリソース「SecretProviderClass」を編集してspec.secretObjects.secretNameで設定したKubernetes Secret名「synced-secret」が作成されています。これをクリックして、中身を確認しましょう。

image.png

こちらもカスタムリソース「SecretProviderClass」を編集した通り、

  • key: gaenoueno
  • value: ponyo
    がデータに反映されました。

おわりに

お疲れさまでした。OpenShift 4.18よりGAされた「Secrets Store CSI Driver」の仕組みを用いて、Vaultと連携したシークレット情報の管理を試してみました。Vault上でシークレット情報を適切に管理しつつ、カスタムリソースを利用してコンテナアプリケーションにシークレット情報を連携するという、一連の流れがわかっていただけたと思います。

Vault CSI Driver方式は、コンテナアプリケーション内に直接機密情報を安全に渡したい場合に特に有効です。オプションとしてKubernetes Secretを作成することも可能ですが、クラスタ内に機密情報を保存することなく、Vaultなどの外部のシークレットストアからPod/コンテナに直接シークレットを渡せる点が、特定のユースケースで大きなメリットになります。

例えば、データベースへのアクセス情報や、各種外部APIのキー・トークンなど、アプリケーションの動作に必要なパラメータを安全にコンテナ内へ連携しながら、しかしそうした情報をクラスタ自体には保存しない運用が可能となります。

Vault以外の外部シークレットストアとの連携方法

本記事では説明しきれないので、OpenShiftのドキュメントを確認いただくことをお勧めします。

おわり

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?