使用 Vault 保護 Kubernetes 機密
一、簡介
在本教程中,我們將探索從 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 提供者的透明支持
- 使用 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 框架進行低階 REST 操作:
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>2.3.4</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
現在,我們可以在需要存取 Vault API 的任何地方注入VaultTemplate
。作為一個簡單的範例,讓我們建立一個CommandLineRunner
@Bean
來列出所有機密的內容:
@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 在/secrets
路徑上安裝了一個 KV 版本 2 機密引擎,因此我們使用opsForKeyValue
方法來取得VaultKeyValueOperations
對象,我們將使用該物件列出所有機密。其他秘密引擎也有專用的操作對象,提供客製化的方法來存取它們。
對於沒有專用VaultXYZOperations
外觀的秘密引擎,我們可以使用通用方法來存取任何路徑:
-
read(path)
:從指定路徑讀取數據 -
write(path, data):
在指定路徑寫入數據 -
list(path):
傳回指定路徑下的條目列表 -
delete
(path
):刪除指定路徑下的secret
6. 半顯式檢索
在前面的方法中,我們直接存取 Vault 的 API 引入了強耦合,這可能會帶來一些障礙。例如,這意味著開發人員在開發期間和運行 CI 管道時將需要 Vault 實例或建立模擬。
或者,我們可以在專案中使用 Spring Cloud Vault 函式庫,使 Vault 的秘密查找對應用程式的程式碼透明。該庫透過向 Spring 公開自訂PropertySource
來實現這一點,該自訂 PropertySource 將在應用程式引導期間被拾取和配置。
我們將此方法稱為“半顯式”,因為雖然應用程式的程式碼確實不知道 Vault 的使用情況,但我們仍然必須向專案添加所需的依賴項。實現此目標的最簡單方法是使用可用的入門庫:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
<version>3.1.3</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 的環境中取得配置值:
@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 取得其機密,那麼使用Vault 的 sidecar方法是一個合適的替代方案。唯一的要求是應用程式已經能夠從環境變數和設定檔中選取值。
sidecar 模式是 Kubernetes 環境中的常見做法,其中應用程式將某些特定功能委託給在同一pod上運行的另一個容器。這種模式的一個流行應用是Istio 服務網格,用於向現有應用程式添加流量控制策略和服務發現以及其他功能。
我們可以將此方法用於任何 Kubernetes 工作負載類型,例如Deployment
、 Statefulset,
或Job
。此外,我們可以使用Mutating Webhook
在建立 pod 時自動注入 sidecar,從而使用戶無需手動將其新增至工作負載規格。
Vault sidecar 使用工作負載 pod 範本的元資料部分中存在的註解來指示 sidecar 從 Vault 提取哪些機密。然後,這些秘密將被放入一個檔案中,該檔案儲存在 sidecar 與同一 Pod 中的任何其他容器之間的共用磁碟區中。如果這些秘密中的任何一個是動態的,則 sidecar 也會負責追蹤其更新,並在需要時重新呈現文件。
7.1. Sidecar注入器部署
在使用這種方法之前,首先我們需要部署Vault的Sidecar Injector組件。最簡單的方法是使用 Hashicorp 提供的Helm Chart ,預設情況下,它已經將注入器新增為 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 代理 sidecar 容器:
- 使用
baeldung-test-role
自動登錄 - 位於
secrets/baeldung-test
路徑下的機密將渲染到預設機密目錄 (/vault/secrets
) 下名為baeldung.properties
的文件 - 使用提供的範本產生的文件內容
還有更多可用的註釋,我們可以使用它們來自訂用於呈現秘密的位置和模板。支援的註釋的完整清單可在Vault 文件中找到。
8. 使用 Vault Secret CSI 提供者的透明支持
CSI(容器儲存介面)提供者允許供應商擴展 Kubernetes 叢集支援的磁碟區類型。 Vault CSI 提供者是使用 sidecar 的替代方案,允許 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 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 頭盔圖表。 CSI 提供者預設為未啟用,因此我們需要使用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 CSI 提供者使用 Vault 機密配置工作負載需要兩個步驟。首先,我們定義一個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
在volumes
部分,請注意我們如何使用指向先前定義的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 Secrets Operator 的透明支持
Vault Secrets Operator 將自訂資源定義 (CRD) 新增至 Kubernetes 叢集,我們可以使用從 Vault 執行個體提取的值來填入常規金鑰。
與 CSI 方法相比,該操作員的主要優點是我們不需要對現有工作負載進行任何更改即可從標準機密轉移到 Vault 支援的機密。
9.1. Vault Secrets Operator 部署
Operator 有自己的圖表,可將所有必要的工件部署到叢集中:
$ 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>
可以是Static
、Dynamic,
或PKI
,並且對應於金鑰類型。
9.2. Vault Secret 操作員使用情況
讓我們透過一個簡單的範例來展示如何使用此運算符。首先,我們需要建立一個指向我們的 Vault 實例的VaultConnection
資源:
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
:驗證時使用的服務帳戶
現在,讓我們定義一個VaultStaticSecret
將 Vault 上的secrets/baeldung-test
中的機密對應到名為baeldung-test
的機密:
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 實例中的機密的不同方法。
與往常一樣,所有程式碼都可以在 GitHub 上取得。