Managing Secrets in .env Files
A practical guide to managing secrets in .env files, covering API keys, passwords, tokens, Git security, encryption, secret rotation, CI/CD, Docker, production deployments and common mistakes.
Environment variables are one of the most common ways to provide applications with configuration and credentials. Database passwords, API keys, access tokens, encryption keys and other sensitive values are often loaded through environment variables, especially during local development.
The convenience of .env files also creates a security risk: they are ordinary text files. Anyone who can read the file can potentially read the secrets stored inside it. A .env file is therefore a configuration mechanism, not a secure vault.
Safe secret management requires more than adding .env to .gitignore. Developers need to understand how secrets enter a project, where they are stored, how they reach the application, how they can accidentally leak and what to do when a secret is exposed.
What Counts as a Secret?
A secret is information that should be available only to authorized applications or people. In software projects, secrets commonly include credentials, private keys and authentication material.
| Secret type | Examples |
|---|---|
| API key | Third-party service API credentials |
| Access token | OAuth or service access token |
| Database credential | Database username and password |
| Signing secret | Secret used to sign sessions or tokens |
| Encryption key | Key used to encrypt or decrypt data |
| Private key | TLS, SSH or application cryptographic private key |
| Webhook secret | Secret used to verify webhook requests |
| Service credential | Credential used to authenticate with another service |
Not every environment variable is a secret. Values such as PORT=3000, NODE_ENV=production or LOG_LEVEL=info may be ordinary configuration. The distinction matters because secrets require stronger protection and access controls.
Why Secrets Are Stored in Environment Variables
Hard-coding credentials directly into application source code makes configuration difficult to change and creates an obvious path for accidental disclosure. Environment variables provide a separate configuration channel.
const apiKey = process.env.PAYMENT_API_KEY;
if (!apiKey) {
throw new Error("PAYMENT_API_KEY is not configured");
}The application source code does not need to contain the actual key. A development machine, CI system or hosting platform can provide the value when the application starts.
A .env File Is Not a Secret Store
A .env file is usually plain text. If it contains a value such as an API key, opening the file with a text editor is enough to reveal the key.
DATABASE_URL=postgresql://user:[email protected]:5432/app
PAYMENT_API_KEY=secret-api-key
SESSION_SECRET=another-secretThe file may be appropriate for local development, but its security depends on the operating system, file permissions, repository configuration and every place where the file is copied.
Never Commit Real Secrets to Git
One of the most important rules of secret management is to keep real credentials out of source control. A common first step is adding local environment files to .gitignore.
.env
.env.local
.env.*.localThis prevents untracked matching files from being added accidentally, but .gitignore does not remove a file that has already been committed. It also does not protect secrets that were copied into another file, pasted into source code or exposed through logs.
What If a Secret Was Already Committed?
If a real credential has been committed to a repository, deleting the line in a later commit does not make the credential safe again. The secret may still exist in previous commits, clones, forks, caches or other copies of the repository.
The first priority is usually to revoke or rotate the exposed credential. Repository cleanup can then be handled separately using the organization's procedures and appropriate history-rewriting tools when necessary.
Secret Rotation
Secret rotation means replacing an existing credential with a new one. Rotation limits the lifetime of a credential and provides a recovery mechanism when a secret is exposed or needs to be replaced.
A basic rotation process is to create a new credential, update the applications that depend on it, verify that the new credential works and then revoke the old credential when it is no longer required.
- Generate or obtain a new credential.
- Update the protected environment configuration.
- Deploy or restart the affected application.
- Verify that requests using the new credential succeed.
- Revoke the old credential.
- Check logs and monitoring for authentication failures.
Avoid Hard-Coding Secrets
Hard-coding a credential in source code creates several problems. The secret becomes part of the repository, can appear in code review, can be copied into builds and may remain in version history even after the code is changed.
// Avoid
const apiKey = "sk-example-secret";
// Prefer
const apiKey = process.env.API_KEY;The second approach does not make the value automatically secure, but it keeps the application code independent from a specific credential.
Use .env.example Without Real Secrets
A project usually needs to tell developers which environment variables are required. The safest way is to commit a template containing variable names and non-sensitive example values.
DATABASE_URL=
PAYMENT_API_KEY=
SESSION_SECRET=
API_BASE_URL=https://api.example.comThe actual .env file remains local or is provided by the development environment. This gives developers a documented configuration contract without putting real credentials in the repository.
Use Placeholders Carefully
A placeholder should be obviously non-functional. Do not put a real API key into .env.example merely because it is convenient for development.
PAYMENT_API_KEY=your-api-key-here
DATABASE_PASSWORD=replace-me
SESSION_SECRET=generate-a-local-secretFor local services, a clearly documented development-only credential may be acceptable when it has no access to production resources. Production credentials should never be reused as examples.
Use Separate Credentials for Each Environment
Development, testing and production should not normally share the same credentials. Separating credentials limits the consequences of a development machine being compromised or a test environment being accidentally exposed.
| Environment | Recommended approach |
|---|---|
| Development | Local or development-only credentials |
| Testing | Dedicated test credentials and services |
| Staging | Separate staging credentials |
| Production | Protected production credentials |
The exact setup depends on the project, but production credentials should be treated as a separate security boundary rather than copied into developer machines.
Do Not Reuse Production Secrets Locally
Using production API keys or database passwords on a developer computer increases the number of places where highly sensitive credentials exist. It also makes it easier for local debugging tools, terminal history, backups or accidental uploads to expose production access.
Whenever possible, create credentials with only the permissions and environment required for development.
Principle of Least Privilege
A secret should provide only the permissions required for the application or task that uses it. If a service supports separate credentials with different permissions, use the narrowest scope that satisfies the application's needs.
For example, a read-only reporting process should not need a credential capable of deleting production data. Limiting permissions reduces the potential impact if the credential is exposed.
Secret Permissions Matter
A secret is only as protected as the systems that can access it. If every developer, CI job and production service receives every secret, the security boundary becomes unnecessarily large.
Separate credentials by application, environment and purpose when practical. This also makes rotation easier because changing one credential does not necessarily require updating unrelated systems.
Encrypting .env Files
Some teams encrypt environment files so that encrypted configuration can be stored or transferred more safely. An encrypted .env file is fundamentally different from an ordinary .env file because the sensitive values are not directly readable without the decryption key.
Encryption can be useful when a project needs to distribute encrypted configuration through a controlled workflow. However, encryption does not eliminate the need to protect the decryption key.
Encrypted configuration
+
Decryption key
+
Trusted runtime
=
Application configurationThe decryption key must be protected separately from the encrypted data. If both the encrypted file and its decryption key are stored together without meaningful access controls, encryption provides little practical protection.
Encryption Is Not the Same as Encoding
Encoding transforms data into another representation without providing confidentiality. Base64 is a common example. Anyone who has the encoded value can decode it.
Secret:
my-api-key
Base64:
bXktYXBpLWtleQ==Where Should the Encryption Key Live?
The key used to decrypt an encrypted configuration file must come from a protected source. Storing the encrypted file and its decryption key in the same repository defeats much of the purpose of encryption.
Possible sources include a deployment platform's secret configuration, a dedicated secret-management service or another protected runtime mechanism. The appropriate choice depends on the infrastructure and threat model.
Secret Generators
Secrets should generally be generated using a cryptographically secure random source rather than manually invented strings. This is particularly important for session secrets, encryption keys, reset tokens and other values where unpredictability matters.
import { randomBytes } from "node:crypto";
const secret = randomBytes(32).toString("hex");
console.log(secret);The required length and representation depend on how the secret will be used. A cryptographic key has different requirements from a session secret or an API credential issued by an external service.
Do Not Generate Secrets with Math.random()
JavaScript's Math.random() is not intended for generating security-sensitive secrets. Use a cryptographically secure random number generator provided by the runtime or a trusted cryptographic library.
// Not appropriate for security-sensitive secrets
const weakSecret = Math.random().toString(36);
// Use a cryptographically secure generator instead
import { randomBytes } from "node:crypto";
const secureSecret = randomBytes(32).toString("hex");Mask Secrets in Logs
Logging configuration is useful when debugging an application, but logging secret values can turn an otherwise protected credential into a leaked credential.
const apiKey = process.env.API_KEY;
console.log({
apiKeyConfigured: Boolean(apiKey),
});When it is necessary to display a secret-like value for debugging, masking can reduce accidental exposure. However, masking is not a replacement for keeping the secret out of logs in the first place.
Secret Masking
A masking function can replace most characters with a placeholder while retaining a small amount of information for identification.
function maskSecret(value: string, visible = 4) {
if (value.length <= visible) {
return "*".repeat(value.length);
}
return "*".repeat(value.length - visible) + value.slice(-visible);
}
console.log(maskSecret("example-secret-value"));Even masked values should be handled carefully. If a secret is extremely short or has a recognizable structure, revealing its last characters may still provide information that should not be exposed.
Do Not Log process.env
Printing the entire process environment is a common debugging shortcut that can expose database credentials, API keys, tokens and deployment secrets.
// Avoid
console.log(process.env);
// Prefer
console.log({
nodeEnv: process.env.NODE_ENV,
databaseConfigured: Boolean(process.env.DATABASE_URL),
apiConfigured: Boolean(process.env.API_KEY),
});Browser Exposure Is a Major Risk
A server-side environment variable can be private, but once its value is sent to browser JavaScript it is no longer a secret. Client-side applications can be inspected by users through browser developer tools, downloaded JavaScript bundles or network requests.
// Server-side secret
const privateKey = process.env.PAYMENT_SECRET_KEY;
// Never send the private key to the browser.Next.js and Secret Variables
Next.js uses a NEXT_PUBLIC_ prefix for environment variables intended to be available to browser-side code. This makes the prefix an important security boundary in a Next.js application.
# Server-only
DATABASE_URL=postgresql://localhost:5432/app
PAYMENT_SECRET_KEY=secret-value
# Public
NEXT_PUBLIC_API_URL=https://api.example.comOnly values that are genuinely safe to expose should use the NEXT_PUBLIC_ prefix. A payment secret, database password, signing secret or private API credential should never be placed there.
Secrets in Docker
Containers add another layer to secret management. Developers often use .env files with Docker Compose during local development, but production containers should not automatically receive every local environment file.
services:
app:
image: example-app
env_file:
- .envThis can be convenient locally. For production, use the secret and environment-variable facilities provided by the deployment platform or container orchestration system when available.
Do Not Copy Secrets into Docker Images
Copying a .env file into an image can make secrets part of the image's filesystem or build history. Anyone with sufficient access to the image may then be able to inspect those values.
.env
.env.*
node_modulesA safer pattern is to build an image without private configuration and provide secrets when the container starts.
Secrets in CI/CD
CI/CD systems commonly provide protected variables or secret stores that can be injected into build and deployment jobs. This avoids committing production credentials into repositories.
A pipeline might expose variables such as DATABASE_URL or DEPLOY_TOKEN only to the specific job that needs them. Restricting which workflows can access a secret reduces unnecessary exposure.
Pull Requests and Code Reviews
Secrets can leak without ever being committed intentionally. Developers may paste a .env fragment into a pull request, issue, chat message or screenshot while asking for help with a configuration problem.
Before sharing configuration publicly or with people who do not need access, replace real credentials with clearly fake placeholders.
DATABASE_URL=postgresql://user:REDACTED@host:5432/database
API_KEY=REDACTED
SESSION_SECRET=REDACTEDSecret Scanning
Secret scanning tools inspect source code and repository changes for patterns that resemble credentials. They can catch some accidental leaks before they become a larger incident.
Secret scanning is useful as a second line of defense, but it should not replace good repository hygiene. A scanner can miss credentials that do not match known patterns, while false positives are also possible.
Pre-Commit Protection
Teams can add checks before commits or pushes to detect suspicious credentials. This can reduce the chance that a secret reaches the remote repository.
However, client-side hooks are not a complete security boundary because developers can bypass them and credentials can leak through other channels. Server-side repository scanning and secret-management controls are valuable additional layers.
Secret Management Services
For production systems, dedicated secret-management services can provide stronger controls than manually maintained .env files. These systems can support access policies, auditing, rotation workflows and controlled retrieval of credentials.
The specific service is less important than the capabilities required by the application. A small personal project may only need platform-managed environment variables, while a larger organization may need centralized secret management with fine-grained access controls.
When a .env File Is Enough
For local development, a .env file is often a practical choice. It is simple, widely supported and easy to integrate with frameworks such as Next.js and Node.js tooling.
The main requirements are that the file is protected locally, excluded from source control when appropriate and never copied into client-side code or public artifacts.
When to Use a Secret Manager
A dedicated secret-management solution becomes more useful as the number of applications, environments and people increases. Centralized management can make access control and rotation easier than distributing individual .env files.
| Situation | Typical approach |
|---|---|
| Personal local project | Local .env file |
| Small development team | .env + protected deployment variables |
| Production application | Hosting platform secrets or protected environment variables |
| Multiple production services | Centralized secret management may be useful |
| Highly sensitive infrastructure | Dedicated secret-management and access-control systems |
Do Not Store Secrets in Frontend Repositories
A frontend application that runs entirely in the browser cannot keep a secret from its users. If a credential is required by browser code, that credential should be considered public from a security perspective.
When a third-party API requires a private credential, the usual architecture is to keep that credential on a server and have the browser communicate with the server. The server can then authenticate with the third-party service without exposing its private credential to users.
Secrets and Server-Side API Routes
Frameworks such as Next.js can keep private credentials in server-side route handlers or server functions. The browser sends a request to the application, while the server uses the protected environment variable to communicate with the external service.
const apiKey = process.env.EXTERNAL_API_KEY;
export async function GET() {
if (!apiKey) {
return new Response("Server configuration error", {
status: 500,
});
}
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
return Response.json(await response.json());
}The private key remains on the server while the browser receives only the response that the application chooses to expose.
Avoid Putting Secrets in URLs
Sensitive credentials should generally not be placed in URLs or query parameters. URLs can appear in browser history, proxy logs, analytics systems, monitoring tools and server logs.
Avoid:
https://api.example.com/data?api_key=secret-value
Prefer:
Authorization: Bearer secret-valueThe exact authentication mechanism should follow the API's documentation, but putting secrets into URLs creates additional places where they can be recorded accidentally.
Avoid Secrets in Error Messages
Configuration errors should explain what is missing without printing the actual secret value.
if (!process.env.PAYMENT_API_KEY) {
throw new Error("PAYMENT_API_KEY is not configured");
}The same principle applies to exception tracking, monitoring systems and client-facing error responses. Diagnostic information should help identify the problem without exposing credentials.
A Secret Management Workflow
A practical workflow begins by identifying which configuration values are actually sensitive. Separate secrets from ordinary configuration and decide where each value should live in development, testing and production.
For local development, store credentials in an ignored .env file or another appropriate local mechanism. Commit a safe .env.example that documents required variable names without containing production secrets.
For production, provide credentials through protected deployment configuration or a dedicated secret-management service. Keep server-only values on the server and expose only intentionally public configuration to browser code.
Finally, establish a rotation procedure. When a credential is leaked, rotate it first and investigate how the exposure occurred afterward. A secret-management strategy should include recovery, not only prevention.
A Practical Secret Checklist
- Keep real secrets out of source code.
- Add local .env files to .gitignore when appropriate.
- Commit a safe .env.example instead of real credentials.
- Use separate credentials for development, testing and production.
- Give credentials only the permissions they require.
- Keep server-only secrets away from browser code.
- Never use NEXT_PUBLIC_ for private values in Next.js.
- Do not copy .env files containing secrets into Docker images.
- Avoid printing process.env in logs.
- Do not put credentials in URLs or query parameters.
- Use cryptographically secure random generation for secrets you create yourself.
- Rotate credentials immediately when they are exposed.
- Use protected deployment variables or secret-management services for production.
- Consider secret scanning as an additional layer of protection.
Common Secret Management Mistakes
- Assuming that .env files are encrypted by default.
- Committing .env to Git.
- Deleting a leaked secret without rotating it.
- Using production credentials during local development.
- Putting private keys in NEXT_PUBLIC_ variables.
- Logging API keys for debugging.
- Printing the entire process.env object.
- Encoding secrets with Base64 and treating the result as encryption.
- Storing encryption keys next to encrypted configuration.
- Using Math.random() for security-sensitive secrets.
- Giving one credential access to more resources than necessary.
- Pasting real credentials into issues, pull requests or chat messages.
- Copying secrets into frontend bundles.
Frequently Asked Questions
Are .env files secure?
A .env file is normally plain text and is not encrypted by default. It can be appropriate for local development, but its security depends on file permissions, repository configuration and how the values are used.
Should I commit .env to Git?
Real .env files containing secrets should generally not be committed to Git. A safer approach is to keep the real file outside version control and commit a .env.example containing variable names and safe placeholder values.
What should I do if an API key was committed to Git?
Treat the key as exposed. Revoke or rotate the credential as soon as possible, then investigate and clean up the repository exposure according to the project's security procedures. Simply deleting the key from the latest commit is not sufficient.
Is encrypting a .env file enough to protect secrets?
Encryption can protect the contents of the file while it is stored or transferred, but the decryption key must also be protected. If an attacker can obtain both the encrypted file and its decryption key, the encryption provides little practical protection.
Can I put API keys in frontend environment variables?
Only credentials that are intentionally public should be exposed to frontend code. Anything required to remain secret must stay on the server because browser code and network requests can be inspected by users.
Should development and production use the same secrets?
They generally should not. Separate credentials reduce the impact of a compromised development machine or test environment and make it easier to rotate one environment without affecting another.
How should I generate a secure secret?
Use a cryptographically secure random generator provided by the runtime or a trusted cryptographic library. Do not use predictable values, manually invented strings or Math.random() for security-sensitive secrets.
Helpful Environment Tools
A dotenv Encryptor can help protect environment-file contents when an encrypted configuration workflow is appropriate, while a dotenv Decryptor can restore an encrypted file for an authorized development or deployment process. A Secret Generator is useful for creating strong random values for application secrets, session secrets and similar configuration. A Secret Mask Generator can help create masked representations for safe debugging or configuration displays, while an Environment Variable Generator can speed up the creation of correctly formatted environment-variable definitions.
Conclusion
Managing secrets in .env files is primarily about controlling where sensitive values exist and who can access them. A .env file is convenient for local development, but it should never be confused with a secure secret vault.
Keep real credentials out of Git, use .env.example for documentation, separate development and production credentials, apply least-privilege access and avoid exposing server-only values to browser code. In production, use protected deployment configuration or dedicated secret-management infrastructure when appropriate.
Good secret management also requires a response plan. If a credential leaks, rotate or revoke it rather than simply deleting the visible copy. Encryption, secret scanning, masking and access controls can provide additional layers of protection, but none of them replaces careful handling of credentials throughout the application's lifecycle.