How to Secure API Keys in Your Code: Complete Guide
Exposed API keys are one of the most common causes of security breaches. GitHub alone scans billions of commits and finds millions of leaked secrets every year. This guide covers every layer of API key protection: where to store them, how to rotate them, what tools to use, and the exact mistakes that cause leaks.
Why API Key Security Matters More Than You Think
An API key is a credential. It grants access to a third-party service — a payment gateway, a cloud provider, a messaging platform, a data API — on your behalf. When a key is exposed, an attacker can:
- Run up massive cloud bills (AWS, GCP, and Azure credential leaks regularly cause five- and six-figure unexpected charges).
- Access or exfiltrate customer data stored in services like Twilio, SendGrid, or Stripe.
- Use your paid API quota to power their own applications.
- Send spam or fraudulent messages through your communication accounts.
- Pivot into other internal systems if the key has broad permissions.
The most dangerous aspect: automated scanners continuously crawl GitHub, GitLab, Bitbucket, npm packages, Docker Hub images, and even Google search results for patterns that look like API keys. Within minutes of a key being pushed to a public repository, it can be found and exploited.
In 2023, a developer accidentally committed an AWS root access key to a public GitHub repo. Within 4 minutes, automated bots had detected it and spun up hundreds of EC2 instances for crypto mining. The final bill: $53,000 before AWS detected the anomaly.
Layer 1: Never Hardcode API Keys in Source Code
This is the foundational rule. A hardcoded API key looks like this:
# Python — NEVER do this
import openai
openai.api_key = "sk-proj-abc123xyz789..."
const stripe = require('stripe')('sk_live_51H...'); // JS — NEVER do this
db := sql.Open("postgres", "postgresql://user:mypassword@prod-host/db") // Go — NEVER
The problem is not just public repos. Even private repositories present risks:
- A repo made public by mistake (happens more often than you think).
- A developer's laptop being compromised — git history is a goldmine for attackers.
- A disgruntled employee with repo access.
- CI/CD systems that clone repos may log output.
- Code review tools, IDE plugins, and AI coding assistants that send code snippets to external servers.
The rule is absolute: credentials never belong in source code, regardless of whether the repository is public or private.
Layer 2: Environment Variables — The Right Way
Environment variables are the standard mechanism for injecting secrets into a running process without embedding them in code. The process reads the value at runtime; the value never touches the codebase.
JavaScript / Node.js
// Install dotenv for local development only
// npm install --save-dev dotenv
// At the top of your entry point (index.js / server.js):
require('dotenv').config(); // loads .env file in development only
// Access keys via process.env:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Always validate at startup — fail fast if a required key is missing:
const required = ['STRIPE_SECRET_KEY', 'OPENAI_API_KEY', 'DATABASE_URL'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
Python
import os
from dotenv import load_dotenv # pip install python-dotenv
load_dotenv() # loads .env for local dev; no-op in production if .env absent
stripe.api_key = os.environ['STRIPE_SECRET_KEY'] # raises KeyError if missing
openai_key = os.getenv('OPENAI_API_KEY') # returns None if missing
# Validate at startup:
import sys
required = ['STRIPE_SECRET_KEY', 'OPENAI_API_KEY', 'DATABASE_URL']
missing = [k for k in required if not os.getenv(k)]
if missing:
print(f"ERROR: Missing env vars: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
Go
package main
import (
"fmt"
"os"
"log"
)
func mustGetenv(key string) string {
val := os.Getenv(key)
if val == "" {
log.Fatalf("Required environment variable %s is not set", key)
}
return val
}
func main() {
stripeKey := mustGetenv("STRIPE_SECRET_KEY")
dbURL := mustGetenv("DATABASE_URL")
// use stripeKey and dbURL...
_ = fmt.Sprintf("%s %s", stripeKey, dbURL)
}
Layer 3: .env Files and .gitignore
A .env file is a plain text file that stores key-value pairs, loaded by tools like dotenv. It lives on disk only on the developer's machine (or in a secrets manager for production). It must never be committed to version control.
# .env — local development only, NEVER commit this file
STRIPE_SECRET_KEY=sk_test_51H...
OPENAI_API_KEY=sk-proj-abc...
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
Your .gitignore must explicitly exclude it. Use our .gitignore Generator to create a comprehensive ignore file for your stack. The critical entries:
# .gitignore — these must always be present
.env
.env.local
.env.*.local
.env.production
.env.staging
*.pem
*.key
*.p12
*.pfx
secrets/
config/secrets.yml
A common mistake is creating a .env.example file (with placeholder values, no real secrets) and committing that as documentation. This is the correct pattern — it shows other developers which variables they need to set, without exposing real values:
# .env.example — safe to commit, placeholder values only
STRIPE_SECRET_KEY=sk_test_your_key_here
OPENAI_API_KEY=sk-proj-your_key_here
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
Read our dedicated guide on .gitignore best practices for a complete reference.
Layer 4: Secret Managers for Production
Environment variables on a developer's laptop are fine for local development. In production — containers, cloud functions, Kubernetes — you need a proper secret management system. Environment variables set directly on production servers are still a risk: they show up in process listings, crash dumps, and logs.
AWS Secrets Manager
AWS Secrets Manager stores secrets as JSON key-value pairs, rotates them automatically, and provides fine-grained IAM access control. The application fetches the secret at startup (or on each request for frequently-rotating credentials):
import boto3
import json
def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# Usage:
secrets = get_secret("myapp/production/api-keys")
stripe_key = secrets["STRIPE_SECRET_KEY"]
Key benefits: automatic rotation for supported services (RDS, Redshift, DocumentDB), CloudTrail audit log of every access, cross-account sharing, and integration with ECS/EKS via IAM roles — no credentials needed in the container itself.
HashiCorp Vault
Vault is the open-source standard for self-hosted secret management. It supports dynamic secrets (generates a short-lived credential on demand and revokes it automatically), transit encryption, PKI certificate generation, and dozens of auth backends (AWS IAM, Kubernetes, LDAP, GitHub).
# Fetch a secret from Vault using the CLI
vault kv get -field=STRIPE_SECRET_KEY secret/myapp/production
# Or in application code (Python hvac library):
import hvac
client = hvac.Client(url='https://vault.internal:8200')
client.auth.aws.iam_login(role='myapp-production')
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/production',
mount_point='secret'
)
stripe_key = secret['data']['data']['STRIPE_SECRET_KEY']
Other Options by Platform
- Google Cloud Secret Manager — Native GCP equivalent to AWS Secrets Manager, integrates with GKE Workload Identity.
- Azure Key Vault — Managed HSM-backed secret storage for Azure workloads.
- Kubernetes Secrets — Base64-encoded (not encrypted by default). Must be combined with encryption at rest (KMS provider) and RBAC. Use ExternalSecrets Operator to sync from AWS SM or Vault.
- GitHub / GitLab / CircleCI Secrets — Built-in CI/CD secret stores. Values are masked in logs and injected as env vars at build time. Read our CI/CD pipeline guide for setup details.
- Doppler / Infisical / 1Password Secrets Automation — Third-party platforms that sync secrets to multiple environments and cloud providers simultaneously.
Layer 5: API Key Rotation
Rotation is the practice of regularly replacing a credential with a new one and invalidating the old one. Even if a key is never compromised, rotation limits the blast radius: an attacker who obtained the key six months ago finds it no longer works.
Rotation Strategy
- Generate a new key with the same permissions as the old one.
- Deploy the new key to all environments that use it (update the secret manager value, trigger pod/container restarts).
- Validate that the new key works correctly in all environments (monitor error rates for 15-30 minutes).
- Revoke the old key only after confirming the new one is working everywhere.
- Log the rotation event with a timestamp (useful for incident response).
Rotation Frequency Guidelines
- Immediately: Whenever there is any possibility the key was exposed (public commit, employee departure, security incident).
- Every 90 days: For keys with write access to critical systems (payment, authentication, data stores).
- Every 180 days: For read-only keys and lower-risk integrations.
- Automatically: AWS Secrets Manager can rotate RDS credentials daily with zero downtime using Lambda rotation functions.
Layer 6: Scope and Principle of Least Privilege
Every API key should have exactly the permissions it needs — no more. A key used only to read data should not have write permissions. A key used by a background job should not have access to billing or admin APIs.
- Stripe: Use restricted keys with only the specific API resource permissions your integration needs. A webhook-processing service only needs to read charges, not create them.
- AWS: Create separate IAM users or roles for each service/application. Attach only the specific policies needed. Never use root credentials in application code.
- Google Cloud: Use service accounts per application with only the required IAM roles. Avoid the Editor and Owner roles entirely.
- GitHub: Use fine-grained personal access tokens (PATs) scoped to specific repos and permissions. Classic PATs with broad org access are a security risk.
Also scope keys by environment: production, staging, and development should all use separate API keys. This prevents a misconfigured development environment from accidentally writing to production data, and lets you revoke a dev key without affecting production.
Layer 7: Scanning for Leaked Secrets
Even with good practices, accidents happen. Pre-commit hooks and CI scanning catch leaks before they reach a remote repository.
Pre-commit Hooks with gitleaks or truffleHog
# Install gitleaks (macOS)
brew install gitleaks
# Scan the current repo for secrets in all commits:
gitleaks detect --source . --verbose
# Add as a pre-commit hook:
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
gitleaks protect --staged --verbose
if [ $? -ne 0 ]; then
echo "ERROR: Potential secret detected. Commit blocked."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
GitHub Secret Scanning
GitHub automatically scans all public repositories (and private repos on GitHub Advanced Security) for 200+ secret patterns from providers like AWS, Stripe, Twilio, and Google. When a match is found, GitHub notifies the repository owner and (for participating providers) automatically revokes the exposed key.
Enable it at: Settings → Security → Secret scanning. For organizations: it can be enforced at the org level via a policy.
What to Do When a Key Is Exposed
- Revoke the key immediately — do not wait to assess impact first. Revocation takes seconds; an attacker can act in minutes.
- Generate a replacement key and deploy it.
- Audit the access logs for the exposed key: when was it used, from what IPs, what endpoints were called, was any data accessed or modified?
- Remove the key from git history using
git filter-repo(notgit filter-branchwhich is deprecated). Note: even after removal, anyone who forked or cloned the repo before the rewrite still has the key. - Report if required — some regulations (GDPR, HIPAA) require breach notification if customer data was accessed.
# Remove a secret from all of git history (DANGEROUS — rewrites history)
# Install: pip install git-filter-repo
git filter-repo --replace-text <(echo "sk_live_abc123=>[REDACTED_KEY]") --force
Common Mistakes and How to Avoid Them
Mistake 1: Committing a .env file
The fix: add .env to .gitignore before the first commit. If it was already committed, remove it with git rm --cached .env, add it to .gitignore, then commit the removal. Then rotate every credential in it.
Mistake 2: Logging request headers or bodies that contain keys
Many logging setups capture full request/response data for debugging. If the Authorization header or a JSON body field contains an API key, it ends up in plaintext in log files. Always sanitize log output: mask or omit authorization headers and any field named key, secret, token, password, or credential.
Mistake 3: Embedding keys in client-side JavaScript or mobile apps
Any secret in a browser-executed JavaScript file is publicly visible via browser DevTools. Any key in a mobile app binary can be extracted with tools like jadx (Android) or strings on the binary. Client-side code must only use short-lived, scoped tokens issued by your backend — never raw API keys for third-party services.
Mistake 4: Hardcoding keys in Docker images
Docker layers are permanent. Even if you run RUN rm /app/.env in a later layer, the file still exists in the previous layer and anyone with access to the image can extract it with docker save and inspect the tar archive. Always inject secrets at runtime via environment variables or secret mounts, never bake them into the image. See our Docker multi-stage build guide for patterns that keep secrets out of build layers.
Mistake 5: Using the same key across all environments
When a developer accidentally runs a production database migration against staging (or vice versa), having environment-specific keys acts as a safeguard. Always generate separate credentials per environment.
Encrypt Sensitive Text Before Sharing
Need to share an API key or secret with a teammate? Use SecureBin's Text Encryption tool to AES-256 encrypt it with a password before pasting it anywhere. Zero-knowledge — we never see the plaintext.
Encrypt a Secret NowAPI Key Security Checklist
- No hardcoded credentials in source code (in any language, any file)
.envin.gitignore(and checked before first commit).env.examplecommitted as documentation with placeholder values- Production secrets stored in a secret manager (AWS SM, Vault, etc.), not in env vars set manually on servers
- Each key scoped to minimum required permissions
- Separate keys per environment (dev/staging/prod)
- Rotation schedule defined and documented
- Pre-commit hook scanning for secret patterns (gitleaks or truffleHog)
- GitHub Secret Scanning enabled on all repos
- Logs sanitized — no keys in log output
- No keys embedded in Docker images or client-side code
- Incident response plan documented: what to do if a key leaks
The Bottom Line
Securing API keys is not a one-time task — it is an ongoing practice. The fundamentals are straightforward: never hardcode secrets, use environment variables with a proper secret manager for production, scope keys to minimum permissions, rotate them regularly, and scan continuously for accidental exposure. The cost of getting this wrong — a single leaked key — can be catastrophic. The cost of getting it right is a few hours of setup and a small operational overhead.
Related reading: API Security Best Practices, Environment Variables Complete Guide, .gitignore Best Practices, Docker Multi-Stage Builds, SSL/TLS Explained. Related tools: Text Encryption, Hash Generator, ENV Validator, JWT Decoder.