From 22ea5213285b9c7b13540318c71e02dde6f1a437 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:19:19 -0500 Subject: [PATCH 01/11] feat: python build stage --- .dockerignore | 1 - Dockerfile | 177 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 105 insertions(+), 73 deletions(-) diff --git a/.dockerignore b/.dockerignore index c0836803b..a7ae3b0eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,6 @@ docker-compose.yml Dockerfile # git -**/.git **/.gitattributes **/.gitignore **/.gitmodules diff --git a/Dockerfile b/Dockerfile index b9085b342..5916ae959 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,13 @@ -# This file uses a staged build, using a different stage to build the UI (magma) -# Build the UI -FROM node:23 AS ui-build +ARG PYTHON_VERSION=3.13 +ARG NODE_VERSION=23 -WORKDIR /usr/src/app - -ADD . . -# Build VueJS front-end -RUN (cd plugins/magma; npm install && npm run build) - -# This is the runtime stage -# It containes all dependencies required by caldera -FROM debian:bookworm-slim AS runtime +#----( UI Build Stage )-------------------------------- +FROM node:${NODE_VERSION}-bookworm-slim AS ui-build # There are two variants - slim and full -# The slim variant excludes some dependencies of *emu* and *atomic* that can be downloaded on-demand if needed -# They are very large +# The slim variant excludes some dependencies of *emu* and *atomic* that +# can be downloaded on-demand if needed. ARG VARIANT=full -ENV VARIANT=${VARIANT} # Display an error if variant is set incorrectly, otherwise just print information regarding which variant is in use RUN if [ "$VARIANT" = "full" ]; then \ @@ -28,72 +19,105 @@ RUN if [ "$VARIANT" = "full" ]; then \ exit 1; \ fi -WORKDIR /usr/src/app +RUN apt-get update -qy \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV APP_DIR=/usr/src/app +ADD . ${APP_DIR} +WORKDIR ${APP_DIR} + +# Ensure plugin submodules are loaded +RUN git config --global --add safe.directory ${APP_DIR} \ + && git submodule sync --recursive \ + && git submodule update --init --recursive -# Copy in source code and compiled UI -# IMPORTANT NOTE: the .dockerignore file is very important in preventing weird issues. -# Especially if caldera was ever compiled outside of Docker - we don't want those files to interfere with this build process, -# which should be repeatable. -ADD . . -COPY --from=ui-build /usr/src/app/plugins/magma/dist /usr/src/app/plugins/magma/dist +# Fetch atomic data or disable it in slim +RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/atomic/data/atomic-red-team" ]; then \ + git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git ${APP_DIR}/plugins/atomic/data/atomic-red-team; \ + else \ + sed -i '/\- atomic/d' ${APP_DIR}/conf/default.yml; \ + fi -# From https://docs.docker.com/build/building/best-practices/ -# Install caldera dependencies -RUN apt-get update && \ -apt-get --no-install-recommends -y install git curl unzip python3-dev python3-pip mingw-w64 zlib1g gcc && \ -rm -rf /var/lib/apt/lists/* +# Fetch emu data +# (Emu is not enabled by default, no need to disable it if slim variant is being built) +RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/emu/data/adversary-emulation-plans" ]; then \ + git clone --depth 1 https://github.com/center-for-threat-informed-defense/adversary_emulation_library.git ${APP_DIR}/plugins/emu/data/adversary-emulation-plans; \ + fi -# Install Golang from source (apt version is too out-of-date) -RUN curl -k -L https://go.dev/dl/go1.25.0.linux-amd64.tar.gz -o go1.25.0.linux-amd64.tar.gz && \ -tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz && rm go1.25.0.linux-amd64.tar.gz -ENV PATH="$PATH:/usr/local/go/bin" -RUN go version +# Remove .git folders +RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf -# Fix line ending error that can be caused by cloning the project in a Windows environment -RUN cd /usr/src/app/plugins/sandcat && \ -cp ./update-agents.sh ./update-agents_orig.sh && \ -tr -d '\15\32' < ./update-agents_orig.sh > ./update-agents.sh +# Build VueJS front-end +RUN cd ${APP_DIR}/plugins/magma \ + && npm install \ + && npm run build + +#----( Python/Go Build Stage )---------------------- +FROM python:${PYTHON_VERSION}-slim-bookworm AS build + +# Install Go +ARG TARGETARCH +ARG GO_VERSION=1.25.4 + +RUN apt-get update -qy \ + && apt-get install -y --no-install-recommends ca-certificates curl bash build-essential python3-dev\ + && rm -rf /var/lib/apt/lists/* + +RUN set -eux; \ + case "$TARGETARCH" in \ + amd64|arm64) \ + echo "Installing Go ${GO_VERSION} for $TARGETARCH"; \ + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" -o /tmp/go.tgz; \ + tar -C /usr/local -xzf /tmp/go.tgz; \ + rm -f /tmp/go.tgz ;; \ + *) \ + echo "Unsupported arch $TARGETARCH, ignoring Go install"; \ + mkdir -p /usr/local/go/bin ;; \ + esac + +ENV APP_DIR=/usr/src/app +RUN python3 -m venv ${APP_DIR} +ENV PATH="/usr/local/go/bin:$PATH" +ENV PATH="/usr/src/app/bin:$PATH" + +COPY --from=ui-build /usr/src/app /usr/src/app +WORKDIR ${APP_DIR} + +# Install Python dependencies, allowing failed installs for plugin requirements +RUN pip install --upgrade pip \ + && sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \ + && pip install -r ${APP_DIR}/requirements.txt \ + && find ${APP_DIR}/plugins/ -type f -name 'requirements.txt' -print0 | xargs -0 -n1 pip install --no-cache-dir -r || true + +# Rebuild Sandcat agents if Go is installed +RUN set -eux; \ + if [ -x /usr/local/go/bin/go ]; then \ + echo "Building Sandcat agents"; \ + cd ${APP_DIR}/plugins/sandcat/gocat; \ + go mod tidy; \ + go mod download; \ + cd ${APP_DIR}/plugins/sandcat; \ + sed -i 's/\r$//' update-agents.sh; \ + chmod +x update-agents.sh; \ + ./update-agents.sh; \ + fi + +#----( Runtime Stage )------------------------------------------- +FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime + +COPY --from=build /usr/local/go /usr/local/go +COPY --from=build /usr/src/app /app # Set timezone (default to UTC) ARG TZ="UTC" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ echo $TZ > /etc/timezone -# Install pip requirements -RUN pip3 install --break-system-packages --no-cache-dir -r requirements.txt - -# For offline atomic (disable it by default in slim image) -# Disable atomic if this is not downloaded -RUN if [ ! -d "/usr/src/app/plugins/atomic/data/atomic-red-team" ] && [ "$VARIANT" = "full" ]; then \ - git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git \ - /usr/src/app/plugins/atomic/data/atomic-red-team; \ - else \ - sed -i '/\- atomic/d' conf/default.yml; \ -fi - -# For offline emu -# (Emu is disabled by default, no need to disable it if slim variant is being built) -RUN if [ ! -d "/usr/src/app/plugins/emu/data/adversary-emulation-plans" ] && [ "$VARIANT" = "full" ]; then \ - git clone --depth 1 https://github.com/center-for-threat-informed-defense/adversary_emulation_library \ - /usr/src/app/plugins/emu/data/adversary-emulation-plans; \ -fi - -# Download emu payloads -# emu doesn't seem capable of running this itself - always download -RUN cd /usr/src/app/plugins/emu; ./download_payloads.sh - -# The commands above (git clone) will generate *huge* .git folders - remove them -RUN (find . -type d -name ".git") | xargs rm -rf - -# Install Go dependencies -RUN cd /usr/src/app/plugins/sandcat/gocat; go mod tidy && go mod download - -# Update sandcat agents -RUN cd /usr/src/app/plugins/sandcat; ./update-agents.sh - -# Make sure emu can always be used in container (even if not enabled right now) -RUN cd /usr/src/app/plugins/emu; \ - pip3 install --break-system-packages -r requirements.txt +# Install caldera dependencies TODO: what are the actual requirements? +RUN apt-get update -qy \ + && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g gcc \ + && rm -rf /var/lib/apt/lists/* STOPSIGNAL SIGINT @@ -119,4 +143,13 @@ EXPOSE 8022 # Default FTP port for FTP C2 channel EXPOSE 2222 -ENTRYPOINT ["python3", "server.py"] +# Run as user: app +RUN groupadd -r app && \ + useradd -r -d /app -g app -N app; + +USER app +WORKDIR /app +ENV PATH="/usr/local/go/bin:$PATH" +ENV PATH="/app/bin:$PATH" + +CMD ["python3", "-I", "/app/server.py"] From 7ce01558f7d1463dd4720daeca6821d010cb2e83 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:42:08 -0500 Subject: [PATCH 02/11] fix: match app directories in every stage --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5916ae959..47c53ae62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,7 @@ ARG TARGETARCH ARG GO_VERSION=1.25.4 RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends ca-certificates curl bash build-essential python3-dev\ + && apt-get install -y --no-install-recommends ca-certificates curl bash build-essential \ && rm -rf /var/lib/apt/lists/* RUN set -eux; \ @@ -107,7 +107,7 @@ RUN set -eux; \ FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime COPY --from=build /usr/local/go /usr/local/go -COPY --from=build /usr/src/app /app +COPY --from=build /usr/src/app /usr/src/app # Set timezone (default to UTC) ARG TZ="UTC" @@ -145,11 +145,11 @@ EXPOSE 2222 # Run as user: app RUN groupadd -r app && \ - useradd -r -d /app -g app -N app; + useradd -r -d /usr/src/app -g app -N app; USER app -WORKDIR /app +WORKDIR /usr/src/app ENV PATH="/usr/local/go/bin:$PATH" -ENV PATH="/app/bin:$PATH" +ENV PATH="/usr/src/app/bin:$PATH" -CMD ["python3", "-I", "/app/server.py"] +CMD ["python3", "-I", "/usr/src/app/server.py"] From b6f7ef1aaaa599d78b63976611bffcd051e03af3 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:52:22 -0500 Subject: [PATCH 03/11] feat: run in insecure --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 47c53ae62..d5c52e5c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -152,4 +152,4 @@ WORKDIR /usr/src/app ENV PATH="/usr/local/go/bin:$PATH" ENV PATH="/usr/src/app/bin:$PATH" -CMD ["python3", "-I", "/usr/src/app/server.py"] +CMD ["python3", "-I", "/usr/src/app/server.py", "--insecure"] From f9c3b18c5f804a0116e23b54bfe56bfec129accc Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:11:06 -0500 Subject: [PATCH 04/11] fix: include gitmodules --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index a7ae3b0eb..73963d1e2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ Dockerfile # git **/.gitattributes **/.gitignore -**/.gitmodules **/.github # dev From c5ded8ac5fab38255165e7092daec0092e302cc5 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:27:25 -0500 Subject: [PATCH 05/11] feat: restore 2 stage --- Dockerfile | 141 +++++++++++++++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/Dockerfile b/Dockerfile index d5c52e5c5..d442dccb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,89 +1,71 @@ ARG PYTHON_VERSION=3.13 -ARG NODE_VERSION=23 +ARG GO_VERSION=1.25.4 +ARG NODE_VERSION=23.9.0 -#----( UI Build Stage )-------------------------------- -FROM node:${NODE_VERSION}-bookworm-slim AS ui-build +#----( Build Stage )-------------------------------- +FROM python:${PYTHON_VERSION}-slim-bookworm AS build # There are two variants - slim and full # The slim variant excludes some dependencies of *emu* and *atomic* that # can be downloaded on-demand if needed. ARG VARIANT=full - -# Display an error if variant is set incorrectly, otherwise just print information regarding which variant is in use RUN if [ "$VARIANT" = "full" ]; then \ - echo "Building \"full\" container suitable for offline use!"; \ + echo "Building full Caldera container - downloading emu and atomic dependencies for offline use"; \ elif [ "$VARIANT" = "slim" ]; then \ - echo "Building slim container - some plugins (emu, atomic) may not be available without an internet connection!"; \ + echo "Building slim Caldera container - emu and atomic may not be available without an internet connection"; \ else \ echo "Invalid Docker build-arg for VARIANT! Please provide either \"full\" or \"slim\"."; \ exit 1; \ fi RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends git ca-certificates \ + && apt-get install -y --no-install-recommends git ca-certificates curl bash xz-utils build-essential \ && rm -rf /var/lib/apt/lists/* -ENV APP_DIR=/usr/src/app -ADD . ${APP_DIR} -WORKDIR ${APP_DIR} - -# Ensure plugin submodules are loaded -RUN git config --global --add safe.directory ${APP_DIR} \ - && git submodule sync --recursive \ - && git submodule update --init --recursive - -# Fetch atomic data or disable it in slim -RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/atomic/data/atomic-red-team" ]; then \ - git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git ${APP_DIR}/plugins/atomic/data/atomic-red-team; \ - else \ - sed -i '/\- atomic/d' ${APP_DIR}/conf/default.yml; \ - fi - -# Fetch emu data -# (Emu is not enabled by default, no need to disable it if slim variant is being built) -RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/emu/data/adversary-emulation-plans" ]; then \ - git clone --depth 1 https://github.com/center-for-threat-informed-defense/adversary_emulation_library.git ${APP_DIR}/plugins/emu/data/adversary-emulation-plans; \ - fi - -# Remove .git folders -RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf - -# Build VueJS front-end -RUN cd ${APP_DIR}/plugins/magma \ - && npm install \ - && npm run build - -#----( Python/Go Build Stage )---------------------- -FROM python:${PYTHON_VERSION}-slim-bookworm AS build - -# Install Go +# Install Node ARG TARGETARCH -ARG GO_VERSION=1.25.4 - -RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends ca-certificates curl bash build-essential \ - && rm -rf /var/lib/apt/lists/* +ARG NODE_VERSION +RUN set -eux; \ + arch="${TARGETARCH:-amd64}"; \ + case "$arch" in \ + amd64) node_arch="x64" ;; \ + arm64) node_arch="arm64" ;; \ + *) node_arch="x64" ;; \ + esac; \ + curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" -o /tmp/node.tar.xz; \ + mkdir -p /usr/local/lib/node; \ + tar -xJf /tmp/node.tar.xz -C /usr/local/lib/node --strip-components=1; \ + rm -f /tmp/node.tar.xz +# Install Go +ARG GO_VERSION RUN set -eux; \ - case "$TARGETARCH" in \ + arch="${TARGETARCH:-amd64}"; \ + case "$arch" in \ amd64|arm64) \ - echo "Installing Go ${GO_VERSION} for $TARGETARCH"; \ - curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" -o /tmp/go.tgz; \ + go_arch="$arch"; \ + echo "Installing Go ${GO_VERSION} for ${go_arch}"; \ + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz" -o /tmp/go.tgz; \ tar -C /usr/local -xzf /tmp/go.tgz; \ rm -f /tmp/go.tgz ;; \ *) \ - echo "Unsupported arch $TARGETARCH, ignoring Go install"; \ - mkdir -p /usr/local/go/bin ;; \ + echo "Unsupported arch ${arch}, ignoring Go install"; \ + mkdir -p /usr/local/go/bin && touch /usr/local/go/bin/.install_failed ;; \ esac ENV APP_DIR=/usr/src/app RUN python3 -m venv ${APP_DIR} -ENV PATH="/usr/local/go/bin:$PATH" -ENV PATH="/usr/src/app/bin:$PATH" +ENV PATH="/usr/local/go/bin:${PATH}" +ENV PATH="${APP_DIR}/bin:$PATH" -COPY --from=ui-build /usr/src/app /usr/src/app +ADD . ${APP_DIR} WORKDIR ${APP_DIR} +# Ensure plugin submodules have been cloned +RUN git config --global --add safe.directory ${APP_DIR} \ + && git submodule sync --recursive \ + && git submodule update --init --recursive + # Install Python dependencies, allowing failed installs for plugin requirements RUN pip install --upgrade pip \ && sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \ @@ -92,7 +74,7 @@ RUN pip install --upgrade pip \ # Rebuild Sandcat agents if Go is installed RUN set -eux; \ - if [ -x /usr/local/go/bin/go ]; then \ + if command -v go >/dev/null 2>&1; then \ echo "Building Sandcat agents"; \ cd ${APP_DIR}/plugins/sandcat/gocat; \ go mod tidy; \ @@ -103,11 +85,37 @@ RUN set -eux; \ ./update-agents.sh; \ fi +# Fetch atomic data or disable it in slim +RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/atomic/data/atomic-red-team" ]; then \ + git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git ${APP_DIR}/plugins/atomic/data/atomic-red-team; \ + else \ + sed -i '/\- atomic/d' ${APP_DIR}/conf/default.yml; \ + fi + +# Fetch emu data +# (Emu is not enabled by default, no need to disable it if slim variant is being built) +RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/emu/data/adversary-emulation-plans" ]; then \ + git clone --depth 1 https://github.com/center-for-threat-informed-defense/adversary_emulation_library.git ${APP_DIR}/plugins/emu/data/adversary-emulation-plans; \ + fi + +# Remove .git folders +RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf \ + && rm ${APP_DIR}/.gitmodules + + #----( Runtime Stage )------------------------------------------- FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime +ENV APP_DIR=/usr/src/app + +# Create runtime user: app +RUN groupadd -r app \ + && useradd -r -d ${APP_DIR} -g app -N app + COPY --from=build /usr/local/go /usr/local/go -COPY --from=build /usr/src/app /usr/src/app +COPY --from=build /usr/local/lib/node /usr/local/lib/node +COPY --from=build --chown=app:app /usr/src/app ${APP_DIR} +ENV PATH="/usr/local/lib/node/bin:${PATH}" # Set timezone (default to UTC) ARG TZ="UTC" @@ -116,9 +124,14 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ # Install caldera dependencies TODO: what are the actual requirements? RUN apt-get update -qy \ - && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g gcc \ + && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g \ && rm -rf /var/lib/apt/lists/* +# Build VueJS front-end +RUN cd ${APP_DIR}/plugins/magma \ + && npm install \ + && npm run build + STOPSIGNAL SIGINT # Default HTTP port for web interface and agent beacons over HTTP @@ -144,12 +157,10 @@ EXPOSE 8022 EXPOSE 2222 # Run as user: app -RUN groupadd -r app && \ - useradd -r -d /usr/src/app -g app -N app; - USER app -WORKDIR /usr/src/app -ENV PATH="/usr/local/go/bin:$PATH" -ENV PATH="/usr/src/app/bin:$PATH" +WORKDIR ${APP_DIR} +ENV PATH="/usr/local/go/bin:${PATH}" +ENV PATH="${APP_DIR}/bin:${PATH}" +ENV PATH="/usr/local/lib/node/bin:${PATH}" -CMD ["python3", "-I", "/usr/src/app/server.py", "--insecure"] +CMD ["python3", "-I", "/usr/bin/app/server.py", "--insecure"] From 996a0c1db2f40eda620bf6b56f8e71f09b5d12b0 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:25:22 -0500 Subject: [PATCH 06/11] fix: build ui as app user --- Dockerfile | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index d442dccb6..c0c3bcb3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,7 +66,9 @@ RUN git config --global --add safe.directory ${APP_DIR} \ && git submodule sync --recursive \ && git submodule update --init --recursive -# Install Python dependencies, allowing failed installs for plugin requirements +# Install Python dependencies +# Note: Ignoring core lxml version due to failed builds +# Note: Allowing failed installs for plugin requirements RUN pip install --upgrade pip \ && sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \ && pip install -r ${APP_DIR}/requirements.txt \ @@ -103,35 +105,29 @@ RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf \ && rm ${APP_DIR}/.gitmodules -#----( Runtime Stage )------------------------------------------- +#----( Runtime Stage )-------------------------------- FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime ENV APP_DIR=/usr/src/app # Create runtime user: app -RUN groupadd -r app \ - && useradd -r -d ${APP_DIR} -g app -N app +RUN groupadd --system app \ + && useradd --system --home-dir ${APP_DIR} --uid 1001 --gid app -N app COPY --from=build /usr/local/go /usr/local/go COPY --from=build /usr/local/lib/node /usr/local/lib/node COPY --from=build --chown=app:app /usr/src/app ${APP_DIR} -ENV PATH="/usr/local/lib/node/bin:${PATH}" # Set timezone (default to UTC) ARG TZ="UTC" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ echo $TZ > /etc/timezone -# Install caldera dependencies TODO: what are the actual requirements? +# Install Caldera runtime dependencies RUN apt-get update -qy \ && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g \ && rm -rf /var/lib/apt/lists/* -# Build VueJS front-end -RUN cd ${APP_DIR}/plugins/magma \ - && npm install \ - && npm run build - STOPSIGNAL SIGINT # Default HTTP port for web interface and agent beacons over HTTP @@ -163,4 +159,9 @@ ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="${APP_DIR}/bin:${PATH}" ENV PATH="/usr/local/lib/node/bin:${PATH}" +# Build VueJS front-end +RUN cd ${APP_DIR}/plugins/magma \ + && npm install \ + && npm run build + CMD ["python3", "-I", "/usr/bin/app/server.py", "--insecure"] From 003ca5ddfe2cd2f9dc1fcdba5aa3975f4c7996f9 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:42:56 -0500 Subject: [PATCH 07/11] fix: typo in command --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c0c3bcb3d..3660eaf2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -164,4 +164,4 @@ RUN cd ${APP_DIR}/plugins/magma \ && npm install \ && npm run build -CMD ["python3", "-I", "/usr/bin/app/server.py", "--insecure"] +CMD ["python3", "/usr/src/app/server.py", "--insecure"] From 0953b98785cd3465509efd0d683ccd1c4e82a1d3 Mon Sep 17 00:00:00 2001 From: Devon Colmer <935806+d3vco@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:34:54 -0500 Subject: [PATCH 08/11] feat: add dev stage --- Dockerfile | 63 +++++++++++++++++++++++++++++++--------------- docker-compose.yml | 8 +++--- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3660eaf2b..d1fcdd383 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,9 +54,10 @@ RUN set -eux; \ esac ENV APP_DIR=/usr/src/app -RUN python3 -m venv ${APP_DIR} +ENV VENV_DIR=/usr/local/venv +RUN python3 -m venv ${VENV_DIR} ENV PATH="/usr/local/go/bin:${PATH}" -ENV PATH="${APP_DIR}/bin:$PATH" +ENV PATH="${VENV_DIR}/bin:$PATH" ADD . ${APP_DIR} WORKDIR ${APP_DIR} @@ -66,9 +67,7 @@ RUN git config --global --add safe.directory ${APP_DIR} \ && git submodule sync --recursive \ && git submodule update --init --recursive -# Install Python dependencies -# Note: Ignoring core lxml version due to failed builds -# Note: Allowing failed installs for plugin requirements +# Install Python dependencies, allowing failed installs for plugin requirements RUN pip install --upgrade pip \ && sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \ && pip install -r ${APP_DIR}/requirements.txt \ @@ -105,50 +104,74 @@ RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf \ && rm ${APP_DIR}/.gitmodules -#----( Runtime Stage )-------------------------------- -FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime +#----( Dev Stage )-------------------------------- +FROM python:${PYTHON_VERSION}-slim-bookworm AS dev -ENV APP_DIR=/usr/src/app +# Set timezone (default to UTC) +ARG TZ="UTC" +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ + echo $TZ > /etc/timezone -# Create runtime user: app -RUN groupadd --system app \ - && useradd --system --home-dir ${APP_DIR} --uid 1001 --gid app -N app +# Install caldera dependencies +RUN apt-get update -qy \ + && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g \ + && rm -rf /var/lib/apt/lists/* COPY --from=build /usr/local/go /usr/local/go COPY --from=build /usr/local/lib/node /usr/local/lib/node -COPY --from=build --chown=app:app /usr/src/app ${APP_DIR} +COPY --from=build /usr/local/venv /usr/local/venv + +ENV APP_DIR=/usr/src/app +ENV VENV_DIR=/usr/local/venv +ENV PATH="/usr/local/go/bin:${PATH}" +ENV PATH="${VENV_DIR}/bin:${PATH}" +ENV PATH="/usr/local/lib/node/bin:${PATH}" + +WORKDIR ${APP_DIR} + + +#----( Production Stage )-------------------------- +FROM python:${PYTHON_VERSION}-slim-bookworm AS prod # Set timezone (default to UTC) ARG TZ="UTC" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ echo $TZ > /etc/timezone -# Install Caldera runtime dependencies +# Install caldera dependencies RUN apt-get update -qy \ && apt-get --no-install-recommends -y install git curl ca-certificates unzip mingw-w64 zlib1g \ && rm -rf /var/lib/apt/lists/* +ARG APP_UID=1001 +ARG APP_GID=1001 +ENV APP_DIR=/usr/src/app +ENV VENV_DIR=/usr/local/venv + +# Create runtime user: app +RUN groupadd --system --gid ${APP_GID} app \ + && useradd --system --home-dir ${APP_DIR} --uid ${APP_UID} --gid ${APP_GID} -N app + +COPY --from=build /usr/local/go /usr/local/go +COPY --from=build /usr/local/lib/node /usr/local/lib/node +COPY --from=build /usr/local/venv /usr/local/venv +COPY --from=build --chown=app:app /usr/src/app ${APP_DIR} + STOPSIGNAL SIGINT # Default HTTP port for web interface and agent beacons over HTTP EXPOSE 8888 - # Default HTTPS port for web interface and agent beacons over HTTPS (requires SSL plugin to be enabled) EXPOSE 8443 - # TCP and UDP contact ports EXPOSE 7010 EXPOSE 7011/udp - # Websocket contact port EXPOSE 7012 - # Default port to listen for DNS requests for DNS tunneling C2 channel EXPOSE 8853 - # Default port to listen for SSH tunneling requests EXPOSE 8022 - # Default FTP port for FTP C2 channel EXPOSE 2222 @@ -156,7 +179,7 @@ EXPOSE 2222 USER app WORKDIR ${APP_DIR} ENV PATH="/usr/local/go/bin:${PATH}" -ENV PATH="${APP_DIR}/bin:${PATH}" +ENV PATH="${VENV_DIR}/bin:${PATH}" ENV PATH="/usr/local/lib/node/bin:${PATH}" # Build VueJS front-end diff --git a/docker-compose.yml b/docker-compose.yml index de739a067..728b285ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,12 @@ -version: '3' - services: caldera: build: context: . dockerfile: Dockerfile + target: dev args: TZ: "UTC" # Timezone to use in container - VARIANT: "full" - image: caldera:latest + VARIANT: "slim" ports: - "8888:8888" - "8443:8443" @@ -20,4 +18,4 @@ services: - "2222:2222" volumes: - ./:/usr/src/app - command: --log DEBUG + command: ["python", "/usr/src/app/server.py", "--build"] From b32a492401bf3f1d055d6f6324bbfdc2c4c85ebe Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 00:33:33 -0400 Subject: [PATCH 09/11] fix: address Copilot review feedback - Add SHA256 checksum verification for Node and Go downloads to guard against supply-chain / MITM issues - Fix .git removal step: use xargs -r to handle empty find output and rm -f for .gitmodules to make the step idempotent - Move VueJS UI build from prod stage to build stage so prod only receives compiled dist output, reducing image size and build surface - Remove --insecure from default CMD; secure mode is now the default and --insecure must be passed explicitly as an opt-in - Re-add **/.git to .dockerignore to prevent .git dirs from being sent in the Docker build context (submodule init uses git submodule update inside the build stage, not the build context's .git) --- .dockerignore | 1 + Dockerfile | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index 73963d1e2..43eba28aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ docker-compose.yml Dockerfile # git +**/.git **/.gitattributes **/.gitignore **/.github diff --git a/Dockerfile b/Dockerfile index d1fcdd383..3afff1126 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apt-get update -qy \ && apt-get install -y --no-install-recommends git ca-certificates curl bash xz-utils build-essential \ && rm -rf /var/lib/apt/lists/* -# Install Node +# Install Node (with SHA256 checksum verification) ARG TARGETARCH ARG NODE_VERSION RUN set -eux; \ @@ -33,11 +33,13 @@ RUN set -eux; \ *) node_arch="x64" ;; \ esac; \ curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" -o /tmp/node.tar.xz; \ + curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" -o /tmp/node.SHASUMS256.txt; \ + grep "node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/node.SHASUMS256.txt | sha256sum -c -; \ mkdir -p /usr/local/lib/node; \ tar -xJf /tmp/node.tar.xz -C /usr/local/lib/node --strip-components=1; \ - rm -f /tmp/node.tar.xz + rm -f /tmp/node.tar.xz /tmp/node.SHASUMS256.txt -# Install Go +# Install Go (with SHA256 checksum verification) ARG GO_VERSION RUN set -eux; \ arch="${TARGETARCH:-amd64}"; \ @@ -46,8 +48,10 @@ RUN set -eux; \ go_arch="$arch"; \ echo "Installing Go ${GO_VERSION} for ${go_arch}"; \ curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz" -o /tmp/go.tgz; \ + curl -fsSL "https://dl.google.com/go/go${GO_VERSION}.linux-${go_arch}.tar.gz.sha256" -o /tmp/go.tgz.sha256; \ + echo "$(cat /tmp/go.tgz.sha256) /tmp/go.tgz" | sha256sum -c -; \ tar -C /usr/local -xzf /tmp/go.tgz; \ - rm -f /tmp/go.tgz ;; \ + rm -f /tmp/go.tgz /tmp/go.tgz.sha256 ;; \ *) \ echo "Unsupported arch ${arch}, ignoring Go install"; \ mkdir -p /usr/local/go/bin && touch /usr/local/go/bin/.install_failed ;; \ @@ -58,6 +62,7 @@ ENV VENV_DIR=/usr/local/venv RUN python3 -m venv ${VENV_DIR} ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="${VENV_DIR}/bin:$PATH" +ENV PATH="/usr/local/lib/node/bin:${PATH}" ADD . ${APP_DIR} WORKDIR ${APP_DIR} @@ -100,8 +105,14 @@ RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/emu/data/adversary- fi # Remove .git folders -RUN (find ${APP_DIR} -type d -name ".git") | xargs rm -rf \ - && rm ${APP_DIR}/.gitmodules +RUN find ${APP_DIR} -type d -name ".git" | xargs -r rm -rf \ + && rm -f ${APP_DIR}/.gitmodules + +# Build VueJS front-end in the build stage so the production image only +# receives the compiled dist output rather than node_modules and build tooling. +RUN if [ -d "${APP_DIR}/plugins/magma" ] && [ -f "${APP_DIR}/plugins/magma/package-lock.json" ]; then \ + cd ${APP_DIR}/plugins/magma && npm ci && npm run build; \ + fi #----( Dev Stage )-------------------------------- @@ -182,9 +193,4 @@ ENV PATH="/usr/local/go/bin:${PATH}" ENV PATH="${VENV_DIR}/bin:${PATH}" ENV PATH="/usr/local/lib/node/bin:${PATH}" -# Build VueJS front-end -RUN cd ${APP_DIR}/plugins/magma \ - && npm install \ - && npm run build - -CMD ["python3", "/usr/src/app/server.py", "--insecure"] +CMD ["python3", "/usr/src/app/server.py"] From c8e70060049c0f385d781688fcd430916f9874aa Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 01:32:14 -0400 Subject: [PATCH 10/11] fix: update Go version from nonexistent 1.25.4 to current stable 1.26.1 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3afff1126..638d49c83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG PYTHON_VERSION=3.13 -ARG GO_VERSION=1.25.4 +ARG GO_VERSION=1.26.1 ARG NODE_VERSION=23.9.0 #----( Build Stage )-------------------------------- From f16ff25b04a3c5a9482ff0d05f5968e121d8c2c7 Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 09:55:18 -0400 Subject: [PATCH 11/11] Fix atomic plugin conditional: only disable when VARIANT != full The else branch previously ran for both VARIANT=slim and when VARIANT=full with an existing atomic data dir, unintentionally removing atomic from conf/default.yml in full builds. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 638d49c83..a564f6bf8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,7 +94,7 @@ RUN set -eux; \ # Fetch atomic data or disable it in slim RUN if [ "$VARIANT" = "full" ] && [ ! -d "${APP_DIR}/plugins/atomic/data/atomic-red-team" ]; then \ git clone --depth 1 https://github.com/redcanaryco/atomic-red-team.git ${APP_DIR}/plugins/atomic/data/atomic-red-team; \ - else \ + elif [ "$VARIANT" != "full" ]; then \ sed -i '/\- atomic/d' ${APP_DIR}/conf/default.yml; \ fi