Skip to main content
C
CodeUtil

Environment Variables Security: Secrets Management Best Practices

Secure your environment variables and secrets with proven practices. Learn about .env files, secrets managers, encryption, and CI/CD security for modern applications.

2025-08-2115 min
Related toolBase64 Encoder/Decoder

Use the tool alongside this guide for hands-on practice.

The time I leaked an AWS key to GitHub

True story: Five years ago, I committed an AWS access key to a public GitHub repo. Within 15 minutes, someone had spun up a crypto mining operation on my account. AWS emailed me a $2,400 bill before I even noticed the commit. That was an expensive lesson in secrets management.

Environment variables store everything critical: database passwords, API keys, encryption secrets. One leaked secret can mean data breaches, unauthorized access, or - in my case - a surprise cloud bill. Modern apps connect to dozens of services, each with credentials. Managing them securely across dev, staging, and production is non-trivial.

This is everything I've learned about secrets management since that AWS incident. At Šikulovi s.r.o., we have strict practices now. You should too.

Env vars 101

Environment variables are key-value pairs available to your running process. They separate config from code - same codebase, different environments, different settings.

Access them via process.env in Node.js, os.environ in Python, or ENV in shell scripts. Values are always strings, so parse as needed.

Child processes inherit them from parents. They only persist for the process lifetime unless you set them at system level.

The dotenv pattern loads variables from .env files during development. Libraries like dotenv (Node.js) or python-dotenv read these files and populate the environment. This simulates production locally.

  • Key-value pairs available to processes
  • Inherited by child processes
  • Access via process.env, os.environ, etc.
  • .env files simulate production environment locally
  • Values are always strings - remember to parse them

Mistakes I see constantly (and made myself)

Some mistakes appear in every security audit. I've made most of them personally. Learn from my pain.

Committing secrets to version control is #1. Once in Git history, secrets persist forever unless you rewrite history. Add .env to .gitignore IMMEDIATELY. Use git-secrets or gitleaks to catch accidental commits before they happen.

Hardcoding secrets seems convenient until you need to rotate them. Now it requires a code change. And anyone with code access sees your secrets. Different environments can't have different values.

Logging secrets accidentally is sneaky. You log a config object for debugging, forget there's a secret in there. Now it's in your log files forever. Never log env vars wholesale.

Sharing secrets insecurely—via email, chat, or shared documents—leaves copies in multiple places. Use dedicated secret sharing tools that provide one-time links or automatic expiration.

  • Never commit .env files to Git
  • Add .env to .gitignore immediately
  • Never hardcode secrets in source code
  • Be careful logging configuration objects
  • Use secure channels for secret sharing
  • Rotate secrets after any potential exposure

.env files: best practices

The .env file pattern is standard for local development. Following best practices keeps development convenient while maintaining security.

Create a .env.example file with placeholder values and commit it. This documents required variables without exposing actual secrets. New developers copy it to .env and fill in their values.

Use different .env files for different environments: .env.development, .env.test, .env.local. Configure your tooling to load the appropriate file. Never create .env.production with real secrets locally.

The Base64 Encoder/Decoder helps when debugging encoded values in environment variables. Some secrets arrive Base64-encoded; decode them to inspect their contents during debugging.

Validate environment variables at application startup. Fail fast if required variables are missing rather than crashing later with cryptic errors. Libraries like envalid (Node.js) provide typed validation.

  • Commit .env.example with placeholders
  • Use .env.development, .env.test for different environments
  • Never create .env.production locally
  • Validate required variables at startup
  • Use typed validation libraries

Secrets managers I actually use

For production, you need more than env vars. Dedicated secrets managers provide encryption, access control, and audit logging.

HashiCorp Vault is the industry standard. Encrypted storage, fine-grained access control, audit logging, dynamic secrets that rotate automatically. We use it at Šikulovi s.r.o. for anything serious.

Cloud providers have their own: AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault. If you're already in their ecosystem, the integration is smooth.

For smaller projects, Doppler and 1Password are developer-friendly. They sync secrets across environments and integrate with common deployment tools.

Pick based on scale and platform. Managed services reduce operational burden; self-hosted gives more control. All are better than raw env vars in production.

  • HashiCorp Vault: Industry standard, self-hosted or cloud
  • AWS Secrets Manager: Native AWS integration
  • Google Cloud Secret Manager: Native GCP integration
  • Azure Key Vault: Native Azure integration
  • Doppler, 1Password: Developer-friendly alternatives

Encryption: at rest and in transit

Secrets should be encrypted whenever stored and protected during transmission. This limits damage even if other defenses fail.

Encryption at rest: stored secrets are encrypted. Secrets managers handle this automatically. If you must store secrets in files, use tools like sops or age to encrypt them.

Encryption in transit: protect secrets during transmission. Always HTTPS. Ensure your secrets manager connections are encrypted. Verify TLS certificates.

You can use the Hash Generator on CodeUtil to verify secret integrity - store hashes of secrets and verify after retrieval to detect tampering.

Key management is the hard part. Encryption keys need secure storage too. Cloud KMS services handle this for cloud secrets managers. For self-hosted, plan key rotation and backup carefully.

  • Secrets managers encrypt at rest automatically
  • Use sops or age for encrypted file storage
  • Always HTTPS for transmission
  • Verify TLS certificates
  • Plan encryption key management carefully

CI/CD pipeline security

CI/CD pipelines need secrets for deployments, tests, and integrations. This is a common attack vector - I've seen breaches from leaked pipeline secrets.

Never store secrets in pipeline config files. Use your platform's secure variables. GitHub Actions has secrets, GitLab has protected/masked variables. Use them.

Limit secret scope. If a secret is only for production deployment, don't make it available to all pipeline jobs. Environment-specific secrets exist for a reason.

Be paranoid about PR builds. External contributors can craft PRs that expose secrets through logging. Most platforms restrict secret access in fork PRs for this reason.

Audit pipeline access. Anyone who can modify pipeline configs can potentially expose secrets. Treat these changes as security-sensitive.

  • Use platform secret variables, never config files
  • Limit secret scope to necessary jobs only
  • Restrict secrets in PR builds from forks
  • Audit who can modify pipeline configurations
  • Monitor for secrets appearing in build logs

Docker and Kubernetes secrets

Container environments have their own secrets patterns. Get them wrong and secrets get baked into images or leaked to logs.

Never include secrets in Docker images. Secrets in build arguments or copied files persist in image layers. Anyone with image access can extract them. Use runtime injection instead.

Kubernetes Secrets store sensitive data separately from pod specs. Not encrypted by default in etcd, but they integrate with pods and enable RBAC access control.

For production K8s, enable encryption at rest for secrets in etcd. Consider external secrets operators that sync from Vault or cloud managers. Sealed Secrets give you GitOps-compatible encrypted secrets.

Mount secrets as files rather than env vars when possible. Env vars can appear in process listings or environment dumps.

  • Never bake secrets into Docker images
  • Use runtime injection, not build-time
  • K8s Secrets for basic storage
  • Enable etcd encryption for production
  • Consider external secrets operators
  • Mount as files when possible

Rotation and expiration

Secrets shouldn't live forever. Regular rotation limits damage from undetected compromises. Expiration ensures unused secrets don't persist.

Define rotation schedules by sensitivity. Database passwords maybe monthly. API keys for low-risk services quarterly. Critical secrets rotate immediately after any team changes.

Automate rotation. Dynamic secrets in Vault create and revoke credentials automatically. Cloud managers offer rotation lambda functions. Automation ensures consistency and reduces burden.

Plan the mechanics. Applications must handle secret changes without downtime. Maybe support two valid credentials during transition, or implement credential refresh logic.

Set expiration dates where supported. JWT tokens, API keys, certificates should have defined lifetimes. Prevents forgotten secrets from staying valid forever.

  • Define rotation schedules by sensitivity
  • Automate rotation with Vault or cloud managers
  • Support graceful credential transitions
  • Set expiration dates where possible
  • Rotate immediately after team changes

Auditing and monitoring

Knowing who accessed which secrets and when is crucial for incident response. Monitoring catches suspicious patterns early.

Enable audit logging in your secrets manager. Vault, AWS Secrets Manager - they all log every access. Review periodically and alert on unusual patterns.

Monitor for secrets in unexpected places. Scan repos, logs, public resources. GitGuardian and TruffleHog automate this scanning. Run them regularly.

Implement access alerts. Notify when secrets are accessed from unusual locations or times. This catches compromised credentials in use.

Prepare incident response procedures. Know how to rotate secrets fast, what depends on each secret, how to investigate exposures. Practice these procedures.

  • Enable audit logging for all secret access
  • Scan for exposed secrets in repos and logs
  • Alert on unusual access patterns
  • Document what depends on each secret
  • Practice incident response procedures

My security checklist

I use this to audit secrets management on every project at Šikulovi s.r.o.:

Version control: .env in .gitignore? Pre-commit hooks checking for secrets? .env.example documenting variables? No hardcoded secrets in source?

Development: Local .env files only? Secure secret sharing? Non-production secrets for dev?

Production: Secrets in a secrets manager? Access limited by role? Encrypted at rest and in transit? Rotation scheduled and automated?

CI/CD: Secrets in platform secure variables? PR builds have limited access? Pipeline configs are access-controlled?

Monitoring: Audit logs enabled? Secret scanning active? Unusual access triggers alerts? Incident response documented?

  • Version control: .gitignore, pre-commit hooks, no hardcoding
  • Development: Local .env, secure sharing, non-production secrets
  • Production: Secrets manager, RBAC, encryption, rotation
  • CI/CD: Secure variables, limited PR access, config controls
  • Monitoring: Audit logs, scanning, alerts, incident procedures

FAQ

Should I use .env files in production?

Not typically. .env files are for development convenience. In production, use proper secrets managers (Vault, AWS Secrets Manager), container orchestration secrets (Kubernetes Secrets), or platform environment variables (Heroku Config Vars). These provide encryption, access control, and auditing that .env files lack.

How do I safely share secrets with team members?

Use dedicated secret sharing tools like 1Password, Vault, or one-time secret services (onetimesecret.com). Never share via email, Slack, or other persistent channels where secrets remain in history. For initial setup, encrypted files with separately communicated passphrases work if dedicated tools are unavailable.

What should I do if I accidentally commit a secret?

First, rotate the secret immediately—consider it compromised. Then remove it from Git history using git filter-branch or BFG Repo Cleaner. Force push to remote. Even with history rewriting, anyone who previously cloned the repo may have the secret. Rotation is the priority; history cleanup prevents future discovery.

How often should I rotate secrets?

It depends on the secret sensitivity. Database credentials and encryption keys: monthly or quarterly. API keys: quarterly. Less critical service credentials: annually. Always rotate immediately after team member departures, security incidents, or potential exposures. Automation makes frequent rotation practical.

Is Base64 encoding secrets secure?

No. Base64 is encoding, not encryption. Anyone can decode Base64 data instantly. It provides no security. Base64 is often used to safely transport binary data as text, but it does not protect the data. Use proper encryption (AES, at rest encryption by secrets managers) for security. Use the Base64 Encoder/Decoder to verify this—encoding and decoding is trivial.

How do I prevent secrets from appearing in logs?

Never log environment variables or configuration objects wholesale. Explicitly construct log messages with only non-sensitive data. Use structured logging with redaction rules. Some frameworks offer automatic secret detection and masking. Test by reviewing logs for secret patterns before production deployment.

Martin Šikula

Founder of CodeUtil. Web developer building tools I actually use. When I'm not coding, I experiment with productivity techniques (with mixed success).

Related articles