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
@@ -1,62 +1,115 @@
name: Release standalone docker image name: Release standalone docker image
on: on:
push: push:
tags: tags:
- "infisical/v*.*.*-postgres" - "infisical/v*.*.*-postgres"
jobs: jobs:
infisical-tests: infisical-tests:
name: Run tests before deployment name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview # https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres infisical-standalone:
runs-on: ubuntu-latest name: Build infisical standalone image postgres
needs: [infisical-tests] runs-on: ubuntu-latest
steps: needs: [infisical-tests]
- name: Extract version from tag steps:
id: extract_version - name: Extract version from tag
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" id: extract_version
- name: ☁️ Checkout source run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
uses: actions/checkout@v3 - name: ☁️ Checkout source
with: uses: actions/checkout@v3
fetch-depth: 0 with:
- name: 📦 Install dependencies to test all dependencies fetch-depth: 0
run: npm ci --only-production - name: 📦 Install dependencies to test all dependencies
working-directory: backend run: npm ci --only-production
- name: version output working-directory: backend
run: | - name: version output
echo "Output Value: ${{ steps.version.outputs.major }}" run: |
echo "Output Value: ${{ steps.version.outputs.minor }}" echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.patch }}" echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.version }}" echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}" echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.increment }}" echo "Output Value: ${{ steps.version.outputs.version_type }}"
- name: Save commit hashes for tag echo "Output Value: ${{ steps.version.outputs.increment }}"
id: commit - name: Save commit hashes for tag
uses: pr-mpt/actions-commit-hash@v2 id: commit
- name: 🔧 Set up Docker Buildx uses: pr-mpt/actions-commit-hash@v2
uses: docker/setup-buildx-action@v2 - name: 🔧 Set up Docker Buildx
- name: 🐋 Login to Docker Hub uses: docker/setup-buildx-action@v2
uses: docker/login-action@v2 - name: 🐋 Login to Docker Hub
with: uses: docker/login-action@v2
username: ${{ secrets.DOCKERHUB_USERNAME }} with:
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Set up Depot CLI password: ${{ secrets.DOCKERHUB_TOKEN }}
uses: depot/setup-action@v1 - name: Set up Depot CLI
- name: 📦 Build backend and export to Docker uses: depot/setup-action@v1
uses: depot/build-push-action@v1 - name: 📦 Build backend and export to Docker
with: uses: depot/build-push-action@v1
project: 64mmf0n610 with:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} project: 64mmf0n610
push: true token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: . push: true
tags: | context: .
infisical/infisical:latest-postgres tags: |
infisical/infisical:${{ steps.commit.outputs.short }} infisical/infisical:latest-postgres
infisical/infisical:${{ steps.extract_version.outputs.version }} infisical/infisical:${{ steps.commit.outputs.short }}
platforms: linux/amd64,linux/arm64 infisical/infisical:${{ steps.extract_version.outputs.version }}
file: Dockerfile.standalone-infisical platforms: linux/amd64,linux/arm64
build-args: | file: Dockerfile.standalone-infisical
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} build-args: |
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} 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 }}
+1
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 docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
docs/mint.json:generic-api-key:651 docs/mint.json:generic-api-key:651
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
+167
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"]
+6
View File
@@ -72,6 +72,9 @@ RUN addgroup --system --gid 1001 nodejs \
WORKDIR /app WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./ COPY backend/package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
@@ -85,6 +88,9 @@ FROM base AS backend-runner
WORKDIR /app WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
COPY backend/package*.json ./ COPY backend/package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
+12 -1
View File
@@ -3,6 +3,12 @@ FROM node:20-alpine AS build
WORKDIR /app WORKDIR /app
# Required for pkcs11js
RUN apk --update add \
python3 \
make \
g++
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only-production RUN npm ci --only-production
@@ -11,12 +17,17 @@ RUN npm run build
# Production stage # Production stage
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app WORKDIR /app
ENV npm_config_cache /home/node/.npm ENV npm_config_cache /home/node/.npm
COPY package*.json ./ COPY package*.json ./
RUN apk --update add \
python3 \
make \
g++
RUN npm ci --only-production && npm cache clean --force RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app . COPY --from=build /app .
+39
View File
@@ -1,5 +1,44 @@
FROM node:20-alpine 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 \ RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add --no-cache git && apk add infisical=0.8.1 && apk add --no-cache git
+7 -1
View File
@@ -16,6 +16,7 @@ import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue"; import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore"; import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default { export default {
@@ -54,7 +55,12 @@ export default {
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL); const queue = queueServiceFactory(cfg.REDIS_URL);
const keyStore = keyStoreFactory(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 // @ts-expect-error type
globalThis.testServer = server; globalThis.testServer = server;
// @ts-expect-error type // @ts-expect-error type
+31 -3
View File
@@ -83,6 +83,7 @@
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"pkcs11js": "^2.1.6",
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.3.8", "probot": "^13.3.8",
@@ -120,6 +121,7 @@
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6", "@types/safe-regex": "^1.1.6",
@@ -8830,6 +8832,17 @@
"integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==",
"dev": true "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": { "node_modules/@types/prompt-sync": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz",
@@ -17066,6 +17079,20 @@
"node": ">= 6" "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": { "node_modules/pkg-conf": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz",
@@ -19761,9 +19788,10 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.3", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
"license": "0BSD"
}, },
"node_modules/tsup": { "node_modules/tsup": {
"version": "8.0.1", "version": "8.0.1",
+2
View File
@@ -84,6 +84,7 @@
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
"@types/pkcs11js": "^1.0.4",
"@types/prompt-sync": "^4.2.3", "@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6", "@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6", "@types/safe-regex": "^1.1.6",
@@ -188,6 +189,7 @@
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"pkcs11js": "^2.1.6",
"pkijs": "^3.2.4", "pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.3.8", "probot": "^13.3.8",
+2
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 { 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 { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-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 { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-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"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
@@ -184,6 +185,7 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory; rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory; userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory; externalKms: TExternalKmsServiceFactory;
hsm: THsmServiceFactory;
orgAdmin: TOrgAdminServiceFactory; orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory; slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory;
@@ -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);
});
}
+4 -1
View File
@@ -11,7 +11,10 @@ import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({ export const KmsRootConfigSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
encryptedRootKey: zodBuffer encryptedRootKey: zodBuffer,
encryptionStrategy: z.string(),
createdAt: z.date(),
updatedAt: z.date()
}); });
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>; export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
+58
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
};
};
+470
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
};
};
+11
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"
}
@@ -29,6 +29,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false, auditLogStreams: false,
auditLogStreamLimit: 3, auditLogStreamLimit: 3,
samlSSO: false, samlSSO: false,
hsm: false,
oidcSSO: false, oidcSSO: false,
scim: false, scim: false,
ldap: false, ldap: false,
@@ -46,6 +46,7 @@ export type TFeatureSet = {
auditLogStreams: false; auditLogStreams: false;
auditLogStreamLimit: 3; auditLogStreamLimit: 3;
samlSSO: false; samlSSO: false;
hsm: false;
oidcSSO: false; oidcSSO: false;
scim: false; scim: false;
ldap: false; ldap: false;
+17 -1
View File
@@ -163,10 +163,22 @@ const envSchema = z
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), 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_ID: zpStr(z.string().optional()),
WORKFLOW_SLACK_CLIENT_SECRET: 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) => ({ .transform((data) => ({
...data, ...data,
DB_READ_REPLICAS: data.DB_READ_REPLICAS DB_READ_REPLICAS: data.DB_READ_REPLICAS
? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS)) ? databaseReadReplicaSchema.parse(JSON.parse(data.DB_READ_REPLICAS))
: undefined, : undefined,
@@ -175,10 +187,14 @@ const envSchema = z
isRedisConfigured: Boolean(data.REDIS_URL), isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development", isDevelopmentMode: data.NODE_ENV === "development",
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isSecretScanningConfigured: isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), 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, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
})); }));
+8 -1
View File
@@ -1,6 +1,8 @@
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path"; import path from "path";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
import { initAuditLogDbConnection, initDbConnection } from "./db"; import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore"; import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
@@ -53,13 +55,17 @@ const run = async () => {
const queue = queueServiceFactory(appCfg.REDIS_URL); const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(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 }); const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line // eslint-disable-next-line
process.on("SIGINT", async () => { process.on("SIGINT", async () => {
await server.close(); await server.close();
await db.destroy(); await db.destroy();
hsmModule.finalize();
process.exit(0); process.exit(0);
}); });
@@ -67,6 +73,7 @@ const run = async () => {
process.on("SIGTERM", async () => { process.on("SIGTERM", async () => {
await server.close(); await server.close();
await db.destroy(); await db.destroy();
hsmModule.finalize();
process.exit(0); process.exit(0);
}); });
+8 -4
View File
@@ -14,6 +14,7 @@ import fastify from "fastify";
import { Knex } from "knex"; import { Knex } from "knex";
import { Logger } from "pino"; import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env"; import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
@@ -36,16 +37,19 @@ type TMain = {
logger?: Logger; logger?: Logger;
queue: TQueueServiceFactory; queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory; keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
}; };
// Run the server! // 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 appCfg = getConfig();
const server = fastify({ const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger, logger: appCfg.NODE_ENV === "test" ? false : logger,
trustProxy: true, trustProxy: true,
connectionTimeout: 30 * 1000, connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true ignoreTrailingSlash: true,
pluginTimeout: 40_000
}).withTypeProvider<ZodTypeProvider>(); }).withTypeProvider<ZodTypeProvider>();
server.setValidatorCompiler(validatorCompiler); 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(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore }); await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) { if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, { await server.register(registerExternalNextjs, {
+25 -4
View File
@@ -1,5 +1,4 @@
import { CronJob } from "cron"; import { CronJob } from "cron";
// import { Redis } from "ioredis";
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod"; 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 { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; 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 { 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 { 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"; 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, auditLogDb,
db, db,
hsmModule,
smtp: smtpService, smtp: smtpService,
queue: queueService, queue: queueService,
keyStore 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(); const appCfg = getConfig();
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" }); await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
@@ -352,14 +361,21 @@ export const registerRoutes = async (
projectDAL projectDAL
}); });
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const hsmService = hsmServiceFactory({
hsmModule
});
const kmsService = kmsServiceFactory({ const kmsService = kmsServiceFactory({
kmsRootConfigDAL, kmsRootConfigDAL,
keyStore, keyStore,
kmsDAL, kmsDAL,
internalKmsDAL, internalKmsDAL,
orgDAL, orgDAL,
projectDAL projectDAL,
hsmService
}); });
const externalKmsService = externalKmsServiceFactory({ const externalKmsService = externalKmsServiceFactory({
kmsDAL, kmsDAL,
kmsService, kmsService,
@@ -556,6 +572,7 @@ export const registerRoutes = async (
userDAL, userDAL,
authService: loginService, authService: loginService,
serverCfgDAL: superAdminDAL, serverCfgDAL: superAdminDAL,
kmsRootConfigDAL,
orgService, orgService,
keyStore, keyStore,
licenseService, licenseService,
@@ -1261,10 +1278,13 @@ export const registerRoutes = async (
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
//
// setup the communication with license key server // setup the communication with license key server
await licenseService.init(); await licenseService.init();
// Start HSM service if it's configured/enabled.
await hsmService.startService();
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp(); await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
@@ -1342,6 +1362,7 @@ export const registerRoutes = async (
secretSharing: secretSharingService, secretSharing: secretSharingService,
userEngagement: userEngagementService, userEngagement: userEngagementService,
externalKms: externalKmsService, externalKms: externalKmsService,
hsm: hsmService,
cmek: cmekService, cmek: cmekService,
orgAdmin: orgAdminService, orgAdmin: orgAdminService,
slack: slackService, slack: slackService,
@@ -7,6 +7,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; 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 { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-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({ server.route({
method: "POST", method: "POST",
url: "/signup", url: "/signup",
+2
View File
@@ -1,5 +1,7 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { SymmetricEncryption } from "@app/lib/crypto/cipher";
export const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => { export const getByteLengthForAlgorithm = (encryptionAlgorithm: SymmetricEncryption) => {
switch (encryptionAlgorithm) { switch (encryptionAlgorithm) {
case SymmetricEncryption.AES_GCM_128: case SymmetricEncryption.AES_GCM_128:
+115 -24
View File
@@ -2,13 +2,14 @@ import slugify from "@sindresorhus/slugify";
import { Knex } from "knex"; import { Knex } from "knex";
import { z } from "zod"; 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 { AwsKmsProviderFactory } from "@app/ee/services/external-kms/providers/aws-kms";
import { import {
ExternalKmsAwsSchema, ExternalKmsAwsSchema,
KmsProviders, KmsProviders,
TExternalKmsProviderFns TExternalKmsProviderFns
} from "@app/ee/services/external-kms/providers/model"; } 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 { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto"; 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 { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; 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 { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -27,6 +28,7 @@ import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { import {
KmsDataKey, KmsDataKey,
KmsType, KmsType,
RootKeyEncryptionStrategy,
TDecryptWithKeyDTO, TDecryptWithKeyDTO,
TDecryptWithKmsDTO, TDecryptWithKmsDTO,
TEncryptionWithKeyDTO, TEncryptionWithKeyDTO,
@@ -40,15 +42,14 @@ type TKmsServiceFactoryDep = {
kmsDAL: TKmsKeyDALFactory; kmsDAL: TKmsKeyDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "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">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">; internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
hsmService: THsmServiceFactory;
}; };
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>; 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_KEY = "wait_till_ready_kms_root_key";
const KMS_ROOT_CREATION_WAIT_TIME = 10; const KMS_ROOT_CREATION_WAIT_TIME = 10;
@@ -63,7 +64,8 @@ export const kmsServiceFactory = ({
keyStore, keyStore,
internalKmsDAL, internalKmsDAL,
orgDAL, orgDAL,
projectDAL projectDAL,
hsmService
}: TKmsServiceFactoryDep) => { }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); 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 // by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key // none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms // NOTICE: If changing here update migrations/utils/kms
@@ -771,7 +832,6 @@ export const kmsServiceFactory = ({
}, },
tx tx
); );
return kmsDAL.findByIdWithAssociatedKms(key.id, 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 // akhilmhdh: a copy of this is made in migrations/utils/kms
const startService = async () => { 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); const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
if (!lock) { if (!lock) {
await keyStore.waitTillReady({ await keyStore.waitTillReady({
@@ -813,31 +865,69 @@ export const kmsServiceFactory = ({
// check if KMS root key was already generated and saved in DB // check if KMS root key was already generated and saved in DB
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID); 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 (kmsRootConfig) {
if (lock) await lock.release(); if (lock) await lock.release();
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting."); logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`);
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start 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"); await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Loading ROOT Key into Memory."); logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey; ROOT_ENCRYPTION_KEY = decryptedRootKey;
return; 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 newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer); const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => {
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID }); 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"); await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Saved and loaded ROOT Key into memory"); logger.info("KMS: Saved and loaded ROOT Key into memory");
if (lock) await lock.release(); if (lock) await lock.release();
ROOT_ENCRYPTION_KEY = newRootKey; 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 { return {
startService, startService,
generateKmsKey, generateKmsKey,
@@ -849,6 +939,7 @@ export const kmsServiceFactory = ({
encryptWithRootKey, encryptWithRootKey,
decryptWithRootKey, decryptWithRootKey,
getOrgKmsKeyId, getOrgKmsKeyId,
updateEncryptionStrategy,
getProjectSecretManagerKmsKeyId, getProjectSecretManagerKmsKeyId,
updateProjectSecretManagerKmsKey, updateProjectSecretManagerKmsKey,
getProjectKeyBackup, getProjectKeyBackup,
+5
View File
@@ -56,3 +56,8 @@ export type TUpdateProjectSecretManagerKmsKeyDTO = {
projectId: string; projectId: string;
kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }; kms: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string };
}; };
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
@@ -10,7 +10,10 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service"; import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type"; 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 { TKmsServiceFactory } from "../kms/kms-service";
import { RootKeyEncryptionStrategy } from "../kms/kms-types";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
@@ -20,7 +23,8 @@ type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory; serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
authService: Pick<TAuthLoginFactory, "generateUserTokens">; authService: Pick<TAuthLoginFactory, "generateUserTokens">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">; keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
@@ -47,6 +51,7 @@ export const superAdminServiceFactory = ({
authService, authService,
orgService, orgService,
keyStore, keyStore,
kmsRootConfigDAL,
kmsService, kmsService,
licenseService licenseService
}: TSuperAdminServiceFactoryDep) => { }: 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 { return {
initServerCfg, initServerCfg,
updateServerCfg, updateServerCfg,
adminSignUp, adminSignUp,
getUsers, getUsers,
deleteUser, deleteUser,
getAdminSlackConfig getAdminSlackConfig,
updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies
}; };
}; };
+2 -2
View File
@@ -136,8 +136,8 @@ type GetOrganizationsResponse struct {
} }
type SelectOrganizationResponse struct { type SelectOrganizationResponse struct {
Token string `json:"token"` Token string `json:"token"`
MfaEnabled bool `json:"isMfaEnabled"` MfaEnabled bool `json:"isMfaEnabled"`
} }
type SelectOrganizationRequest struct { type SelectOrganizationRequest struct {
+8 -2
View File
@@ -2,6 +2,12 @@ export {
useAdminDeleteUser, useAdminDeleteUser,
useCreateAdminUser, useCreateAdminUser,
useUpdateAdminSlackConfig, useUpdateAdminSlackConfig,
useUpdateServerConfig useUpdateServerConfig,
useUpdateServerEncryptionStrategy
} from "./mutation"; } from "./mutation";
export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries"; export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetServerConfig,
useGetServerRootKmsEncryptionDetails
} from "./queries";
+13
View File
@@ -7,6 +7,7 @@ import { User } from "../users/types";
import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import { import {
AdminSlackConfig, AdminSlackConfig,
RootKeyEncryptionStrategy,
TCreateAdminUserDTO, TCreateAdminUserDTO,
TServerConfig, TServerConfig,
TUpdateAdminSlackConfigDTO 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());
}
});
};
+24 -4
View File
@@ -3,7 +3,12 @@ import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-que
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { User } from "../types"; import { User } from "../types";
import { AdminGetUsersFilters, AdminSlackConfig, TServerConfig } from "./types"; import {
AdminGetUsersFilters,
AdminSlackConfig,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
export const adminStandaloneKeys = { export const adminStandaloneKeys = {
getUsers: "get-users" getUsers: "get-users"
@@ -12,7 +17,8 @@ export const adminStandaloneKeys = {
export const adminQueryKeys = { export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const, serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] 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 () => { const fetchServerConfig = async () => {
@@ -61,8 +67,8 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
}); });
}; };
export const useGetAdminSlackConfig = () => export const useGetAdminSlackConfig = () => {
useQuery({ return useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(), queryKey: adminQueryKeys.getAdminSlackConfig(),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<AdminSlackConfig>( const { data } = await apiRequest.get<AdminSlackConfig>(
@@ -72,3 +78,17 @@ export const useGetAdminSlackConfig = () =>
return data; 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;
}
});
};
+12
View File
@@ -54,3 +54,15 @@ export type AdminSlackConfig = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
}; };
export type TGetServerRootKmsEncryptionDetails = {
strategies: {
strategy: RootKeyEncryptionStrategy;
enabled: boolean;
}[];
};
export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
@@ -23,6 +23,7 @@ export type SubscriptionPlan = {
workspacesUsed: number; workspacesUsed: number;
environmentLimit: number; environmentLimit: number;
samlSSO: boolean; samlSSO: boolean;
hsm: boolean;
oidcSSO: boolean; oidcSSO: boolean;
scim: boolean; scim: boolean;
ldap: boolean; ldap: boolean;
@@ -22,15 +22,21 @@ import {
Tabs Tabs
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useServerConfig, useUser } from "@app/context"; 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 { AuthPanel } from "./AuthPanel";
import { EncryptionPanel } from "./EncryptionPanel";
import { IntegrationPanel } from "./IntegrationPanel"; import { IntegrationPanel } from "./IntegrationPanel";
import { RateLimitPanel } from "./RateLimitPanel"; import { RateLimitPanel } from "./RateLimitPanel";
import { UserPanel } from "./UserPanel"; import { UserPanel } from "./UserPanel";
enum TabSections { enum TabSections {
Settings = "settings", Settings = "settings",
Encryption = "encryption",
Auth = "auth", Auth = "auth",
RateLimit = "rate-limit", RateLimit = "rate-limit",
Integrations = "integrations", Integrations = "integrations",
@@ -55,6 +61,7 @@ type TDashboardForm = z.infer<typeof formSchema>;
export const AdminDashboardPage = () => { export const AdminDashboardPage = () => {
const router = useRouter(); const router = useRouter();
const data = useServerConfig(); const data = useServerConfig();
const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails();
const { config } = data; const { config } = data;
const { const {
@@ -137,6 +144,7 @@ export const AdminDashboardPage = () => {
<TabList> <TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600"> <div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab> <Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Encryption}>Encryption</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab> <Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab> <Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab> <Tab value={TabSections.Integrations}>Integrations</Tab>
@@ -321,6 +329,9 @@ export const AdminDashboardPage = () => {
</Button> </Button>
</form> </form>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Encryption}>
<EncryptionPanel rootKmsDetails={serverRootKmsDetails} />
</TabPanel>
<TabPanel value={TabSections.Auth}> <TabPanel value={TabSections.Auth}>
<AuthPanel /> <AuthPanel />
</TabPanel> </TabPanel>
@@ -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}
/>
</>
);
};
+13 -1
View File
@@ -7,7 +7,8 @@
"name": "infisical", "name": "infisical",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@radix-ui/react-radio-group": "^1.1.3" "@radix-ui/react-radio-group": "^1.1.3",
"secrets.js-grempe": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
@@ -1392,6 +1393,12 @@
"loose-envify": "^1.1.0" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2457,6 +2464,11 @@
"loose-envify": "^1.1.0" "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": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+2 -1
View File
@@ -24,6 +24,7 @@
"husky": "^8.0.3" "husky": "^8.0.3"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-radio-group": "^1.1.3" "@radix-ui/react-radio-group": "^1.1.3",
"secrets.js-grempe": "^2.0.0"
} }
} }