diff --git a/.changeset/fips-mode-support.md b/.changeset/fips-mode-support.md new file mode 100644 index 0000000000000..56c45fc870e88 --- /dev/null +++ b/.changeset/fips-mode-support.md @@ -0,0 +1,13 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/federation-matrix': minor +'@rocket.chat/account-service': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/queue-worker': minor +--- + +Adds support for running Rocket.Chat in FIPS mode. The monolith and all microservices (ddp-streamer, account-service, authorization-service, presence-service, queue-worker, omnichannel-transcript) can now enforce FIPS-compliant cryptography via Node.js/OpenSSL FIPS, with dedicated FIPS Docker images. Running in FIPS mode requires a license including the new `fips` module, and FIPS status is now reported in server logs and statistics. diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 87389cbf52495..fb3b5adf2ae70 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -8,6 +8,12 @@ inputs: CR_PAT: required: true description: 'GitHub Container Registry Personal Access Token' + DOCKER_USER: + required: false + description: 'DockerHub username (required for FIPS builds to pull private base image)' + DOCKER_PASS: + required: false + description: 'DockerHub password (required for FIPS builds to pull private base image)' deno-version: required: true description: 'Deno version' @@ -28,7 +34,7 @@ inputs: default: 'true' type: required: false - description: 'production or coverage' + description: 'production, coverage, or fips' default: 'coverage' runs: @@ -43,11 +49,19 @@ runs: username: ${{ inputs.CR_USER }} password: ${{ inputs.CR_PAT }} + - name: Login to DockerHub + # FIPS base image (rocketchat/dhi-node) lives in a private DockerHub repo and requires auth to pull. + if: inputs.type == 'fips' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ inputs.DOCKER_USER }} + password: ${{ inputs.DOCKER_PASS }} + - name: Restore meteor build if: inputs.service == 'rocketchat' uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: - name: build-${{ inputs.type }} + name: build-${{ inputs.type == 'fips' && ((github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production') || inputs.type }} path: /tmp/build - name: Unpack meteor build @@ -112,6 +126,10 @@ runs: GITHUB_REF: ${{ github.ref }} run: | set -o xtrace + compose_fips_override='' + if [[ "$INPUT_TYPE" == 'fips' ]]; then + compose_fips_override='-f docker-compose-ci.fips.yml' + fi # Removes unnecessary swc cores and sharp binaries to reduce image size swc_arch='x64' @@ -136,10 +154,10 @@ runs: fi # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) - IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$INPUT_SERVICE" '.services[$s].image') + IMAGE=$(docker compose -f docker-compose-ci.yml $compose_fips_override config --format json 2>/dev/null | jq -r --arg s "$INPUT_SERVICE" '.services[$s].image') docker buildx bake \ - -f docker-compose-ci.yml \ + -f docker-compose-ci.yml $compose_fips_override \ ${LOAD_OR_PUSH} \ --allow=fs.read=/tmp/build \ --set "*.tags+=${IMAGE}-gha-run-${GITHUB_RUN_ID}" \ @@ -160,6 +178,8 @@ runs: SERVICE_SUFFIX='' if [[ "$INPUT_SERVICE" == 'rocketchat' && "$INPUT_TYPE" == 'coverage' ]] && [[ "$GITHUB_EVENT_NAME" == 'release' || "$GITHUB_REF" == 'refs/heads/develop' ]]; then SERVICE_SUFFIX='-cov' + elif [[ "$INPUT_TYPE" == 'fips' ]]; then + SERVICE_SUFFIX='-fips' fi mkdir -p "/tmp/manifests/${INPUT_SERVICE}${SERVICE_SUFFIX}/${INPUT_ARCH}" @@ -187,9 +207,13 @@ runs: TYPE: ${{ inputs.type }} run: | set -o xtrace + compose_fips_override='' + if [[ "$TYPE" == 'fips' ]]; then + compose_fips_override='-f docker-compose-ci.fips.yml' + fi # Get image name from docker-compose-ci.yml - IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$SERVICE" '.services[$s].image') + IMAGE=$(docker compose -f docker-compose-ci.yml $compose_fips_override config --format json 2>/dev/null | jq -r --arg s "$SERVICE" '.services[$s].image') # Create directory for image archives mkdir -p /tmp/docker-images diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index be682452ba934..1fec4a979a109 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -26,6 +26,10 @@ on: release: required: true type: string + fips: + default: false + required: false + type: boolean shard: default: '[1]' required: false @@ -139,7 +143,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' with: - pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || 'docker-image-*-amd64-coverage' }} + pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || (inputs.fips && 'docker-image-*-amd64-fips' || 'docker-image-*-amd64-coverage') }} path: /tmp/docker-images merge-multiple: true @@ -199,8 +203,10 @@ jobs: COMPOSE_PROFILES: ${{ inputs.type == 'api' && 'api' || '' }} TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} APPS_ENGINE_RUNTIME_BACKEND: ${{ inputs.type == 'api-apps-node' && 'node' || '' }} + FIPS_OVERRIDE: ${{ inputs.fips && '-f docker-compose-ci.fips.yml' || '' }} run: | - DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait + read -r -a fips_override <<< "$FIPS_OVERRIDE" + DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml "${fips_override[@]}" up -d --wait - uses: ./.github/actions/setup-playwright if: inputs.type == 'ui' @@ -308,7 +314,7 @@ jobs: if: inputs.type == 'ui' && always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} + name: playwright-test-trace-${{ inputs.release }}${{ inputs.fips && '-fips' || '' }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: ./apps/meteor/tests/e2e/.playwright* include-hidden-files: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f980f1a10f476..1656c435591dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -360,20 +360,16 @@ jobs: [rocketchat], ] type: - # if running in a PR build with coverage - ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }} + - fips exclude: - # when images aren't published (merge queue, fork PRs) only amd64 tars are uploaded - # as artifacts, so arm64 builds would be discarded — skip them - - arch: ${{ (github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'arm64' || '' }} + - arch: arm64 + type: fips include: - # if not, build with coverage for tests - arch: amd64 service: [rocketchat] type: coverage - # resolves to amd64 when arm64 is excluded above, matching the entry before this - # one instead of re-adding an arm64 job (includes bypass exclude) - - arch: ${{ (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && 'arm64' || 'amd64' }} + - arch: arm64 service: [rocketchat] type: coverage @@ -392,6 +388,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[0] }} @@ -406,6 +404,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[1] }} @@ -421,6 +421,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[2] }} @@ -436,6 +438,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[3] }} @@ -458,6 +462,7 @@ jobs: with: sparse-checkout: | docker-compose-ci.yml + docker-compose-ci.fips.yml sparse-checkout-cone-mode: false ref: ${{ github.ref }} @@ -500,6 +505,9 @@ jobs: # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) if [ "$service" == "rocketchat-cov" ]; then IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "rocketchat" '.services[$s].image')-cov + elif [[ "$service" == *"-fips" ]]; then + base_service="${service%-fips}" + IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$base_service" '.services[$s].image')-fips else IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$service" '.services[$s].image') fi @@ -753,6 +761,76 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + test-api-fips: + name: 🔨 Test API (FIPS) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + test-api-livechat-fips: + name: 🔨 Test API Livechat (FIPS) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api-livechat + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + test-ui-fips: + name: 🔨 Test UI (FIPS) + if: contains(github.event.pull_request.labels.*.name, 'fips') + needs: [checks, build-gh-docker-publish, release-versions] + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: ui + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + shard: '[1, 2, 3, 4, 5]' + total-shard: 5 + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} + REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} + REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + test-federation-matrix: name: 🔨 Test Federation Matrix needs: [checks, build-gh-docker-publish, packages-build, release-versions] @@ -907,7 +985,7 @@ jobs: tests-done: name: ✅ Tests Done runs-on: ubuntu-24.04-arm - needs: [test-guard, checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-api-livechat, test-api-livechat-ee, test-api-apps-node-ee, test-federation-matrix] + needs: [test-guard, checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-api-livechat, test-api-livechat-ee, test-api-apps-node-ee, test-api-fips, test-api-livechat-fips, test-ui-fips, test-federation-matrix] if: always() && needs.test-guard.outputs.skip-tests != 'true' steps: - name: Test finish aggregation @@ -948,6 +1026,18 @@ jobs: exit 1 fi + if [[ '${{ needs.test-api-fips.result }}' != 'success' ]]; then + exit 1 + fi + + if [[ '${{ needs.test-api-livechat-fips.result }}' != 'success' ]]; then + exit 1 + fi + + if [[ '${{ needs.test-ui-fips.result }}' != 'success' && '${{ needs.test-ui-fips.result }}' != 'skipped' ]]; then + exit 1 + fi + if [[ '${{ needs.test-federation-matrix.result }}' != 'success' ]]; then exit 1 fi @@ -1020,6 +1110,7 @@ jobs: with: sparse-checkout: | docker-compose-ci.yml + docker-compose-ci.fips.yml sparse-checkout-cone-mode: false ref: ${{ github.ref }} @@ -1103,7 +1194,12 @@ jobs: fi # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) - SRC=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "${service}" '.services[$s].image') + if [[ "$service" == *"-fips" ]]; then + base_service="${service%-fips}" + SRC=$(docker compose -f docker-compose-ci.yml -f docker-compose-ci.fips.yml config --format json 2>/dev/null | jq -r --arg s "$base_service" '.services[$s].image') + else + SRC=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "${service}" '.services[$s].image') + fi DEST_REPO="docker.io/${IMAGE_NAME}" echo "Copying $SRC to ${DEST_REPO}:${PRIMARY}" diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 2bee7b45f36e3..307c801f261a0 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -20,7 +20,7 @@ RUN cd /app/bundle/programs/server \ && find /app/bundle/programs/server/npm/node_modules -type f -name '*.map' -delete \ && find /app/bundle/programs/web.browser -type f -name '*.map' -delete -FROM node:22.22.3-alpine3.23 +FROM node:22.22.3-alpine3.23 AS release-standard LABEL maintainer="buildmaster@rocket.chat" @@ -65,3 +65,29 @@ WORKDIR /app/bundle EXPOSE 3000 CMD ["node", "main.js"] + +FROM rocketchat/dhi-node:22.22.3-alpine3.23-fips AS release-fips + +LABEL maintainer="buildmaster@rocket.chat" + +# needs a mongo instance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + APPS_ENGINE_RUNTIME_BACKEND=node + +USER node + +COPY --from=builder --chown=node:node /app /app + +VOLUME /app/uploads + +WORKDIR /app/bundle + +EXPOSE 3000 + +CMD ["node", "--force-fips", "main.js"] diff --git a/apps/meteor/ee/server/lib/license/enforceFipsLicense.ts b/apps/meteor/ee/server/lib/license/enforceFipsLicense.ts new file mode 100644 index 0000000000000..bd54339262d62 --- /dev/null +++ b/apps/meteor/ee/server/lib/license/enforceFipsLicense.ts @@ -0,0 +1,35 @@ +import crypto from 'node:crypto'; + +import { License } from '@rocket.chat/license'; + +import { showErrorBox } from '../../../../server/lib/logger/showBox'; + +export function enforceFipsLicense(): void { + if (!crypto.getFips()) { + return; + } + + if (License.hasModule('fips')) { + return; + } + + const reason = !License.encryptedLicense + ? 'No license was found in this workspace' + : (!License.hasValidLicense() && 'The license applied to this workspace is invalid or expired') || + "The license applied to this workspace does not include the 'fips' module"; + + showErrorBox( + 'FIPS LICENSE ERROR', + [ + `${reason}.`, + '', + 'This server is running in FIPS mode, which requires a valid license', + "including the 'fips' module.", + '', + 'Provide your license through the ROCKETCHAT_LICENSE environment variable,', + 'or contact Rocket.Chat to obtain one: https://www.rocket.chat/sales-contact', + ].join('\n'), + ); + + process.exit(1); +} diff --git a/apps/meteor/server/api/v1/misc.ts b/apps/meteor/server/api/v1/misc.ts index c71a6efdb607f..cf762279f59b3 100644 --- a/apps/meteor/server/api/v1/misc.ts +++ b/apps/meteor/server/api/v1/misc.ts @@ -647,7 +647,7 @@ API.v1.post( const connectionId = this.token || crypto - .createHash('md5') + .createHash('sha256') .update((this.requestIp ?? '') + this.user._id) .digest('hex'); @@ -708,7 +708,7 @@ API.v1.post( const connectionId = this.token || crypto - .createHash('md5') + .createHash('sha256') .update(this.requestIp ?? '') .digest('hex'); diff --git a/apps/meteor/server/lib/2fa/code/index.ts b/apps/meteor/server/lib/2fa/code/index.ts index e86a77b3178cb..6d924fb8e8cfe 100644 --- a/apps/meteor/server/lib/2fa/code/index.ts +++ b/apps/meteor/server/lib/2fa/code/index.ts @@ -60,7 +60,7 @@ export function getFingerprintFromConnection(connection: IMethodConnection): str clientAddress: connection.clientAddress, }); - return crypto.createHash('md5').update(data).digest('hex'); + return crypto.createHash('sha256').update(data).digest('hex'); } export function getRememberDate(from: Date = new Date()): Date | undefined { diff --git a/apps/meteor/server/lib/statistics/lib/statistics.ts b/apps/meteor/server/lib/statistics/lib/statistics.ts index ec1f6757cc7ff..9375693a80fbe 100644 --- a/apps/meteor/server/lib/statistics/lib/statistics.ts +++ b/apps/meteor/server/lib/statistics/lib/statistics.ts @@ -1,4 +1,5 @@ import { log } from 'console'; +import crypto from 'node:crypto'; import os from 'node:os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; @@ -345,6 +346,8 @@ export const statistics = { platform: process.env.DEPLOY_PLATFORM || 'selfinstall', }; + statistics.fips = !!crypto.getFips(); + statistics.readReceiptsEnabled = settings.get('Message_Read_Receipt_Enabled'); statistics.readReceiptsDetailed = settings.get('Message_Read_Receipt_Store_Users'); diff --git a/apps/meteor/server/startup/serverRunning.ts b/apps/meteor/server/startup/serverRunning.ts index aee622af81793..8114f8b062d97 100644 --- a/apps/meteor/server/startup/serverRunning.ts +++ b/apps/meteor/server/startup/serverRunning.ts @@ -1,3 +1,4 @@ +import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; @@ -51,6 +52,8 @@ Meteor.startup(async () => { ` Process Port: ${process.env.PORT}`, ` Site URL: ${settings.get('Site_Url')}`, ` Hashed Site URL: ${License.getHashedWorkspaceUrl()}`, + ` OpenSSL Version: ${process.versions.openssl}`, + ` FIPS Provider: ${crypto.getFips() ? 'Enabled' : 'Disabled'}`, ]; if (Info.commit?.hash) { diff --git a/apps/meteor/startRocketChat.ts b/apps/meteor/startRocketChat.ts index d69bf8d0ebc38..196c37d54151b 100644 --- a/apps/meteor/startRocketChat.ts +++ b/apps/meteor/startRocketChat.ts @@ -1,4 +1,5 @@ import { registerEEBroker } from './ee/server'; +import { enforceFipsLicense } from './ee/server/lib/license/enforceFipsLicense'; import { startLicense } from './ee/server/lib/license/startup'; import { startFederationService as startFederationMatrixService } from './ee/server/startup/federation'; @@ -15,5 +16,7 @@ export const startRocketChat = async () => { await startLicense(); + enforceFipsLicense(); + await loadAfterLicense(); }; diff --git a/docker-compose-ci.fips.yml b/docker-compose-ci.fips.yml new file mode 100644 index 0000000000000..9d6c163069de7 --- /dev/null +++ b/docker-compose-ci.fips.yml @@ -0,0 +1,39 @@ +services: + rocketchat: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}-fips + environment: + - APPS_ENGINE_RUNTIME_BACKEND=node + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/livez', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"] + + account-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG}-fips + + authorization-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG}-fips + + presence-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG}-fips + + ddp-streamer-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/ddp-streamer-service:${DOCKER_TAG}-fips + + queue-worker-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/queue-worker-service:${DOCKER_TAG}-fips + + omnichannel-transcript-service: + build: + target: release-fips + image: ghcr.io/${LOWERCASE_REPOSITORY}/omnichannel-transcript-service:${DOCKER_TAG}-fips diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index cef50ced7cbf4..5882d9829f2bf 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -5,6 +5,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/apps/meteor/.docker/Dockerfile.alpine context: /tmp/build + target: release-standard x-bake: platforms: - linux/amd64 @@ -62,6 +63,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/authorization-service + target: release-standard x-bake: platforms: - linux/amd64 @@ -82,6 +84,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/account-service + target: release-standard x-bake: platforms: - linux/amd64 @@ -102,6 +105,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/presence-service + target: release-standard x-bake: platforms: - linux/amd64 @@ -122,6 +126,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/ddp-streamer + target: release-standard x-bake: platforms: - linux/amd64 @@ -148,6 +153,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/queue-worker + target: release-standard x-bake: platforms: - linux/amd64 @@ -168,6 +174,7 @@ services: build: dockerfile: ${GITHUB_WORKSPACE:-}/ee/apps/Dockerfile context: out/omnichannel-transcript + target: release-standard x-bake: platforms: - linux/amd64 diff --git a/ee/apps/Dockerfile b/ee/apps/Dockerfile index 380aaee56b81e..6031ba004f81a 100644 --- a/ee/apps/Dockerfile +++ b/ee/apps/Dockerfile @@ -50,7 +50,7 @@ RUN set -eu; cd /app; \ done; \ find packages ee/packages ee/apps \( -name '*.js.map' -o -name '*.d.ts' \) -delete -FROM node:22.22.3-alpine3.23 +FROM node:22.22.3-alpine3.23 AS release-standard ARG SERVICE @@ -76,3 +76,13 @@ USER rocketchat EXPOSE 3000 9458 CMD ["node", "src/service.js"] + +FROM rocketchat/dhi-node:22.22.3-alpine3.23-fips AS release-fips +ARG SERVICE +ENV NODE_ENV=production \ + PORT=3000 +COPY --chown=node:node --from=builder /app /app +WORKDIR /app/ee/apps/${SERVICE} +USER node +EXPOSE 3000 9458 +CMD ["node", "--force-fips", "--require", "./src/fips.js", "src/service.js"] diff --git a/ee/apps/account-service/src/fips.ts b/ee/apps/account-service/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/account-service/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/authorization-service/src/fips.ts b/ee/apps/authorization-service/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/authorization-service/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/authorization-service/tsconfig.json b/ee/apps/authorization-service/tsconfig.json index 04f7fc0034d1c..7c04aabd6bb1b 100644 --- a/ee/apps/authorization-service/tsconfig.json +++ b/ee/apps/authorization-service/tsconfig.json @@ -4,7 +4,7 @@ "strictPropertyInitialization": false, // TODO: Remove this line "outDir": "./dist" }, - "files": ["./src/service.ts"], + "files": ["./src/service.ts", "./src/fips.ts"], "include": ["../../../apps/meteor/definition/externals/meteor"], "exclude": ["./dist"] } diff --git a/ee/apps/ddp-streamer/src/fips.ts b/ee/apps/ddp-streamer/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/ddp-streamer/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/omnichannel-transcript/src/fips.ts b/ee/apps/omnichannel-transcript/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/omnichannel-transcript/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/omnichannel-transcript/tsconfig.json b/ee/apps/omnichannel-transcript/tsconfig.json index 6c7f2d916a732..253de00013a22 100644 --- a/ee/apps/omnichannel-transcript/tsconfig.json +++ b/ee/apps/omnichannel-transcript/tsconfig.json @@ -4,7 +4,7 @@ "strictPropertyInitialization": false, "outDir": "./dist/ee/apps/omnichannel-transcript/src", }, - "files": ["./src/service.ts"], + "files": ["./src/service.ts", "./src/fips.ts"], "include": ["../../../apps/meteor/definition/externals/meteor"], "exclude": ["./dist"] } diff --git a/ee/apps/presence-service/src/fips.ts b/ee/apps/presence-service/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/presence-service/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/presence-service/tsconfig.json b/ee/apps/presence-service/tsconfig.json index ff29a55af231c..34dfe4e779780 100644 --- a/ee/apps/presence-service/tsconfig.json +++ b/ee/apps/presence-service/tsconfig.json @@ -4,7 +4,7 @@ "strictPropertyInitialization": false, // TODO: Remove this line "outDir": "./dist/ee/apps/presence-service/src", }, - "files": ["./src/service.ts"], + "files": ["./src/service.ts", "./src/fips.ts"], "include": ["../../../apps/meteor/definition/externals/meteor"], "exclude": ["./dist"] } diff --git a/ee/apps/queue-worker/src/fips.ts b/ee/apps/queue-worker/src/fips.ts new file mode 100644 index 0000000000000..fab82191a20c8 --- /dev/null +++ b/ee/apps/queue-worker/src/fips.ts @@ -0,0 +1,9 @@ +import crypto from 'crypto'; + +crypto.setFips(true); + +if (!crypto.getFips()) { + throw new Error('FIPS mode was not enabled after crypto.setFips(true)'); +} + +console.log('FIPS COMPLIANCE CHECK: YES'); diff --git a/ee/apps/queue-worker/tsconfig.json b/ee/apps/queue-worker/tsconfig.json index c12ebd48bcdc0..c36303f2794d1 100644 --- a/ee/apps/queue-worker/tsconfig.json +++ b/ee/apps/queue-worker/tsconfig.json @@ -4,7 +4,7 @@ "strictPropertyInitialization": false, "outDir": "./dist/ee/apps/queue-worker/src", }, - "files": ["./src/service.ts"], + "files": ["./src/service.ts", "./src/fips.ts"], "include": ["../../../apps/meteor/definition/externals/meteor"], "exclude": ["./dist"] } diff --git a/ee/packages/federation-matrix/docker-compose.test.yml b/ee/packages/federation-matrix/docker-compose.test.yml index ad06faeb0ef38..4c9fc6bf44390 100644 --- a/ee/packages/federation-matrix/docker-compose.test.yml +++ b/ee/packages/federation-matrix/docker-compose.test.yml @@ -82,6 +82,7 @@ services: build: context: ${ROCKETCHAT_BUILD_CONTEXT:-./test/dist} dockerfile: ${ROCKETCHAT_DOCKERFILE:-../../../apps/meteor/.docker/Dockerfile.alpine} + target: release-standard image: ${ROCKETCHAT_IMAGE:-rocketchat/rocket.chat:latest} profiles: - test diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts index 9bb991ab0d7e9..262afa5d3249e 100644 --- a/ee/packages/federation-matrix/src/api/.well-known/server.ts +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -32,7 +32,7 @@ export const getWellKnownRoutes = () => { async (c) => { const responseData = federationSDK.getWellKnownHostData(); - const etag = createHash('md5').update(JSON.stringify(responseData)).digest('hex'); + const etag = createHash('sha256').update(JSON.stringify(responseData)).digest('hex'); c.header('ETag', etag); c.header('Content-Type', 'application/json'); diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 37280c86b5cf0..939b3558c8d72 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -273,4 +273,5 @@ export interface IStats extends IRocketChatRecord { abacTotalAttributeValues?: number; abacRoomsEnrolled?: number; allowUnsafeQueryAndFieldsApiParamsEnabled?: boolean; + fips?: boolean; } diff --git a/packages/core-typings/src/license/LicenseModule.ts b/packages/core-typings/src/license/LicenseModule.ts index 5267817164db6..eeb8914d05238 100644 --- a/packages/core-typings/src/license/LicenseModule.ts +++ b/packages/core-typings/src/license/LicenseModule.ts @@ -24,6 +24,7 @@ export const CoreModules = [ 'teams-voip', 'outbound-messaging', 'abac', + 'fips', ] as const; export type InternalModuleName = (typeof CoreModules)[number];