Why centralized secret management is necessary
In modern software development, especially in containerized and collaborative environments, centralized secret management has become increasingly important. Here are
Flexibility in containerized deployment
- Real-time environment variable updates: In containerized deployments, centralized secret management allows us to easily update environment variables without rebuilding or redeploying containers. This greatly improves system flexibility and security.
- Environment consistency: It ensures all container instances use the same up-to-date secrets, reducing problems caused by environment inconsistencies.
Convenience in multi-developer scenarios
- Avoiding
.env
file transfers: Traditionally, developers might need to send.env
files via email or messaging apps, which is not only insecure but can also lead to version confusion. - Permission management: Centralized management allows us to set different access permissions for different team members, enhancing security.
- Version control: You can track the change history of secrets, making audits and rollbacks easier. two main reasons:
A little about Infisical
Infisical is a secret management service similar to HashiCorp Vault, but it focuses more on the developer experience.
Advantages of Infisical
- User-friendly: Offers an intuitive web interface and CLI tools, making secret management simple.
- Integration with development workflows: Provides SDKs in multiple languages, making it easy to integrate into existing projects.
- Team collaboration: Supports secure sharing and management of secrets among team members.
Paid features
- Advanced audit logs
- Custom roles and more granular permission controls
- SAML single sign-on
- Advanced key rotation strategies
Writing a NestJS Module to integrate Infisical
First, install the necessary dependency:
npm install @infisical/sdk
Then, create a new infisical.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common';
import { InfisicalClient } from '@infisical/sdk';
import { InfisicalService } from './infisical.service';
import { InfisicalModuleOptions } from './infisical-module-options.type';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Global()
@Module({})
export class InfisicalModule {
static forRoot(options: InfisicalModuleOptions): DynamicModule {
return {
imports: [
// fallback to dotenv
ConfigModule.forRoot({
envFilePath: options.fallbackFile,
}),
],
module: InfisicalModule,
providers: [
{
provide: 'INFISICAL_OPTIONS',
useValue: { ...options },
},
{
provide: InfisicalClient,
useFactory: (config: ConfigService) => {
return new InfisicalClient({
siteUrl: config.get<string>('INFISICAL_SITE_URL'),
auth: {
universalAuth: {
clientId: config.get<string>('INFISICAL_CLIENT_ID', ''),
clientSecret: config.get<string>('INFISICAL_CLIENT_SECRET', ''),
},
},
});
},
inject: [ConfigService],
},
InfisicalService,
],
exports: [InfisicalService],
};
}
}
The infisical.service.ts
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InfisicalClient } from '@infisical/sdk';
import { InfisicalModuleOptions } from './infisical-module-options.type';
@Injectable()
export class InfisicalService implements OnModuleInit {
private logger = new Logger(InfisicalService.name);
private fallbackToConfig = false;
private secrets: Record<string, string | boolean | undefined> = {};
private readonly initializationPromise: Promise<void>;
private readonly PROCESS_ENVS: string[] = [
'DATABASE_URL',
'GOOGLE_APPLICATION_CREDENTIALS',
];
constructor(
private readonly config: ConfigService,
private readonly client: InfisicalClient,
@Inject('INFISICAL_OPTIONS') private readonly options: InfisicalModuleOptions,
) {
this.initializationPromise = this.init();
}
async onModuleInit() {
await this.initializationPromise;
}
private async init() {
if (!this.config.get<string>('INFISICAL_SITE_URL')) {
this.logger.log('Use config from ConfigService');
this.fallbackToConfig = true;
return;
}
try {
const secrets = await this.client.listSecrets({
environment: this.config.get<string>('INFISICAL_ENV', ''),
projectId: this.config.get<string>('INFISICAL_PROJECT_ID', ''),
path: this.options.path || '/', // path to infisical project's path
includeImports: true,
});
secrets.forEach(secret => {
this.secrets[secret.secretKey] = secret.secretValue;
if (this.PROCESS_ENVS.includes(secret.secretKey)) {
// ENVs where should load directly into process
// like prisma's DATABASE_URL & google cloud credential
process.env[secret.secretKey] = secret.secretValue;
}
});
this.logger.log('Secrets loaded from Infisical');
} catch (error) {
this.logger.warn(
'Failed to fetch secrets from Infisical, falling back to ConfigService',
);
this.fallbackToConfig = true;
}
}
public get<T = string>(key: string): T {
if (this.fallbackToConfig) {
return this.config.get<T>(key) as T;
}
if (Object.keys(this.secrets).length > 0) {
return this.secrets[key] as T;
}
const value = this.secrets[key];
if (value === undefined) {
return this.config.get<T>(key) as T;
}
return value as T;
}
}
The infisical-module-options.type
export type InfisicalModuleOptions = {
path?: string;
fallbackFile?: string | string[];
};
Use it
Write env in your dotenv
INFISICAL_ENV=dev # the slot of environments
INFISICAL_PROJECT_ID=<your-infisical-project-id>
INFISICAL_SITE_URL=<your-infisical-site-url>
INFISICAL_CLIENT_ID=<your-infisical-client-id>
INFISICAL_CLIENT_SECRET=<your-infisical-client-secret>
And import it into your app.module.ts
@Module({
imports: [InfisicalModule.forRoot({path: '/'})]
})
Then, you can use it as ConfigService
of nestjs
infisicalService.get<string>('YOUR_ENV_SETUP_IN_INFISICAL')
That is
Top comments (0)