mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +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
|
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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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
@@ -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 .
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Generated
+31
-3
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Vendored
+2
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Generated
+13
-1
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user