Skip to main content

Secrets Management

Secrets Management

Centralized secrets management with HashiCorp Vault and Kubernetes Secrets.

Overview

All secrets are managed securely:

  • Secrets Engine: HashiCorp Vault (primary)
  • Kubernetes: Encrypted at rest with KMS
  • Access Control: RBAC with audit logging
  • Rotation: Automatic rotation every 90 days
  • Compliance: NIST 800-53, FedRAMP, SOC 2

Architecture

graph TB A[Application] --> B[Vault Agent] B --> C[HashiCorp Vault] C --> D[AWS KMS/HSM] A --> E[Kubernetes Secrets] E --> F[etcd Encryption]

HashiCorp Vault

Vault Setup

Installation:

# Install Vault curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" sudo apt-get update && sudo apt-get install vault # Start Vault server vault server -config=/etc/vault/config.hcl

config.hcl:

storage "raft" { path = "/opt/vault/data" node_id = "vault-1" } listener "tcp" { address = "0.0.0.0:8200" tls_cert_file = "/etc/certs/vault.crt" tls_key_file = "/etc/certs/vault.key" } seal "awskms" { region = "us-west-2" kms_key_id = "arn:aws:kms:us-west-2:123456789012:key/abc123" } api_addr = "https://vault.local.bluefly.io:8200" cluster_addr = "https://vault.local.bluefly.io:8201" ui = true

Secret Engines

KV Secrets Engine (v2)

# Enable KV secrets engine vault secrets enable -path=secret kv-v2 # Write secret vault kv put secret/database/postgres \ username=postgres_user \ password=secure-password-123 # Read secret vault kv get secret/database/postgres # List secrets vault kv list secret/database

Database Dynamic Secrets

# Enable database secrets engine vault secrets enable database # Configure PostgreSQL connection vault write database/config/postgresql \ plugin_name=postgresql-database-plugin \ allowed_roles="readonly,readwrite" \ connection_url="postgresql://{{username}}:{{password}}@postgres.local:5432/llm_platform" \ username="vault_admin" \ password="vault-admin-password" # Create role for dynamic credentials vault write database/roles/readwrite \ db_name=postgresql \ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \ default_ttl="1h" \ max_ttl="24h" # Generate dynamic credentials vault read database/creds/readwrite

Output:

Key                Value
---                -----
lease_id           database/creds/readwrite/abc123
lease_duration     1h
lease_renewable    true
password           A1a-random-password-xyz
username           v-token-readwrite-abc123

Transit Secrets Engine

# Enable transit engine vault secrets enable transit # Create encryption key vault write -f transit/keys/bluefly-platform # Encrypt data vault write transit/encrypt/bluefly-platform \ plaintext=$(echo "sensitive-data" | base64) # Decrypt data vault write transit/decrypt/bluefly-platform \ ciphertext="vault:v1:encrypted-data-here" # Rotate key vault write -f transit/keys/bluefly-platform/rotate

Access Policies

policies/developer.hcl:

# Read application secrets path "secret/data/app/*" { capabilities = ["read", "list"] } # Read database credentials path "database/creds/readwrite" { capabilities = ["read"] } # Encrypt/decrypt with transit path "transit/encrypt/bluefly-platform" { capabilities = ["update"] } path "transit/decrypt/bluefly-platform" { capabilities = ["update"] }

Apply Policy:

# Create policy vault policy write developer policies/developer.hcl # Assign policy to user vault write auth/userpass/users/developer \ password=dev-password \ policies=developer

Authentication Methods

AppRole (for services)

# Enable AppRole vault auth enable approle # Create role vault write auth/approle/role/agent-brain \ secret_id_ttl=24h \ token_ttl=1h \ token_max_ttl=4h \ policies=agent-brain # Get role ID vault read auth/approle/role/agent-brain/role-id # Generate secret ID vault write -f auth/approle/role/agent-brain/secret-id

Application Login:

import Vault from 'node-vault'; const vault = Vault({ apiVersion: 'v1', endpoint: 'https://vault.local.bluefly.io:8200' }); // Login with AppRole const result = await vault.approleLogin({ role_id: process.env.VAULT_ROLE_ID, secret_id: process.env.VAULT_SECRET_ID }); // Set token vault.token = result.auth.client_token; // Read secret const secret = await vault.read('secret/data/app/config'); console.log(secret.data.data);

Kubernetes Auth

# Enable Kubernetes auth vault auth enable kubernetes # Configure vault write auth/kubernetes/config \ kubernetes_host=https://kubernetes.default.svc:443 \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token # Create role vault write auth/kubernetes/role/agent-brain \ bound_service_account_names=agent-brain \ bound_service_account_namespaces=llm-platform \ policies=agent-brain \ ttl=1h

Pod Annotation:

apiVersion: v1 kind: Pod metadata: name: agent-brain annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "agent-brain" vault.hashicorp.com/agent-inject-secret-config: "secret/data/app/agent-brain" spec: serviceAccountName: agent-brain containers: - name: agent-brain image: agent-brain:latest env: - name: DATABASE_URL value: "file:///vault/secrets/config"

Kubernetes Secrets

Encrypted at Rest

kube-apiserver:

apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: <base64-encoded-32-byte-key> - identity: {}

Apply Configuration:

# Create encryption config kubectl create secret generic -n kube-system \ encryption-config --from-file=encryption-config.yaml # Update kube-apiserver --encryption-provider-config=/etc/kubernetes/encryption-config.yaml

Sealed Secrets

Install Sealed Secrets Controller:

kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.18.0/controller.yaml

Create Sealed Secret:

# Create regular secret kubectl create secret generic my-secret \ --from-literal=password=super-secret \ --dry-run=client -o yaml > secret.yaml # Seal it kubeseal < secret.yaml > sealed-secret.yaml # Apply sealed secret (safe to commit) kubectl apply -f sealed-secret.yaml

External Secrets Operator

Install ESO:

helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets \ external-secrets/external-secrets \ -n external-secrets-system \ --create-namespace

SecretStore:

apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: vault-backend namespace: llm-platform spec: provider: vault: server: "https://vault.local.bluefly.io:8200" path: "secret" version: "v2" auth: kubernetes: mountPath: "kubernetes" role: "external-secrets" serviceAccountRef: name: "external-secrets"

ExternalSecret:

apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: database-credentials namespace: llm-platform spec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: SecretStore target: name: postgres-credentials creationPolicy: Owner data: - secretKey: username remoteRef: key: secret/database/postgres property: username - secretKey: password remoteRef: key: secret/database/postgres property: password

Secret Types

API Keys

Storage in Vault:

vault kv put secret/api-keys/openai \ api_key=sk-proj-abc123... \ organization=org-xyz789

Usage:

const secret = await vault.read('secret/data/api-keys/openai'); const apiKey = secret.data.data.api_key; const openai = new OpenAI({ apiKey });

Database Credentials

Dynamic Credentials (recommended):

// Get dynamic credentials const creds = await vault.read('database/creds/readwrite'); const pool = new Pool({ host: 'postgres.local.bluefly.io', database: 'llm_platform', user: creds.data.username, password: creds.data.password }); // Credentials auto-expire after 1 hour // Vault automatically revokes them

OAuth Client Secrets

vault kv put secret/oauth/gitlab \ client_id=abc123 \ client_secret=secret-xyz789 \ redirect_uri=https://llm.bluefly.io/auth/callback

Encryption Keys

# Store in transit engine (never leaves Vault) vault write -f transit/keys/data-encryption # Encrypt vault write transit/encrypt/data-encryption \ plaintext=$(echo "data" | base64) # Decrypt (key material never exposed) vault write transit/decrypt/data-encryption \ ciphertext="vault:v1:..."

Secret Rotation

Automatic Rotation

Rotation Schedule:

  • API keys: 90 days
  • Database passwords: 30 days
  • OAuth secrets: 90 days
  • Encryption keys: 90 days

Rotation Script:

class SecretRotationService { async rotateSecret(path: string): Promise<void> { // 1. Generate new secret const newSecret = crypto.randomBytes(32).toString('hex'); // 2. Write to Vault with new version await this.vault.write(`secret/data/${path}`, { data: { value: newSecret } }); // 3. Update applications (gradual rollout) await this.updateApplications(path, newSecret); // 4. Audit log await this.auditLog.log({ event: 'secret_rotated', path, timestamp: new Date() }); // 5. Delete old version after grace period setTimeout(async () => { await this.vault.delete(`secret/metadata/${path}`); }, 24 * 60 * 60 * 1000); // 24 hours } }

Database Password Rotation

// Vault automatically handles rotation vault write database/rotate-root/postgresql // Application automatically gets new credentials const creds = await vault.read('database/creds/readwrite');

Secret Injection

Environment Variables

Vault Agent:

vault { address = "https://vault.local.bluefly.io:8200" } auto_auth { method { type = "approle" config = { role_id_file_path = "/vault/role-id" secret_id_file_path = "/vault/secret-id" } } } template { source = "/vault/templates/config.env.tmpl" destination = "/app/.env" }

config.env.tmpl:

{{ with secret "secret/data/app/config" }}
DATABASE_URL=postgresql://{{ .Data.data.db_user }}:{{ .Data.data.db_password }}@postgres.local:5432/llm_platform
OPENAI_API_KEY={{ .Data.data.openai_key }}
{{ end }}

File Injection

Sidecar Container:

apiVersion: v1 kind: Pod metadata: name: app spec: initContainers: - name: vault-agent image: vault:1.15 command: ["vault", "agent", "-config=/vault/config.hcl"] volumeMounts: - name: vault-config mountPath: /vault - name: secrets mountPath: /secrets containers: - name: app image: myapp:latest volumeMounts: - name: secrets mountPath: /etc/secrets readOnly: true

Audit & Compliance

Audit Logging

Enable Audit:

vault audit enable file file_path=/var/log/vault/audit.log

Audit Events:

{ "time": "2025-01-15T10:00:00Z", "type": "response", "auth": { "client_token": "hmac-sha256:abc123", "accessor": "hmac-sha256:xyz789", "display_name": "approle", "policies": ["default", "agent-brain"] }, "request": { "operation": "read", "path": "secret/data/app/config" }, "response": { "secret": true, "data": { "metadata": { "created_time": "2025-01-15T09:00:00Z", "version": 1 } } } }

Compliance Reports

# Generate secrets audit report vault audit list -detailed # List all secrets vault kv list -format=json secret/ > secrets-inventory.json # Check secret age vault kv metadata get secret/app/config

Best Practices

1. Never Commit Secrets

Good:

const apiKey = process.env.OPENAI_API_KEY;

Bad:

const apiKey = "sk-proj-abc123..."; // NEVER!

2. Use Short-Lived Credentials

// Dynamic credentials with 1-hour TTL const creds = await vault.read('database/creds/readwrite'); // Auto-revoked after 1 hour

3. Rotate Regularly

# Scheduled rotation 0 0 * * 0 /usr/bin/rotate-secrets.sh # Weekly

4. Least Privilege

# Only grant required permissions path "secret/data/app/myapp/*" { capabilities = ["read"] }

5. Monitor Access

// Alert on unusual access patterns if (accessCount > 1000 && timeWindow < 60) { alert('Unusual secret access pattern detected'); }

Next Steps