1. 引言
本文将探讨在Kubernetes上运行的应用程序访问HashiCorp Vault中存储密钥的多种实现方式。Vault作为企业级密钥管理工具,提供了静态和动态密钥的安全存储方案,而Kubernetes环境下的集成方案尤其值得关注。
2. 快速回顾
在之前的教程中,我们已经介绍了Vault的安装和密钥填充。简单来说,Vault为应用密钥提供安全存储服务,支持静态密钥和动态生成的密钥两种类型。
访问Vault服务时,应用必须通过可用机制进行身份验证。当应用运行在Kubernetes环境中时,Vault可以基于其关联的Service Account进行认证,无需额外凭证。
这种场景下,Kubernetes Service Account会绑定到Vault角色,该角色定义了访问策略。策略决定了应用可以访问哪些密钥。
3. 为应用提供密钥
在Kubernetes环境中,开发者有多种方式获取Vault管理的密钥,这些方式按侵入性程度可分为不同级别。"侵入性"指应用对密钥来源的感知程度。
我们将覆盖的方法总结如下:
- 通过Vault API显式获取
- 使用Spring Boot的Vault支持半显式获取
- 通过Vault Sidecar透明支持
- 通过Vault Secret CSI Provider透明支持
- 通过Vault Secret Operator透明支持
4. 身份验证设置
所有方法中,测试应用都将使用Kubernetes身份验证访问Vault API。在集群内运行时,这会自动提供。但要从集群外使用这种身份验证,我们需要关联Service Account的有效令牌。
实现方式之一是创建Service Account令牌密钥。密钥和Service Account是命名空间范围的资源,我们先创建命名空间:
$ kubectl create namespace baeldung
接着创建Service Account:
$ kubectl create serviceaccount --namespace baeldung vault-test-sa
最后生成24小时有效期的令牌并保存到文件:
$ kubectl create token --namespace baeldung vault-test-sa --duration 24h > sa-token.txt
现在需要将Kubernetes Service Account与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>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辅助类,简化配置过程。推荐方式是从应用的*@Configuration*类导入:
@Configuration
@PropertySource("vault-config-k8s.properties")
@Import(EnvironmentVaultConfiguration.class)
public class VaultConfig {
// 无需代码!
}
这里还添加了vault-config-k8s属性源,用于添加连接详情。至少需要提供Vault的接口URI和认证机制。由于开发期间应用运行在集群外,我们还需提供Service Account令牌文件位置:
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 Version 2密钥引擎,因此使用opsForKeyValue方法获取VaultKeyValueOperations*对象。其他密钥引擎也有专用操作对象。
对于没有专用VaultXYZOperations外观的密钥引擎,可使用通用方法访问任何路径:
- read(path): 读取指定路径数据
- write(path, data): 在指定路径写入数据
- list(path): 返回指定路径下的条目列表
- delete(path): 删除指定路径的密钥
6. 半显式获取
前一种方法直接访问Vault API引入了强耦合,可能带来一些障碍。例如,开发者在开发阶段和CI管道运行时需要Vault实例或创建模拟。
替代方案是使用Spring Cloud Vault库,使密钥查找对应用代码透明。该库通过向Spring暴露自定义PropertySource实现,在应用启动时自动配置。
我们称这种方法为"半显式",因为虽然应用代码不感知Vault的使用,但仍需添加依赖。最简单方式是使用starter库:
<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 Profile,便于快速切换密钥来源。
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
此配置启用挂载在服务器secrets路径的KV后端。库将使用配置的应用名称作为后端下的路径,从中获取密钥。
spring.config.import属性是启用Vault作为属性源所必需的。注意该属性在Spring Boot 2.4引入,同时弃用了bootstrap上下文初始化。迁移旧版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获取密钥,使用Vault sidecar是合适的替代方案。唯一要求是应用能从环境变量和配置文件读取值。
Sidecar模式是Kubernetes中的常见实践,应用将特定功能委托给同一pod中的其他容器。典型应用是Istio服务网格,用于为现有应用添加流量控制和服务发现等功能。
此方法适用于任何Kubernetes工作负载类型,如Deployment、Statefulset或Job。此外,可使用Mutating Webhook在创建pod时自动注入sidecar,无需手动添加到工作负载规范中。
Vault sidecar使用工作负载pod模板元数据中的注解,指示从Vault拉取哪些密钥。这些密钥随后存储在sidecar与pod中其他容器共享的卷文件中。如果是动态密钥,sidecar还会跟踪续期,必要时重新渲染文件。
7.1. Sidecar注入器部署
使用此方法前,需先部署Vault的Sidecar Injector组件。最简单方式是使用HashiCorp提供的helm chart,默认情况下它已作为Kubernetes上常规Vault部署的一部分添加。
若未启用,需使用injector.enabled属性升级现有helm release:
$ 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路径的密钥将渲染为默认密钥目录(/vault/secrets)下的baeldung.properties文件
- 使用提供的模板生成文件内容
还有更多可用注解,可用于自定义渲染密钥的位置和模板。完整注解列表见Vault文档。
8. 使用Vault Secret CSI Provider透明支持
CSI(容器存储接口)提供商允许供应商扩展Kubernetes集群支持的卷类型。Vault CSI Provider是sidecar的替代方案,允许将Vault密钥作为常规卷暴露给pod。
主要优势是每个pod没有附加sidecar,因此运行工作负载所需资源(CPU/内存)更少。虽然sidecar资源消耗不大,但其成本随活跃pod数量增加。而CSI使用DaemonSet,意味着集群每个节点只有一个pod。
8.1. 启用Vault CSI Provider
安装此提供商前,需检查目标集群是否已安装CSI Secret Store Driver:
$ kubectl get csidrivers
结果应包含secrets-store.csi.k8s.io驱动:
NAME ATTACHREQUIRED PODINFOONMOUNT ...
secrets-store.csi.k8s.io false true ...
若未安装,只需应用相应的helm chart:
$ 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提供商默认未启用,需使用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 Provider使用
使用Vault CSI Provider配置工作负载需要两步。首先定义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 Driver知道使用哪个提供商所必需的。parameters部分包含提供商定位请求密钥所需的信息:
- 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的CSI定义。
验证此部署,可进入主容器的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向Kubernetes集群添加自定义资源定义(CRD),可用于用从Vault实例获取的值填充常规密钥。
与CSI方法相比,Operator的主要优势是无需更改现有工作负载即可从标准密钥迁移到Vault支持的密钥。
9.1. Vault Secrets Operator部署
Operator有自己的chart,可将所有必需的工件部署到集群:
$ 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
截至撰写本文时,Operator定义了这些CRD:
- VaultConnection: 定义Vault连接详情,如地址、TLS证书等
- VaultAuth: 特定VaultConnection使用的身份验证详情
- Vault
Secret : 定义Kubernetes与Vault密钥之间的映射,其中*可以是Static、Dynamic或PKI*,对应密钥类型
9.2. Vault Secrets Operator使用
通过简单示例展示Operator的使用方式。首先创建指向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: 身份验证时使用的Service Account
现在定义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密钥的方案多种多样。为帮助选择最适合特定用例的方案,我们总结了各方法的特点对比:
特性/特点 | 显式获取 | 半显式获取 | Sidecar注入 | CSI Provider | Operator |
---|---|---|---|---|---|
需要代码更改 | ✅ 是 | ❌ 仅依赖 | ❌ 否 | ❌ 否 | ❌ 否 |
访问Vault API | 完全控制 | 只读 | 部分(如无管理API) | 有限 | 有限 |
额外资源需求 | ❌ 无 | ❌ 无 | ✅ 每个pod一个容器 | ✅ 每节点一个 | ✅ 每集群一个 |
对现有应用透明 | ❌ 否 | ❌ 否 | ⚠️ 需额外注解 | ⚠️ 需额外卷 | ✅ 完全透明 |
需要集群变更 | ❌ 否 | ❌ 否 | ✅ 是 | ✅ 是 | ✅ 是 |
11. 结论
本文探讨了基于Kubernetes的应用访问Vault实例中存储密钥的多种方式。每种方案各有优劣,选择时应考虑团队技术栈、现有架构和运维复杂度。
所有代码示例可在GitHub获取。实际部署时,建议先在测试环境验证方案可行性,避免生产环境踩坑。