Integrate Self-host Infisical into your NestJS project

whchi

whchi

Posted on September 11, 2024

Integrate Self-host Infisical into your NestJS project

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
Enter fullscreen mode Exit fullscreen mode

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],
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

The infisical-module-options.type

export type InfisicalModuleOptions = {
  path?: string;
  fallbackFile?: string | string[];
};

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
  • INFISICAL_ENV
    INFISICAL_ENV

  • INFISICAL_PROJECT_ID
    INFISICAL_PROJECT_ID

  • INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET
    id and secret

And import it into your app.module.ts

@Module({
  imports: [InfisicalModule.forRoot({path: '/'})]
})
Enter fullscreen mode Exit fullscreen mode

Then, you can use it as ConfigService of nestjs

infisicalService.get<string>('YOUR_ENV_SETUP_IN_INFISICAL')
Enter fullscreen mode Exit fullscreen mode

That is

💖 💪 🙅 🚩
whchi
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.

Related