Secrets management & Vault
Keep passwords, keys and tokens out of code — and make them short-lived, audited and easy to rotate.
In 30 seconds
Secrets are the passwords, API keys, certificates and tokens your software needs to talk to other systems. Secrets management means storing them in one protected place, giving each app only what it needs, logging every access and changing them regularly. Tools like HashiCorp Vault, OpenBao and cloud secret managers can even create short-lived credentials on demand, so there is little left to steal.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- Leaked credentials are one of the most common starting points of breaches, and secrets committed to Git or pasted into chats stay discoverable for years.
- Centralised secrets give one place to answer 'who can read the production database password, and who did?' — something scattered .env files cannot answer.
- Rotation becomes routine instead of a frightening manual project, which matters when an employee leaves or a vendor is breached.
- A team needs a secret manager once it has several environments, services or people with production access; a solo app on one server can start with well-protected environment files.
How it works
01What counts as a secret, and why .env files are not enough
A secret is anything that grants access: database passwords, API keys for payment or email providers, OAuth client secrets, JWT signing keys, TLS private keys, SSH keys, encryption keys and webhook signing secrets. The Twelve-Factor approach of putting configuration in environment variables was a big step forward from hard-coding, and .env files on a server are still a reasonable start. But as a system grows, problems appear: copies of the same .env on laptops, CI and servers; no record of who read what; no easy rotation; secrets visible in process listings, crash dumps or error pages; and the ever-present risk of committing the file. Secrets management solves this by treating secrets as data with an owner, an access policy, an audit log and a lifecycle.
- Never in Git, Docker images, client bundles or logs
- Each secret has an owner, a scope and a rotation plan
- Access is granted by identity and recorded
02How a secret manager works
Think of a bank vault with a guard and a ledger. The secret manager stores secrets encrypted at rest, usually under a key protected by a cloud KMS or hardware security module. Applications authenticate with an identity — a cloud IAM role, a Kubernetes service account, a CI job's OIDC token, or an AppRole — instead of a shared password. Policies map identities to paths they may read: the orders service can read secret/prod/orders/* and nothing else. Every read and write is written to an audit log. The main options: HashiCorp Vault (source-available under the BSL licence since 2023, now part of IBM), OpenBao (an open-source fork of Vault under the Linux Foundation), cloud services such as AWS Secrets Manager, Google Secret Manager and Azure Key Vault, and developer-focused platforms like Doppler, Infisical or 1Password Secrets Automation.
03Vault basics: KV secrets and policies
Vault organises everything by paths. The key/value (KV) secrets engine stores static secrets with versioning, so you can roll back. Policies, written in HCL, grant capabilities such as read, create, update or list on paths. Applications log in through an auth method that matches where they run — kubernetes, aws, jwt/oidc for CI, or approle — and receive a short-lived Vault token limited by policy. Development mode (vault server -dev) is for learning only: it runs in memory, unsealed, with a root token. Production needs storage (integrated Raft storage is the common choice), TLS, auto-unseal with a cloud KMS or HSM, audit devices enabled, and the root token revoked after setup. The same commands work with OpenBao's bao CLI.
# Store and read a versioned secret (KV v2)
vault kv put secret/prod/orders db_password='use-a-generated-value'
vault kv get -field=db_password secret/prod/orders
# orders-policy.hcl — read-only access to one app's secrets
path "secret/data/prod/orders/*" {
capabilities = ["read"]
}
vault policy write orders orders-policy.hcl
vault audit enable file file_path=/var/log/vault_audit.log
04Dynamic secrets: credentials that expire on their own
The strongest idea in Vault is dynamic secrets. Instead of one long-lived database password shared by every instance, Vault creates a unique database user for each application instance on request, with a lease — say one hour. When the lease expires or is revoked, Vault deletes that user. A leaked credential is useless soon after, every connection is attributable to one instance, and revoking access for a compromised service is a single command. The same pattern exists for cloud credentials (temporary AWS keys), PKI (short-lived TLS certificates) and SSH (signed certificates instead of authorised keys). Cloud providers offer similar ideas natively: IAM roles for workloads and IAM database authentication avoid stored passwords entirely. Applications must handle renewal, which client libraries and agents such as Vault Agent do for you.
# One-time setup by an admin
vault secrets enable database
vault write database/config/app-db plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/app" \
allowed_roles="orders-ro" username="vault_admin" password="initial-admin-pass"
vault write -force database/rotate-root/app-db # Vault now owns the admin password
vault write database/roles/orders-ro db_name=app-db default_ttl=1h max_ttl=24h \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
# Each app instance asks for its own short-lived user
vault read database/creds/orders-ro
05Getting secrets into applications
There are three common delivery patterns. Direct API: the app uses an SDK to read secrets at startup and renews leases — flexible but couples code to the vault. Sidecar or agent: Vault Agent (or a cloud equivalent) authenticates, fetches secrets and renders them into a file or environment for the app, renewing them automatically; the app just reads a file. Platform sync: in Kubernetes, the External Secrets Operator or the Secrets Store CSI driver syncs from the secret manager into pods. In CI/CD, avoid storing long-lived cloud keys at all: GitHub Actions, GitLab and others can issue an OIDC token per job, which the cloud or Vault exchanges for short-lived credentials. For Docker Compose on a VPS, a simple, sound pattern is root-owned env files with mode 600 outside the repository, or Docker secrets mounted as files.
# GitHub Actions: no stored cloud keys — exchange the job's OIDC token
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy-prod
aws-region: eu-central-1
- run: aws secretsmanager get-secret-value --secret-id prod/orders --query SecretString --output text > /dev/null
06Rotation, least privilege and leak response
Rotation means replacing a secret with a new one on a schedule and after any suspected exposure. To rotate without downtime, support two valid versions at once: create the new credential, deploy it, confirm nothing uses the old one, then revoke the old. Cloud secret managers can automate rotation for databases; for third-party API keys, rotation is usually a scripted runbook. Least privilege means each service, person and pipeline gets only the secrets it needs, in the environment it needs, ideally read-only. Prevention: run secret scanners such as gitleaks or TruffleHog in pre-commit hooks and CI, and enable your Git host's push protection. When a secret leaks, the order matters: revoke or rotate it first, then remove it from history, because rewriting Git history does not un-leak a key that has already been cloned or scraped.
- Rotate: create new → deploy → verify → revoke old
- Scan: pre-commit + CI + push protection
- Leak: revoke first, clean history second, review access logs third
Use it when
- Several services or environments sharing credentials, where copies of .env files are spreading.
- Compliance or customer requirements for audit logs, access reviews and rotation (SOC 2, ISO 27001, PCI DSS).
- Systems that benefit from short-lived credentials: databases, cloud access, internal TLS certificates.
- Teams where people join and leave, so access must be granted and removed per person quickly.
Skip it when
- A small single-server app run by one person: protected environment files, a secret scanner and good backups may be enough for now.
- Running self-hosted Vault without the capacity to operate it — an unsealed, unpatched or unbacked-up Vault is a single point of failure; a managed cloud secret manager is safer.
- Non-secret configuration such as feature flags or URLs, which belongs in normal config.
Alternatives
- Cloud secret managers
AWS Secrets Manager, Google Secret Manager, Azure Key Vault: managed, IAM-integrated, with rotation hooks; tied to one cloud.
- OpenBao
Open-source fork of Vault with a compatible API, for teams that want Vault features under an open-source licence.
- SOPS + age/KMS
Encrypts secret values inside files committed to Git; simple GitOps workflow, but no dynamic secrets or per-read audit.
- Developer secret platforms
Doppler, Infisical, 1Password: easy syncing to apps and CI with good UX, fewer advanced dynamic features.
Best practices
- Authenticate workloads by platform identity (IAM roles, Kubernetes service accounts, CI OIDC), not by stored master passwords.
- Prefer short-lived or dynamic credentials over long-lived static secrets wherever the target system supports it.
- Write narrow policies per service and environment; production secrets are never readable from development.
- Enable audit logging and alert on unusual access, such as a human reading production database credentials.
- Automate rotation and rehearse it, including two-version overlap for zero-downtime changes.
- Run secret scanning in pre-commit, CI and Git push protection.
- Keep secrets out of logs, error pages, Docker image layers and frontend bundles.
Common mistakes
- Committing .env files or keys, then 'fixing' it by deleting the file without revoking the secret.
- Baking secrets into Docker images with ENV or COPY, where anyone with the image can read them.
- One shared admin credential used by every service, so no access can be revoked independently.
- Running Vault in dev mode or with the root token in daily use.
- Never rotating anything because nobody knows which systems use a key.
Deep dive
07Vault architecture: seal, storage and high availability
Vault's security model rests on the barrier: everything that leaves Vault for storage is encrypted with a keyring, protected by a root key. Sealing means Vault holds no keys in memory. With Shamir unseal, the root key is split into shares (for example five, with three needed), held by different people; with auto-unseal, a cloud KMS or HSM decrypts it automatically at startup, which is the norm in production because restarts should not need humans at night. Integrated storage (Raft) replicates data across typically three or five nodes; one is the leader, others are standbys that forward requests, and quorum is required for writes. Back up with Raft snapshots and test restores. Enterprise and HCP editions add performance and disaster-recovery replication across regions. Operationally, Vault is tier-zero infrastructure: if it is down, apps cannot start, so plan for caching, sensible lease lengths and monitoring of seal status, leader changes and lease counts.
# Production-style server config (excerpt)
storage "raft" {
path = "/opt/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/vault/tls/vault.crt"
tls_key_file = "/etc/vault/tls/vault.key"
}
seal "awskms" {
region = "eu-central-1"
kms_key_id = "alias/vault-unseal"
}
api_addr = "https://vault-1.internal:8200"
cluster_addr = "https://vault-1.internal:8201"
08Secret zero: how the first credential is trusted
Every secrets system faces the 'secret zero' problem: to fetch secrets, an app must first authenticate, so what secret does it use for that? The modern answer is to rely on an identity the platform already proves. On AWS, the instance or task role signs a request Vault or Secrets Manager can verify. In Kubernetes, the pod's projected service-account token is validated against the cluster. In CI, the job's OIDC token is signed by the CI provider. On bare VPS servers there is no such platform identity, so options include AppRole with a role id baked into configuration and a secret id delivered once by the deployment tool with a short TTL and single use, or a TLS client certificate issued at provisioning. Whatever you choose, bind the identity to narrow policies and short tokens, and make the bootstrap credential worthless after first use.
09Encryption as a service and envelope encryption
Beyond storing secrets, Vault's transit engine and cloud KMS services encrypt data for applications without ever revealing the key. The app sends plaintext, receives ciphertext, and stores it in its own database; decryption requires a policy-checked call. Keys can rotate, and old ciphertexts stay decryptable and can be re-wrapped to the new version. For large data, use envelope encryption: generate a random data key, encrypt the data locally with it (for example AES-256-GCM), then encrypt the data key with the KMS key and store it next to the data. You get fast local encryption, a central audit trail, and the ability to cut off access by disabling one master key. This pattern underpins field-level encryption of personal data, encrypted backups and secrets in GitOps tools such as SOPS.
# Transit: the app never sees the key
vault secrets enable transit
vault write -f transit/keys/customer-pii
vault write transit/encrypt/customer-pii plaintext=$(printf '%s' 'AM12345678' | base64)
# -> ciphertext: vault:v1:8SDd3WHDOjf7mq69CyCqYjBXAiQQAVZRkFM13ok481zoCmHnSeDX9vyf7w==
vault write transit/decrypt/customer-pii ciphertext='vault:v1:8SDd3WHDOjf7...'
vault write -f transit/keys/customer-pii/rotate # new version, old data still readable
10Kubernetes secrets: what base64 is not
A Kubernetes Secret is base64-encoded, not encrypted; anyone who can read Secrets in a namespace, or read etcd backups, sees the values. Harden it in layers. Enable encryption at rest for etcd, ideally with a KMS provider. Restrict RBAC so only the workloads and few operators who need Secrets can get or list them — 'list' on Secrets effectively reveals them all. Avoid committing Secret manifests to Git; use External Secrets Operator to sync from a central manager, or Sealed Secrets or SOPS for GitOps where values are encrypted in the repository. Prefer mounting secrets as files over environment variables, so values can rotate without restarting and are less likely to appear in crash dumps. Finally, use workload identity so pods can call cloud APIs and secret managers without any static keys in Secrets at all.
Real-world scenarios
Moving a growing team off shared .env files
Problem
Five developers share production .env files over chat. Three services run on two VPS servers with Docker Compose, and nobody knows which API keys are still in use.
How to solve it
Start with an inventory: list every secret, its owner, where it is used and when it was last rotated. Choose a managed secret manager or a developer secrets platform rather than self-hosting Vault for this size. Create separate projects per environment, give developers access to development only, and limit production to deploy pipelines and one or two maintainers. Deliver secrets to servers at deploy time as root-owned files with mode 600. Rotate every production secret during the migration, since the old ones lived in chat. Add gitleaks to pre-commit and CI.
A database password must change with zero downtime
Problem
Security requires rotating the production database password quarterly. Last time it caused a 20-minute outage because services restarted at different times.
How to solve it
Switch to two-user rotation. Create two database users with identical grants. Services read the current username and password from the secret manager. To rotate, set a new password on the inactive user, update the secret to point to it, and roll services gradually; connections pick up the new credentials on reconnect. After verifying that no sessions use the old user (check pg_stat_activity), lock or re-password it. Automate the steps with the cloud provider's rotation function, or move to Vault dynamic credentials so each instance gets its own short-lived user.
An API key appeared in a public repository
Problem
A secret scanner alerts that a payment provider's live API key was pushed to a public repository ten minutes ago in a test script.
How to solve it
Act in this order. Revoke or roll the key in the provider dashboard immediately and deploy the replacement from the secret manager. Review the provider's logs for any calls made with the old key since the push. Remove the file and purge it from history with git filter-repo, then force-push; contact the Git host to clear caches if needed. Write a short incident note covering timeline, impact and fix. Prevent repeats: enable push protection, add pre-commit scanning, and give developers restricted test-mode keys so live keys never sit on laptops.
Cheat sheet
- Start a throwaway dev server (never prod)
vault server -dev- Write a KV secret
vault kv put secret/prod/app api_key='generated-value'- Read one field
vault kv get -field=api_key secret/prod/app- Get dynamic DB credentials
vault read database/creds/orders-ro- Revoke all leases under a path
vault lease revoke -prefix database/creds/orders-ro- Read from AWS Secrets Manager
aws secretsmanager get-secret-value --secret-id prod/app --query SecretString --output text- Scan a repo for leaked secrets
gitleaks detect --source . --verbose- Encrypt a file with SOPS + age
sops --encrypt --age age1yourpublickey secrets.yaml > secrets.enc.yaml- Lock down an env file on a server
chown root:root /etc/app/.env && chmod 600 /etc/app/.env
Questions & answers
Q1Why not just use environment variables?
Environment variables are a good delivery mechanism and far better than hard-coding, but they are not a management system. They say nothing about where the value is stored, who can read it, who changed it, or how it gets rotated. Copies spread across laptops, CI settings and servers. They can leak through process inspection, crash reports or debug pages that dump the environment. A secret manager becomes the source of truth with access policies, audit logs and rotation, and can still deliver values to the app as environment variables or files.
Q2What are dynamic secrets?
Dynamic secrets are credentials generated on demand for a specific client, with a built-in expiry. For example, when a service starts, Vault creates a unique database user for it, valid for one hour, and deletes it when the lease expires or is revoked. Benefits: no long-lived password to leak, every credential is traceable to one consumer, and revoking a compromised service is instant without affecting others. The cost is that the application, or an agent beside it, must renew credentials and handle reconnection, and the target system must support programmatic user management.
Q3What do you do if a secret is committed to Git?
Assume it is compromised, even in a private repository, because clones, forks and CI logs may already contain it. First revoke or rotate the secret at the provider and deploy the new value. Then check the provider's logs for misuse during the exposure window. After that, remove it from the repository history with git filter-repo or BFG and force-push, and ask collaborators to re-clone, but treat this as clean-up, not the fix. Finally, add secret scanning to pre-commit and CI so it does not happen again.
Q4How does Vault protect the secrets it stores?
Vault encrypts all data before writing it to storage, using an encryption key that is itself protected by a root key. When Vault starts, it is sealed: it cannot decrypt anything until it is unsealed, either with key shares held by several operators (Shamir secret sharing) or automatically with a cloud KMS or HSM (auto-unseal). Clients authenticate and receive tokens limited by policies, and every request can be recorded by audit devices, with sensitive values hashed in the log. So reading the storage backend alone reveals nothing usable.
Q5How do CI/CD pipelines get secrets safely?
The best current pattern is workload identity federation: the CI system issues a short-lived OIDC token for each job, describing the repository, branch and environment. The cloud provider or Vault trusts that issuer and exchanges the token for temporary credentials, restricted by conditions such as 'only the main branch of this repository'. No long-lived cloud keys are stored in CI at all. When static secrets are unavoidable, store them in the CI system's protected environment secrets, restrict them to protected branches, require approval for production deployments, and mask them in logs.
Q6How often should secrets be rotated?
Rotate immediately after any suspected exposure or when someone with access leaves; otherwise follow a risk-based schedule — common policies are 30 to 90 days for high-value static secrets, and some compliance frameworks require defined periods. The better answer is to make rotation so easy that frequency stops mattering: automate it, support two valid versions during the switch, and prefer dynamic or short-lived credentials that expire in hours. Keys used for signing, such as JWT keys, rotate with overlap: publish the new key, start signing with it, and retire the old one after tokens expire.
Senior-level questions
S1Self-hosted Vault or a cloud secret manager — how do you decide?
A cloud secret manager is the default when the workload lives mostly in one cloud: no servers to run, IAM integration, KMS-backed encryption, rotation hooks and audit logs via the cloud's trail — at a per-secret and per-call price and with lock-in. Vault (or OpenBao) earns its operational cost when you span several clouds or on-premise, need dynamic secrets for many systems, PKI and SSH certificate issuance, transit encryption, or one consistent policy model across platforms. Running it means owning high availability, unseal, upgrades, backups and being tier-zero during incidents; HCP Vault removes much of that for a fee. For small teams on VPS servers, a managed developer secrets platform or a cloud manager plus disciplined file permissions is usually the pragmatic choice.
S2How would you design secrets handling for a multi-tenant SaaS that stores customers' third-party credentials?
Customer credentials — their payment gateway keys or ERP passwords — are a different class from your own infrastructure secrets. I would store them encrypted per tenant using envelope encryption: a per-tenant data key wrapped by a KMS or Vault transit key, with the ciphertext in the application database and decryption only in the service that calls the third party. Access is checked against the tenant on every decrypt and logged. Plaintext never goes to logs, admin UIs or support tools; screens show only the last characters. Support rotation by the customer and immediate deletion on offboarding by destroying the tenant's data key (crypto-shredding). The trade-offs: KMS calls add latency and cost, so cache decrypted keys briefly in memory, and per-tenant keys increase key-management overhead, but they shrink the blast radius dramatically.
S3How do you handle signing-key rotation for JWTs or webhooks without breaking clients?
Use overlapping keys identified by a key id. For JWTs, publish the new public key in the JWKS alongside the old one first, wait until verifiers' caches have refreshed, then start signing with the new key and its kid. Keep the old public key published until every token signed with it has expired, then remove it. Private keys live in a KMS, HSM or Vault transit so they never leave protected storage. For webhook signing secrets, support two active secrets per endpoint during rotation: sign with the new one, include a version in the signature header, and let receivers accept either for a defined window. The trade-off is complexity in key distribution and caching, which is far cheaper than a big-bang rotation that invalidates every session or drops partner webhooks.
Glossary
- Secret
- Any value that grants access, such as a password, API key, token, certificate key or encryption key.
- Secret manager
- A service that stores secrets encrypted, controls access by identity and logs every use.
- Dynamic secret
- A credential created on demand for one client with an automatic expiry.
- Lease
- Vault's time limit on a secret or token, after which it is revoked unless renewed.
- Seal / unseal
- Vault's locked state where it cannot decrypt data, and the process of loading its keys.
- Rotation
- Replacing a secret with a new value and retiring the old one, on schedule or after exposure.
- Secret zero
- The first credential an app needs to authenticate to the secret manager itself.
- Envelope encryption
- Encrypting data with a data key, then encrypting that key with a master key held in KMS.
- Workload identity
- Proving an app's identity using its platform (cloud role, pod, CI job) instead of stored keys.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.