Modern web applications built on Next.js and Node.js process increasingly sensitive data—API keys, JWT tokens, database credentials. Improper storage of these secrets leads to leaks that can cost an organization millions of dollars. In this article we present a complete threat model, describe how HashiCorp Vault works, and show practical code snippets—from vulnerable to hardened—as well as a mitigation checklist based on OWASP, CWE, and the least‑privilege principle.
Threat model – where does the risk appear?
A typical secret flow in a Next.js/Node.js project looks like this: a developer writes the value to a .env file, the CI/CD repository fetches the file and injects it into a Docker container, and the application reads the environment variable at runtime. In this model three critical attack points exist:
- storing the secret in the repository (even a private repo can be cloned or leaked);
- passing the secret as an unencrypted string in container environment variables;
- lack of access restrictions for the application to a specific secret (no least‑privilege principle).
All these scenarios are described in OWASP Top 10 as Security Misconfiguration (A05) and in CWE‑522 – Insufficiently Protected Credentials. The best way to reduce risk is central secret management, which provides audit, rotation, and minimal permission scope.
HashiCorp Vault – how does it work?
Vault is a secret‑management system that stores encrypted values in a secure backend (e.g., consensus storage). Access to secrets is obtained after authentication using methods such as AppRole, Kubernetes Auth, or TLS certificates. Upon successful authentication Vault issues a short‑lived token (TTL from a few seconds to a few hours) and allows reading or generating dynamic credentials (e.g., temporary database users).
Key features that meet OWASP and CWE requirements:
- encryption‑at‑rest and encryption‑in‑transit (TLS);
- strict policy control (policy‑as‑code) defining which paths and operations are allowed;
- automatic rotation and revocation of tokens.
Integrating HashiCorp Vault with Node.js – code example
Below is a simple yet vulnerable code snippet where secrets are loaded from a .env file and used directly in API calls:
require('dotenv').config();
const axios = require('axios');
const apiKey = process.env.API_KEY; // vulnerable: key in .env
async function callExternal() {
const resp = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return resp.data;
}
The problem is that API_KEY can end up in CI logs, Docker images, or container memory, and there is no control over which processes can read it.
The hardened version uses the Vault SDK, authenticates with AppRole, and fetches the secret at runtime. This way the token has a short TTL, and the key is not kept in memory longer than necessary:
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_APPROLE_TOKEN // short‑lived token, not permanent
});
async function getApiKey() {
const result = await vault.read('secret/data/app/api-key');
return result.data.data.key;
}
async function callExternal() {
const apiKey = await getApiKey();
const resp = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return resp.data;
}
Note that the Vault SDK automatically handles token renewal, and the app/api-key policy can restrict access to read‑only for this specific key.
Docker and Kubernetes – secure storage of JWT tokens
In container environments, environment variables are commonly used to pass tokens. Instead, you can use:
- Vault Agent Injector – automatically injects the secret as a file in
/vault/secretsinside the container; - Sidecar container with Vault Agent, which refreshes the token in the background.
Kubernetes manifest example with a sidecar:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-app
spec:
replicas: 2
template:
spec:
containers:
- name: app
image: myregistry/nextjs:latest
env:
- name: VAULT_ADDR
value: "https://vault.mycompany.com"
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
- name: vault-agent
image: hashicorp/vault-agent:latest
args:
- "agent"
- "-config=/etc/vault/agent-config.hcl"
volumeMounts:
- name: vault-config
mountPath: /etc/vault
- name: vault-secrets
mountPath: /vault/secrets
volumes:
- name: vault-config
configMap:
name: vault-agent-config
- name: vault-secrets
emptyDir: {}
The sidecar writes the refreshed token to /vault/secrets/jwt, and the application reads it at startup. This way the token never appears in environment variables and is automatically revoked when it expires.
Mitigation checklist – from OWASP to least‑privilege
- Use a central store (Vault) instead of
.envfiles in the repository. - Apply short TTLs for tokens and enable automatic rotation.
- Define Vault policies like
path "secret/data/app/*" { capabilities = ["read"] }– limit access to a specific namespace. - In Docker and Kubernetes inject secrets as files or volumes, not as env vars.
- Audit access – enable an audit device in Vault and integrate logs with a SIEM.
- Validate application permissions (least‑privilege) – the app should only have access to the secrets it truly needs.
Common mistakes and trade‑offs
Even experienced teams fall into certain traps:
- Hard‑coding tokens – placing static tokens in source code under the pretext of rapid prototyping; solution: always use AppRole or Kubernetes Auth.
- Setting overly broad policies – e.g.,
path "secret/*" { capabilities = ["read"] }grants access to all secrets; solution: split secrets into domains and assign minimal permissions. - No rotation – leaving immutable API keys for years; solution: configure
max_ttlandrotation_periodin Vault. - Logging secrets – unintentionally printing values in application logs; solution: use a logger that masks fields named
*secret*,*key*,*token*.
Security is not a one‑time configuration; it is a continuous process of monitoring, auditing, and privilege reduction.
Practical example – full workflow
1. Vault configuration: create a namespace app, enable an audit device, define a policy app-read with access to secret/data/app/*.
2. Create AppRole: role_id and secret_id are stored in a Kubernetes Secret (not in the repository).
3. Deploy the application: manifest with a Vault Agent sidecar, as shown in the example above.
4. In the Next.js application (pages/api/data.ts):
import { createClient } from '@next-auth/vault';
export default async function handler(req, res) {
const client = createClient({
endpoint: process.env.VAULT_ADDR,
roleId: process.env.VAULT_ROLE_ID,
secretId: process.env.VAULT_SECRET_ID
});
const { data } = await client.read('secret/data/app/jwt');
const jwt = data.data.token;
// use the token to authorize an external service
res.status(200).json({ status: 'ok' });
}
5. Security testing: run an OWASP ZAP scan, verify that no environment variables leak in container logs.
After implementing this workflow, the engineering team gains:
- a central place to manage all secrets;
- the ability to rotate instantly in case of an incident;
- full audit control aligned with OWASP and CWE.
If you want your Next.js and Node.js projects to be resilient against data leaks, contact Coderia.it – we’ll help you implement Vault, tailor policies, and conduct a security audit.



