Merge pull request #2671 from Infisical/daniel/hsm

feat: hardware security module's support
This commit is contained in:
Maidul Islam
2024-11-11 15:38:02 -07:00
committed by GitHub
36 changed files with 1467 additions and 111 deletions

View File

@@ -1,62 +1,115 @@
name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*-postgres"
push:
tags:
- "infisical/v*.*.*-postgres"
jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
infisical-fips-standalone:
name: Build infisical standalone image postgres
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical-fips:latest-postgres
infisical/infisical-fips:${{ steps.commit.outputs.short }}
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.fips.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View File

@@ -6,3 +6,4 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134

View File

@@ -0,0 +1,167 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-slim AS base
FROM base AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM base AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
# Production image
FROM base AS frontend-runner
WORKDIR /app
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM base AS backend-build
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
WORKDIR /app
# Required for pkcs11js
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm i -D tsconfig-paths
RUN npm run build
# Production stage
FROM base AS backend-runner
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /app
# Required for pkcs11js
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage
FROM base AS production
# Install necessary packages
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Install Infisical CLI
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.31.1 \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
# Give non-root-user permission to update SSL certs
RUN chown -R non-root-user /etc/ssl/certs
RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
RUN chmod -R u+rwx /etc/ssl/certs
RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
RUN chown non-root-user /usr/sbin/update-ca-certificates
RUN chmod u+rx /usr/sbin/update-ca-certificates
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
ENV PORT 8080
ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /backend
ENV TELEMETRY_ENABLED true
EXPOSE 8080
EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

View File

@@ -72,6 +72,9 @@ RUN addgroup --system --gid 1001 nodejs \
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./
RUN npm ci --only-production
@@ -85,6 +88,9 @@ FROM base AS backend-runner
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./
RUN npm ci --only-production

View File

@@ -3,6 +3,12 @@ FROM node:20-alpine AS build
WORKDIR /app
# Required for pkcs11js
RUN apk --update add \
python3 \
make \
g++
COPY package*.json ./
RUN npm ci --only-production
@@ -11,12 +17,17 @@ RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./
RUN apk --update add \
python3 \
make \
g++
RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app .

View File

@@ -1,5 +1,44 @@
FROM node:20-alpine
# ? Setup a test SoftHSM module. In production a real HSM is used.
ARG SOFTHSM2_VERSION=2.5.0
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
SOFTHSM2_SOURCES=/tmp/softhsm2
# install build dependencies including python3
RUN apk --update add \
alpine-sdk \
autoconf \
automake \
git \
libtool \
openssl-dev \
python3 \
make \
g++
# build and install SoftHSM2
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
WORKDIR ${SOFTHSM2_SOURCES}
RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
&& sh autogen.sh \
&& ./configure --prefix=/usr/local --disable-gost \
&& make \
&& make install
WORKDIR /root
RUN rm -fr ${SOFTHSM2_SOURCES}
# install pkcs11-tool
RUN apk --update add opensc
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
# ? App setup
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add --no-cache git

View File

@@ -16,6 +16,7 @@ import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
@@ -54,7 +55,12 @@ export default {
const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL);
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
const hsmModule = initializeHsmModule();
hsmModule.initialize();
const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule() });
// @ts-expect-error type
globalThis.testServer = server;
// @ts-expect-error type

View File

@@ -83,6 +83,7 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkcs11js": "^2.1.6",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2",
"probot": "^13.3.8",
@@ -120,6 +121,7 @@
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
@@ -8830,6 +8832,17 @@
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
"dev": true
},
"node_modules/@types/pkcs11js": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/pkcs11js/-/pkcs11js-1.0.4.tgz",
"integrity": "sha512-Pkq8VbwZZv7o/6ODFOhxw0s0M8J4ucg4/I4V1dSCn8tUwWgIKIYzuV4Pp2fYuir81DgQXAF5TpGyhBMjJ3FjFw==",
"deprecated": "This is a stub types definition for pkcs11js (https://github.com/PeculiarVentures/pkcs11js). pkcs11js provides its own type definitions, so you don't need @types/pkcs11js installed!",
"dev": true,
"license": "MIT",
"dependencies": {
"pkcs11js": "*"
}
},
"node_modules/@types/prompt-sync": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
@@ -17066,6 +17079,20 @@
"node": ">= 6"
}
},
"node_modules/pkcs11js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-2.1.6.tgz",
"integrity": "sha512-+t5jxzB749q8GaEd1yNx3l98xYuaVK6WW/Vjg1Mk1Iy5bMu/A5W4O/9wZGrpOknWF6lFQSb12FXX+eSNxdriwA==",
"hasInstallScript": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/PeculiarVentures"
}
},
"node_modules/pkg-conf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz",
@@ -19761,9 +19788,10 @@
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
"license": "0BSD"
},
"node_modules/tsup": {
"version": "8.0.1",

View File

@@ -84,6 +84,7 @@
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
@@ -188,6 +189,7 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkcs11js": "^2.1.6",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2",
"probot": "^13.3.8",

View File

@@ -44,6 +44,7 @@ import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
@@ -184,6 +185,7 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory;
hsm: THsmServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;

View File

@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
if (!hasEncryptionStrategy) t.string("encryptionStrategy").defaultTo("SOFTWARE");
if (!hasTimestampsCol) t.timestamps(true, true, true);
});
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptionStrategy = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "encryptionStrategy");
const hasTimestampsCol = await knex.schema.hasColumn(TableName.KmsServerRootConfig, "createdAt");
await knex.schema.alterTable(TableName.KmsServerRootConfig, (t) => {
if (hasEncryptionStrategy) t.dropColumn("encryptionStrategy");
if (hasTimestampsCol) t.dropTimestamps(true);
});
}

View File

@@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer
encryptedRootKey: zodBuffer,
encryptionStrategy: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;

View File

@@ -0,0 +1,58 @@
import * as pkcs11js from "pkcs11js";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { HsmModule } from "./hsm-types";
export const initializeHsmModule = () => {
const appCfg = getConfig();
// Create a new instance of PKCS11 module
const pkcs11 = new pkcs11js.PKCS11();
let isInitialized = false;
const initialize = () => {
if (!appCfg.isHsmConfigured) {
return;
}
try {
// Load the PKCS#11 module
pkcs11.load(appCfg.HSM_LIB_PATH!);
// Initialize the module
pkcs11.C_Initialize();
isInitialized = true;
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error("Failed to initialize PKCS#11 module:", err);
throw err;
}
};
const finalize = () => {
if (isInitialized) {
try {
pkcs11.C_Finalize();
isInitialized = false;
logger.info("PKCS#11 module finalized");
} catch (err) {
logger.error("Failed to finalize PKCS#11 module:", err);
throw err;
}
}
};
const getModule = (): HsmModule => ({
pkcs11,
isInitialized
});
return {
initialize,
finalize,
getModule
};
};

View File

@@ -0,0 +1,470 @@
import pkcs11js from "pkcs11js";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { HsmKeyType, HsmModule } from "./hsm-types";
type THsmServiceFactoryDep = {
hsmModule: HsmModule;
};
export type THsmServiceFactory = ReturnType<typeof hsmServiceFactory>;
type SyncOrAsync<T> = T | Promise<T>;
type SessionCallback<T> = (session: pkcs11js.Handle) => SyncOrAsync<T>;
// eslint-disable-next-line no-empty-pattern
export const hsmServiceFactory = ({ hsmModule: { isInitialized, pkcs11 } }: THsmServiceFactoryDep) => {
const appCfg = getConfig();
// Constants for buffer structures
const IV_LENGTH = 16; // Luna HSM typically expects 16-byte IV for cbc
const BLOCK_SIZE = 16;
const HMAC_SIZE = 32;
const AES_KEY_SIZE = 256;
const HMAC_KEY_SIZE = 256;
const $withSession = async <T>(callbackWithSession: SessionCallback<T>): Promise<T> => {
const RETRY_INTERVAL = 200; // 200ms between attempts
const MAX_TIMEOUT = 90_000; // 90 seconds maximum total time
let sessionHandle: pkcs11js.Handle | null = null;
const removeSession = () => {
if (sessionHandle !== null) {
try {
pkcs11.C_Logout(sessionHandle);
pkcs11.C_CloseSession(sessionHandle);
logger.info("HSM: Terminated session successfully");
} catch (error) {
logger.error(error, "HSM: Failed to terminate session");
} finally {
sessionHandle = null;
}
}
};
try {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
// Get slot list
let slots: pkcs11js.Handle[];
try {
slots = pkcs11.C_GetSlotList(false); // false to get all slots
} catch (error) {
throw new Error(`Failed to get slot list: ${(error as Error)?.message}`);
}
if (slots.length === 0) {
throw new Error("No slots available");
}
if (appCfg.HSM_SLOT >= slots.length) {
throw new Error(`HSM slot ${appCfg.HSM_SLOT} not found or not initialized`);
}
const slotId = slots[appCfg.HSM_SLOT];
const startTime = Date.now();
while (Date.now() - startTime < MAX_TIMEOUT) {
try {
// Open session
// eslint-disable-next-line no-bitwise
sessionHandle = pkcs11.C_OpenSession(slotId, pkcs11js.CKF_SERIAL_SESSION | pkcs11js.CKF_RW_SESSION);
// Login
try {
pkcs11.C_Login(sessionHandle, pkcs11js.CKU_USER, appCfg.HSM_PIN);
logger.info("HSM: Successfully authenticated");
break;
} catch (error) {
// Handle specific error cases
if (error instanceof pkcs11js.Pkcs11Error) {
if (error.code === pkcs11js.CKR_PIN_INCORRECT) {
// We throw instantly here to prevent further attempts, because if too many attempts are made, the HSM will potentially wipe all key material
logger.error(error, `HSM: Incorrect PIN detected for HSM slot ${appCfg.HSM_SLOT}`);
throw new Error("HSM: Incorrect HSM Pin detected. Please check the HSM configuration.");
}
if (error.code === pkcs11js.CKR_USER_ALREADY_LOGGED_IN) {
logger.warn("HSM: Session already logged in");
}
}
throw error; // Re-throw other errors
}
} catch (error) {
logger.warn(`HSM: Session creation failed. Retrying... Error: ${(error as Error)?.message}`);
if (sessionHandle !== null) {
try {
pkcs11.C_CloseSession(sessionHandle);
} catch (closeError) {
logger.error(closeError, "HSM: Failed to close session");
}
sessionHandle = null;
}
// Wait before retrying
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, RETRY_INTERVAL);
});
}
}
if (sessionHandle === null) {
throw new Error("HSM: Failed to open session after maximum retries");
}
// Execute callback with session handle
const result = await callbackWithSession(sessionHandle);
removeSession();
return result;
} catch (error) {
logger.error(error, "HSM: Failed to open session");
throw error;
} finally {
// Ensure cleanup
removeSession();
}
};
const $findKey = (sessionHandle: pkcs11js.Handle, type: HsmKeyType) => {
const label = type === HsmKeyType.HMAC ? `${appCfg.HSM_KEY_LABEL}_HMAC` : appCfg.HSM_KEY_LABEL;
const keyType = type === HsmKeyType.HMAC ? pkcs11js.CKK_GENERIC_SECRET : pkcs11js.CKK_AES;
const template = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: keyType },
{ type: pkcs11js.CKA_LABEL, value: label }
];
try {
// Initialize search
pkcs11.C_FindObjectsInit(sessionHandle, template);
try {
// Find first matching object
const handles = pkcs11.C_FindObjects(sessionHandle, 1);
if (handles.length === 0) {
throw new Error("Failed to find master key");
}
return handles[0]; // Return the key handle
} finally {
// Always finalize the search operation
pkcs11.C_FindObjectsFinal(sessionHandle);
}
} catch (error) {
return null;
}
};
const $keyExists = (session: pkcs11js.Handle, type: HsmKeyType): boolean => {
try {
const key = $findKey(session, type);
// items(0) will throw an error if no items are found
// Return true only if we got a valid object with handle
return !!key && key.length > 0;
} catch (error) {
// If items(0) throws, it means no key was found
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call
logger.error(error, "HSM: Failed while checking for HSM key presence");
if (error instanceof pkcs11js.Pkcs11Error) {
if (error.code === pkcs11js.CKR_OBJECT_HANDLE_INVALID) {
return false;
}
}
return false;
}
};
const encrypt: {
(data: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(data: Buffer): Promise<Buffer>;
} = async (data: Buffer, providedSession?: pkcs11js.Handle) => {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
const $performEncryption = (sessionHandle: pkcs11js.Handle) => {
try {
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
if (!aesKey) {
throw new Error("HSM: Encryption failed, AES key not found");
}
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
if (!hmacKey) {
throw new Error("HSM: Encryption failed, HMAC key not found");
}
const iv = Buffer.alloc(IV_LENGTH);
pkcs11.C_GenerateRandom(sessionHandle, iv);
const encryptMechanism = {
mechanism: pkcs11js.CKM_AES_CBC_PAD,
parameter: iv
};
pkcs11.C_EncryptInit(sessionHandle, encryptMechanism, aesKey);
// Calculate max buffer size (input length + potential full block of padding)
const maxEncryptedLength = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE;
// Encrypt the data - this returns the encrypted data directly
const encryptedData = pkcs11.C_Encrypt(sessionHandle, data, Buffer.alloc(maxEncryptedLength));
// Initialize HMAC
const hmacMechanism = {
mechanism: pkcs11js.CKM_SHA256_HMAC
};
pkcs11.C_SignInit(sessionHandle, hmacMechanism, hmacKey);
// Sign the IV and encrypted data
pkcs11.C_SignUpdate(sessionHandle, iv);
pkcs11.C_SignUpdate(sessionHandle, encryptedData);
// Get the HMAC
const hmac = Buffer.alloc(HMAC_SIZE);
pkcs11.C_SignFinal(sessionHandle, hmac);
// Combine encrypted data and HMAC [Encrypted Data | HMAC]
const finalBuffer = Buffer.alloc(encryptedData.length + hmac.length);
encryptedData.copy(finalBuffer);
hmac.copy(finalBuffer, encryptedData.length);
return Buffer.concat([iv, finalBuffer]);
} catch (error) {
logger.error(error, "HSM: Failed to perform encryption");
throw new Error(`HSM: Encryption failed: ${(error as Error)?.message}`);
}
};
if (providedSession) {
return $performEncryption(providedSession);
}
const result = await $withSession($performEncryption);
return result;
};
const decrypt: {
(encryptedBlob: Buffer, providedSession: pkcs11js.Handle): Promise<Buffer>;
(encryptedBlob: Buffer): Promise<Buffer>;
} = async (encryptedBlob: Buffer, providedSession?: pkcs11js.Handle) => {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
const $performDecryption = (sessionHandle: pkcs11js.Handle) => {
try {
// structure is: [IV (16 bytes) | Encrypted Data (N bytes) | HMAC (32 bytes)]
const iv = encryptedBlob.subarray(0, IV_LENGTH);
const encryptedDataWithHmac = encryptedBlob.subarray(IV_LENGTH);
// Split encrypted data and HMAC
const hmac = encryptedDataWithHmac.subarray(-HMAC_SIZE); // Last 32 bytes are HMAC
const encryptedData = encryptedDataWithHmac.subarray(0, -HMAC_SIZE); // Everything except last 32 bytes
// Find the keys
const aesKey = $findKey(sessionHandle, HsmKeyType.AES);
if (!aesKey) {
throw new Error("HSM: Decryption failed, AES key not found");
}
const hmacKey = $findKey(sessionHandle, HsmKeyType.HMAC);
if (!hmacKey) {
throw new Error("HSM: Decryption failed, HMAC key not found");
}
// Verify HMAC first
const hmacMechanism = {
mechanism: pkcs11js.CKM_SHA256_HMAC
};
pkcs11.C_VerifyInit(sessionHandle, hmacMechanism, hmacKey);
pkcs11.C_VerifyUpdate(sessionHandle, iv);
pkcs11.C_VerifyUpdate(sessionHandle, encryptedData);
try {
pkcs11.C_VerifyFinal(sessionHandle, hmac);
} catch (error) {
logger.error(error, "HSM: HMAC verification failed");
throw new Error("HSM: Decryption failed"); // Generic error for failed verification
}
// Only decrypt if verification passed
const decryptMechanism = {
mechanism: pkcs11js.CKM_AES_CBC_PAD,
parameter: iv
};
pkcs11.C_DecryptInit(sessionHandle, decryptMechanism, aesKey);
const tempBuffer = Buffer.alloc(encryptedData.length);
const decryptedData = pkcs11.C_Decrypt(sessionHandle, encryptedData, tempBuffer);
// Create a new buffer from the decrypted data
return Buffer.from(decryptedData);
} catch (error) {
logger.error(error, "HSM: Failed to perform decryption");
throw new Error("HSM: Decryption failed"); // Generic error for failed decryption, to avoid leaking details about why it failed (such as padding related errors)
}
};
if (providedSession) {
return $performDecryption(providedSession);
}
const result = await $withSession($performDecryption);
return result;
};
// We test the core functionality of the PKCS#11 module that we are using throughout Infisical. This is to ensure that the user doesn't configure a faulty or unsupported HSM device.
const $testPkcs11Module = async (session: pkcs11js.Handle) => {
try {
if (!pkcs11 || !isInitialized) {
throw new Error("PKCS#11 module is not initialized");
}
if (!session) {
throw new Error("HSM: Attempted to run test without a valid session");
}
const randomData = pkcs11.C_GenerateRandom(session, Buffer.alloc(500));
const encryptedData = await encrypt(randomData, session);
const decryptedData = await decrypt(encryptedData, session);
const randomDataHex = randomData.toString("hex");
const decryptedDataHex = decryptedData.toString("hex");
if (randomDataHex !== decryptedDataHex && Buffer.compare(randomData, decryptedData)) {
throw new Error("HSM: Startup test failed. Decrypted data does not match original data");
}
return true;
} catch (error) {
logger.error(error, "HSM: Error testing PKCS#11 module");
return false;
}
};
const isActive = async () => {
if (!isInitialized || !appCfg.isHsmConfigured) {
return false;
}
let pkcs11TestPassed = false;
try {
pkcs11TestPassed = await $withSession($testPkcs11Module);
} catch (err) {
logger.error(err, "HSM: Error testing PKCS#11 module");
}
return appCfg.isHsmConfigured && isInitialized && pkcs11TestPassed;
};
const startService = async () => {
if (!appCfg.isHsmConfigured || !pkcs11 || !isInitialized) return;
try {
await $withSession(async (sessionHandle) => {
// Check if master key exists, create if not
const genericAttributes = [
{ type: pkcs11js.CKA_TOKEN, value: true }, // Persistent storage
{ type: pkcs11js.CKA_EXTRACTABLE, value: false }, // Cannot be extracted
{ type: pkcs11js.CKA_SENSITIVE, value: true }, // Sensitive value
{ type: pkcs11js.CKA_PRIVATE, value: true } // Requires authentication
];
if (!$keyExists(sessionHandle, HsmKeyType.AES)) {
// Template for generating 256-bit AES master key
const keyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_AES },
{ type: pkcs11js.CKA_VALUE_LEN, value: AES_KEY_SIZE / 8 },
{ type: pkcs11js.CKA_LABEL, value: appCfg.HSM_KEY_LABEL! },
{ type: pkcs11js.CKA_ENCRYPT, value: true }, // Allow encryption
{ type: pkcs11js.CKA_DECRYPT, value: true }, // Allow decryption
...genericAttributes
];
// Generate the key
pkcs11.C_GenerateKey(
sessionHandle,
{
mechanism: pkcs11js.CKM_AES_KEY_GEN
},
keyTemplate
);
logger.info(`HSM: Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
}
// Check if HMAC key exists, create if not
if (!$keyExists(sessionHandle, HsmKeyType.HMAC)) {
const hmacKeyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_SECRET_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_GENERIC_SECRET },
{ type: pkcs11js.CKA_VALUE_LEN, value: HMAC_KEY_SIZE / 8 }, // 256-bit key
{ type: pkcs11js.CKA_LABEL, value: `${appCfg.HSM_KEY_LABEL!}_HMAC` },
{ type: pkcs11js.CKA_SIGN, value: true }, // Allow signing
{ type: pkcs11js.CKA_VERIFY, value: true }, // Allow verification
...genericAttributes
];
// Generate the HMAC key
pkcs11.C_GenerateKey(
sessionHandle,
{
mechanism: pkcs11js.CKM_GENERIC_SECRET_KEY_GEN
},
hmacKeyTemplate
);
logger.info(`HSM: HMAC key created successfully with label: ${appCfg.HSM_KEY_LABEL}_HMAC`);
}
// Get slot info to check supported mechanisms
const slotId = pkcs11.C_GetSessionInfo(sessionHandle).slotID;
const mechanisms = pkcs11.C_GetMechanismList(slotId);
// Check for AES CBC PAD support
const hasAesCbc = mechanisms.includes(pkcs11js.CKM_AES_CBC_PAD);
if (!hasAesCbc) {
throw new Error(`Required mechanism CKM_AEC_CBC_PAD not supported by HSM`);
}
// Run test encryption/decryption
const testPassed = await $testPkcs11Module(sessionHandle);
if (!testPassed) {
throw new Error("PKCS#11 module test failed. Please ensure that the HSM is correctly configured.");
}
});
} catch (error) {
logger.error(error, "HSM: Error initializing HSM service:");
throw error;
}
};
return {
encrypt,
startService,
isActive,
decrypt
};
};

View File

@@ -0,0 +1,11 @@
import pkcs11js from "pkcs11js";
export type HsmModule = {
pkcs11: pkcs11js.PKCS11;
isInitialized: boolean;
};
export enum HsmKeyType {
AES = "AES",
HMAC = "hmac"
}

View File

@@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
hsm: false,
oidcSSO: false,
scim: false,
ldap: false,

View File

@@ -46,6 +46,7 @@ export type TFeatureSet = {
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
hsm: false;
oidcSSO: false;
scim: false;
ldap: false;

View File

@@ -163,10 +163,22 @@ const envSchema = z
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true")
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
// HSM
HSM_LIB_PATH: zpStr(z.string().optional()),
HSM_PIN: zpStr(z.string().optional()),
HSM_KEY_LABEL: zpStr(z.string().optional()),
HSM_SLOT: z.coerce.number().optional().default(0)
})
// To ensure that basic encryption is always possible.
.refine(
(data) => Boolean(data.ENCRYPTION_KEY) || Boolean(data.ROOT_ENCRYPTION_KEY),
"Either ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY must be defined."
)
.transform((data) => ({
...data,
DB_READ_REPLICAS: data.DB_READ_REPLICAS
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
: undefined,
@@ -175,10 +187,14 @@ const envSchema = z
isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development",
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
}));

View File

@@ -1,6 +1,8 @@
import dotenv from "dotenv";
import path from "path";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
@@ -53,13 +55,17 @@ const run = async () => {
const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore });
const hsmModule = initializeHsmModule();
hsmModule.initialize();
const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore });
const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line
process.on("SIGINT", async () => {
await server.close();
await db.destroy();
hsmModule.finalize();
process.exit(0);
});
@@ -67,6 +73,7 @@ const run = async () => {
process.on("SIGTERM", async () => {
await server.close();
await db.destroy();
hsmModule.finalize();
process.exit(0);
});

View File

@@ -14,6 +14,7 @@ import fastify from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
@@ -36,16 +37,19 @@ type TMain = {
logger?: Logger;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
};
// Run the server!
export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
const appCfg = getConfig();
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true,
connectionTimeout: 30 * 1000,
ignoreTrailingSlash: true
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true,
pluginTimeout: 40_000
}).withTypeProvider<ZodTypeProvider>();
server.setValidatorCompiler(validatorCompiler);
@@ -95,7 +99,7 @@ export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TM
await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {

View File

@@ -1,5 +1,4 @@
import { CronJob } from "cron";
// import { Redis } from "ioredis";
import { Knex } from "knex";
import { z } from "zod";
@@ -31,6 +30,8 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@@ -223,10 +224,18 @@ export const registerRoutes = async (
{
auditLogDb,
db,
hsmModule,
smtp: smtpService,
queue: queueService,
keyStore
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
}: {
auditLogDb?: Knex;
db: Knex;
hsmModule: HsmModule;
smtp: TSmtpService;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
}
) => {
const appCfg = getConfig();
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
@@ -352,14 +361,21 @@ export const registerRoutes = async (
projectDAL
});
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const hsmService = hsmServiceFactory({
hsmModule
});
const kmsService = kmsServiceFactory({
kmsRootConfigDAL,
keyStore,
kmsDAL,
internalKmsDAL,
orgDAL,
projectDAL
projectDAL,
hsmService
});
const externalKmsService = externalKmsServiceFactory({
kmsDAL,
kmsService,
@@ -556,6 +572,7 @@ export const registerRoutes = async (
userDAL,
authService: loginService,
serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
orgService,
keyStore,
licenseService,
@@ -1261,10 +1278,13 @@ export const registerRoutes = async (
});
await superAdminService.initServerCfg();
//
// setup the communication with license key server
await licenseService.init();
// Start HSM service if it's configured/enabled.
await hsmService.startService();
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
@@ -1342,6 +1362,7 @@ export const registerRoutes = async (
secretSharing: secretSharingService,
userEngagement: userEngagementService,
externalKms: externalKmsService,
hsm: hsmService,
cmek: cmekService,
orgAdmin: orgAdminService,
slack: slackService,

View File

@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -195,6 +196,57 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/encryption-strategies",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
strategies: z
.object({
strategy: z.nativeEnum(RootKeyEncryptionStrategy),
enabled: z.boolean()
})
.array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const encryptionDetails = await server.services.superAdmin.getConfiguredEncryptionStrategies();
return encryptionDetails;
}
});
server.route({
method: "PATCH",
url: "/encryption-strategies",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
strategy: z.nativeEnum(RootKeyEncryptionStrategy)
})
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.updateRootEncryptionStrategy(req.body.strategy);
}
});
server.route({
method: "POST",
url: "/signup",

View File

@@ -1,5 +1,7 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128:

View File

@@ -2,13 +2,14 @@ import slugify from "@sindresorhus/slugify";
import { Knex } from "knex";
import { z } from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { KmsKeysSchema, TKmsRootConfig } from "@app/db/schemas";
import { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import {
ExternalKmsAwsSchema,
KmsProviders,
TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model";
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto";
@@ -17,7 +18,7 @@ import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getByteLengthForAlgorithm } from "@app/services/kms/kms-fns";
import { getByteLengthForAlgorithm, KMS_ROOT_CONFIG_UUID } from "@app/services/kms/kms-fns";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@@ -27,6 +28,7 @@ import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import {
KmsDataKey,
KmsType,
RootKeyEncryptionStrategy,
TDecryptWithKeyDTO,
TDecryptWithKmsDTO,
TEncryptionWithKeyDTO,
@@ -40,15 +42,14 @@ type TKmsServiceFactoryDep = {
kmsDAL: TKmsKeyDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create" | "updateById">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
hsmService: THsmServiceFactory;
};
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
const KMS_ROOT_CREATION_WAIT_TIME = 10;
@@ -63,7 +64,8 @@ export const kmsServiceFactory = ({
keyStore,
internalKmsDAL,
orgDAL,
projectDAL
projectDAL,
hsmService
}: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
@@ -610,6 +612,65 @@ export const kmsServiceFactory = ({
}
};
const $getBasicEncryptionKey = () => {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey)
throw new Error(
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
);
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
return encryptionKeyBuffer;
};
const $decryptRootKey = async (kmsRootConfig: TKmsRootConfig) => {
// case 1: root key is encrypted with HSM
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to decrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
}
const decryptedKey = await hsmService.decrypt(kmsRootConfig.encryptedRootKey);
return decryptedKey;
}
// case 2: root key is encrypted with software encryption
if (kmsRootConfig.encryptionStrategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
}
throw new Error(`Invalid root key encryption strategy: ${kmsRootConfig.encryptionStrategy}`);
};
const $encryptRootKey = async (plainKeyBuffer: Buffer, strategy: RootKeyEncryptionStrategy) => {
if (strategy === RootKeyEncryptionStrategy.HSM) {
const hsmIsActive = await hsmService.isActive();
if (!hsmIsActive) {
throw new Error("Unable to encrypt root KMS key. HSM service is inactive. Did you configure the HSM?");
}
const encrypted = await hsmService.encrypt(plainKeyBuffer);
return encrypted;
}
if (strategy === RootKeyEncryptionStrategy.Software) {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const encryptionKeyBuffer = $getBasicEncryptionKey();
return cipher.encrypt(plainKeyBuffer, encryptionKeyBuffer);
}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Invalid root key encryption strategy: ${strategy}`);
};
// by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms
@@ -771,7 +832,6 @@ export const kmsServiceFactory = ({
},
tx
);
return kmsDAL.findByIdWithAssociatedKms(key.id, tx);
});
@@ -794,14 +854,6 @@ export const kmsServiceFactory = ({
// akhilmhdh: a copy of this is made in migrations/utils/kms
const startService = async () => {
const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
if (!lock) {
await keyStore.waitTillReady({
@@ -813,31 +865,69 @@ export const kmsServiceFactory = ({
// check if KMS root key was already generated and saved in DB
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
// case 1: a root key already exists in the DB
if (kmsRootConfig) {
if (lock) await lock.release();
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`);
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
// set the flag so that other instance nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
return;
}
logger.info("KMS: Generating ROOT Key");
// case 2: no config is found, so we create a new root key with basic encryption
logger.info("KMS: Generating new ROOT Key");
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => {
logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
throw err;
});
// set the flag so that other instancen nodes can start
await kmsRootConfigDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: KMS_ROOT_CONFIG_UUID,
encryptedRootKey,
encryptionStrategy: RootKeyEncryptionStrategy.Software
});
// set the flag so that other instance nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Saved and loaded ROOT Key into memory");
if (lock) await lock.release();
ROOT_ENCRYPTION_KEY = newRootKey;
};
const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
if (!kmsRootConfig) {
throw new NotFoundError({ message: "KMS root config not found" });
}
if (kmsRootConfig.encryptionStrategy === strategy) {
return;
}
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
const encryptedRootKey = await $encryptRootKey(decryptedRootKey, strategy);
if (!encryptedRootKey) {
logger.error("KMS: Failed to re-encrypt ROOT Key with selected strategy");
throw new Error("Failed to re-encrypt ROOT Key with selected strategy");
}
await kmsRootConfigDAL.updateById(KMS_ROOT_CONFIG_UUID, {
encryptedRootKey,
encryptionStrategy: strategy
});
ROOT_ENCRYPTION_KEY = decryptedRootKey;
};
return {
startService,
generateKmsKey,
@@ -849,6 +939,7 @@ export const kmsServiceFactory = ({
encryptWithRootKey,
decryptWithRootKey,
getOrgKmsKeyId,
updateEncryptionStrategy,
getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey,
getProjectKeyBackup,

View File

@@ -56,3 +56,8 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = {
projectId: string;
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
};
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}

View File

@@ -10,7 +10,10 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns";
import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal";
@@ -20,7 +23,8 @@ type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
@@ -47,6 +51,7 @@ export const superAdminServiceFactory = ({
authService,
orgService,
keyStore,
kmsRootConfigDAL,
kmsService,
licenseService
}: TSuperAdminServiceFactoryDep) => {
@@ -288,12 +293,70 @@ export const superAdminServiceFactory = ({
};
};
const getConfiguredEncryptionStrategies = async () => {
const appCfg = getConfig();
const kmsRootCfg = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
if (!kmsRootCfg) {
throw new NotFoundError({ name: "KmsRootConfig", message: "KMS root configuration not found" });
}
const selectedStrategy = kmsRootCfg.encryptionStrategy;
const enabledStrategies: { enabled: boolean; strategy: RootKeyEncryptionStrategy }[] = [];
if (appCfg.ROOT_ENCRYPTION_KEY || appCfg.ENCRYPTION_KEY) {
const basicStrategy = RootKeyEncryptionStrategy.Software;
enabledStrategies.push({
enabled: selectedStrategy === basicStrategy,
strategy: basicStrategy
});
}
if (appCfg.isHsmConfigured) {
const hsmStrategy = RootKeyEncryptionStrategy.HSM;
enabledStrategies.push({
enabled: selectedStrategy === hsmStrategy,
strategy: hsmStrategy
});
}
return {
strategies: enabledStrategies
};
};
const updateRootEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
if (!licenseService.onPremFeatures.hsm) {
throw new BadRequestError({
message: "Failed to update encryption strategy due to plan restriction. Upgrade to Infisical's Enterprise plan."
});
}
const configuredStrategies = await getConfiguredEncryptionStrategies();
const foundStrategy = configuredStrategies.strategies.find((s) => s.strategy === strategy);
if (!foundStrategy) {
throw new BadRequestError({ message: "Invalid encryption strategy" });
}
if (foundStrategy.enabled) {
throw new BadRequestError({ message: "The selected encryption strategy is already enabled" });
}
await kmsService.updateEncryptionStrategy(strategy);
};
return {
initServerCfg,
updateServerCfg,
adminSignUp,
getUsers,
deleteUser,
getAdminSlackConfig
getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies
};
};

View File

@@ -136,8 +136,8 @@ type GetOrganizationsResponse struct {
}
type SelectOrganizationResponse struct {
Token string `json:"token"`
MfaEnabled bool `json:"isMfaEnabled"`
Token string `json:"token"`
MfaEnabled bool `json:"isMfaEnabled"`
}
type SelectOrganizationRequest struct {

View File

@@ -2,6 +2,12 @@ export {
useAdminDeleteUser,
useCreateAdminUser,
useUpdateAdminSlackConfig,
useUpdateServerConfig
useUpdateServerConfig,
useUpdateServerEncryptionStrategy
} from "./mutation";
export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries";
export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetServerConfig,
useGetServerRootKmsEncryptionDetails
} from "./queries";

View File

@@ -7,6 +7,7 @@ import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import {
AdminSlackConfig,
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TServerConfig,
TUpdateAdminSlackConfigDTO
@@ -85,3 +86,15 @@ export const useUpdateAdminSlackConfig = () => {
}
});
};
export const useUpdateServerEncryptionStrategy = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (strategy: RootKeyEncryptionStrategy) => {
await apiRequest.patch("/api/v1/admin/encryption-strategies", { strategy });
},
onSuccess: () => {
queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies());
}
});
};

View File

@@ -3,7 +3,12 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que
import { apiRequest } from "@app/config/request";
import { User } from "../types";
import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types";
import {
AdminGetUsersFilters,
AdminSlackConfig,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
export const adminStandaloneKeys = {
getUsers: "get-users"
@@ -12,7 +17,8 @@ export const adminStandaloneKeys = {
export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
};
const fetchServerConfig = async () => {
@@ -61,8 +67,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
});
};
export const useGetAdminSlackConfig = () =>
useQuery({
export const useGetAdminSlackConfig = () => {
return useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(),
queryFn: async () => {
const { data } = await apiRequest.get<AdminSlackConfig>(
@@ -72,3 +78,17 @@ export const useGetAdminSlackConfig = () =>
return data;
}
});
};
export const useGetServerRootKmsEncryptionDetails = () => {
return useQuery({
queryKey: adminQueryKeys.getServerEncryptionStrategies(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetServerRootKmsEncryptionDetails>(
"/api/v1/admin/encryption-strategies"
);
return data;
}
});
};

View File

@@ -54,3 +54,15 @@ export type AdminSlackConfig = {
clientId: string;
clientSecret: string;
};
export type TGetServerRootKmsEncryptionDetails = {
strategies: {
strategy: RootKeyEncryptionStrategy;
enabled: boolean;
}[];
};
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}

View File

@@ -23,6 +23,7 @@ export type SubscriptionPlan = {
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
hsm: boolean;
oidcSSO: boolean;
scim: boolean;
ldap: boolean;

View File

@@ -22,15 +22,21 @@ import {
Tabs
} from "@app/components/v2";
import { useOrganization, useServerConfig, useUser } from "@app/context";
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
import {
useGetOrganizations,
useGetServerRootKmsEncryptionDetails,
useUpdateServerConfig
} from "@app/hooks/api";
import { AuthPanel } from "./AuthPanel";
import { EncryptionPanel } from "./EncryptionPanel";
import { IntegrationPanel } from "./IntegrationPanel";
import { RateLimitPanel } from "./RateLimitPanel";
import { UserPanel } from "./UserPanel";
enum TabSections {
Settings = "settings",
Encryption = "encryption",
Auth = "auth",
RateLimit = "rate-limit",
Integrations = "integrations",
@@ -55,6 +61,7 @@ type TDashboardForm = z.infer<typeof formSchema>;
export const AdminDashboardPage = () => {
const router = useRouter();
const data = useServerConfig();
const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails();
const { config } = data;
const {
@@ -137,6 +144,7 @@ export const AdminDashboardPage = () => {
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Encryption}>Encryption</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
@@ -321,6 +329,9 @@ export const AdminDashboardPage = () => {
</Button>
</form>
</TabPanel>
<TabPanel value={TabSections.Encryption}>
<EncryptionPanel rootKmsDetails={serverRootKmsDetails} />
</TabPanel>
<TabPanel value={TabSections.Auth}>
<AuthPanel />
</TabPanel>

View File

@@ -0,0 +1,137 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Select, SelectItem, UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useUpdateServerEncryptionStrategy } from "@app/hooks/api";
import {
RootKeyEncryptionStrategy,
TGetServerRootKmsEncryptionDetails
} from "@app/hooks/api/admin/types";
const formSchema = z.object({
encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy)
});
const strategies: Record<RootKeyEncryptionStrategy, string> = {
[RootKeyEncryptionStrategy.Software]: "Software-based Encryption",
[RootKeyEncryptionStrategy.HSM]: "Hardware Security Module (HSM)"
};
type TForm = z.infer<typeof formSchema>;
type Props = {
rootKmsDetails?: TGetServerRootKmsEncryptionDetails;
};
export const EncryptionPanel = ({ rootKmsDetails }: Props) => {
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const {
control,
handleSubmit,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
encryptionStrategy:
rootKmsDetails?.strategies?.find((s) => s.enabled)?.strategy ??
RootKeyEncryptionStrategy.Software
}
});
const onSubmit = useCallback(async (formData: TForm) => {
if (!subscription) return;
if (!subscription.hsm) {
handlePopUpOpen("upgradePlan", {
description: "Hardware Security Module's (HSM's), are only available on Enterprise plans."
});
return;
}
try {
await updateEncryptionStrategy(formData.encryptionStrategy);
createNotification({
type: "success",
text: "Encryption strategy updated successfully"
});
} catch {
createNotification({
type: "error",
text: "Failed to update encryption strategy"
});
}
}, []);
return (
<>
<form
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col justify-start">
<div className="flex w-full justify-between">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
KMS Encryption Strategy
</div>
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select which type of encryption strategy you want to use for your KMS root key. HSM is
supported on Enterprise plans.
</div>
{!!rootKmsDetails && (
<Controller
control={control}
name="encryptionStrategy"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="max-w-sm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800"
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
{...field}
>
{rootKmsDetails.strategies?.map((strategy) => (
<SelectItem key={strategy.strategy} value={strategy.strategy}>
{strategies[strategy.strategy]}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
</div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</form>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</>
);
};

14
package-lock.json generated
View File

@@ -7,7 +7,8 @@
"name": "infisical",
"license": "ISC",
"dependencies": {
"@radix-ui/react-radio-group": "^1.1.3"
"@radix-ui/react-radio-group": "^1.1.3",
"secrets.js-grempe": "^2.0.0"
},
"devDependencies": {
"@types/uuid": "^9.0.7",
@@ -1392,6 +1393,12 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/secrets.js-grempe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz",
"integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2457,6 +2464,11 @@
"loose-envify": "^1.1.0"
}
},
"secrets.js-grempe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/secrets.js-grempe/-/secrets.js-grempe-2.0.0.tgz",
"integrity": "sha512-4xkOIaDAg998dTFXZUJTOoVbdLHfB818SMeLJ69ABccgGEKokxsoRFupAFfAImloUSKv4QUGNMgKVbKMf6z0Ug=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -24,6 +24,7 @@
"husky": "^8.0.3"
},
"dependencies": {
"@radix-ui/react-radio-group": "^1.1.3"
"@radix-ui/react-radio-group": "^1.1.3",
"secrets.js-grempe": "^2.0.0"
}
}