Kubernetes Secrets Management with Vault

Managing secrets securely in Kubernetes is critical for production deployments. While Kubernetes has a native Secrets resource, it stores data as base64-encoded text by default, which is not encryption. HashiCorp Vault provides enterprise-grade secrets management with dynamic secrets, encryption, audit logging, and fine-grained access control. This guide covers installing Vault in Kubernetes, configuring the sidecar injector, CSI drivers, and implementing dynamic secrets.

Table of Contents

Vault Fundamentals

Why Vault?

Kubernetes native Secrets have limitations:

  • Default storage in etcd is not encrypted
  • No secrets rotation
  • No audit logging for secret access
  • No dynamic secret generation
  • Secrets exposed in pod environment variables

Vault addresses these by providing:

  • Encryption at rest and in transit
  • Dynamic secret generation
  • Audit logging
  • Lease management
  • Multiple authentication methods
  • Fine-grained access control

Vault Concepts

Secrets Engine: Interface for reading/writing secrets (database, AWS, SSH)

Authentication Method: Way to prove identity (Kubernetes, JWT, LDAP)

Policies: Define what paths a client can access

Lease: Time-limited access to secrets with automatic revocation

Audit Log: Complete record of all requests to Vault

Vault Installation on Kubernetes

Prerequisites

  • Running Kubernetes cluster
  • Helm installed
  • kubectl configured

Installing Vault with Helm

Add Vault Helm repository:

helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

Create values file for Vault:

# vault-values.yaml
server:
  ha:
    enabled: true
    replicas: 3
    config: |
      ui = true
      
      listener "tcp" {
        address = "[::]:8200"
        tls_disable = false
        tls_cert_file = "/vault/userconfig/vault-ha-tls/tls.crt"
        tls_key_file = "/vault/userconfig/vault-ha-tls/tls.key"
      }
      
      storage "raft" {
        path = "/vault/data"
        node_id = "node-PLACEHOLDER"
        retry_join {
          leader_api_addr = "https://vault-0.vault-internal:8200"
        }
        retry_join {
          leader_api_addr = "https://vault-1.vault-internal:8200"
        }
        retry_join {
          leader_api_addr = "https://vault-2.vault-internal:8200"
        }
      }
      
      service_registration "kubernetes" {}
  
  dataStorage:
    size: 10Gi
    storageClass: fast-ssd
  
  logLevel: "info"
  
  resources:
    requests:
      memory: "256Mi"
      cpu: "100m"
    limits:
      memory: "512Mi"
      cpu: "500m"
  
  affinity: |
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchExpressions:
                - key: app.kubernetes.io/name
                  operator: In
                  values:
                    - vault
            topologyKey: kubernetes.io/hostname

ui:
  enabled: true
  serviceType: ClusterIP

injector:
  enabled: true
  replicas: 1
  
csi:
  enabled: true

Install Vault:

helm install vault hashicorp/vault \
  -n vault \
  --create-namespace \
  -f vault-values.yaml

Wait for Vault to be ready:

kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault -n vault --timeout=300s

Initializing Vault

Initialize Vault (creates unseal keys and root token):

kubectl exec -n vault vault-0 -- vault operator init \
  -key-shares=5 \
  -key-threshold=3 \
  > vault-init.txt

Store the unseal keys and root token securely. Extract and unseal:

# Extract unseal keys from vault-init.txt
UNSEAL_KEY_1=$(grep "Unseal Key 1" vault-init.txt | awk '{print $NF}')
UNSEAL_KEY_2=$(grep "Unseal Key 2" vault-init.txt | awk '{print $NF}')
UNSEAL_KEY_3=$(grep "Unseal Key 3" vault-init.txt | awk '{print $NF}')

# Unseal vault-0
kubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_1
kubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_2
kubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_3

# Unseal vault-1 and vault-2
kubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_1
kubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_2
kubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_3

kubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_1
kubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_2
kubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_3

Vault Authentication

Kubernetes Authentication Method

Enable Kubernetes auth:

ROOT_TOKEN=$(grep "Initial Root Token" vault-init.txt | awk '{print $NF}')

kubectl exec -n vault vault-0 -- vault login $ROOT_TOKEN

kubectl exec -n vault vault-0 -- vault auth enable kubernetes

Configure Kubernetes auth:

kubectl exec -n vault vault-0 -- vault write auth/kubernetes/config \
  kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT" \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token

Sidecar Injector

Configuring the Injector

The Vault Agent Injector automatically injects secrets into pods via Init and Agent sidecars.

Annotations for pod injection:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: production
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-database: "secret/data/database/postgres"
    vault.hashicorp.com/agent-inject-template-database: |
      {{- with secret "secret/data/database/postgres" -}}
      export DB_USER="{{ .Data.data.username }}"
      export DB_PASSWORD="{{ .Data.data.password }}"
      export DB_HOST="{{ .Data.data.host }}"
      {{- end }}
    vault.hashicorp.com/role: "my-app"
spec:
  serviceAccountName: my-app
  containers:
  - name: app
    image: my-app:1.0
    command: ["/bin/sh", "-c"]
    args:
      - |
        source /vault/secrets/database
        ./app

Policy for Sidecar Injector

Create a Vault policy:

kubectl exec -n vault vault-0 -- vault policy write my-app - <<EOF
path "secret/data/database/postgres" {
  capabilities = ["read"]
}

path "auth/token/renew-self" {
  capabilities = ["update"]
}
EOF

Vault Role for Kubernetes

Create a Vault role that binds to ServiceAccount:

kubectl exec -n vault vault-0 -- vault write auth/kubernetes/role/my-app \
  bound_service_account_names=my-app \
  bound_service_account_namespaces=production \
  policies=my-app \
  ttl=24h

CSI Driver

Installing Vault CSI Driver

The CSI driver mounts secrets as files in pods:

# vault-csi-values.yaml
server:
  ha:
    enabled: false

injector:
  enabled: false

csi:
  enabled: true

Install:

helm install vault-csi hashicorp/vault \
  -n vault \
  -f vault-csi-values.yaml

Using CSI Driver

Create a SecretProviderClass:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-database
  namespace: production
spec:
  provider: vault
  parameters:
    vaultAddress: "https://vault.vault.svc:8200"
    vaultKubernetesMountPath: "kubernetes"
    vaultRole: "my-app"
    secretPath: "secret/data/database/postgres"
    objects: |
      - objectName: "username"
        secretPath: "secret/data/database/postgres"
        secretKey: "username"
      - objectName: "password"
        secretPath: "secret/data/database/postgres"
        secretKey: "password"
      - objectName: "host"
        secretPath: "secret/data/database/postgres"
        secretKey: "host"

Mount in pod:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  namespace: production
spec:
  serviceAccountName: my-app
  containers:
  - name: app
    image: my-app:1.0
    volumeMounts:
    - name: vault-secrets
      mountPath: /mnt/secrets
      readOnly: true
  volumes:
  - name: vault-secrets
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: "vault-database"

Dynamic Secrets

Database Secret Engine

Enable database secrets engine:

kubectl exec -n vault vault-0 -- vault secrets enable database

Configure PostgreSQL connection:

kubectl exec -n vault vault-0 -- vault write database/config/postgres \
  plugin_name=postgresql-database-plugin \
  allowed_roles="readonly" \
  connection_url="postgresql://{{username}}:{{password}}@postgres.data.svc:5432/production?sslmode=require" \
  username="vault_admin" \
  password="VaultAdminPassword123"

Create database role:

kubectl exec -n vault vault-0 -- vault write database/roles/readonly \
  db_name=postgres \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

Request dynamic credentials:

kubectl exec -n vault vault-0 -- vault read database/creds/readonly

AWS Secret Engine

Enable AWS secrets:

kubectl exec -n vault vault-0 -- vault secrets enable aws

Configure AWS connection:

kubectl exec -n vault vault-0 -- vault write aws/config/root \
  access_key=AKIAIOSFODNN7EXAMPLE \
  secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
  region=us-east-1

Create AWS role:

kubectl exec -n vault vault-0 -- vault write aws/roles/app-role \
  credential_type=iam_user \
  policy_document=-<<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}
EOF

Vault Policies

Creating Policies

kubectl exec -n vault vault-0 -- vault policy write app-secret-reader - <<EOF
path "secret/data/apps/*" {
  capabilities = ["read", "list"]
}

path "database/creds/readonly" {
  capabilities = ["read"]
}

path "auth/token/renew-self" {
  capabilities = ["update"]
}

path "auth/token/lookup-self" {
  capabilities = ["read"]
}
EOF

Policy Best Practices

Use path templating:

kubectl exec -n vault vault-0 -- vault policy write app-namespace-admin - <<EOF
path "secret/data/{{identity.entity.metadata.namespace}}/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

path "database/creds/{{identity.entity.metadata.namespace}}" {
  capabilities = ["read"]
}
EOF

Practical Examples

Example: Application with Database Secrets

Pod with injected database credentials:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: database-app
  namespace: production
---
apiVersion: v1
kind: Pod
metadata:
  name: app-with-db
  namespace: production
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-db: "database/creds/readonly"
    vault.hashicorp.com/agent-inject-template-db: |
      {{- with secret "database/creds/readonly" -}}
      DB_USER="{{ .Data.username }}"
      DB_PASSWORD="{{ .Data.password }}"
      DB_HOST="postgres.data.svc"
      DB_NAME="production"
      DB_PORT="5432"
      {{- end }}
    vault.hashicorp.com/role: "app-role"
spec:
  serviceAccountName: database-app
  containers:
  - name: app
    image: my-app:1.0
    env:
    - name: APP_ENV
      value: production
    command:
    - /bin/sh
    - -c
    - |
      source /vault/secrets/db
      python app.py

Example: Multi-Secret Injection

Inject multiple secret sources:

apiVersion: v1
kind: Pod
metadata:
  name: multi-secret-app
  namespace: production
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-db: "database/creds/app-db"
    vault.hashicorp.com/agent-inject-secret-aws: "aws/creds/app-role"
    vault.hashicorp.com/agent-inject-secret-api-keys: "secret/data/production/api-keys"
    vault.hashicorp.com/role: "app-role"
spec:
  serviceAccountName: app
  containers:
  - name: app
    image: my-app:1.0
    volumeMounts:
    - name: vault-secrets
      mountPath: /vault/secrets
  volumes:
  - name: vault-secrets
    emptyDir:
      medium: Memory

Conclusion

Vault provides enterprise-grade secrets management for Kubernetes deployments on VPS and baremetal infrastructure. By implementing the sidecar injector or CSI driver for automatic secret injection, leveraging dynamic secrets for automatic rotation, and enforcing fine-grained policies, you create a secure secrets management system. Start with basic static secrets, then gradually implement dynamic secrets and advanced features. Regular audits of Vault logs and policy access ensure ongoing security and compliance with your organization's requirements.