FuntranslatorCreate Fun Language Translations
Free
Back to Blog
February 8, 202610 min read

Where to Store JWT Secrets: Env Vars vs Vault vs KMS

Compare storage options for JWT secrets: environment variables, HashiCorp Vault, and cloud KMS. Learn when to use each and implementation best practices.

Where you store JWT secrets is as important as how you generate them. This guide compares three common approaches: environment variables, HashiCorp Vault, and cloud Key Management Services (KMS).

The Problem

JWT secrets must be:

  • Accessible - Your application needs them at runtime
  • Secure - Unauthorized access is catastrophic
  • Manageable - You need to rotate them regularly
  • Auditable - You need to know who accessed what, when

Let's compare the three main storage options.

Option 1: Environment Variables

The simplest approach. Secrets are set in the environment and read by your application at startup.

Implementation

// .env (never commit this!)
JWT_SECRET=a1b2c3d4e5f6...

// app.js
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;

// Signing
const token = jwt.sign({sub: '123'}, secret, {algorithm: 'HS256'});

// Verifying
const decoded = jwt.verify(token, secret);

Pros

  • Simple to implement
  • No external dependencies
  • Works everywhere (containers, serverless, VMs)
  • No network calls (fast)

Cons

  • No audit logging
  • Rotation requires restart or custom logic
  • Can leak via logs, error messages, process listings
  • Hard to manage across many services
  • Often stored in config files or CI/CD systems

Best For

  • Development environments
  • Simple applications with few secrets
  • Teams without dedicated security infrastructure

Option 2: HashiCorp Vault

Vault is a dedicated secret management solution with enterprise features.

Implementation

const vault = require('node-vault')({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN
});

async function getJwtSecret() {
  const result = await vault.read('secret/data/jwt');
  return result.data.data.secret;
}

// Usage
const secret = await getJwtSecret();
const token = jwt.sign({sub: '123'}, secret);
// More secure than static tokens
const vault = require('node-vault')({
  endpoint: process.env.VAULT_ADDR
});

async function authenticate() {
  const result = await vault.approleLogin({
    role_id: process.env.VAULT_ROLE_ID,
    secret_id: process.env.VAULT_SECRET_ID
  });
  vault.token = result.auth.client_token;
  return vault;
}

Pros

  • Full audit logging
  • Automatic secret rotation
  • Dynamic secrets (database credentials)
  • Lease-based access (secrets expire)
  • Cross-cloud (works with any infrastructure)
  • Encryption as a service

Cons

  • Operational complexity (must run Vault)
  • Network dependency (adds latency)
  • Learning curve for team
  • Cost (especially for enterprise features)

Best For

  • Teams with security expertise
  • Multi-cloud environments
  • Applications requiring audit compliance
  • Large organizations with many secrets

Option 3: Cloud KMS

Cloud providers offer managed secret storage integrated with their IAM systems.

AWS Secrets Manager

const { SecretsManager } = require('@aws-sdk/client-secrets-manager');

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

async function getJwtSecret() {
  const response = await client.getSecretValue({
    SecretId: 'jwt-secret'
  });
  
  return JSON.parse(response.SecretString).secret;
}

// With caching
const secret = await getJwtSecret();
const token = jwt.sign({sub: '123'}, secret);

Google Cloud Secret Manager

const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');

const client = new SecretManagerServiceClient();

async function getJwtSecret() {
  const [version] = await client.accessSecretVersion({
    name: 'projects/my-project/secrets/jwt-secret/versions/latest'
  });
  
  return version.payload.data.toString();
}

Azure Key Vault

const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');

const credential = new DefaultAzureCredential();
const client = new SecretClient(
  'https://my-vault.vault.azure.net',
  credential
);

async function getJwtSecret() {
  const secret = await client.getSecret('jwt-secret');
  return secret.value;
}

Pros

  • Fully managed (no infrastructure)
  • Integrated with cloud IAM
  • Automatic rotation (AWS)
  • Version history
  • Native audit logging

Cons

  • Vendor lock-in
  • Network latency
  • Cost per API call
  • Secret size limits

Best For

  • Cloud-native applications
  • Teams already using cloud IAM
  • Want managed solution without operations

Comparison Matrix

FeatureEnv VarsVaultCloud KMS
Setup ComplexityLowHighMedium
Audit LoggingNoneFullFull
RotationManualAutomaticAutomatic
Network DependencyNoneRequiredRequired
CostFreeServer + LicensePer API call
Cloud Lock-inNoneNoneYes

Recommendations

Development

Use environment variables. Add .env to .gitignore. Never commit secrets.

Small Production

Start with cloud KMS if you're on a cloud platform. Use environment variables if on bare metal.

Enterprise Production

Use HashiCorp Vault for full control, or cloud KMS with proper IAM policies. Implement caching to reduce latency.

Secret Caching

Fetching secrets on every request adds latency. Implement caching:

class SecretCache {
  constructor(fetchFn, ttlSeconds = 3600) {
    this.fetchFn = fetchFn;
    this.ttl = ttlSeconds * 1000;
    this.cache = null;
    this.lastFetch = 0;
  }

  async get() {
    const now = Date.now();
    
    if (this.cache && (now - this.lastFetch) < this.ttl) {
      return this.cache;
    }

    this.cache = await this.fetchFn();
    this.lastFetch = now;
    return this.cache;
  }
}

// Usage
const secretCache = new SecretCache(getJwtSecret, 300); // 5 min cache
const secret = await secretCache.get();

Important: Cache in memory, not on disk. Restart applications to pick up rotated secrets.

Security Best Practices

  1. Never log secrets - Redact before logging
  2. Never return secrets in error messages
  3. Use least privilege - Apps only read, admins manage
  4. Rotate regularly - Every 90 days minimum
  5. Separate environments - Dev/staging/prod use different secrets
  6. Monitor access - Alert on unusual patterns

Tools

Related Articles

How to Rotate JWT Secrets Without Downtime (kid + Multiple Keys)

Learn how to implement zero-downtime JWT secret rotation using the kid header claim and multiple key support. Includes Node.js code examples and migration strategies.

Read Article

JWT Best Practices Checklist (Copy/Paste for Production)

A comprehensive checklist of JWT security best practices for production applications. Copy and paste to ensure your JWT implementation is secure.

Read Article