How to Store JWT Secrets Securely: Env Vars, Vaults, and KMS
One of the most consequential decisions in your application's security posture is where and how you store your JWT signing secrets. A hardcoded secret is a ticking time bomb; a properly managed secret in a vault is as safe as you can get.
Rule Zero: Never Hardcode Secrets
// WRONG — never do this
const secret = 'my-super-secret-key-1234';
// CORRECT — read from environment
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET environment variable is not set');Hardcoded secrets end up in git history, Docker images, log files, and error messages. They cannot be rotated without a code deployment.
Option 1: Environment Variables
The simplest approach. Store the secret in your deployment environment and read it at runtime.
# .env (development only — never commit)
JWT_SECRET=your-256-bit-random-secret-here
# Production: set via your hosting platform
railway variables set JWT_SECRET=your-secret
heroku config:set JWT_SECRET=your-secretBest for: Small projects, early-stage products, simple deployments with a single environment.
Risks: Secrets appear in process environment listings, may be logged accidentally, and rotation requires restart.
Option 2: AWS Secrets Manager
AWS Secrets Manager stores secrets encrypted with KMS and provides IAM-based access control. Your application fetches the secret at startup or on each request.
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getJwtSecret() {
const response = await client.send(
new GetSecretValueCommand({ SecretId: 'prod/app/jwt-secret' })
);
return response.SecretString;
}Best for: Production deployments on AWS, teams that need audit logging and automatic rotation.
Option 3: HashiCorp Vault
A dedicated secrets management platform with dynamic secrets, lease TTLs, and detailed audit logging.
const vault = require('node-vault')({ endpoint: process.env.VAULT_ADDR });
async function getJwtSecret() {
await vault.approleLogin({ role_id: process.env.VAULT_ROLE_ID, secret_id: process.env.VAULT_SECRET_ID });
const { data } = await vault.read('secret/data/jwt');
return data.data.secret;
}Best for: Medium-to-large teams with dedicated platform engineering, multi-cloud environments, fine-grained access control needs.
Option 4: Cloud KMS (Envelope Encryption)
For the highest security, store only an encrypted version of your secret in your database or config, and use cloud KMS to decrypt at runtime. The plaintext key material never persists outside memory.
Best for: Enterprise, regulated industries (HIPAA, PCI-DSS), highest security requirements.
Quick Decision Matrix
| Stage | Recommendation |
|---|---|
| Development | .env file (never committed) |
| Small production | Platform env vars (Railway, Render, Vercel) |
| Growing team | AWS Secrets Manager or Doppler |
| Enterprise | HashiCorp Vault or Cloud KMS |
Generate your JWT secret with the JWT Secret Generator, then store it using the method appropriate for your deployment stage.