Kubernetes Secrets Management with Vault

Managing secrets securely in Kubernetes is critical for production implementacións. 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 registro, and fine-grained access control. Esta guía cubre installing Vault in Kubernetes, configuring the sidecar injector, CSI drivers, and implementing dynamic secrets.

Tabla de contenidos

Vault Fundamentals

Why Vault?

Kubernetes native Secrets have limitations:

  • Default almacenamiento in etcd is not encrypted
  • No secrets rotation
  • No audit registro 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 registro
  • 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 Instalaation on Kubernetes

Requisitos previos

  • Running Kubernetes clúster
  • Helm installed
  • kubectl configured

Instalaing 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

Instala 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. Extrae 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

Configura 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

Crea un 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

Crea un 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

Instalaing 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

Instala:

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

Using CSI Driver

Crea un 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

Configura 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

Configura 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

Ejemplo: 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

Ejemplo: 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

Conclusión

Vault provides enterprise-grade secrets management for Kubernetes implementacións 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 seguridad and compliance with your organization's requirements.