Tuesday, October 27, 2026

Injecting HashiCorp Vault Secrets into Kubernetes Pods

Kubernetes Secrets are only base64-encoded, they end up in Git or Helm values far too easily, and rotating a database password means redeploying everything. With HashiCorp Vault and the Vault Agent Injector, pods authenticate with their Kubernetes service account and receive secrets as files, without the application knowing Vault exists.

1. Enable Kubernetes auth in Vault

vault auth enable kubernetes

vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc:443"

When Vault runs inside the same cluster, it can use its own service account token and CA to validate other pods' tokens.

2. Policy and role

# account-search.hcl
path "secret/data/account-search/*" {
  capabilities = ["read"]
}
vault policy write account-search account-search.hcl

vault write auth/kubernetes/role/account-search \
    bound_service_account_names=account-search \
    bound_service_account_namespaces=apps \
    policies=account-search \
    ttl=1h

Only pods running as the account-search service account in the apps namespace can log in with this role, and they can only read their own path.

3. Annotate the Deployment

spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "account-search"
        vault.hashicorp.com/agent-inject-secret-db.properties: "secret/data/account-search/db"
        vault.hashicorp.com/agent-inject-template-db.properties: |
          {{- with secret "secret/data/account-search/db" -}}
          spring.datasource.username={{ .Data.data.username }}
          spring.datasource.password={{ .Data.data.password }}
          {{- end }}
    spec:
      serviceAccountName: account-search

The injector adds an init container and a sidecar. The rendered file appears at /vault/secrets/db.properties and the sidecar keeps it fresh when the secret changes or the lease renews.

4. Let Spring Boot read it

# application.yml
spring:
  config:
    import: "optional:file:/vault/secrets/db.properties"

No Vault SDK, no code change. Locally, developers use their own properties file at the same path.

Things to watch

  • The template path uses secret/data/... for KV version 2. Forgetting /data/ is the most common “permission denied”.
  • Rotation is not reload. The file updates, but a running connection pool keeps the old password until restart unless your app re-reads it. Pair rotation with a rolling restart or use Vault dynamic database credentials.
  • Alternatives: the Vault Secrets Operator syncs into native Kubernetes Secrets, and the Secrets Store CSI driver mounts secrets as volumes. Pick one approach per cluster and stick to it.
  • Keep Vault patched and restrict who can restore snapshots; Vault is now the most valuable target in the cluster.

No comments: