whchi
Posted on September 11, 2024
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
💖 💪 🙅 🚩
whchi
Posted on September 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.