How to securely store API keys in 2026
But environment variables are only a baseline for low-impact configuration; production needs a complete secrets strategy. Anything shipped to browser code is public, regardless of minification or obfuscation.
So store each value according to the damage a leak would cause. Make that choice executable from local development through revocation. We’ll identify ordinary leak paths and choose a storage tier. Then we’ll fetch secrets at runtime, set a risk-based rotation policy, and cover Git, CI/CD, browsers, and AI coding assistants.
Your key is a credential, not a harmless string
But the usual advice stops at .env and .gitignore. That handles repository hygiene while ignoring runtime access, CI logs, and revocation.
So classify a value by what an unauthorized person could do with it. A leaked credential may open a database or authorize payments. It may expose private data, alter infrastructure, or sign tokens, depending on its permissions. A public analytics identifier has a different risk profile from a cloud credential with administrative access.
And the available summary of the 42Crunch State of API Security 2026 report says missing authentication was the most frequently reported API vulnerability in 2025. It also flags broken object-level and function-level authorization, along with the trusted-client fallacy: frontend behavior cannot enforce authorization.
Secret storage won’t fix a missing authorization check. But it can make the credential harder for an attacker to obtain.
Most leaks are ordinary operational mistakes
Most secret incidents are boring, so a storage policy must survive ordinary debugging, deployment, and collaboration.
Source-control exposure preserves copies over time. Runtime exposure reveals values loaded by a process, container, pod, or pipeline. So defend both paths separately.
| Location | How it leaks | Safer practice |
|---|---|---|
| Git repository | A key is hardcoded or a .env file is committed; clones, forks, caches, and backups can retain it | Ignore local secret files. Scan history. Trigger incident response |
| Running container | docker exec <ctr> env prints the process environment to someone with sufficient access | Restrict runtime access and request only required values |
| Kubernetes pod | kubectl exec <pod> -- env dumps environment variables to an operator with shell access | Use workload identity and controlled injection or runtime retrieval |
| CI/CD job | echo $SECRET_KEY or printenv sends values into logs and artifacts | Mask values and restrict pipeline permissions. Isolate production credentials |
| Chat or shared drive | Developers send .env files to unblock a deployment | Give individuals access through an auditable secret service |
| Debugging | Configuration objects, request data, or connection strings enter logs and exceptions | Redact diagnostics. Exclude secrets from error messages |
Environment variables keep values out of source code and support per-environment settings, but live in process memory readable by privileged users. Treat them as a repository boundary, then add stronger protection where impact warrants it.
.env is the floor; a secrets manager is the ceiling
Think of this ladder as increasing suitability for application use, not a universal maturity ranking:
- Hardcoded source code: the worst option. The value enters version control, where history is built to remember changes.
- Environment variables: a useful baseline. They separate values from application code and support per-environment configuration.
- Secrets managers: the production choice for high-impact credentials. They add encrypted storage, access policies, expiry, rotation workflows, and access review.
- Client-side encrypted notes: personal reference only. A notes application can help an individual remember a credential; a server must never depend on an engineer copying it into production.
As FloopFloop puts it, “Environment variables are the floor; a proper secrets manager is the ceiling.” The line is memorable because the distinction matters.
A manager adds SDKs, agents, identity policies, deployment changes, failure handling, and operational ownership. For a local prototype with no production credentials or shared deployment, that may be needless ceremony; use an ignored local file and revisit before production.
On local machines, keep .env readable only by the application user and never use it as a team credential-distribution mechanism.
Use one question to decide where a value belongs
Ask:
If this value leaked, could someone gain access or cause damage?
A yes makes it a secret — in production, store it in a manager or equivalent controlled injection system. A no makes it ordinary configuration. “Public” means intentionally public and restricted by design, only when the provider intends that exposure.
| Ordinary configuration | Production secret |
|---|---|
APP_ENV and LOG_LEVEL | Database passwords and database URLs containing credentials or granting connection access |
| Feature flags, ports, and filesystem paths | Stripe secret keys and GitHub tokens |
| Hostnames and base URLs | Webhook secrets |
| Public OAuth client IDs intended for browser use | JWT signing keys |
| Analytics identifiers designed for client use | TLS private keys, IAM keys, and service-account credentials |
A backend Stripe credential remains sensitive because its provider calls it an API key. A browser-visible analytics identifier may be public because its permissions and quota were designed for that exposure.
Take a small Node.js app processing Stripe webhooks with orders in PostgreSQL. APP_ENV and LOG_LEVEL describe runtime behavior; STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and DATABASE_URL can authorize actions or expose data. Put them in separate tiers.
Fetch secrets at runtime, not through copy-and-paste
Across providers and languages: authenticate the workload, request named values, validate at startup, keep them out of logs. The implementation changes by platform; the boundary does not.
Node.js
This provider-neutral pseudocode shows the boundary, not a runnable SDK example; replace secretManager.getMany with your provider’s client and workload-identity setup.
// Pseudocode: replace with your provider's SDK.
const config = await secretManager.getMany([
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"DATABASE_URL",
]);
for (const name of [
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"DATABASE_URL",
]) {
if (!config[name]) {
throw new Error(`Required secret is unavailable: ${name}`);
}
}
const stripe = createStripeClient(config.STRIPE_SECRET_KEY);
const database = createDatabase(config.DATABASE_URL);
// Pass only the credential or client each component needs.
// Never log config or include secret values in errors.
This is where the orders service receives all three sensitive values; APP_ENV and LOG_LEVEL remain ordinary deployment configuration. (See the Node.js API security guide.)
Python
For Azure, Microsoft documents DefaultAzureCredential with SecretClient (azure-identity, azure-keyvault-secrets); the omitted vault_url comes from non-secret deployment configuration.
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(vault_url=vault_url, credential=credential)
stripe_secret_key = client.get_secret("StripeSecretKey").value
stripe_webhook_secret = client.get_secret("StripeWebhookSecret").value
database_url = client.get_secret("DatabaseUrl").value
if not all((stripe_secret_key, stripe_webhook_secret, database_url)):
raise RuntimeError("Required secrets are unavailable")
Treat retrieval failure as a deployment or startup failure; log provider exceptions only through a redacted, access-controlled channel.
Go
Here too, manager.Get and OpenDatabase are placeholders for your provider SDK and database package.
// Pseudocode: replace manager.Get and OpenDatabase with real clients.
secrets, err := manager.Get(ctx, []string{
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"DATABASE_URL",
})
if err != nil {
return fmt.Errorf("load required secrets: %w", err)
}
for _, name := range []string{
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"DATABASE_URL",
} {
if secrets[name] == "" {
return fmt.Errorf("required secret %s is unavailable", name)
}
}
db, err := OpenDatabase(secrets["DATABASE_URL"])
if err != nil {
return fmt.Errorf("open database: %w", err)
}
Pass a narrowly scoped client or credential handle rather than the full configuration map. That limits accidental access inside the process.
Pick the manager your infrastructure can enforce
Do not buy a secrets manager because a comparison table calls it “best.” Choose in this order:
- Start with your cloud’s native service when your workloads already use that cloud’s identity and monitoring.
- Match the deployment model: hybrid infrastructure may justify Vault; developer-focused teams may prefer a simpler distribution workflow.
- Check operating capacity: a powerful service that nobody can patch, monitor, or recover is a liability.
The CiphersSecurity comparison describes broad fits:
- HashiCorp Vault: complex hybrid environments needing dynamic secrets and a team prepared to operate Vault.
- AWS Secrets Manager: AWS-standardized teams using native identity and deployment controls.
- Azure Key Vault: Azure workloads using Microsoft Entra identities and Azure monitoring.
- Doppler or Infisical: developer-first workflows that need straightforward distribution across environments.
- Akeyless: organizations evaluating a vaultless, zero-knowledge model.
Use those as fit statements, not rankings. At minimum require encrypted storage, workload identity, least-privilege reads, and auditable access; require expiry or rotation support and CI/CD integration that keeps values out of output. Retrieval logging varies by service, so verify what the chosen path records.
No option costs least for every architecture; measure setup time, incident response, and developer friction in your own environment.
Azure Key Vault demonstrates the control pattern
The Microsoft Learn example creates a vault with RBAC authorization and purge protection:
az keyvault create --name "<vault-name>" \
--resource-group "myResourceGroup" \
--enable-rbac-authorization true \
--enable-purge-protection true
Add a secret with the documented 180-day expiry example:
az keyvault secret set \
--vault-name "<vault-name>" \
--name "MyApiKey" \
--value "<secret-value>" \
--expires "$(date -u -d '+180 days' +'%Y-%m-%dT%H:%M:%SZ')"
That date syntax is GNU/Linux; macOS users adapt it. Substitute your vault name, subscription, identity, region, and policies before running.
A practical setup then assigns the application the Key Vault Secrets User role rather than broad administrative access. Consider separate vaults per environment or application where isolation and administration justify them.
Configure the remaining pieces as a procedure:
- Subscribe to
SecretNearExpiryevents through Event Grid. - Send
AuditEventdiagnostic logs to Log Analytics. - Alert on unauthorized
SecretGetoperations. - Restrict network access with Private Link or firewall rules.
- Use the service’s HTTPS endpoints for all communication.
The provider-specific names change, but the control pattern generalizes: workload identity, narrow permissions, expiry, alerts, network restriction, and access logs.
Rotation is a risk schedule, not a magic number
A calendar reminder that says “rotate key” does little if nobody can revoke the old value, verify the replacement, or see who accessed it.
This is Blackhawk’s recommended starting policy, not a universal standard:
- High-impact or frequently exposed credentials: rotate daily or per deployment where practical, and prefer short-lived or dynamic credentials.
- Human or long-lived production API keys: rotate every 30–90 days, subject to provider limits and operational risk.
- Certificates: monitor expiry and review them monthly; renew according to their certificate lifetime.
- Signing and private keys: define a key-specific rollover plan, including where old and new versions can overlap.
- Lower-risk service credentials: use up to 180 days as a starting point, with shorter periods when impact is higher.
Microsoft uses 180 days in its Azure example and documents near-expiry notifications. Infisical describes dynamic secrets as credentials generated on demand with automatic expiration, which narrows the window between issuance and revocation.
If the provider can’t support overlap or rollback, don’t force daily rotation: reduce permissions, shorten token lifetimes, and document the provider limit. Whether daily rotation is safe depends on rollback time, overlap, and usage — deployment facts, not universal policy.
Use dual-key rollover when the provider supports it:
- Issue the replacement.
- Store and deploy the new version.
- Verify traffic and application health.
- Revoke or disable the old credential.
- Check logs for continuing use of the old value.
For the Stripe example, keep the old key accepted while the new deployment is verified, then revoke it. For PostgreSQL, use a replacement role or a maintenance window with a write check before disabling the old credential.
A browser cannot hide a key from its user
If a secret reaches browser JavaScript, source maps, network requests, or a mobile binary, assume the user can obtain it. A build step doesn’t change that.
If a vendor calls a browser-delivered value an API key, treat permissions, not labels, as the boundary.
Use this flow:
- The browser authenticates to your application.
- Your backend authorizes the requested operation.
- Your backend calls the third-party API with its server-side secret.
- Your backend returns only permitted data.
When a provider requires a browser-visible key, use a deliberately public key restricted by domain, origin, operation, quota, and other available mechanisms. A privileged server credential belongs on the server.
AI assistants make secret handling harder to enforce
Coding assistants add places where developers paste or generate secrets: prompts, context windows, config edits, generated patches, fixtures, and chat transcripts.
Keep secrets out of prompts and test fixtures. Use placeholders and injected test credentials. Review generated configuration and patches for hardcoded credentials before they enter the repository.
Add repository and pipeline scanning with TruffleHog, Gitleaks, or GitHub Secret Scanning. Scanning catches mistakes; workload permissions limit what those mistakes can expose.
Move existing secrets in four passes
Emergency branch: if a credential is exposed now, revoke it immediately when the provider supports it; otherwise rotate and disable the exposed version. Repository cleanup comes afterward.
Normal migration: audit first, then move the highest-impact values.
1. Audit every copy
Search source code, Git history, .env files, Dockerfiles, and CI/CD configuration. Check build artifacts, logs, and shared documents too. Run TruffleHog, Gitleaks, or GitHub Secret Scanning, then review findings manually.
2. Move critical secrets first
Create replacement credentials where possible. Put them in the selected manager, grant the application a narrow identity, and change the application to retrieve the values at runtime.
3. Clean plaintext
Remove values from current files, logs, fixtures, and pipeline output. Deleting the line is housekeeping. Revoking the credential is incident response.
History rewriting reduces rediscovery but doesn’t replace revocation; assume anyone who cloned the repo may still possess the old value.
4. Communicate and refine
Document owner, purpose, and permitted readers; record expiry, rotation, emergency revocation, and local-development workflow. If developers still exchange .env files in chat, fix the workflow rather than documenting the habit.
The secure-storage checklist belongs in the pull request
Use this as the review instrument:
- Can the reviewer identify the storage location?
- Does the workload use managed identity, OIDC, or an equivalent mechanism?
- Are permitted secret names and readers explicit?
- Is the revocation owner named?
- Is there an expiry or rotation signal?
- Which access log proves retrieval?
- Are browser bundles, prompts, fixtures, logs, Dockerfiles, and Git history clean?
- Has an exposed credential been revoked?
If any answer is “we’ll decide later,” keep the credential out of production. That is the policy: use .env for low-impact configuration, use a controlled manager for damaging secrets, and make revocation part of the design rather than an emergency improvisation.