はじめに
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を統合することで、ハイブリッドクラウド環境全体で強力なシークレット管理とセキュリティー機能を提供します。
やることのイメージ
これからやることのイメージをざっくり描いてみました。
ちょっと分かりづらいので、それぞれの登場人物の役割や、サンプルアプリに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
)を表示するだけのアプリです。
今回使用するソースコード等の一式
今回使うするマニフェストファイルやサンプルアプリのソースコード一式は、以下の公開リポジトリに格納しています。
また、サンプルのソースコードからビルドしたコンテナイメージは以下に格納しています。
実践
それでは、早速やっていきましょう。なお、今回は「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」と入力します。
これをデフォルト設定のままインストールします。カスタムリソース「Secret Provider Class」は後ほど作成しますので、このタイミングではインストールが完了すればOKです。
Vaultをインストール
さて、次はVaultをインストールします。
Namespaceの作成と設定
その前に、VaultをデプロイするためのNamespaceを作成します。OpenShiftのコンソール画面を開発者向け表示に切り替え、「すべてのプロジェクト」をクリック、「プロジェクトの作成」をクリックします。
プロジェクト名には「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チャート」を選択します。
VaultのインストールはHashicorpが提供しているHelmチャートを利用します。検索欄に「vault」と入力します。
「Hashicorp Vault」をクリックし、「作成」をクリックします。
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をデプロイします。
実はまだいくつか設定が必要です。このままだと、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が正しく動作するようになります。
明示的に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のデプロイが完了しました。
もし一部のComputeノードにTaints
を付与している場合は、DaemonSet側にTorelations
を設定してあげないと、そのComputeノードにDaemonSetがデプロイされないので、お気をつけください。
後からTorelations(容認)を設定する場合は、以下の手順で可能です。
Podの台数が所望の台数に増えればOKです。
Vaultをセットアップ
さて、VaultのGUIにアクセスし、初期設定を行います。Routeの提供するURLをブラウザで開きます。
Unseal
Vaultは起動直後は暗号化(Sealed)されています。暗号化を解除(Unseal)するためには鍵が必要なのですが、その鍵の設定画面が最初に開きます。
-
Key shares
: 払い出す鍵の数 -
Key threshold
: 払い出した鍵のうち、Unsealするために必要な鍵の数
今回は簡単のため、それぞれ1つずつにしておきます。なお、VaultのPodが再起動した際等には、再び鍵によるUnsealが必要になります。鍵を無くすと初期化が必要になり、その際にはVault内のシークレット情報や設定値が消えることになります。
適当なKey shares
とKey threshold
を設定したら、「Initialize」をクリックします。
「Downlaod keys」をクリックして、鍵と初期ルートトークンをダウンロードします。これらは適切に管理するようにしましょう。
今保存した鍵を使ってUnsealします。
最後に、初期ルートトークンを用いてルートユーザでログインします。
これでVaultの初期設定が完了しました。
Kubernetes認証
次にVaultと連携先のOpenShft(=Kubernetes)の認証設定を行います。VaultのGUIの左側にある「Access」メニューから「Authentication Methods」を選択します。
画面右上にある「Enable new method +」をクリックし、
「Kubernetes」を選択、
「Enable method」をクリックします。次の画面にはなにやら色々情報を入力する必要がありそうです。
今回入力が必要な情報について説明します。
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
の値です。
Token Reviewer JWT
値:コンテナ「vault-0」/var/run/secrets/kubernetes.io/serviceaccount/token
の中身
「Token Reviewer JWT」とは、KubernetesのServiceAccountに自動で割り当てられるトークンであり、ServiceAccountが関連づけられたPodの同パスに格納されます。 つまり、Kubernetes APIに対して当該トークンを持つServiceAccount「Vault」を確認するために使う認証情報です。
必要な値がそれぞれ入力できたら、「Save」をクリックしてKubernete認証設定を保存します。
なお、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」をクリックします。
Vaultはさまざまなサービスや連携先に対応したSecret Engineの種類を用意していますが、今回は最も汎用的な「Key-Valune形式(KV)」を選択します。
「Path」欄にsecret
と入力し、「Enable engine」をクリックしましょう。
シークレット情報を登録
次にサンプルとなるKey-Value形式のシークレット情報を登録します。「Create secret」をクリックします。
今回は以下の通り値を登録します。
- Path for this secret:
example
- Secret data:
- key:
testSecret1
- value:
value123
- key:
値を入力したら「Save」をクリックして保存します。
今登録したシークレット情報「example」の詳細を確認できる画面になります。ここで「API path」が後ほど必要な情報なので、覚えておいてください。
なお、この一連の操作はコマンドでも実行可能です。
## Vaultにログイン
vault login <root token>
## KVタイプのSecret Engineを有効化しシークレット情報を追加
vault kv put secret/example testSecret1=value123
あきらかにコマンドの方が楽ですね...
ポリシーを作成
次に、今作成したシークレット情報に対する参照権限を持つポリシーを作成します。GUIの左メニューから「Policies」を選択し、さらに「ACL Policies」をクリックします。
すでに2つのポリシーが作成されています。「default」はその名の通り、defaultで用意されているもの、「root」はルートトークンでログインするユーザ(ルートユーザ)のポリシーです。ここでは「Create ACL policy +」をクリックします。今回は以下の通り値を入力します。
- Name: csi
- Policy:
path "secret/data/*" { capabilities = ["read"] }
このポリシーは、先ほど作成した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」認証をクリックします。
続いて「Create role」をクリックします。
ここでは以下の通り値を設定します。
- 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」という名称のロール」を意味しています。
値を入力し終えたら「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を作成しておいてください。
SecretProviderClassを作成
次にカスタムリソース「SecretProviderClass」を作成します。OpenShiftのコンソール画面を「管理者向け表示」とし、左メニューの「Operator」から「インストール済みのOperator」をクリック、インストール済みOperator一覧から「Secrets Store CSI Driver Operator」を選択します。
「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
: コンテナにマウントする際のファイル名
YAMLをコピペしたら「作成」をクリックします。
カスタムリソース「SecretProviderClass」が作成できました。それではいよいよ、サンプルアプリをデプロイしましょう。
サンプルアプリのマニフェストを適用
以下にマニフェストを用意しました。これらをNamespace「sample-app」に適用しています。このサンプルアプリのソースコードはこちらに格納しています。このアプリは環境変数SECRET_PATH
を持っており、このパスに格納されているテキストファイル(シークレット情報)の内容を画面に表示することができます。
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-csi-driver-app
data:
SECRET_PATH: '/mnt/secrets-store/secret-info'
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
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
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)にマニフェストが適用されます。
また、本記事で利用するマニフェストは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」を確認すると、アプリケーションがデプロイされていることを確認できます。
RouteのURLをクリックして、アプリの画面にアクセスしてみます。
このように、シークレット情報のkey「testSecret1」に対応するvalue「value123」が表示されました。
これにより、Vaultからコンテナアプリケーションに対してシークレット情報が渡されたことが確認できます。
次に、コンテナにターミナルで接続し、/mnt/secrets-store/
にsecret-info
が存在するのか確認しておきます。Pod名「vault-csi-driver-<ランダム文字列>」をクリックし、「ターミナル」タブから以下のコマンドを入力してみます。
cat /mnt/secrets-store/secret-info
確かにシークレット情報が指定したパスにファイルとしてマウントされていることが確認できました。
Vault上のシークレット情報変更が反映されるか確認
次に、Vault上のシークレット情報を変更した場合、アプリケーションにちゃんと反映されるか?を確認します。
シークレット情報の更新
再びVaultのKV型シークレットエンジン「secret」にアクセスし、先ほど作成したシークレット情報「example」を確認します。ここで右上の「+ Create new version」をクリックします。
ここではシークレット情報の編集・更新が可能です。ここでは、key「testSecret1」のvalueを「value456」に変更してみましょう。変更できたら、「Save」をクリックし、変更を保存してください。
アプリケーションに反映
Vault上のシークレット情報が変更されたものの、アプリケーションに表示されるシークレット情報は「value123」のままのはずです。シークレット情報の変更反映は、コンテナを再作成したタイミングで反映されます。では、Deploymentをロールアウトしましょう。
OpenShiftのコンソール画面の「トポロジー表示」において、Deploymentアイコンの右側「︙」をクリックし、メニュー一覧から「ロールアウトの再開」をクリックします。
すると、コンテナが再作成され、あたらしいPodが立ち上がります。この状態でサンプルアプリの画面を更新します。
このようにVault上のシークレット情報の変更が、コンテナアプリケーションに反映されていることがわかります。
Kubernetes Secret自動作成
最後に、Vaultのシークレット情報をPodにマウントする際に、「Kubernetes Secret を自動的に作成する」というオプションを試してみましょう。
というのも、Vaultのシークレットはコンテナ内の指定パスにファイルとしてマウントされるものの、Kubernetesクラスタ内にはその情報が保存されていないためです。ユースケースによっては、KV型シークレットエンジンで管理している情報を、コンテナアプリケーションだけでなく、Kubernetes Secretとしてクラスタ内に保持したいケースもあるでしょう。
現在のKunbernetes Secretの情報一覧を確認
事前にNamespace「sample-app」に作成されているKubernetes Secret一覧を確認しておきましょう。開発者向け表示の「シークレット」メニューを選択します。ここには、OpenShiftの機能によってデフォルトで作成されたKubernetes Secretしかありません。
カスタムリソースの編集
それでは、カスタムリソース「SecretProviderClass」を編集し、Vaultからシークレット情報がコンテナに反映されるタイミングでKubernetes Secretが作成されるようにします。OpenShiftのコンソール画面を管理者向け表示に切り替え、「管理」メニューから「CustomResourceDefinitions」を選択、検索欄に「SecretProviderClass」と入力し、「SecretProviderClass」をクリックします。
次の画面で「インスタンス」タブに切り替え、先ほど作成した「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」としてみます。
Deploymentをロールアウト
先ほどと同じくDeploymentをロールアウトし、コンテナアプリケーションにシークレット情報の変更を再反映します。
ロールアウトが完了したら、再びアプリケーションを更新します。
アプリケーションにシークレット情報の変更が再び反映されました。
Namespace「sample-app」のKubernetes Secret一覧を再確認
では、再びNamespace「sample-app」のKubernetes Secret一覧を見てみます。
お!先ほどカスタムリソース「SecretProviderClass」を編集してspec.secretObjects.secretName
で設定したKubernetes Secret名「synced-secret」が作成されています。これをクリックして、中身を確認しましょう。
こちらもカスタムリソース「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のドキュメントを確認いただくことをお勧めします。
おわり