Skip to content
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Dockerfile
**/.git
**/.gitattributes
**/.gitignore
**/.gitmodules
**/.github

# dev
Expand Down
230 changes: 152 additions & 78 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,122 +1,196 @@
# 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 GO_VERSION=1.26.1
ARG NODE_VERSION=23.9.0

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
#----( 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
# 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 \
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

WORKDIR /usr/src/app
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 (with SHA256 checksum verification)
ARG TARGETARCH
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; \
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 /tmp/node.SHASUMS256.txt

# Install Go (with SHA256 checksum verification)
ARG GO_VERSION
RUN set -eux; \
arch="${TARGETARCH:-amd64}"; \
case "$arch" in \
amd64|arm64) \
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 /tmp/go.tgz.sha256 ;; \
*) \
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
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}

# Ensure plugin submodules have been cloned
RUN git config --global --add safe.directory ${APP_DIR} \
&& git submodule sync --recursive \
&& git submodule update --init --recursive
Comment on lines +70 to +73
Comment on lines +70 to +73
Comment on lines +70 to +73

# Install Python dependencies, allowing failed installs for plugin requirements
RUN pip install --upgrade pip \
&& sed -i '/^lxml.*/d' ${APP_DIR}/requirements.txt \
Comment on lines +75 to +77
&& 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
Comment on lines +75 to +79
Comment on lines +75 to +79

Comment on lines +75 to +80
# Rebuild Sandcat agents if Go is installed
RUN set -eux; \
if command -v go >/dev/null 2>&1; 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

# 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; \
elif [ "$VARIANT" != "full" ]; then \
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 -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 )--------------------------------
FROM python:${PYTHON_VERSION}-slim-bookworm AS dev

# 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
# Set timezone (default to UTC)
ARG TZ="UTC"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone

# 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/*
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/*

# 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
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

# 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
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}
Comment on lines +118 to +141


#----( 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 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 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/*

# Install Go dependencies
RUN cd /usr/src/app/plugins/sandcat/gocat; go mod tidy && go mod download
ARG APP_UID=1001
ARG APP_GID=1001
ENV APP_DIR=/usr/src/app
ENV VENV_DIR=/usr/local/venv

# Update sandcat agents
RUN cd /usr/src/app/plugins/sandcat; ./update-agents.sh
# 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

# 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
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

ENTRYPOINT ["python3", "server.py"]
# Run as user: app
USER app
WORKDIR ${APP_DIR}
ENV PATH="/usr/local/go/bin:${PATH}"
ENV PATH="${VENV_DIR}/bin:${PATH}"
ENV PATH="/usr/local/lib/node/bin:${PATH}"

CMD ["python3", "/usr/src/app/server.py"]
8 changes: 3 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -20,4 +18,4 @@ services:
- "2222:2222"
volumes:
- ./:/usr/src/app
command: --log DEBUG
command: ["python", "/usr/src/app/server.py", "--build"]
Loading