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工作负载类型,如DeploymentStatefulsetJob此外,可使用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使用的身份验证详情
  • VaultSecret: 定义Kubernetes与Vault密钥之间的映射,其中*可以是StaticDynamicPKI*,对应密钥类型

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获取。实际部署时,建议先在测试环境验证方案可行性,避免生产环境踩坑。


原始标题:Secure Kubernetes Secrets with Vault