mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2671 from Infisical/daniel/hsm
feat: hardware security module's support
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
167
Dockerfile.fips.standalone-infisical
Normal file
167
Dockerfile.fips.standalone-infisical
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -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;
|
||||
|
||||
23
backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts
Normal file
23
backend/src/db/migrations/20241111175154_kms-root-cfg-hsm.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
58
backend/src/ee/services/hsm/hsm-fns.ts
Normal file
58
backend/src/ee/services/hsm/hsm-fns.ts
Normal 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
backend/src/ee/services/hsm/hsm-service.ts
Normal file
470
backend/src/ee/services/hsm/hsm-service.ts
Normal 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
backend/src/ee/services/hsm/hsm-types.ts
Normal file
11
backend/src/ee/services/hsm/hsm-types.ts
Normal 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,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
|
||||
@@ -46,6 +46,7 @@ export type TFeatureSet = {
|
||||
auditLogStreams: false;
|
||||
auditLogStreamLimit: 3;
|
||||
samlSSO: false;
|
||||
hsm: false;
|
||||
oidcSSO: false;
|
||||
scim: false;
|
||||
ldap: false;
|
||||
|
||||
@@ -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(",")
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type SubscriptionPlan = {
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
samlSSO: boolean;
|
||||
hsm: boolean;
|
||||
oidcSSO: boolean;
|
||||
scim: boolean;
|
||||
ldap: boolean;
|
||||
|
||||
@@ -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>
|
||||
|
||||
137
frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx
Normal file
137
frontend/src/views/admin/DashboardPage/EncryptionPanel.tsx
Normal 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
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user