Secrets Management

⭐ Interview Importance: LOW
⏱️ Revision Time: 10 min

Secrets Management involves securely storing, retrieving, and handling highly sensitive information (like API keys, database passwords, and cryptographic certificates) so they are never exposed in source code or easily accessible to unauthorized users.

Overview

While storing a database password in a .env file on your local machine is fine for development, it is often insufficient for enterprise production environments.

If a hacker gains access to your production server’s filesystem, they can read the .env file. To mitigate this, large applications use external Secrets Managers (like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault) to fetch secrets dynamically at runtime.

Key Concepts

  • Secrets Manager: An external, highly secure service designed solely to store encrypted strings.
  • Runtime Injection: Fetching the secret via an API call when the application boots up, keeping it in memory rather than on disk.
  • Rotation: The ability for a Secrets Manager to automatically change passwords every 30 days without requiring you to redeploy your application.

Code Examples

1. Integrating AWS Secrets Manager in NestJS

This pattern demonstrates how to fetch secrets asynchronously during application bootstrap before standard modules load.

Because we need these secrets before AppModule fully initializes, we often write a custom configuration factory that makes an HTTP request to the secrets manager.

// aws-secrets.config.ts
import { registerAs } from '@nestjs/config';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

export default registerAs('database', async () => {
  // If we are local, just use the .env file to save time/money
  if (process.env.NODE_ENV === 'development') {
    return {
      password: process.env.DATABASE_PASSWORD,
    };
  }

  // If in production, fetch from AWS Secrets Manager
  const client = new SecretsManagerClient({ region: 'us-east-1' });
  const command = new GetSecretValueCommand({ SecretId: 'prod/db/password' });
  
  try {
    const response = await client.send(command);
    // Secrets are often stored as JSON strings in AWS
    const secrets = JSON.parse(response.SecretString);
    
    return {
      password: secrets.password,
    };
  } catch (error) {
    console.error('Failed to fetch secrets!', error);
    throw error; // Crash the app. We can't run without the DB password.
  }
});

2. Loading the Async Configuration

You use the ConfigModule exactly as you would with a synchronous custom configuration file. NestJS is smart enough to wait for the Promise to resolve.

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import databaseConfig from './aws-secrets.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      // NestJS will await the factory function before booting the rest of the app!
      load: [databaseConfig], 
    }),
  ],
})
export class AppModule {}

Best Practices

  • Never Log Secrets: Be incredibly careful not to console.log(config) when debugging, as cloud logging providers (like DataDog or CloudWatch) will permanently ingest those secrets in plain text.
  • Fail Fast: If the Secrets Manager is down or the API call fails, the application should crash immediately. Do not attempt to fall back to a default password or continue booting.
  • Least Privilege: The IAM Role (or service account) that your NestJS application uses to talk to the Secrets Manager should ONLY have permission to read the specific secrets it needs, not all secrets in the cloud account.