feat(fips): fips inside, AWS patch-up and docker improvements

This commit is contained in:
Daniel Hougaard
2025-07-07 21:56:07 +04:00
parent ce88b0cbb1
commit 30e901c00c
17 changed files with 1048 additions and 59 deletions

View File

@@ -73,6 +73,17 @@ RUN apt-get update && apt-get install -y \
# Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# Build and install FIPS validated OpenSSL
WORKDIR /openssl-build
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& tar -xf openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& make \
&& make install_fips
COPY backend/package*.json ./
RUN npm ci --only-production
@@ -103,6 +114,15 @@ RUN apt-get update && apt-get install -y \
# Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# Build and install FIPS validated OpenSSL
WORKDIR /openssl-build
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& tar -xf openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& make \
&& make install_fips
COPY backend/package*.json ./
RUN npm ci --only-production
@@ -173,6 +193,12 @@ ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
ENV NODE_OPTIONS="--max-old-space-size=1024"
# FIPS mode of operation:
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
ENV NODE_OPTIONS=--force-fips
ENV FIPS_ENABLED=true
WORKDIR /backend
ENV TELEMETRY_ENABLED true

View File

@@ -1,7 +1,5 @@
FROM node:20-slim
RUN echo "RUNNING FIPS BUILD"
# Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
RUN apt-get update && apt-get install -y \
build-essential \
@@ -29,11 +27,26 @@ RUN apt-get install -y \
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# 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 apt-get install -y opensc
RUN mkdir -p /etc/softhsm2/tokens && \
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
# Build and install FIPS validated OpenSSL
WORKDIR /openssl-build
RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& tar -xf openssl-3.1.2.tar.gz \
@@ -59,7 +72,7 @@ RUN npm install
COPY . .
ENV HOST=0.0.0.0
ENV OPENSSL_CONF=/app/nodejs.cnf
ENV OPENSSL_CONF=/app/nodejs.fips.cnf
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
ENV NODE_OPTIONS=--force-fips
ENV FIPS_ENABLED=true

File diff suppressed because it is too large Load Diff

View File

@@ -84,6 +84,7 @@
"@babel/plugin-syntax-import-attributes": "^7.24.7",
"@babel/preset-env": "^7.18.10",
"@babel/preset-react": "^7.24.7",
"@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2",
"@types/crypto-js": "^4.2.2",
"@types/jmespath": "^0.15.2",

View File

@@ -12,6 +12,8 @@ import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -39,8 +41,11 @@ type TDeleteElastiCacheUserInput = z.infer<typeof DeleteElasticCacheUserSchema>;
const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => {
const elastiCache = new ElastiCache({
region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials
});
const infisicalGroup = "infisical-managed-group-elasticache";
const ensureInfisicalGroupExists = async (clusterName: string) => {

View File

@@ -19,6 +19,7 @@ import {
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
@@ -49,6 +50,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
const stsClient = new STSClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
? {
@@ -72,6 +75,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
}
const client = new IAMClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: assumeRes.Credentials?.AccessKeyId,
secretAccessKey: assumeRes.Credentials?.SecretAccessKey,
@@ -83,6 +88,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const client = new IAMClient({
region: providerInputs.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: providerInputs.accessKey,
secretAccessKey: providerInputs.secretAccessKey

View File

@@ -1,6 +1,7 @@
import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, KMSClient } from "@aws-sdk/client-kms";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto/cryptography";
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
@@ -9,7 +10,9 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) {
const awsCredential = providerInputs.credential.data;
const stsClient = new STSClient({
region: providerInputs.awsRegion
region: providerInputs.awsRegion,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher
});
const command = new AssumeRoleCommand({
RoleArn: awsCredential.assumeRoleArn,
@@ -23,6 +26,8 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
const kmsClient = new KMSClient({
region: providerInputs.awsRegion,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: response.Credentials.AccessKeyId,
secretAccessKey: response.Credentials.SecretAccessKey,
@@ -35,6 +40,8 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
const awsCredential = providerInputs.credential.data;
const kmsClient = new KMSClient({
region: providerInputs.awsRegion,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: {
accessKeyId: awsCredential.accessKey,
secretAccessKey: awsCredential.secretKey

View File

@@ -18,7 +18,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
environmentsUsed: 0,
identityLimit: null,
identitiesUsed: 0,
dynamicSecret: false,
dynamicSecret: true,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,

View File

@@ -6,6 +6,7 @@ import {
} from "@aws-sdk/client-iam";
import { SecretType } from "@app/db/schemas";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { getConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
@@ -226,6 +227,8 @@ export const secretRotationQueueFactory = ({
if (provider.template.type === TProviderFunctionTypes.AWS) {
if (provider.template.client === TAwsProviderSystems.IAM) {
const client = new IAMClient({
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
region: newCredential.inputs.manager_user_aws_region as string,
credentials: {
accessKeyId: newCredential.inputs.manager_user_access_key as string,

View File

@@ -0,0 +1,57 @@
/* eslint-disable no-underscore-dangle */
import type { SourceData } from "@smithy/types";
import { Hash, Hmac } from "crypto";
import { crypto } from "@app/lib/crypto";
export class CustomAWSHasher {
public algorithmIdentifier: string = "sha256";
public secret: SourceData | undefined;
public hash: Hash | Hmac | undefined;
private _hash: Hash | Hmac | undefined;
constructor(secret?: SourceData) {
this.secret = secret;
this.reset();
}
reset() {
if (this.secret) {
// Convert any secret type to Buffer
let secretBuffer = this.secret as Buffer;
if (this.secret instanceof ArrayBuffer) {
secretBuffer = Buffer.from(this.secret);
} else if (ArrayBuffer.isView && ArrayBuffer.isView(this.secret)) {
secretBuffer = Buffer.from(this.secret.buffer, this.secret.byteOffset, this.secret.byteLength);
}
this._hash = crypto.rawCrypto.createHmac(this.algorithmIdentifier, secretBuffer);
} else {
this._hash = crypto.rawCrypto.createHash(this.algorithmIdentifier);
}
return this;
}
update(data: SourceData) {
// Handle all possible data types
let buffer: Buffer = data as Buffer;
if (typeof data === "string") {
buffer = Buffer.from(data, "utf8");
} else if (data instanceof ArrayBuffer) {
buffer = Buffer.from(data);
} else if (ArrayBuffer.isView && ArrayBuffer.isView(data)) {
buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
}
this._hash?.update(buffer);
return this;
}
digest(): Promise<Uint8Array> {
const result = new Uint8Array(this._hash?.digest() || []);
this.reset();
return Promise.resolve(result);
}
}

View File

@@ -193,13 +193,12 @@ const encryptAsymmetricFipsValidated = (data: string, publicKey: string, private
format: "der"
});
// Generate shared secret using X25519
// Generate shared secret using x25519 curve
const sharedSecret = crypto.diffieHellman({
privateKey: privKeyObj,
publicKey: pubKeyObj
});
// Generate 24-byte nonce (same as NaCl)
const nonce = crypto.randomBytes(24);
// Derive 32-byte key from shared secret
@@ -209,7 +208,7 @@ const encryptAsymmetricFipsValidated = (data: string, publicKey: string, private
const iv = nonce.subarray(0, 12);
// Encrypt with AES-256-GCM
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
const ciphertext = cipher.update(data, "utf8");
cipher.final();
@@ -665,7 +664,8 @@ const cryptographyFactory = () => {
},
constants: crypto.constants,
X509Certificate: crypto.X509Certificate,
KeyObject: crypto.KeyObject
KeyObject: crypto.KeyObject,
Hash: crypto.Hash
}
};
};

View File

@@ -2,6 +2,7 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
import { AxiosError } from "axios";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -35,6 +36,8 @@ export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig
case AwsConnectionMethod.AssumeRole: {
const client = new STSClient({
region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID && appCfg.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
? {

View File

@@ -3,6 +3,7 @@ import * as x509 from "@peculiar/x509";
import acme from "acme-client";
import { TableName } from "@app/db/schemas";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
@@ -102,6 +103,8 @@ export const route53InsertTxtRecord = async (
) => {
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
const route53Client = new Route53Client({
sha256: CustomAWSHasher,
useFipsEndpoint: crypto.isFipsModeEnabled(),
credentials: config.credentials!,
region: config.region
});

View File

@@ -30,6 +30,7 @@ import RE2 from "re2";
import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto/cryptography";
@@ -796,6 +797,8 @@ const syncSecretsAWSParameterStore = async ({
if (awsAssumeRoleArn) {
const client = new STSClient({
region: integration.region as string,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials:
appCfg.CLIENT_ID_AWS_INTEGRATION && appCfg.CLIENT_SECRET_AWS_INTEGRATION
? {

View File

@@ -24,6 +24,8 @@ import {
Tag
} from "aws-sdk/clients/secretsmanager";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
@@ -46,6 +48,8 @@ const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCre
const secretsManagerClient = new SecretsManagerClient({
region: config.region,
useFipsEndpoint: crypto.isFipsModeEnabled(),
sha256: CustomAWSHasher,
credentials: config.credentials!
});

View File

@@ -1,12 +1,14 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { useServerConfig, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useGetServerRootKmsEncryptionDetails,
@@ -29,6 +31,7 @@ export const EncryptionPageForm = () => {
const { data: rootKmsDetails } = useGetServerRootKmsEncryptionDetails();
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
const { config } = useServerConfig();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -117,14 +120,27 @@ export const EncryptionPageForm = () => {
)}
</div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
<div className="flex w-full items-center justify-between">
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
{config.fipsEnabled && (
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
<div>
<Badge className="flex items-center gap-2" variant="primary">
FIPS Mode: Enabled
<FontAwesomeIcon icon={faInfoCircle} />
</Badge>
</div>
</Tooltip>
)}
</div>
</form>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}