CodingNic

Security Hardening

Secrets Management and Defense in Depth

Security Hardening 15 min read

Secrets Management and Defense in Depth

Objectives

By the end of this lesson, you should be able to:

  • Explain why secrets like JWT_SECRET don’t belong in source code
  • Load secrets from environment variables using dotenv
  • Explain defense in depth, and name the layers built across this course so far

💡 Why this matters: Every JWT example so far (Module 3) hardcoded a secret string directly in the file, fine for learning, a real liability in a real codebase, this lesson fixes that, and steps back to see how every security measure in this course fits together.

⚠️ A note on verification: every snippet and every output in this lesson was actually run.

Why Hardcoded Secrets Are a Problem

javascript
// Module 3's examples, for learning purposes:
const token = jwt.sign({ sub: user.id }, 'my-secret-key');

A secret committed directly into source code ends up in version control history permanently, visible to anyone with repository access, past or present, and, if the repository is ever made public, or a laptop with local clone access is lost, exposed entirely. Anyone holding a JWT secret can forge valid tokens for any user, including an admin, exactly the signature-forging scenario Module 3 covered, a leaked secret undermines every protection built on top of it.

Loading Secrets from the Environment

bash
npm install dotenv
text
# .env, never committed to version control
JWT_SECRET=a-long-randomly-generated-string-not-committed-to-git
PORT=3000
javascript
require('dotenv').config();

console.log(typeof process.env.JWT_SECRET, process.env.JWT_SECRET ? 'present' : 'missing');
text
string present

dotenv reads a .env file and loads its values into process.env, readable anywhere in the application as process.env.JWT_SECRET. The .env file itself is added to .gitignore, never committed, each environment, a developer’s laptop, a staging server, production, has its own .env with its own values, the source code stays identical everywhere, only the environment changes.

Using the Loaded Secret

javascript
const jwt = require('jsonwebtoken');

const token = jwt.sign({ sub: 'user_1' }, process.env.JWT_SECRET, { expiresIn: '1h' });
const verified = jwt.verify(token, process.env.JWT_SECRET);
console.log(verified.sub);
text
user_1

Every JWT example from Module 3 continues to work exactly as before, process.env.JWT_SECRET simply replaces the hardcoded string, the actual signing and verification logic is unchanged.

What Belongs in Environment Variables

Beyond JWT_SECRET: database connection strings (Course 2), session secrets (express-session’s secret option, Module 2), API keys for any third-party service, and any other value that grants access or reveals infrastructure details, none of it belongs in source code, all of it varies by environment, and all of it needs to stay out of version control.

Defense in Depth

No single security measure in this course is sufficient on its own, each one covers a different failure mode, together they form layers, an attacker has to get through more than one to succeed:

  • Hashing (Module 1) protects passwords if the database itself is ever leaked.
  • Sessions or JWTs (Modules 2 and 3) confirm a request actually comes from an authenticated user.
  • Role and ownership checks (Module 4) confirm an authenticated user is allowed to do the specific thing they’re attempting.
  • Validation (Module 5) rejects malformed or attacker-crafted input, and closes mass-assignment gaps.
  • Rate limiting (this module) slows down automated attacks against public endpoints.
  • Secrets management (this lesson) keeps the keys to all of the above out of source code and version control.

Removing any one layer doesn’t necessarily break the whole system immediately, but it removes exactly the protection that layer provided, hashing without validation still leaves mass assignment open, rate limiting without hashing still leaves a leaked database fully readable. Defense in depth means assuming any single layer might eventually fail or be bypassed, and not relying on just one.

Try It

  1. Move a hardcoded secret from an earlier module’s example into a .env file, load it with dotenv, and confirm the example still works identically.
  2. Add .env to a .gitignore file, and explain, in one sentence, what would go wrong if it were committed instead.
  3. For each of the six layers listed above, write one sentence describing specifically what kind of attack or mistake it protects against, in your own words.
  4. Pick any two layers, and describe a scenario where the first layer failing would still leave the second layer protecting the system.

Recap

  • Secrets, JWT_SECRET, database URLs, session secrets, don’t belong in source code, dotenv loads them from a .env file that’s never committed to version control.
  • Losing a secret from version control history is effectively permanent, prevention matters more than cleanup here.
  • This course’s protections form layers, hashing, session or token authentication, authorization, validation, rate limiting, and secrets management, each covering a different failure mode, together forming defense in depth.

Next lesson: exercises, combining a rate limiter and environment-loaded secrets into a single hardened login route.