From d31f9bc31fe6a482df9ba625a196eb06bb485224 Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Mon, 3 Aug 2026 09:55:29 +0200 Subject: [PATCH 1/7] Unite matrix and changes files in CI pipeline --- .github/workflows/changes.yaml | 106 +++++++++++++++++++++++++++++++++ .github/workflows/changes.yml | 51 ---------------- .github/workflows/release.yml | 39 ++++++++---- .github/workflows/test.yml | 36 ++++++----- 4 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/changes.yaml delete mode 100644 .github/workflows/changes.yml diff --git a/.github/workflows/changes.yaml b/.github/workflows/changes.yaml new file mode 100644 index 0000000000..5d524fea6a --- /dev/null +++ b/.github/workflows/changes.yaml @@ -0,0 +1,106 @@ +name: Detect Changes + +on: + workflow_call: + outputs: + images_matrix: + description: "Images matrix configuration for all components" + value: ${{ jobs.collect.outputs.images_matrix }} + changes: + description: "JSON object mapping each component key to true/false" + value: ${{ jobs.collect.outputs.changes }} + timestamp: + value: ${{ jobs.collect.outputs.timestamp }} + commit_sha: + value: ${{ jobs.collect.outputs.commit_sha }} + test_commit_sha: + value: ${{ jobs.collect.outputs.test_commit_sha }} + +jobs: + collect: + name: Changes and Matrix + runs-on: ubuntu-latest + outputs: + images_matrix: ${{ steps.define.outputs.images_matrix }} + changes: ${{ steps.changes.outputs.changes }} + timestamp: ${{ steps.setup_env.outputs.timestamp }} + commit_sha: ${{ steps.setup_env.outputs.commit_sha }} + test_commit_sha: ${{ steps.setup_env.outputs.test_commit_sha }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up environment + id: setup_env + run: | + set -euo pipefail + echo "timestamp=$(date +%s)" >> "$GITHUB_OUTPUT" + echo "commit_sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT" + echo "test_commit_sha=test-${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT" + + - name: Define components + id: define + run: | + set -euo pipefail + + IMAGES='[ + { + "name": "meshcentral", + "paths": [ + "./docker/**", + "./package.json", + "./translate/**", + "./*.js", + "./views/**", + "./public/**" + ] + } + ]' + + ADDONS='{ + "helm": ["./charts/**"] + }' + + images_matrix=$(jq -c '[ .[] | { + name, + path: (.path // "./docker"), + context: (.context // ".") + } ]' <<< "$IMAGES") + + filters=$(jq -nc \ + --argjson images "$IMAGES" \ + --argjson addons "$ADDONS" ' + ( [ $images[] | { key: .name, value: (.paths // ["./\(.name)/**"]) } ] + | from_entries ) + + $addons + ') + + keys=$(jq -nc --argjson f "$filters" '$f | keys') + + # fail fast if the matrix or filters came out empty + [ "$(jq 'length' <<< "$images_matrix")" -gt 0 ] || { echo "images_matrix is empty" >&2; exit 1; } + [ "$(jq 'length' <<< "$keys")" -gt 0 ] || { echo "no change filters" >&2; exit 1; } + + { + echo "images_matrix=$images_matrix" + echo "filters=$filters" + echo "keys=$keys" + } >> "$GITHUB_OUTPUT" + + - uses: dorny/paths-filter@v4.0.2 + id: filter + with: + filters: ${{ steps.define.outputs.filters }} + + - name: Build changes map + id: changes + env: + CHANGED: ${{ steps.filter.outputs.changes || '[]' }} + KEYS: ${{ steps.define.outputs.keys }} + run: | + set -euo pipefail + changes=$(jq -nc --argjson changed "$CHANGED" --argjson keys "$KEYS" ' + reduce $keys[] as $k ({}; .[$k] = ((($changed | index($k)) != null) | tostring)) + ') + echo "changes=$changes" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/changes.yml b/.github/workflows/changes.yml deleted file mode 100644 index d182c9bff3..0000000000 --- a/.github/workflows/changes.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Generate Changes - -on: - workflow_call: - outputs: - docker: - value: ${{ jobs.changes.outputs.docker }} - helm: - value: ${{ jobs.changes.outputs.helm }} - timestamp: - value: ${{ jobs.changes.outputs.timestamp }} - commit_sha: - value: ${{ jobs.changes.outputs.commit_sha }} - test_commit_sha: - value: ${{ jobs.changes.outputs.test_commit_sha }} - -jobs: - changes: - name: Changes check - runs-on: ubuntu-latest - outputs: - docker: ${{ steps.filter.outputs.docker }} - helm: ${{ steps.filter.outputs.helm }} - timestamp: ${{ steps.setup_env.outputs.timestamp }} - commit_sha: ${{ steps.setup_env.outputs.commit_sha }} - test_commit_sha: ${{ steps.setup_env.outputs.test_commit_sha }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - name: Set up environment - id: setup_env - run: | - echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT - echo "commit_sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT - echo "test_commit_sha=test-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT - - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - docker: - - 'docker/**' - - 'package.json' - - 'translate/**' - - '*.js' - - 'views/**' - - 'public/**' - helm: - - 'charts/**' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a160202e90..a1501e2628 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,29 +41,37 @@ jobs: changes: name: Detect Changes - uses: ./.github/workflows/changes.yml + uses: ./.github/workflows/changes.yaml if: | github.event_name == 'workflow_dispatch' || github.event_name == 'push' build: - name: "Build meshcentral" + name: "Build: ${{ matrix.name }}" needs: [version, changes] runs-on: ubuntu-latest - if: | - (github.event_name == 'push' && needs.changes.outputs.docker == 'true') || - (github.event_name == 'workflow_dispatch' && needs.version.outputs.version != '') + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.changes.outputs.images_matrix) }} steps: + - name: Check if build should run + id: should_run + run: echo "run=${{ (github.event_name == 'workflow_dispatch' && needs.version.outputs.version != '') || (github.event_name == 'push' && fromJSON(needs.changes.outputs.changes || '{}')[matrix.name] == 'true') }}" >> "$GITHUB_OUTPUT" + - name: Checkout uses: actions/checkout@v4 + if: steps.should_run.outputs.run == 'true' - name: Set up Node.js uses: actions/setup-node@v6 + if: steps.should_run.outputs.run == 'true' with: node-version: "24.x" - name: Run translations + if: steps.should_run.outputs.run == 'true' working-directory: translate run: | node translate.js || true @@ -73,6 +81,7 @@ jobs: - name: Log in to GitHub Container Registry uses: docker/login-action@v3 + if: steps.should_run.outputs.run == 'true' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -80,17 +89,20 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + if: steps.should_run.outputs.run == 'true' with: platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + if: steps.should_run.outputs.run == 'true' - name: Generate metadata id: meta uses: docker/metadata-action@v5 + if: steps.should_run.outputs.run == 'true' with: - images: ${{ env.REGISTRY }}/${{ env.ORGANISATION }}/${{ env.REPOSITORY }}/meshcentral + images: ${{ env.REGISTRY }}/${{ env.ORGANISATION }}/${{ env.REPOSITORY }}/${{ matrix.name }} tags: | type=ref,event=tag type=raw,value=latest,enable={{is_default_branch}} @@ -103,9 +115,10 @@ jobs: - name: Build and push image id: docker_build uses: docker/build-push-action@v6 + if: steps.should_run.outputs.run == 'true' with: - context: . - file: ./docker/Dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.path }}/Dockerfile platforms: linux/amd64,linux/arm64 push: true provenance: false @@ -118,7 +131,7 @@ jobs: uses: dataaxiom/ghcr-cleanup-action@v1 continue-on-error: true with: - package: ${{ env.REPOSITORY }}/meshcentral + package: ${{ env.REPOSITORY }}/${{ matrix.name }} delete-ghost-images: true delete-tags: '.*' exclude-tags: '^(?:[0-9]+\.[0-9]+\.[0-9]+|latest|test-.*)$' @@ -130,7 +143,7 @@ jobs: needs: [version, changes] runs-on: ubuntu-latest if: | - (github.event_name == 'push' && needs.changes.outputs.helm == 'true') || + (github.event_name == 'push' && fromJSON(needs.changes.outputs.changes || '{}').helm == 'true') || (github.event_name == 'workflow_dispatch' && needs.version.outputs.version != '') steps: - name: Checkout @@ -153,7 +166,7 @@ jobs: release: name: "Create Release" - needs: [version, build, build_helm] + needs: [version, changes, build, build_helm] if: ${{ !failure() && !cancelled() }} runs-on: ubuntu-latest steps: @@ -163,9 +176,11 @@ jobs: - name: Generate release header run: | VERSION="${{ needs.version.outputs.version }}" + IMAGES=$(echo '${{ needs.changes.outputs.images_matrix }}' | jq -r '.[] | "- `${{ env.REGISTRY }}/${{ github.repository }}/\(.name):'"${VERSION}"'`"') + cat > RELEASE_HEADER.md <> "$GITHUB_OUTPUT" + - name: Checkout uses: actions/checkout@v4 + if: steps.should_run.outputs.run == 'true' - name: Set up Node.js uses: actions/setup-node@v6 + if: steps.should_run.outputs.run == 'true' with: node-version: "24.x" - name: Log in to GitHub Container Registry uses: docker/login-action@v3 + if: steps.should_run.outputs.run == 'true' with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -65,17 +72,20 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + if: steps.should_run.outputs.run == 'true' with: platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + if: steps.should_run.outputs.run == 'true' - name: Generate metadata id: meta uses: docker/metadata-action@v5 + if: steps.should_run.outputs.run == 'true' with: - images: ${{ env.REGISTRY }}/${{ env.ORGANISATION }}/${{ env.REPOSITORY }}/meshcentral + images: ${{ env.REGISTRY }}/${{ env.ORGANISATION }}/${{ env.REPOSITORY }}/${{ matrix.name }} tags: | type=ref,event=tag type=raw,value=${{ needs.changes.outputs.test_commit_sha }} @@ -88,9 +98,10 @@ jobs: - name: Build image id: docker_build uses: docker/build-push-action@v6 + if: steps.should_run.outputs.run == 'true' with: - context: . - file: ./docker/Dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.path }}/Dockerfile platforms: linux/amd64,linux/arm64 push: false provenance: false @@ -103,10 +114,7 @@ jobs: name: "Test Helm Chart" needs: [changes] runs-on: ubuntu-latest - if: | - github.event_name == 'pull_request' && - !github.event.pull_request.draft && - needs.changes.outputs.helm == 'true' + if: fromJSON(needs.changes.outputs.changes || '{}').helm == 'true' # draft gate inherited from changes steps: - name: Checkout uses: actions/checkout@v4 From 2843f4ba636abad86a94c1a4ec6272bccdc45810 Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Mon, 3 Aug 2026 10:16:29 +0200 Subject: [PATCH 2/7] Update alpine to 3.24 and slim the image --- .dockerignore | 3 ++- docker/Dockerfile | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.dockerignore b/.dockerignore index f48e764d11..16922b80eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,4 +12,5 @@ docker/ *.njsproj *.md examples -tests \ No newline at end of file +tests +charts/ \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 58148e2c1a..9d672ef480 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,8 @@ ### STAGE 1 PRECOMPILE DEPS MODULE -FROM alpine:3.23 AS dep-compiler +FROM alpine:3.24 AS dep-compiler -RUN echo -e "----------\nINSTALLING ALPINE PACKAGES...\n----------"; \ - apk add --no-cache --update \ +RUN apk add --no-cache --update \ bash gcc g++ jq make nodejs npm python3 tzdata COPY ./ /opt/meshcentral/meshcentral/ @@ -16,8 +15,7 @@ RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22", "mong ### STAGE 2 OPENFRAME RUNTIME # Use same alpine version as dep-compiler to ensure native module ABI compatibility - -FROM alpine:3.23 +FROM alpine:3.24 ARG MESH_INSTALL_DIR=/opt/meshcentral ARG MESH_DIR=/opt/mesh @@ -27,18 +25,20 @@ ENV MESH_INSTALL_DIR=${MESH_INSTALL_DIR} ENV MESH_DIR=${MESH_DIR} ENV MESH_TEMP_DIR=${MESH_TEMP_DIR} -RUN mkdir -p ${MESH_TEMP_DIR} ${MESH_DIR} ${MESH_INSTALL_DIR} \ - && apk add --no-cache bash xxd nodejs mongodb-tools \ - && addgroup -g 1000 node && adduser -D -u 1000 -G node node +# bash is required by the chart's init container; mongodb-tools only by autobackup. +RUN apk upgrade --no-cache && \ + apk add --no-cache bash nodejs mongodb-tools && \ + addgroup -g 1000 node && \ + adduser -D -u 1000 -G node node && \ + install -d -o node -g node ${MESH_INSTALL_DIR} ${MESH_DIR} ${MESH_TEMP_DIR} # Copy built MeshCentral source + deps (including mongodb driver) from dep-compiler -COPY --from=dep-compiler /opt/meshcentral/meshcentral ${MESH_INSTALL_DIR}/meshcentral/ +COPY --from=dep-compiler --chown=node:node \ + /opt/meshcentral/meshcentral ${MESH_INSTALL_DIR}/meshcentral/ # OpenFrame plugins — chart's init container symlinks from here into the # datapath emptyDir at pod start (read-only, runs from this image layer). -COPY plugins/ ${MESH_TEMP_DIR}/plugins/openframe/ - -RUN chown -R node:node ${MESH_DIR} ${MESH_TEMP_DIR} ${MESH_INSTALL_DIR} +COPY --chown=node:node plugins/ ${MESH_TEMP_DIR}/plugins/openframe/ USER node From def24f13e7c3add22fbc0e6748078c21cdfa0ec2 Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Wed, 5 Aug 2026 10:05:03 +0200 Subject: [PATCH 3/7] Harden the meshcentral chart Read-only rootfs and non-root on both meshcentral containers (uid 1000) and on wait-mongodb (uid 999, the mongo image's own user). Uploads and user files have to move off the rootfs for that to work: TMPDIR gets its own emptyDir, and --filespath points at the data volume. --- charts/meshcentral/templates/deployment.yaml | 26 ++++++++++++++- charts/meshcentral/values.yaml | 33 +++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/charts/meshcentral/templates/deployment.yaml b/charts/meshcentral/templates/deployment.yaml index b62ac647e3..2a00b287c8 100644 --- a/charts/meshcentral/templates/deployment.yaml +++ b/charts/meshcentral/templates/deployment.yaml @@ -29,12 +29,18 @@ spec: {{- include "meshcentral.selectorLabels" . | nindent 8 }} spec: + {{- with .Values.podSecurityContext }} securityContext: - fsGroup: 1000 + {{- toYaml . | nindent 8 }} + {{- end }} initContainers: - name: wait-mongodb image: "{{ .Values.initImage.registry }}/{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + {{- with .Values.initImage.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} # MC_MONGO_URI is assembled by the kubelet via $(VAR) substitution against # envFrom-sourced parts (see pkg/kubelet/kubelet_pods.go makeEnvironmentVariables: # envFrom is processed first, then env[]). bash only sees a fully-resolved URI, @@ -52,6 +58,10 @@ spec: - name: meshcentral-init image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} command: ["/bin/bash", "-c"] args: - | @@ -102,11 +112,17 @@ spec: - name: meshcentral image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} command: ["node"] args: - "/opt/meshcentral/meshcentral/meshcentral.js" - "--datapath" - "/opt/mesh/meshcentral-data" + - "--filespath" + - "/opt/mesh/meshcentral-files" - "--configfile" - "/tmp/config/config.json" - "--configkey" @@ -138,6 +154,10 @@ spec: secretKeyRef: name: {{ .Values.credentials.existingSecret | default (printf "%s-credentials" .Chart.Name) }} key: {{ .Values.credentials.meshConfigKeyKey }} + # multiparty writes uploads to os.tmpdir(); /tmp itself carries the + # image's plugins and the config secret, so use a subdirectory. + - name: TMPDIR + value: /tmp/work resources: {{- toYaml .Values.resources | nindent 12 }} @@ -169,6 +189,8 @@ spec: volumeMounts: - name: data mountPath: /opt/mesh + - name: work + mountPath: /tmp/work - name: config mountPath: /tmp/config readOnly: true @@ -178,3 +200,5 @@ spec: secretName: {{ .Chart.Name }}-config - name: data emptyDir: {} + - name: work + emptyDir: {} diff --git a/charts/meshcentral/values.yaml b/charts/meshcentral/values.yaml index 12ce9f6136..cd459ae5a5 100644 --- a/charts/meshcentral/values.yaml +++ b/charts/meshcentral/values.yaml @@ -10,7 +10,16 @@ initImage: registry: ghcr.io/flamingo-stack/registry repository: mongo tag: "8.0.9" - + # uid and gid 999 belong to the mongodb user in the image + securityContext: + runAsUser: 999 + runAsGroup: 999 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true # -- Sensitive credentials. # If existingSecret is set, the chart reads MESH_USER / MESH_PASS / MESH_CONFIG_KEY @@ -129,6 +138,28 @@ resources: memory: "512Mi" cpu: "300m" +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# meshcentral containers +securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + startupProbe: initialDelaySeconds: 30 periodSeconds: 10 From e7d3d2e73609d79c3a1e54cd93fb0890abb9b1da Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Wed, 5 Aug 2026 10:22:33 +0200 Subject: [PATCH 4/7] rm comment --- charts/meshcentral/templates/deployment.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/charts/meshcentral/templates/deployment.yaml b/charts/meshcentral/templates/deployment.yaml index 2a00b287c8..c79724a7f4 100644 --- a/charts/meshcentral/templates/deployment.yaml +++ b/charts/meshcentral/templates/deployment.yaml @@ -154,8 +154,6 @@ spec: secretKeyRef: name: {{ .Values.credentials.existingSecret | default (printf "%s-credentials" .Chart.Name) }} key: {{ .Values.credentials.meshConfigKeyKey }} - # multiparty writes uploads to os.tmpdir(); /tmp itself carries the - # image's plugins and the config secret, so use a subdirectory. - name: TMPDIR value: /tmp/work From 1f91168040838122a233d1102f4ed9e41dc37ca9 Mon Sep 17 00:00:00 2001 From: Ivan Khropachov Date: Wed, 5 Aug 2026 13:26:18 +0300 Subject: [PATCH 5/7] Non default SA for MeshCentral Chart --- .github/workflows/changes.yaml | 7 +- charts/meshcentral/templates/_helpers.tpl | 8 + charts/meshcentral/templates/deployment.yaml | 3 + charts/meshcentral/templates/sa.yaml | 13 + charts/meshcentral/values.yaml | 6 + docker/Dockerfile | 3 +- readme.md | 301 ++++++++++--------- 7 files changed, 190 insertions(+), 151 deletions(-) create mode 100644 charts/meshcentral/templates/sa.yaml diff --git a/.github/workflows/changes.yaml b/.github/workflows/changes.yaml index 5d524fea6a..b18710462b 100644 --- a/.github/workflows/changes.yaml +++ b/.github/workflows/changes.yaml @@ -49,9 +49,14 @@ jobs: "name": "meshcentral", "paths": [ "./docker/**", + "./.dockerignore", "./package.json", - "./translate/**", + "./package-lock.json", "./*.js", + "./plugins/**", + "./agents/**", + "./emails/**", + "./translate/**", "./views/**", "./public/**" ] diff --git a/charts/meshcentral/templates/_helpers.tpl b/charts/meshcentral/templates/_helpers.tpl index 5495bc1d2a..951d7ba177 100644 --- a/charts/meshcentral/templates/_helpers.tpl +++ b/charts/meshcentral/templates/_helpers.tpl @@ -9,3 +9,11 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{- define "meshcentral.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default .Chart.Name .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end }} diff --git a/charts/meshcentral/templates/deployment.yaml b/charts/meshcentral/templates/deployment.yaml index 2a00b287c8..44cd230dd1 100644 --- a/charts/meshcentral/templates/deployment.yaml +++ b/charts/meshcentral/templates/deployment.yaml @@ -29,6 +29,9 @@ spec: {{- include "meshcentral.selectorLabels" . | nindent 8 }} spec: + serviceAccountName: {{ include "meshcentral.serviceAccountName" . }} + automountServiceAccountToken: false + {{- with .Values.podSecurityContext }} securityContext: {{- toYaml . | nindent 8 }} diff --git a/charts/meshcentral/templates/sa.yaml b/charts/meshcentral/templates/sa.yaml new file mode 100644 index 0000000000..25deb235ec --- /dev/null +++ b/charts/meshcentral/templates/sa.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "meshcentral.serviceAccountName" . }} + labels: + {{- include "meshcentral.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: false +{{- end }} diff --git a/charts/meshcentral/values.yaml b/charts/meshcentral/values.yaml index cd459ae5a5..3853bfe89f 100644 --- a/charts/meshcentral/values.yaml +++ b/charts/meshcentral/values.yaml @@ -138,6 +138,12 @@ resources: memory: "512Mi" cpu: "300m" +serviceAccount: + create: true + # Overrides the generated name ({{ .Chart.Name }}) + name: "" + annotations: {} + podSecurityContext: runAsNonRoot: true runAsUser: 1000 diff --git a/docker/Dockerfile b/docker/Dockerfile index 9d672ef480..9be465ae71 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,7 +19,8 @@ FROM alpine:3.24 ARG MESH_INSTALL_DIR=/opt/meshcentral ARG MESH_DIR=/opt/mesh -ARG MESH_TEMP_DIR=/tmp/mesh +# Not /tmp: an emptyDir mounted there would shadow the plugins baked into this layer. +ARG MESH_TEMP_DIR=/opt/openframe ENV MESH_INSTALL_DIR=${MESH_INSTALL_DIR} ENV MESH_DIR=${MESH_DIR} diff --git a/readme.md b/readme.md index 725399e7ee..736b4b80d4 100644 --- a/readme.md +++ b/readme.md @@ -1,209 +1,212 @@
- - - - - - OpenFrame Logo + + + OpenFrame - -

MeshCentral

- -

Web-based remote monitoring & management that integrates with OpenFrame — remote desktop, terminal access, file transfer, and device control across Windows, macOS, and Linux.

- -

- - License - - - Docs - - - Community - -

---- +

+ License +

-## Quick Links -- [Highlights](#highlights) -- [Quick Start](#quick-start) - - [Prerequisites](#prerequisites) - - [OpenFrame Integration](#openframe-integration) - - [Architecture](#architecture) -- [Security](#security) -- [Contributing](#contributing) -- [License](#license) +# MeshCentral ---- +**MeshCentral** is an open-source, web-based remote device management platform that enables IT administrators and MSPs to securely monitor, access, and control devices anywhere in the world — all from a browser. As part of the [Flamingo](https://flamingo.run) / [OpenFrame](https://openframe.ai) ecosystem, this fork of MeshCentral is enhanced with multi-tenant support, OpenFrame plugin integration, and AI-driven MSP automation. -## Highlights +MeshCentral replaces expensive proprietary remote-access tools with a self-hosted, open-source solution built on Node.js, featuring a full browser-based VNC/RFB client, an advanced Xterm.js terminal engine, RDP clipboard synchronization, and modern UI infrastructure. -- Remote desktop, terminal, and file management via web interface -- Cross-platform agent support (Windows, macOS, Linux) -- Intel AMT support for out-of-band management -- WebRTC-based peer-to-peer connectivity -- Multi-user collaboration and session sharing -- Device grouping and access control policies -- Extensible with plugins and automation scripts -- Integrations: OpenFrame Gateway, Stream (Kafka), Analytics (Pinot), Auth (OIDC/JWT) -- API-first (REST/WebSocket), web console (operator UI) +--- + +## Features + +- **Remote Desktop (VNC/RFB)** — Browser-based KVM using the embedded noVNC client with hardware-accelerated canvas rendering and multi-encoding support (Raw, Tight, ZRLE, JPEG) +- **Remote Terminal** — Full ANSI/VT100-compatible shell sessions over WebSocket via Xterm.js, with SIXEL/OSC 1337 inline image rendering +- **File Management** — Upload, download, and browse files on remote devices directly from the browser +- **Device Monitoring** — Real-time dashboards, charts, and live connectivity tracking across your device fleet +- **RDP Clipboard Sync** — Virtual channel clipboard synchronization for RDP sessions via the `cliprdr` module +- **Secure Transport** — TLS everywhere with Let's Encrypt/ACME certificate automation +- **Multi-Database Support** — NeDB (default, zero-config), MongoDB, MariaDB, MySQL, PostgreSQL, SQLite, and AceBase +- **Intel AMT Management** — Out-of-band management for Intel vPro devices (CIRA, WSMAN, ACM activation, 802.1x/Wi-Fi profiles) +- **Multi-Factor Authentication** — TOTP (otplib), WebAuthn/FIDO2, and hardware security key support +- **Multi-Tenant** — OpenFrame multi-tenant domain isolation for MSP use cases +- **Session Recording** — Binary and text recording of terminal and desktop sessions +- **Plugin Architecture** — Extensible hook-based plugin system, including the OpenFrame integration plugin +- **MeshAgent Protocol** — WebSocket-based agent with binary protocol for full device communication --- -## Quick Start +## Architecture -### Prerequisites +MeshCentral follows a layered architecture from remote device to browser, cleanly separating transport, protocol, rendering, input handling, and UI presentation. -**For OpenFrame Integration:** -- Kubernetes cluster with kubectl -- Telepresence (for local access to services) +```mermaid +flowchart TD + RemoteDevice["Remote Device (Agent / RDP / VNC)"] + Server["MeshCentral Server (Node.js / Express)"] + WebSocket["WebSocket Transport"] + Protocol["Protocol Layer (RFB / RDP / Agent)"] + Decoders["Framebuffer Decoders"] + Display["Display Renderer (HTML5 Canvas)"] + Terminal["Xterm Terminal Engine"] + UI["Web Admin UI"] + DB["Database (NeDB / MongoDB / etc.)"] + + RemoteDevice --> Server + Server --> DB + Server --> WebSocket + WebSocket --> Protocol + Protocol --> Decoders + Decoders --> Display + Protocol --> Terminal + Terminal --> UI + Display --> UI +``` + +### Core Subsystems + +| Subsystem | Role | +|-----------|------| +| **Web Server** | Express HTTPS server, session management, routing | +| **MeshAgent Handler** | WebSocket communication with installed agents | +| **MeshRelay** | Bidirectional WebSocket relay between clients and devices | +| **Database Layer** | Unified abstraction over 7 database backends | +| **Intel AMT Manager** | Out-of-band AMT device lifecycle management | +| **Crypto Layer** | AES-EAX, DES, RSA, DH, FIDO2/WebAuthn | +| **Let's Encrypt** | Automated TLS certificate provisioning via ACME | +| **Plugin Handler** | Hook-based plugin loader, including OpenFrame integration | --- -### OpenFrame Integration +## Technology Stack -MeshCentral is integrated into OpenFrame for remote device access and management. +| Layer | Technology | +|-------|-----------| +| Runtime | Node.js 16+ | +| HTTP Framework | Express 4.x | +| WebSockets | `ws` / `express-ws` | +| Template Engine | Express Handlebars | +| Default Database | NeDB (`@seald-io/nedb`) | +| Cryptography | `node-forge`, `otplib`, native `crypto` module | +| Remote Desktop (browser) | noVNC (RFB protocol) | +| Terminal (browser) | Xterm.js | +| UI Framework | Bootstrap (bundled) | --- -### Architecture +## Quick Start + +### Option 1: Install via npm (Recommended) -MeshCentral runs as a service in OpenFrame and connects to endpoint agents via Gateway. Session events flow into Stream and Analytics for monitoring and audit. +**Step 1: Install MeshCentral** -```mermaid -flowchart LR - - A[Agent] <--commands/sessions--> G[OpenFrame Gateway] - - subgraph OpenFrame - G --> API[(MeshCentral Service API)] - API --> DB[(DB: devices, users, sessions)] - DB --> S[Stream] - S --> K[(Kafka)] - K --> C[(Cassandra)] - K --> P[(Pinot Analytics)] - end - - style A fill:#FFC109,stroke:#1A1A1A,color:#FAFAFA - style G fill:#666666,stroke:#1A1A1A,color:#FAFAFA - style API fill:#212121,stroke:#1A1A1A,color:#FAFAFA +```bash +npm install -g meshcentral ``` -#### Deployment +**Step 2: Start the server** -MeshCentral is deployed automatically as part of OpenFrame via ArgoCD app-of-apps pattern: - -```yaml -# manifests/apps/values.yaml -apps: - meshcentral: - enabled: true - project: integrated-tools - namespace: integrated-tools - syncWave: "3" # Deployed after microservices +```bash +meshcentral ``` -**Access MeshCentral UI:** +**Step 3: Open the web interface** + +Navigate to `https://localhost/` and create your administrator account on the first visit. + +--- + +### Option 2: Run from Source + +**Step 1: Clone the repository** + ```bash -# Connect to integrated-tools namespace -telepresence connect --namespace integrated-tools +git clone https://github.com/flamingo-stack/meshcentral.git +cd meshcentral +``` + +**Step 2: Install dependencies** -# MeshCentral UI will be available at: -# https://meshcentral.integrated-tools.svc.cluster.local:8383 +```bash +npm install ``` -**For standalone MeshCentral deployment** (not recommended - registration job will fail): +**Step 3: Start the server** + ```bash -helm install meshcentral ./manifests/integrated-tools/meshcentral +node meshcentral.js ``` -#### Integration Features +> **Self-signed certificate warning:** Your browser will show a TLS warning on first launch. Click "Advanced" → "Proceed" to continue. For production, configure Let's Encrypt in `meshcentral-data/config.json`. + +--- -**Auto-initialization:** -- Creates default admin user -- Sets up device groups and policies -- Generates API keys for integration -- Persists credentials at `/opt/meshcentral/data/credentials.json` -- Registers as integrated tool in OpenFrame +### System Requirements -**Configuration** is managed via Helm chart at `manifests/integrated-tools/meshcentral/`. +| Resource | Minimum | Recommended | +|----------|---------|-------------| +| Node.js | 16.0.0+ | 18.x or 20.x LTS | +| RAM | 512 MB | 1 GB+ | +| Disk | 1 GB free | 5 GB+ | +| OS | Linux, Windows, macOS | Linux (Ubuntu 20.04+) | + +**Open ports:** 443 (HTTPS/WSS), 80 (Let's Encrypt redirect), 4433 (Intel AMT CIRA) --- -#### Troubleshooting +### Connect Your First Device -**Check deployment status:** -```bash -kubectl get pods -n integrated-tools -l app=meshcentral -kubectl logs -f meshcentral-0 -n integrated-tools -``` +1. In the web interface, go to **My Devices** → **Add Device Group** +2. Click on the group → **Add Agent** → select your OS +3. Download and run the agent installer on the remote device +4. The device appears in your dashboard within seconds -**Access MeshCentral services via Telepresence:** -```bash -# Connect to cluster -telepresence connect --namespace integrated-tools +--- -# Access MeshCentral UI directly -open https://meshcentral.integrated-tools.svc.cluster.local:8383 +## OpenFrame Integration -# Access MongoDB for debugging -mongo meshcentral-mongodb.integrated-tools.svc.cluster.local/meshcentral -``` +This repository includes the **OpenFrame plugin** (`plugins/openframe.js`) that powers the [OpenFrame AI platform](https://openframe.ai): -**Get API credentials manually:** -```bash -kubectl exec -it meshcentral-0 -n integrated-tools -- \ - cat /opt/meshcentral/data/credentials.json -``` +- `GET /generate-msh` — Generates `.msh` agent configuration files for device enrollment +- `GET /api/deviceStatus` — Returns live device connectivity status with multi-tenant isolation -For complete documentation: -- [MeshCentral Official Docs](https://ylianst.github.io/MeshCentral/) -- [MeshCentral User Guide](https://meshcentral.com/docs/MeshCentral2UserGuide.pdf) +**Environment variables for the OpenFrame plugin:** -## Security +| Variable | Default | Description | +|----------|---------|-------------| +| `MESH_DIR` | `/opt/mesh` | Directory containing mesh ID files | +| `MESH_DEVICE_GROUP` | _(empty)_ | Device group name for generated `.msh` files | -- TLS 1.2 enforced for all communication -- JWT authentication via OpenFrame Gateway -- Role-based access control (RBAC) for users and devices -- Database encryption for secrets -- Support for enrollment secrets or pre-shared keys +--- -Found a vulnerability? Email security@flamingo.run instead of opening a public issue. +## Documentation -## Contributing +📚 See the [Documentation](./docs/README.md) for comprehensive guides including: -We welcome PRs! Please follow these guidelines: -- Use branching strategy: `feature/...`, `bugfix/...` -- Add descriptions to the **CHANGELOG** -- Follow consistent Go code style (`go fmt`, linters) -- Keep documentation updated in `docs/` +- [Getting Started](./docs/getting-started/introduction.md) — Introduction and setup +- [Quick Start Guide](./docs/getting-started/quick-start.md) — Up and running in minutes +- [Architecture Overview](./docs/development/architecture/README.md) — System design and component map +- [Development Setup](./docs/development/setup/environment.md) — Local development environment --- -## License +## Community & Support + +Development discussion, questions, and support happen on the **OpenMSP Slack** — not GitHub Issues or Discussions. + +- **OpenMSP Community:** [https://www.openmsp.ai/](https://www.openmsp.ai/) +- **Join Slack:** [https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA](https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA) +- **Flamingo Platform:** [https://flamingo.run](https://flamingo.run) +- **OpenFrame:** [https://openframe.ai](https://openframe.ai) + +--- + +## Contributing -This project is licensed under the **Flamingo Unified License v1.0** ([LICENSE.md](LICENSE.md)). +Contributions are welcome! Please read the [Contributing Guidelines](./CONTRIBUTING.md) before submitting a pull request. Discuss significant features on Slack before starting work to align with the project roadmap. ---
- - - - - -
- Built with 💛 by the Flamingo team - - Website • - Knowledge Base • - LinkedIn • - Community -
+ Built with 💛 by the Flamingo team
From 0d58212f88780dbb5ceabfad0e3f8220ef9d83de Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Wed, 5 Aug 2026 13:34:34 +0200 Subject: [PATCH 6/7] mongosh a writable HOME in wait-mongodb init container --- charts/meshcentral/templates/deployment.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/charts/meshcentral/templates/deployment.yaml b/charts/meshcentral/templates/deployment.yaml index c79724a7f4..7206f969ba 100644 --- a/charts/meshcentral/templates/deployment.yaml +++ b/charts/meshcentral/templates/deployment.yaml @@ -54,6 +54,11 @@ spec: env: - name: MC_MONGO_URI value: "mongodb://$(MC_MONGO_USER):$(MC_MONGO_PASSWORD)@$(MC_MONGO_HOSTS)/$(MC_MONGO_DATABASE)?replicaSet=$(MC_MONGO_REPLICA_SET)&authSource=$(MC_MONGO_AUTH_SOURCE)$(MC_MONGO_URI_OPTIONS)" + - name: HOME + value: /tmp/mongosh + volumeMounts: + - name: mongosh + mountPath: /tmp/mongosh - name: meshcentral-init image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -200,3 +205,5 @@ spec: emptyDir: {} - name: work emptyDir: {} + - name: mongosh + emptyDir: {} From 027b6e39c8ad01920acaec87799b05acb8881754 Mon Sep 17 00:00:00 2001 From: Viktor Ishchenko Date: Wed, 5 Aug 2026 14:59:12 +0200 Subject: [PATCH 7/7] drop mongodb-tools from the image --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9be465ae71..fb09abe46e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,9 +26,9 @@ ENV MESH_INSTALL_DIR=${MESH_INSTALL_DIR} ENV MESH_DIR=${MESH_DIR} ENV MESH_TEMP_DIR=${MESH_TEMP_DIR} -# bash is required by the chart's init container; mongodb-tools only by autobackup. +# bash is required by the chart's init container. RUN apk upgrade --no-cache && \ - apk add --no-cache bash nodejs mongodb-tools && \ + apk add --no-cache bash nodejs && \ addgroup -g 1000 node && \ adduser -D -u 1000 -G node node && \ install -d -o node -g node ${MESH_INSTALL_DIR} ${MESH_DIR} ${MESH_TEMP_DIR}