Integrate Self-host Infisical into your NestJS project
whchi

whchi @whchi

Location:
Taiwan
Joined:
Jul 26, 2022

Integrate Self-host Infisical into your NestJS project

Publish Date: Sep 11 '24
4 0

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

Comments 0 total

    Add comment