知識庫 / Spring RSS 訂閱

使用Vault安全Kubernetes Secrets

Spring
HongKong
4
11:26 AM · Dec 06 ,2025

1. 引言

在本教程中,我們將探索從運行在 Kubernetes 上的應用程序訪問 Hashicorp Vault 存儲的秘密的不同方法。

2. 快速回顧

我們之前在教程中已經介紹了 Hashicorp 的 Vault,其中展示瞭如何安裝和填充它。簡而言之,Vault 提供了一個安全的應用程序機密存儲服務,這些機密可以是靜態的也可以是動態生成的。

為了訪問 Vault 服務,應用程序必須使用可用的機制進行身份驗證。 當應用程序在 Kubernetes 環境中運行時,Vault 可以基於其關聯的服務帳户進行身份驗證,從而消除了單獨憑據的需求

在此場景中,Kubernetes 服務帳户與 Vault 角色關聯,該角色定義了相關的訪問策略。 此策略定義應用程序可以訪問的機密。

3. 嚮應用程序提供密鑰

在 Kubernetes 環境中,開發者有多種方法可以獲取由 Vault 管理的密鑰,這些方法可以大致分為侵入性和不侵入性兩類。“侵入性”在此上下文中指的是應用程序對密鑰來源的認知程度。

以下是我們將涵蓋的方法的總結:

  • 使用 Vault API 進行顯式檢索
  • 使用 Spring Boot 的 Vault 支持進行半顯式檢索
  • 使用 Vault Sidecar 進行透明支持
  • 使用 Vault Secret CSI Provider 進行透明支持
  • 使用 Vault Secret Operator 進行透明支持

4. 身份驗證設置

在所有這些方法中,測試應用程序將使用 Kubernetes 身份驗證來訪問 Vault 的 API。當在 Kubernetes 中運行時,這將會自動提供。 但是,為了從集羣外部使用這種身份驗證,我們需要與服務帳户關聯的有效令牌。

一種實現方法是創建服務帳户令牌機密。機密和服務帳户是命名空間範圍內的資源,因此我們首先創建一個來存儲它們的作用域命名空間:

$ kubectl create namespace baeldung

接下來,我們創建服務帳户:

$ kubectl create serviceaccount --namespace baeldung vault-test-sa

最後,我們生成一個有效期為24小時的令牌,並將其保存到文件中:

$ kubectl create token --namespace baeldung vault-test-sa --duration 24h > sa-token.txt

現在,我們需要將 Kubernetes 服務帳户與 Vault 角色綁定:

$ vault write auth/kubernetes/role/baeldung-test-role \
  bound_service_account_names=vault-test-sa \
  bound_service_account_namespaces=baeldung \
  policies=default,baeldung-test-policy \
  ttl=1h

5. 顯式檢索

在這種場景下,應用程序直接通過 Vault 的 REST API 獲取所需的密鑰,或者更常見的是,使用可用的庫。對於 Java,我們將使用 spring-vault 項目提供的庫,該庫利用 Spring Framework 執行低級別的 REST 操作:

<dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
    <version>3.1.1</version>
</dependency>

此依賴項的最新版本可在 Maven Central 上獲取。

請確保選擇與 Spring Framework 主版本兼容的版本spring-vault-core 3.x 需要 Spring 6.x,而 spring-vault-core 2.x 需要 Spring 5.3.x。

訪問 Vault API 的主要入口是 VaultTemplate 類。該庫提供 EnvironmentVaultConfiguration 輔助類,它簡化了使用所需的訪問和身份驗證詳細信息配置 VaultTemplate 實例的過程。使用它的推薦方式是從應用程序的 @Configuration 類之一導入它。

@Configuration
@PropertySource("vault-config-k8s.properties")
@Import(EnvironmentVaultConfiguration.class)
public class VaultConfig {
    // No code!
}

在此情況下,我們還添加了 vault-config-k8s 屬性源,其中我們將添加所需的連接詳細信息。 至少,我們需要告知 Vault 的端點 URI 和使用的身份驗證機制。 由於我們在開發過程中將不在集羣內運行應用程序,因此我們也需要提供存儲服務帳户令牌的文件位置:

vault.uri=http://localhost:8200
vault.authentication=KUBERNETES
vault.kubernetes.role=baeldung-test-role
vault.kubernetes.service-account-token-file=sa-token.txt

我們現在可以隨時注入 VaultTemplate,以便在需要時訪問 Vault 的 API。作為快速示例,讓我們創建一個 CommandLineRunner @Bean,該 Runner 將列出所有 Secret 的內容:

@Bean
CommandLineRunner listSecrets(VaultTemplate vault) {

    return args -> {
        VaultKeyValueOperations ops = vault.opsForKeyValue("secrets", VaultKeyValueOperationsSupport.KeyValueBackend.KV_2);
        List<String> secrets = ops.list("");
        if (secrets == null) {
            System.out.println("No secrets found");
            return;
        }

        secrets.forEach(s -> {
            System.out.println("secret=" + s);
            var response = ops.get(s);
            var data = response.getRequiredData();

            data.entrySet()
              .forEach(e -> {
                  System.out.println("- key=" + e.getKey() + " => " + e.getValue());
              });
        });
    };
}

在我們的方案中,Vault 的 KV Version 2 密鑰引擎已掛載在 /secrets 路徑上,因此我們使用了 opsForKeyValue 方法獲取一個 VaultKeyValueOperations 對象,用於列出所有密鑰。 其他密鑰引擎也有專門的運維對象,提供定製化的方法來訪問它們。

對於沒有專門的 VaultXYZOperations 面紗的密鑰引擎,可以使用通用方法來訪問任何路徑:

  • read(path): 從指定路徑讀取數據
  • write(path, data): : 在指定路徑寫入數據
  • list(path): : 返回指定路徑下的條目列表
  • delete(path): : 刪除指定路徑下的密鑰

6. 半隱式檢索

在先前的方法中,我們直接訪問 Vault API 引入了強耦合,這可能會帶來一些挑戰。例如,這意味着開發者需要在開發期間和運行 CI 管道時,需要一個 Vault 實例或創建 Mock。

另一種方法是使用 Spring Cloud Vault 庫在我們的項目中,使 Vault 的密鑰查找對應用程序代碼透明。該庫通過向 Spring 暴露一個自定義 PropertySource,從而實現這一點,該 PropertySource 在應用程序的引導期間被拾取並配置。

我們稱這種方法為“半隱式” ,因為雖然應用程序代碼不瞭解 Vault 的使用,但我們仍然需要在項目中添加所需的依賴項。 實現這一點的最簡單方法是使用提供的啓動器庫:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-vault-config</artifactId>
    <version>4.1.1</version>
</dependency>

此依賴項的最新版本可在 Maven Central 上獲取。

如前所述,我們必須選擇與我們項目使用的主要 Spring Boot 版本兼容的版本。 Spring Boot 2.7.x 需要 3.1.x 版本,而 Spring Boot 3.x 需要 4.x 版本。

要啓用 Vault 作為屬性源,必須添加一些配置屬性。 常見的做法是為此使用專門的 Spring 配置文件,這允許我們快速地從 Vault 驅動的密鑰切換到任何其他源。

對於 Kubernetes,以下是一個典型的配置屬性文件:

spring.config.import=vault://
spring.cloud.vault.uri=http://vault-internal.vault.svc.cluster.local:8200
spring.cloud.vault.authentication=KUBERNETES
spring.cloud.vault.kv.backend=secrets
spring.cloud.vault.kv.application-name=baeldung-test

此配置允許 Vault 的 KV 後端掛載在服務器的 secrets 路徑下。該庫將使用配置的應用程序名稱作為該後端的從哪裏獲取密鑰的路徑。

spring.config.import 屬性也需要啓用 Vault 作為屬性源。 請注意,此屬性在 Spring Boot 2.4 中引入,同時廢棄了引導上下文的初始化。 在基於舊版 Spring Boot 的應用程序遷移時,需要特別注意此項。

Spring Cloud Vault 的完整可用配置屬性列表可以在 文檔 中找到。

現在,讓我們展示如何使用此配置與一個簡單的示例,從 Spring 的 Environment 中獲取配置值:

@Bean
CommandLineRunner listSecrets(Environment env) {
    return args -> {
        var foo = env.getProperty("foo");
        Assert.notNull(foo, "foo must have a value");
        System.out.println("foo=" + foo);
    };
}

當我們運行此應用程序時,可以在輸出中看到秘密的值,從而確認集成已成功完成。

7. 利用 Vault Sidecar 實現透明支持

如果不想或無法修改現有應用程序的代碼以獲取其密鑰,使用 Vault Sidecar 方法是一種合適的替代方案。 唯一的要求是應用程序已經能夠從環境變量和配置文件中獲取值。

Sidecar 模式是 Kubernetes 領域中的常見做法,其中應用程序將某些特定功能委託給同一 Pod 內運行的另一個容器。 這種模式的一個流行應用是 Istio 服務網格,它用於為現有應用程序添加流量控制策略和服務發現等功能。

我們可以使用這種方法與任何 Kubernetes 工作負載類型,例如 DeploymentStatefulsetJob此外,我們還可以使用 Mutating Webhook 在 Pod 創建時自動注入 Sidecar,從而減輕用户手動將其添加到工作負載規範的負擔。

Vault Sidecar 使用工作負載 Pod 元數據部分中的註解來指示 Sidecar 從 Vault 提取哪些密鑰。 這些密鑰隨後存儲在 Sidecar 和同一 Pod 中任何其他容器共享的卷中。 如果這些密鑰是動態的,Sidecar 還會負責跟蹤其更新,並在需要時重新渲染文件。

7.1. Sidecar Injector 部署

在採用此方法之前,首先需要部署 Vault 的 Sidecar Injector 組件。 最簡單的方法是使用 Hashicorp 提供的 Helm 圖表,該圖表默認情況下已將 Injector 添加到 Kubernetes 上的常規 Vault 部署中。

如果情況並非如此,則必須使用新的 injector.enabled 值的更新的現有 Helm 發佈版本。

$ helm upgrade vault hashicorp/vault -n vault --set injector.enabled=true

為了驗證注入器是否正確安裝,我們來查詢可用的 WebHookConfiguration 對象:

$ kubectl get mutatingwebhookconfiguration
NAME                       WEBHOOKS   AGE
vault-agent-injector-cfg   1          16d

7.2. 部署註釋

秘密注入是“自選”模式,這意味着除非注入器在工作負載元數據中找到特定的註釋,否則不會發生任何更改。 這是一個使用所需註釋最小集部署清單的示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: baeldung
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1 
  template:
    metadata:
      labels:
        app: nginx
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-inject-secret-baeldung.properties: "secrets/baeldung-test"
        vault.hashicorp.com/role: "baeldung-test-role"
        vault.hashicorp.com/agent-inject-template-baeldung.properties: |
          {{- with secret "secrets/baeldung-test" -}}
          {{- range $k, $v := .Data.data }}
          {{$k}}={{$v}}
          {{- end -}}
          {{ end }}
    spec:
      serviceAccountName: vault-test-sa
      automountServiceAccountToken: true
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

當我們將此清單部署到集羣時,注入器將對其進行修補並注入以下配置的 Vault agent sidecar 容器:

  • 使用 baeldung-test-role 進行自動登錄
  • 位於 secrets/baeldung-test 路徑下的密鑰將被渲染為名為 baeldung.properties 的文件,該文件位於默認密鑰目錄 (/vault/secrets) 下
  • 文件內容使用提供的模板生成

還有許多其他註解可用,我們可以使用它們來自定義密鑰渲染的位置和使用的模板。 支持的註解的完整列表可以在 Vault 的文檔 中找到。

8. 使用 Vault Secret CSI 提供器進行透明支持

CSI (容器存儲接口) 提供器允許供應商擴展 Kubernetes 集羣支持的卷類型。 Vault CSI 提供器 是一個替代方案,允許 Vault 密鑰以常規卷的形式暴露到 pod 中。

主要的優勢在於我們沒有為每個 pod 附加 sidecar,因此我們的工作負載所需的資源(CPU/內存)更少。 雖然不是非常耗資源,但 sidecar 的成本隨着活躍 pod 的數量而增加。 相反,CSI 使用 DaemonSet,這意味着每個節點都有一個 pod。

8.1. 啓用 Vault CSI 提供程序

在安裝此提供程序之前,我們必須檢查目標集羣中是否已存在 CSI Secret Store Driver

$ kubectl get csidrivers

結果應包括 secrets-store.csi.k8s.io 驅動程序:

NAME                       ATTACHREQUIRED   PODINFOONMOUNT  ... 
secrets-store.csi.k8s.io   false            true             ...

如果情況並非如此,只需應用合適的 Helm 圖表即可:

$ helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
$ helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
     --namespace kube-system\
     --set syncSecret.enabled=true

項目的文檔還描述了其他安裝方法,但除非有特殊要求,否則Helm方法是首選。

現在,讓我們轉向Vault CSI Provider的安裝本身。 再次強調,我們將使用官方Vault Helm Chart。 CSI Provider 默認未啓用,因此我們需要使用 csi.enabled 屬性進行升級:

$ helm upgrade vault hashicorp/vault -n vault –-set csi.enabled=true

為了驗證驅動程序是否已正確安裝,我們將檢查其 DaemonSet 是否正常運行:

$ kubectl get daemonsets –n vault
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
vault-csi-provider 1 1 1 1 1 <none> 15d 

8.2. 使用 Vault CSI 提供程序的用法

配置工作負載以使用 Vault 密鑰使用 Vault CSI 提供程序需要兩個步驟。首先,我們需要定義一個 SecretProviderClass 資源,該資源指定要檢索的密鑰和鍵:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: baeldung-csi-secrets
  namespace: baeldung
spec:
  provider: vault
  parameters:
    roleName: 'baeldung-test-role'
    objects: |
      - objectName: 'baeldung.properties'
        secretPath: "secrets/data/baeldung-test"

請注意 spec.provider 屬性,必須設置為 vault。 這對於 CSI 驅動程序來説是必需的,以便它知道使用哪些可用的提供程序。 參數部分包含提供程序用於查找所需密鑰的信息:

  • roleName:在登錄期間使用的 Vault 角色,它定義應用程序將具有的密鑰。
  • objects:該值是 YAML 格式的字符串(因此帶有“|”),包含要檢索的密鑰數組。

每個 objects 數組中的條目都是一個具有三個屬性的對象:

  • secretPath:Vault 中密鑰的路徑。
  • objectName:將包含密鑰的文件名。
  • objectKey:Vault 中密鑰內的鍵,用於將內容放入文件中。 如果未提供,則文件將包含一個包含所有值的 JSON 對象。

現在,讓我們在示例部署工作負載中使用此資源:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-csi
  namespace: baeldung
spec:
  selector:
    matchLabels:
      app: nginx-csi
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx-csi
    spec:
      serviceAccountName: vault-test-sa
      automountServiceAccountToken: true
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
        volumeMounts:
        - name: vault-secrets
          mountPath: /vault/secrets
          readOnly: true
      volumes:
      - name: vault-secrets
        csi:
          driver: 'secrets-store.csi.k8s.io'
          readOnly: true
          volumeAttributes:
            secretProviderClass: baeldung-csi-secrets

部分,請注意我們如何使用一個 CSI 定義,該定義指向我們先前定義的 SecretStorageClass

為了驗證此部署,我們可以打開主容器的 shell 並檢查指定掛載路徑下的密鑰是否存在:

$ kubectl get pods -n baeldung -l app=nginx-csi
NAME                        READY   STATUS    RESTARTS   AGE
nginx-csi-b7866bc69-njzff   1/1     Running   0          19m
$ kubectl exec -it -n baeldung nginx-csi-b7866bc69-njzff -- /bin/sh
# cat /vault/secrets/baeldung.properties
{"request_id":"eb417a64-b1c4-087d-a5f4-30229f27aba1","lease_id":"","lease_duration":0,
   "renewable":false,
   "data":{
      "data":{"foo":"bar"},
  ... more data omitted  

9. 使用 Vault 密鑰運算符的透明支持

Vault 密鑰運算符向 Kubernetes 集羣添加自定義資源定義 (CRD),從而使我們能夠使用從 Vault 實例拉取的值填充常規密鑰。

與 CSI 方法相比,運算符的主要優勢在於,我們無需對現有工作負載進行任何更改,即可從標準密鑰遷移到 Vault 支持的密鑰。

9.1. Vault Secrets Operator 部署

該運算符擁有自己的 Chart,它會將所有必需的 Artifact 部署到集羣中:

$ helm install --create-namespace --namespace vault-secrets-operator \
    vault-secrets-operator hashicorp/vault-secrets-operator \
    --version 0.1.0

現在,讓我們檢查新的CRD:

$ kubectl get customresourcedefinitions | grep vault
vaultauths.secrets.hashicorp.com                            2023-09-13T01:08:11Z
vaultconnections.secrets.hashicorp.com                      2023-09-13T01:08:11Z
vaultdynamicsecrets.secrets.hashicorp.com                   2023-09-13T01:08:11Z
vaultpkisecrets.secrets.hashicorp.com                       2023-09-13T01:08:11Z
vaultstaticsecrets.secrets.hashicorp.com                    2023-09-13T01:08:11Z

截至目前,操作符定義了以下 CRD:

  • VaultConnection: 定義 Vault 連接的詳細信息,例如其地址、TLS 證書等
  • VaultAuth: 用於特定 VaultConnection 的身份驗證詳細信息
  • Vault<type>Secret: 定義 Kubernetes 和 Vault 密鑰之間的映射關係,其中 <type> 可以是 StaticDynamicPKI,對應密鑰類型。

9.2. Vault Secrets Operator 使用

讓我們通過一個簡單的示例來演示如何使用此 Operator。首先,我們需要創建一個 VaultConnection 資源,指向我們的 Vault 實例:

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
  namespace: baeldung
  name: vault-local
spec:
  address: http://vault.vault.svc.cluster.local:8200

接下來,我們需要一個 VaultAuth 資源,其中包含用於訪問密鑰的身份驗證詳細信息:

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  namespace: baeldung
  name: baeldung-test
spec:
  vaultConnectionRef: vault-local
  method: kubernetes
  mount: kubernetes
  Kubernetes:
    role: baeldung-test-role
    serviceAccount: vault-test-sa

以下是必須填寫的關鍵屬性:

  • spec.vaultConnectionRef: 我們剛剛創建的 VaultConnection 資源的名稱
  • spec.method: 設置為 kubernetes,因為我們將使用此身份驗證方法
  • spec.kubernetes.role: 身份驗證時使用的 Vault 角色
  • spec.kubernetes.serviceAccount: 身份驗證時使用的 Service account

現在,讓我們定義一個 VaultStaticSecret,將 Vault 中的 secrets/baeldung-test 映射到名為 baeldung-test 的 secret:

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  namespace: baeldung
  name: baeldung-test
spec:
  vaultAuthRef: baeldung-test
  mount: secrets
  type: kv-v2
  path: baeldung-test
  refreshAfter: 60s
  hmacSecretData: true
  destination:
    create: true
    name: baeldung-test

最後,我們可以使用 kubectl 確認密鑰是否正確創建:

$ kubectl get secret -n baeldung baeldung-test
NAME            TYPE     DATA   AGE
baeldung-test   Opaque   3      24h

10. 方法比較

正如我們所見,基於 Kubernetes 的應用程序擁有多種從 Vault 中訪問密鑰的方法。為了幫助您選擇最適合特定用例的方法,我們整理了一份每種方法的特性/特徵簡要比較:

特性/特徵 顯式 半顯式 注入器 CSI 操作符
需要代碼修改 否(僅依賴項)
訪問 Vault API 完全控制 只讀 部分(例如,無法訪問管理員 API) 有限 有限
額外資源需求 是,每個 Pod 額外一個容器 是,每個節點一個 是,每個集羣一個
對現有應用程序的透明度 部分(需要額外註解) 部分(需要額外卷)
需要集羣變更

11. 結論

在本教程中,我們探討了如何從基於 Kubernetes 的應用程序中訪問 Vault 實例存儲的秘密的不同方法。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.