secrets-injector
v2.0.0
Published
NestJS AWS Secrets Injector
Downloads
265
Maintainers
Readme
Secrets-Injector
Secrets-Injector is a NestJS module that reads secrets from AWS Secrets Manager and, if you want, copies them into process.env while your application starts. You keep sensitive configuration in Secrets Manager instead of .env files, and read it with process.env or @nestjs/config.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Usage
- Options
- Secret format
- Errors and timeouts
- Local development and tests
- Upgrading from 1.x
- Contributing
- Stay in Touch
- License
Features
- Read JSON secrets from AWS Secrets Manager by name or ARN.
- Copy the key/value pairs of one or more secrets into
process.env: inmain.tsbefore the app is created (loadSecretsToEnv()), or while Nest creates the app (isSetToEnv). forRoot()andforRootAsync()(useFactory,useClass,useExisting), global module.- Uses the AWS SDK v3 client you pass in, so region, credentials and endpoint are yours to configure. Every call has a deadline (10 s by default), so an unreachable endpoint cannot block startup.
Requirements
- NestJS 10, 11 or 12 (
@nestjs/common) @aws-sdk/client-secrets-managerv3- Node.js 20.19+, 22.12+ or 24+. NestJS 12 is published as ES modules only; this package is CommonJS and loads it with
require(), which works without flags from Node 20.19 and 22.12.
The AWS SDK is a peer dependency, so you choose its version. AWS SDK v3 releases published after the first week of January 2027 require Node 22 or newer (current releases already print a warning on Node 20). On Node 20, pin @aws-sdk/client-secrets-manager to a release from before then, or move to Node 22.
Installation
npm i secrets-injector @aws-sdk/client-secrets-managerThe repository also has a sample app in samples/quick-start-secrets-to-env (NestJS 12; its Nest CLI needs Node 22.22+ or 24.15+):
git clone https://github.com/Anubhavjain786/secrets-injector.git
cd secrets-injector
npm install # also builds dist
cd samples/quick-start-secrets-to-env
npm install
AWS_REGION=ap-south-1 SECRET_ID=test/sm npm run start:devQuick start
Import AWSSecretsManagerModule into the root AppModule and configure it with forRoot(). It takes an AWSSecretsManagerModuleOptions object. The module is global: AWSSecretsService can be injected anywhere.
import { Module } from '@nestjs/common';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import {
AWSSecretsManagerModule,
AWSSecretsManagerModuleOptions,
} from 'secrets-injector';
import { AppService } from './app.service';
import { AppController } from './app.controller';
import { AWSDBCredentialsService } from './aws-secrets.service';
const AWSSecretsManagerProps: AWSSecretsManagerModuleOptions = {
secretsManager: new SecretsManagerClient({
region: 'ap-south-1',
}),
};
@Module({
imports: [AWSSecretsManagerModule.forRoot(AWSSecretsManagerProps)],
controllers: [AppController],
providers: [AppService, AWSDBCredentialsService],
})
export class AppModule {}The client uses the normal AWS SDK credential chain (environment variables, shared config, IAM role, ...). It needs secretsmanager:GetSecretValue on the secrets you read (and kms:Decrypt if they use a customer managed KMS key).
Usage
Read secrets with AWSSecretsService
getSecretsByID() fetches one secret by name or ARN and returns its parsed JSON value:
import { Injectable } from '@nestjs/common';
import { AWSSecretsService } from 'secrets-injector';
interface DBCredentials {
host: string;
port: number;
user: string;
password: string;
database: string;
}
@Injectable()
export class AWSDBCredentialsService {
constructor(private readonly secretsRetrieverService: AWSSecretsService) {}
async getDBCredentials(): Promise<DBCredentials | undefined> {
// 'db-credentials' is the secret name (or ARN)
return this.secretsRetrieverService.getSecretsByID<DBCredentials>(
'db-credentials',
);
}
}getAllSecrects() fetches every secret in secretsSource and returns their keys merged into one object (later secrets win). Both return undefined if a secret cannot be fetched or parsed (see Errors and timeouts).
Set process env variables from AWS Secrets Manager
With isSetToEnv: true and a secretsSource, the module fetches the secrets once while Nest creates the app and copies every key into process.env:
import { Module } from '@nestjs/common';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import {
AWSSecretsManagerModule,
AWSSecretsManagerModuleOptions,
} from 'secrets-injector';
import { AppService } from './app.service';
import { AppController } from './app.controller';
const AWSSecretsManagerProps: AWSSecretsManagerModuleOptions = {
secretsManager: new SecretsManagerClient({
region: 'ap-south-1',
}),
isSetToEnv: true, // copy the secrets into process.env
secretsSource: 'test/sm', // or a list of names/ARNs: ['db/prod/config', 'app/prod/config']
};
@Module({
imports: [AWSSecretsManagerModule.forRoot(AWSSecretsManagerProps)],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Existing environment variables with the same name are overwritten.
When are the variables available?
With isSetToEnv the secrets are loaded while Nest creates the providers. Nest creates the providers of all modules at the same time, and only the ones that inject AWSSecretsService wait for the secrets. So the variables are available:
- in
process.envonceawait NestFactory.create(AppModule)(orcreateApplicationContext()) has resolved: inmain.tsafter that line, in request handlers, inonModuleInit()and later; - in providers and async factories that inject
AWSSecretsService, throughprocess.envorConfigService.get('DB_HOST')(example below).
They are not available, or not up to date, in these cases:
- code that reads
process.envwhen a file is loaded, in the arguments ofSomeModule.forRoot({ ... }), or in a provider that does not injectAWSSecretsService; registerAs()/ConfigModule.forRoot({ load })factories: they run while the secrets are still loading, soconfig.get('database.host')staysundefined, also aftercreate();ConfigModule.forRoot({ validate })orvalidationSchema: validation runs when the module is imported, so required keys fail. A key that already had a value at that point (from.env, the shell or a schema default) keeps that old value inConfigService.get(), even thoughprocess.envhas the secret.
For those cases, load the secrets in main.ts instead.
Example of an async factory that waits for the secrets. AWSSecretsService is only injected to make Nest create the provider after the secrets are loaded. The same works for other modules' async factories, for example TypeOrmModule.forRootAsync({ inject: [ConfigService, AWSSecretsService], ... }).
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AWSSecretsService } from 'secrets-injector';
export const DB_OPTIONS = 'DB_OPTIONS';
@Module({
imports: [ConfigModule],
providers: [
{
provide: DB_OPTIONS,
inject: [ConfigService, AWSSecretsService],
useFactory: (config: ConfigService) => ({
host: config.get<string>('DB_HOST'),
password: config.get<string>('DB_PASSWORD'),
}),
},
],
exports: [DB_OPTIONS],
})
export class DatabaseConfigModule {}Load secrets before the app is created
loadSecretsToEnv() fetches the secrets and writes them to process.env without Nest. Call it in main.ts and load AppModule only afterwards, so that every module, ConfigModule validation and registerAs() factories see the variables. Do not import AppModule at the top of main.ts: imports run before the rest of the file, so the module files (and ConfigModule.forRoot()) would run before the secrets are there.
import { NestFactory } from '@nestjs/core';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { loadSecretsToEnv } from 'secrets-injector';
async function bootstrap() {
const loaded = await loadSecretsToEnv({
secretsManager: new SecretsManagerClient({
region: process.env.AWS_REGION,
}),
secretsSource: process.env.AWS_SECRET_ID ?? 'app/prod/config',
});
if (!loaded) {
process.exit(1); // or start without the secrets
}
// load the application only now
const { AppModule } = await import('./app.module');
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();It takes the same secretsManager, secretsSource, isDebug and timeout options as the module, logs errors instead of throwing, and resolves to true when the variables were set. You can still import AWSSecretsManagerModule (without isSetToEnv) to use AWSSecretsService in the app.
Async configuration
Use forRootAsync() when the options depend on other providers, for example ConfigService. imports lists the modules whose providers you need; inject lists the providers passed to useFactory. If ConfigModule is global (isGlobal: true) you can leave out imports.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { AWSSecretsManagerModule } from 'secrets-injector';
import { AppService } from './app.service';
import { AppController } from './app.controller';
@Module({
imports: [
ConfigModule.forRoot(),
AWSSecretsManagerModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secretsManager: new SecretsManagerClient({
region: configService.get('AWS_REGION'),
}),
isSetToEnv: true,
secretsSource: [configService.get('AWS_SECRET_ID')],
isDebug: configService.get('NODE_ENV') === 'development',
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}The factory can be async. Instead of useFactory you can pass a class that implements AWSSecretsManagerModuleOptionsFactory, with useClass (the module creates it) or useExisting (an existing provider from one of the imports):
import { Injectable, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import {
AWSSecretsManagerModule,
AWSSecretsManagerModuleOptions,
AWSSecretsManagerModuleOptionsFactory,
} from 'secrets-injector';
@Injectable()
export class SecretsConfig implements AWSSecretsManagerModuleOptionsFactory {
constructor(private readonly config: ConfigService) {}
createAWSSecrectsManagerModuleOptions(): AWSSecretsManagerModuleOptions {
return {
secretsManager: new SecretsManagerClient({
region: this.config.get('AWS_REGION'),
}),
isSetToEnv: true,
secretsSource: this.config.get('AWS_SECRET_ID'),
};
}
}
@Module({
imports: [
ConfigModule.forRoot(),
AWSSecretsManagerModule.forRootAsync({
imports: [ConfigModule],
useClass: SecretsConfig,
}),
],
})
export class AppModule {}Options
The options of AWSSecretsManagerModule are the AWSSecretsManagerModuleOptions interface:
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
export interface AWSSecretsManagerModuleOptions {
secretsManager: SecretsManagerClient;
isSetToEnv?: boolean;
secretsSource?: string | ReadonlyArray<string | undefined>;
isDebug?: boolean;
timeout?: number;
}| Option | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| secretsManager | A SecretsManagerClient from @aws-sdk/client-secrets-manager. Required. |
| isSetToEnv | Copy the secrets in secretsSource into process.env at startup. Default false. |
| secretsSource | Secret name or ARN, or a list of them. Empty and undefined entries are skipped. Used by isSetToEnv and getAllSecrects(). |
| isDebug | Log the loaded secrets including their values after they are set to env. Do not enable in production. |
| timeout | Maximum time in ms for each Secrets Manager call made by this package, SDK retries included. Default 10000; 0 turns it off (only your client's timeouts apply). |
The package root exports AWSSecretsManagerModule, AWSSecretsService, loadSecretsToEnv, the options token AWS_SECRETS_MANAGER_MODULE_OPTIONS and these types:
import {
AWSSecretsManagerModuleOptions,
AWSSecretsManagerModuleAsyncOptions,
AWSSecretsManagerModuleOptionsFactory,
LoadSecretsToEnvOptions,
SecretsSource,
} from 'secrets-injector';Secret format
Each secret must be stored as a JSON object in SecretString (the "Key/value" type in the AWS console), for example {"DB_HOST": "db.internal", "DB_PORT": 5432}. With several secrets the keys are merged in order and later secrets win. process.env only holds strings, so 5432 becomes "5432" and true becomes "true". Plain-text and binary secrets are rejected with an error that names the secret but never contains its value.
Errors and timeouts
Before fetching, the module logs Loading N secret(s) from AWS Secrets Manager (id: [...]) (ids only). It does not throw when Secrets Manager cannot be reached, a secret is missing, or a secret is not a JSON object: it logs Unable to fetch secrets(secret <id>: ...) through the Nest logger, getSecretsByID() / getAllSecrects() return undefined, loadSecretsToEnv() resolves to false, and with isSetToEnv the application starts without the variables. If one secret in secretsSource fails, none of them is set. Check for required variables yourself if the app cannot run without them.
The AWS SDK has no connection or request timeout by default. If the endpoint accepts the connection but never answers, or the connection never completes (for example a private subnet without a NAT gateway or a Secrets Manager VPC endpoint), a call would wait forever and the app would never start. This package therefore gives every call a deadline, timeout (default 10 s, SDK retries included). When it passes, the call is aborted and handled like any other error: Unable to fetch secrets(secret <id>: no answer from AWS Secrets Manager within 10000 ms). Secrets are fetched in parallel, so startup waits at most about timeout. Error messages never contain secret values; only isDebug logs them.
Local development and tests
Point the client at a local Secrets Manager such as LocalStack or moto with the endpoint option, or set AWS_ENDPOINT_URL_SECRETS_MANAGER:
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
export const secretsManager = new SecretsManagerClient({
region: 'us-east-1',
endpoint: 'http://localhost:4566',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
});Upgrading from 1.x
- NestJS 7, 8 and 9 are no longer supported; use NestJS 10, 11 or 12.
reflect-metadatais now a peer dependency (every Nest app has it). - Node.js 20.19+, 22.12+ or 24+ is required.
isSetToEnvis now awaited:NestFactory.create()resolves after the secrets are inprocess.env, and providers that injectAWSSecretsServiceare created after them. In 1.x the secrets were fetched in the background and usually arrived after the app had started. Startup now waits for Secrets Manager, at most abouttimeout(10 s by default).- Each Secrets Manager call made by the package has a 10 s deadline (
timeout). Settimeout: 0to rely only on your client's own timeouts. new AWSSecretsService(options)no longer starts loading the secrets in its constructor. If you create the service yourself, callawait service.setAllSecrectToEnv()or useloadSecretsToEnv(options).getSecretsByID()andgetAllSecrects()are typedPromise<T | undefined>, because they returnundefinedon errors. WithstrictNullChecksyou may need to handleundefined.- Error messages name the secret (
secret <id>: ...) and no longer include the parser message, which quoted part of a plain-text secret value. forRootAsync()exports onlyAWSSecretsService, likeforRoot()(it used to export its internal providers too), supportsuseExisting, and throws if none ofuseFactory,useClassoruseExistingis given.- The files in
dist/have the same names as in 1.x, so deep imports such assecrets-injector/dist/contstantskeep working. The options token is now also exported from the package root asAWS_SECRETS_MANAGER_MODULE_OPTIONS; prefer that.
Contributing
New features and bugfixes are always welcome! In order to contribute to this project, follow a few easy steps:
- Fork this repository and clone it on your machine
- Open the local repository with Visual Studio Code with the remote development feature enabled (install the Remote Development extension)
- Create a branch (e.g.,
my-awesome-feature) and make your changes. - Run the following commands to ensure the code is formatted and passes lint checks:
npm run lint
npm run format
npm run check-types
npm run build- Push your changes and open a pull request
Stay in touch
- Author - Anubhav Jain
- Github - Anubhavjain786
- Twitter - @anubhavjain660
License
secrets-injector is MIT licensed. See LICENSE.
