groundforce Deploy us →
← All insights

Secrets Management with Vault: Stop Committing Passwords to Git

Dynamic secrets, short-lived credentials, and a zero-trust posture for your platform.

Hardcoded secrets are the vulnerability that never dies. They show up in Git history, in environment variables dumped to logs, in Slack messages, and in that one config file everyone forgot about. HashiCorp Vault flips the model: secrets are centralized, access-controlled, audited, and, crucially, short-lived.

The core idea: stop storing static secrets

The most powerful Vault feature is not the encrypted key-value store. It is dynamic secrets. Instead of a database password that lives forever, an application asks Vault for credentials on startup. Vault creates a brand-new database user with a TTL of, say, one hour, hands it back, and revokes it automatically when the lease expires.

The blast radius of a leaked credential drops from "forever" to "one hour", and every issuance is logged.

Authentication: who gets to ask?

Vault does not care about your password. It cares about verifiable identity. In Kubernetes, the kubernetes auth method validates a pod service-account token against the cluster API, then maps it to a Vault policy.

# Enable the Kubernetes auth method
vault auth enable kubernetes

vault write auth/kubernetes/config \
 kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

# Bind a service account to a policy
vault write auth/kubernetes/role/orders-api \
 bound_service_account_names=orders-api \
 bound_service_account_namespaces=production \
 policies=orders-db \
 ttl=1h

A least-privilege policy

Policies are deny-by-default. Grant only the exact paths a workload needs:

# orders-db policy
path "database/creds/orders-readwrite" {
 capabilities = ["read"]
}

path "secret/data/orders/*" {
 capabilities = ["read"]
}

Dynamic database credentials in action

# Configure the database secrets engine once
vault secrets enable database

vault write database/config/orders \
 plugin_name=postgresql-database-plugin \
 connection_url="postgresql://{{username}}:{{password}}@db:5432/orders" \
 allowed_roles="orders-readwrite"

vault write database/roles/orders-readwrite \
 db_name=orders \
 creation_statements="CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO "{{name}}";" \
 default_ttl=1h max_ttl=24h

Now vault read database/creds/orders-readwrite returns a fresh, expiring username and password. The application never sees a permanent credential.

Getting secrets into pods without app changes

The Vault Agent Injector uses a mutating webhook to add a sidecar that authenticates and writes secrets to a shared in-memory volume. Annotations do the wiring:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: orders-api
spec:
 template:
 metadata:
 annotations:
 vault.hashicorp.com/agent-inject: "true"
 vault.hashicorp.com/role: "orders-api"
 vault.hashicorp.com/agent-inject-secret-db: "database/creds/orders-readwrite"
 spec:
 serviceAccountName: orders-api

Operational guardrails

  • Seal/unseal: Vault starts sealed. Use auto-unseal with a cloud KMS so a restart does not page someone at 3am.

  • Audit devices: Enable audit logging before production. Every request is logged, which is your forensic trail.

  • Rotate the root token: Generate it, do the initial setup, then revoke it. Day-to-day work uses scoped tokens.

  • Lease monitoring: Watch for workloads that never renew, a sign of a broken integration hoarding credentials.


Vault is not a place to dump passwords. It is a system for making static, long-lived secrets disappear. Start with one high-value database, prove the dynamic-secrets loop, and expand from there.

// READY TO DEPLOY

Have a problem that needs to ship?

Tell us the terrain. We’ll tell you the fastest path to production — and put a unit on the ground.