Published on

Secrets Management in 2026 — Vault, AWS Secrets Manager, Infisical, and Doppler Compared

Authors

Introduction

.env files are the biggest security vulnerability in most backends. They get committed to git, leaked in logs, and copied insecurely across environments. Teams that stay with .env eventually suffer breaches.

In 2026, there are excellent alternatives: HashiCorp Vault (complex but powerful), AWS Secrets Manager (AWS-native), Infisical (open-source, self-hostable), and Doppler (developer-friendly). This post compares them and shows exactly how to migrate away from .env.

Why .env Files Are Still Killing Teams

Problems with .env:

  1. Git commits: A leaked .env in git history is permanent.
  2. CI/CD logs: Secrets visible in build logs and error messages.
  3. Server copies: Hardcoding in deployment scripts.
  4. Lack of rotation: Secrets stay the same forever.
  5. No audit trail: No way to know who accessed what.
  6. Manual sync: Copy-pasting secrets between environments.

Real example:

# Developer commits by mistake
git add . && git commit -m "Add config"
# .env is in the commit!
# Push to GitHub
git push origin main

# Attacker finds .env in git history
# Gets database password, API keys, everything
# Extracts data, sells it, vanishes

# By the time you notice, it''s too late

Even with .gitignore, mistakes happen. Never commit secrets.

HashiCorp Vault: Dynamic Secrets

Vault is enterprise-grade. It supports dynamic credentials: passwords that expire after hours, not months.

Why dynamic secrets?

If a database password is leaked and expires in 2 hours, damage is limited. If it''s static for 6 months, attacker has 6 months to exploit.

Install Vault:

brew install vault
vault server -dev # Development mode

Configure PostgreSQL secret engine:

vault secrets enable database

vault write database/config/postgresql \
  plugin_name=postgresql-database-plugin \
  allowed_roles="readonly" \
  connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb" \
  username="vault_admin" \
  password="vault_password"

vault write database/roles/readonly \
  db_name=postgresql \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD ''{{password}}'' IN ROLE readonly;" \
  default_ttl="1h" \
  max_ttl="24h"

Read dynamic credential:

vault read database/creds/readonly
# Output:
# Key                Value
# ---                -----
# lease_duration     3600s
# password           a3cd-43-h7x
# username           v-token-readonly-abc123

In Node.js:

import * as Vault from 'node-vault';

const vault = new Vault.default({
  endpoint: 'http://localhost:8200',
  token: process.env.VAULT_TOKEN,
});

async function getDatabasePassword() {
  const secret = await vault.read('database/creds/readonly');
  return {
    username: secret.data.data.username,
    password: secret.data.data.password,
  };
}

const db = new PG.Pool({
  host: 'postgres',
  port: 5432,
  database: 'mydb',
  user: (await getDatabasePassword()).username,
  password: (await getDatabasePassword()).password,
});

Password is rotated automatically. Vault creates new credentials, invalidates old ones.

AWS Secrets Manager: Native AWS Integration

Simpler than Vault if you''re already on AWS.

Create a secret:

aws secretsmanager create-secret \
  --name prod/database-password \
  --secret-string '{"username":"admin","password":"secure_password"}'

Read secret in Node.js:

import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName: string) {
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);
  return JSON.parse(response.SecretString!);
}

const dbCreds = await getSecret('prod/database-password');
const db = new PG.Pool({
  host: 'postgres',
  user: dbCreds.username,
  password: dbCreds.password,
});

Automatic rotation:

# Rotation function (Lambda)
exports.lambda_handler = async (event) => {
  const secretId = event.ClientRequestToken;
  const secret = await secretsManager.getSecretValue({ SecretId: secretId });

  // Generate new password
  const newPassword = generatePassword();

  // Update database
  await updateDatabasePassword(secret.username, newPassword);

  // Store new password in Secrets Manager
  await secretsManager.putSecretValue({
    SecretId: secretId,
    SecretString: JSON.stringify({ ...secret, password: newPassword }),
    ClientRequestToken: event.ClientRequestToken,
  });
};

// Attach to secret for automatic rotation
aws secretsmanager rotate-secret \
  --secret-id prod/database-password \
  --rotation-lambda-arn arn:aws:lambda:...

Cost: ~$0.40 per secret per month + API calls. Scales linearly.

Infisical: Open-Source, Self-Hostable

Infisical is a self-hosted alternative to Doppler. No vendor lock-in.

Docker Compose:

version: '3.8'

services:
  infisical-postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: infisical
      POSTGRES_PASSWORD: password
    volumes:
      - infisical-data:/var/lib/postgresql/data

  infisical-app:
    image: infisical/infisical:latest
    depends_on:
      - infisical-postgres
    environment:
      DATABASE_URL: postgres://postgres:password@infisical-postgres:5432/infisical
      JWT_SIGNUP_SECRET: your_secret_key
    ports:
      - '80:3000'
    volumes:
      - infisical-app-data:/app/data

volumes:
  infisical-data:
  infisical-app-data:

Login and get credentials:

infisical login --domain http://localhost

infisical secrets --path /prod get DATABASE_PASSWORD
# Output: your_password_here

In Node.js:

import { InfisicalClient } from '@infisical/sdk';

const client = new InfisicalClient({
  siteURL: 'https://infisical.example.com',
  auth: {
    universalAuthToken: process.env.INFISICAL_TOKEN,
  },
});

async function getSecret(key: string) {
  const secret = await client.getSecret({
    secretName: key,
    projectId: 'PROJECT_ID',
    environment: 'prod',
  });

  return secret.secretValue;
}

const dbPassword = await getSecret('DATABASE_PASSWORD');

Cost: Free (self-hosted). Requires infrastructure.

Doppler: Developer-Friendly SaaS

Doppler prioritises developer experience. Simple CLI, fast sync, cheap.

Install CLI:

brew install doppler/cli/doppler
doppler login

Create project and environment:

doppler projects create myapp
doppler environments create prod
doppler config create backend --environment prod

Set secrets:

doppler secrets set DATABASE_PASSWORD "secure_password" --config backend
doppler secrets set API_KEY "key_123" --config backend

In Node.js:

import { DopplerSDK } from '@doppler/sdk';

const doppler = new DopplerSDK({
  token: process.env.DOPPLER_TOKEN,
});

async function getSecrets() {
  const secrets = await doppler.secrets.list({
    project: 'myapp',
    config: 'backend',
  });

  return secrets.secrets;
}

const allSecrets = await getSecrets();
const dbPassword = allSecrets.DATABASE_PASSWORD.computed;

Cost: ~$15-30/mo for small teams. Scales to $300+/mo for large teams.

1Password Secrets Automation

1Password can manage secrets and rotate them.

# Get secret via CLI
op read "op://myapp/database/password"

# In scripts
DB_PASSWORD=$(op read "op://myapp/database/password")

Works great if your team already uses 1Password. Limited for non-1Password users.

Injecting Secrets into Kubernetes

Use External Secrets Operator to sync secrets from vault/Doppler to Kubernetes.

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: doppler
  namespace: default
spec:
  provider:
    doppler:
      auth:
        secretRef:
          dopplerToken:
            name: doppler-secret
            key: token
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: default
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: doppler
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
    - secretKey: database_password
      remoteRef:
        key: DATABASE_PASSWORD
    - secretKey: api_key
      remoteRef:
        key: API_KEY

Pod receives Kubernetes secret automatically. No manual env var copying.

Secrets in CI/CD

GitHub Actions, GitLab CI, and other platforms support secret management.

GitHub Actions:

name: Deploy

on: [push]

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  API_KEY: ${{ secrets.API_KEY }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm test
      - run: npm run deploy

Secrets are masked in logs. Safe for CI.

Better: use Vault from CI:

- name: Get secrets from Vault
  run: |
    export VAULT_TOKEN=$(curl -s -X POST \
      https://vault.example.com/v1/auth/jwt/login \
      -d '{"jwt":"${{ secrets.VAULT_JWT }}"}' \
      | jq -r '.auth.client_token')

    DATABASE_URL=$(vault kv get -field=url secret/database)
    echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV

Secrets never stored in GitHub. Rotated at Vault.

Comparison Table

FeatureVaultAWS Secrets ManagerInfisicalDoppler
Dynamic secretsYesLimitedNoNo
Self-hostableYesNoYesNo
CostHighLow (~$0.40/mo)Free (self-hosted)Medium (~$15/mo)
Ease of useHardMediumEasyVery Easy
Audit logsYesYesYesYes
RotationYesYesYesYes
Multi-environmentYesYesYesYes
Team supportYesYesYesYes

Choose Vault if: You need dynamic secrets, have DevOps expertise, and want maximum control.

Choose AWS Secrets Manager if: All your infrastructure is on AWS and you want simplicity.

Choose Infisical if: You want to self-host and have no vendor lock-in.

Choose Doppler if: You want simplicity and team features without infrastructure overhead.

Migrating from .env

// Before: read from .env
const dbPassword = process.env.DATABASE_PASSWORD;

// After: read from Doppler
import { DopplerSDK } from '@doppler/sdk';

const doppler = new DopplerSDK({
  token: process.env.DOPPLER_TOKEN,
});

const secrets = await doppler.secrets.list({ project: 'myapp', config: 'backend' });
const dbPassword = secrets.secrets.DATABASE_PASSWORD.computed;

// Or use Doppler CLI to inject at startup
// doppler run -- node app.js
// Secrets available as env vars

Checklist

  • Stop committing .env files
  • Enable .env in .gitignore
  • Choose secret management tool (Vault, AWS, Infisical, Doppler)
  • Migrate existing secrets to the new tool
  • Rotate all existing secrets (assume they leaked)
  • Implement automatic secret rotation
  • Enable audit logging
  • Update CI/CD to fetch secrets from vault
  • Document secret access for your team
  • Schedule quarterly access reviews

Conclusion

.env files are a liability. Move to a proper secret management tool in 2026. Doppler is the fastest path for teams without DevOps expertise. Vault is the most powerful for teams that can maintain it. Infisical splits the difference. Whatever you choose, stop trusting files. Use proper infrastructure, rotate secrets automatically, and log every access.