DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak hostname fallback. Hardware-bound IDs on DMI-less devices. DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from /sys signals — system battery (scope=Device peripherals excluded, UPS excluded), DRM connector state, framebuffer min-dimension for phone/tablet split. Reports device_type/device_model/os_distro in registration and system-info paths. SERVER-001: migration 061 — device_type, device_type_manual (operator override, never agent-written), device_model, os_distro on agents. effective_device_type computed into every serialized agent. SERVER-002: PUT /admin/agents/:id/device-type — set/clear override, enum-validated, journaled. WEB-001: device-type icons + fleet filter, device model in list, detail header badge with reclassify dropdown, os_distro surfaced. INSTALL-003: arm64 install path unblocked — helper (required manifest component) now cross-built aarch64-unknown-linux-musl via rust-lld in the server image, signed at boot (helperArches += arm64), listed in the release manifest. Install template already handled uname -m and pacman. Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
256 lines
No EOL
10 KiB
Docker
256 lines
No EOL
10 KiB
Docker
# Stage 0: Build the web dashboard for embedding into the server binary
|
|
FROM node:20-alpine AS web-builder
|
|
|
|
WORKDIR /web
|
|
|
|
COPY web/package.json web/package-lock.json ./
|
|
RUN npm ci --ignore-scripts
|
|
|
|
COPY web/ ./
|
|
RUN npx vite build
|
|
|
|
# Desktop variant (Tauri frontend) — built here too because this stage has
|
|
# Node 20; bookworm's packaged Node 18 is below Vite's floor.
|
|
RUN npx tsc && npx vite build --config vite.desktop.config.ts
|
|
|
|
# Stage 0b: Supply-chain self-attestation. Runs the SAME scripts/dep-scan.sh the
|
|
# CI/release pipeline runs, so an operator's own docker-compose build gates its
|
|
# dependency supply chain and produces the attested posture embedded into the
|
|
# server below. A real un-accepted vulnerability fails the build here; an offline
|
|
# build (advisory DBs unreachable) degrades to an honest unattested posture
|
|
# rather than blocking an air-gapped operator.
|
|
FROM golang:1.25-bookworm AS posture-builder
|
|
|
|
WORKDIR /src
|
|
|
|
# Rust toolchain from the canonical official Rust image, not apt and not a
|
|
# curl|sh rustup.rs installer: bookworm's packaged cargo (~1.65) is too old to
|
|
# build a current cargo-audit (needs the 2024 edition), and an old scanner
|
|
# misses new advisories. Copying the toolchain out of rust:1-bookworm -- the same
|
|
# base the desktop-builder stage below uses -- gives a pinned, reproducibly
|
|
# distributed rustc with no pipe-to-shell install inside the stage that produces
|
|
# the attestation. Stronger provenance than a TLS-TOFU rustup.rs fetch, and
|
|
# consistent with the other Rust stages. (CI-003)
|
|
COPY --from=rust:1-bookworm /usr/local/rustup /usr/local/rustup
|
|
COPY --from=rust:1-bookworm /usr/local/cargo /usr/local/cargo
|
|
ENV RUSTUP_HOME=/usr/local/rustup
|
|
ENV CARGO_HOME=/usr/local/cargo
|
|
ENV PATH="/go/bin:/usr/local/cargo/bin:${PATH}"
|
|
|
|
# Node + python3 for the gate scripts (Debian's node is enough for npm audit; no
|
|
# front-end build happens in this stage). curl is gone -- only the removed
|
|
# rustup installer used it.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
nodejs npm python3 pkg-config libssl-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Scanners installed in their own layer so they cache until this line changes.
|
|
RUN go install golang.org/x/vuln/cmd/govulncheck@latest \
|
|
&& cargo install cargo-audit --locked
|
|
|
|
COPY server/ ./server/
|
|
COPY agent/ ./agent/
|
|
COPY web/ ./web/
|
|
COPY helper/ ./helper/
|
|
COPY scripts/ ./scripts/
|
|
COPY .govulncheck-allow ./
|
|
|
|
# exit 1 = real block (fail the build); exit 2 = infra/offline (posture written
|
|
# unattested, continue). Guarantee a posture file exists regardless.
|
|
RUN rc=0; bash scripts/dep-scan.sh --posture-out /posture-build.json || rc=$?; \
|
|
if [ "$rc" = "1" ]; then echo "[ERROR] dep-scan blocked the build"; exit 1; fi; \
|
|
test -f /posture-build.json || \
|
|
printf '%s' '{"attested":false,"generated_at":0,"substrate":{},"scans":[],"exceptions":[]}' > /posture-build.json
|
|
|
|
# Stage 1: Build server binary
|
|
FROM golang:1.25-alpine AS server-builder
|
|
|
|
ARG BUILD_VERSION=dev
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy go.mod and go.sum
|
|
COPY server/go.mod server/go.sum ./
|
|
RUN go mod download
|
|
|
|
# Copy server contents to /app
|
|
COPY server/ ./
|
|
|
|
# Embed the dashboard build — server/internal/webui picks this up via go:embed
|
|
COPY --from=web-builder /web/dist ./internal/webui/dist
|
|
|
|
# Embed the attested supply-chain posture over the committed attested:false stub
|
|
# (services/posture.go picks this up via go:embed).
|
|
COPY --from=posture-builder /posture-build.json ./internal/services/posture-build.json
|
|
|
|
# Build server with version injection
|
|
RUN echo "Building server version: $BUILD_VERSION" && \
|
|
CGO_ENABLED=0 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/server/internal/version.AgentVersion=$BUILD_VERSION" \
|
|
-o redflag-server ./cmd/server/
|
|
|
|
# Stage 2: Build agent binaries for all platforms
|
|
FROM golang:1.25-alpine AS agent-builder
|
|
|
|
ARG BUILD_VERSION=dev
|
|
|
|
WORKDIR /build
|
|
|
|
# Copy agent source code
|
|
COPY agent/ ./
|
|
|
|
# Build for Linux amd64
|
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/linux-amd64/redflag-agent ./cmd/agent
|
|
|
|
# Build for Linux arm64
|
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/linux-arm64/redflag-agent ./cmd/agent
|
|
|
|
# Build for macOS amd64
|
|
RUN CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/darwin-amd64/redflag-agent ./cmd/agent
|
|
|
|
# Build for macOS arm64
|
|
RUN CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/darwin-arm64/redflag-agent ./cmd/agent
|
|
|
|
# Build for Windows amd64
|
|
RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/windows-amd64/redflag-agent.exe ./cmd/agent
|
|
|
|
# Build for Windows arm64
|
|
RUN CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build \
|
|
-ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$BUILD_VERSION" \
|
|
-o binaries/windows-arm64/redflag-agent.exe ./cmd/agent
|
|
|
|
# Stage 2b: Build the Rust capability-gate executor (redflag-helper)
|
|
FROM rust:1-alpine AS helper-builder
|
|
|
|
WORKDIR /helper
|
|
|
|
# musl-dev provides the C runtime cargo links against on alpine.
|
|
RUN apk --no-cache add musl-dev
|
|
|
|
# Copy helper crate and build a static release binary. ed25519-dalek/sha2/serde
|
|
# are pure Rust, so the default x86_64-unknown-linux-musl target builds clean.
|
|
COPY helper/ ./
|
|
RUN cargo build --release && \
|
|
mkdir -p /out/helper-linux-amd64 && \
|
|
cp target/release/redflag-helper /out/helper-linux-amd64/redflag-helper
|
|
|
|
# Cross-build for arm64 (INSTALL-003: Pixel 3 / SBC fleet). The crate is pure
|
|
# Rust and the musl target ships self-contained crt objects, so rust-lld links
|
|
# it without an aarch64 C cross-toolchain. Static musl runs on glibc distros
|
|
# (Arch Linux ARM) the same as the amd64 helper does everywhere else.
|
|
RUN rustup target add aarch64-unknown-linux-musl && \
|
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld \
|
|
cargo build --release --target aarch64-unknown-linux-musl && \
|
|
mkdir -p /out/helper-linux-arm64 && \
|
|
cp target/aarch64-unknown-linux-musl/release/redflag-helper /out/helper-linux-arm64/redflag-helper
|
|
|
|
# Stage 2c: Build the Tauri desktop app (system tray + local UI)
|
|
FROM rust:1-bookworm AS desktop-builder
|
|
|
|
ARG BUILD_VERSION=dev
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
libwebkit2gtk-4.1-dev \
|
|
libjavascriptcoregtk-4.1-dev \
|
|
libappindicator3-dev \
|
|
librsvg2-dev \
|
|
libgtk-3-dev \
|
|
libsoup-3.0-dev \
|
|
libglib2.0-dev \
|
|
pkg-config \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /build
|
|
|
|
# Tauri's build embeds frontendDist (../web/dist-desktop) at compile time —
|
|
# take the bundle from the Node 20 stage instead of building it here.
|
|
COPY --from=web-builder /web/dist-desktop web/dist-desktop
|
|
|
|
# Copy desktop Tauri source
|
|
COPY desktop/ desktop/
|
|
|
|
# Build the Tauri app in release mode
|
|
RUN cd desktop && cargo build --release && \
|
|
mkdir -p /out && \
|
|
cp target/release/redflag-desktop /out/redflag-desktop
|
|
|
|
# Stage 2d: Fetch the prebuilt, signed Windows desktop tray from the newest
|
|
# release and verify it against the release manifest hash. The Windows desktop
|
|
# is Tauri/Rust — cross-compiling it pulls the whole MSVC sysroot (~9GB of build
|
|
# toolchain), which has no business in a from-source server build. CI
|
|
# (release.yml, cargo-xwin) builds it once where the toolchain is ephemeral;
|
|
# this stage fetches the signed result. Optional component: anything short of a
|
|
# hash mismatch degrades to "no Windows tray". See scripts/fetch-desktop-windows.sh.
|
|
FROM alpine:3.21 AS desktop-windows-fetcher
|
|
|
|
ARG BUILD_VERSION=dev
|
|
ARG DESKTOP_RELEASE_REPO_API=https://codeberg.org/api/v1/repos/Fimeg/RedFlag
|
|
ENV DESKTOP_RELEASE_REPO_API=${DESKTOP_RELEASE_REPO_API}
|
|
|
|
RUN apk --no-cache add curl jq unzip
|
|
|
|
COPY scripts/fetch-desktop-windows.sh /usr/local/bin/fetch-desktop-windows.sh
|
|
|
|
# BUILD_VERSION sits in this layer's cache key so bumping the version re-fetches
|
|
# rather than reusing a stale tray from an earlier build.
|
|
RUN echo "fetch Windows tray for build $BUILD_VERSION" && \
|
|
sh /usr/local/bin/fetch-desktop-windows.sh amd64 /out
|
|
|
|
# Stage 3: Final image with server and all agent binaries
|
|
FROM alpine:3.21
|
|
|
|
RUN apk --no-cache add ca-certificates tzdata bash
|
|
WORKDIR /app
|
|
|
|
# Copy server binary
|
|
COPY --from=server-builder /app/redflag-server .
|
|
COPY --from=server-builder /app/internal/database ./internal/database
|
|
|
|
# Copy all agent binaries
|
|
COPY --from=agent-builder /build/binaries ./binaries
|
|
|
|
# Copy the signed-at-startup capability-gate executor alongside the agent
|
|
# binaries so BuildOrchestrator.SignExistingBinary can find it at
|
|
# binaries/helper-linux-<arch>/redflag-helper.
|
|
COPY --from=helper-builder /out/helper-linux-amd64 ./binaries/helper-linux-amd64
|
|
COPY --from=helper-builder /out/helper-linux-arm64 ./binaries/helper-linux-arm64
|
|
|
|
# Copy the Tauri desktop app (system tray + local UI shell)
|
|
COPY --from=desktop-builder /out/redflag-desktop ./binaries/linux-amd64/redflag-desktop
|
|
|
|
# Stage in the Windows desktop tray IF the newest release carried one (fetched +
|
|
# hash-verified by the desktop-windows-fetcher stage). When absent — no release
|
|
# yet, offline build, or a release with no desktop — the server simply serves no
|
|
# Windows tray and the installer 404-skips it. The heavy Windows compile never
|
|
# happens here; only the verified artifact lands.
|
|
COPY --from=desktop-windows-fetcher /out /opt/winfetch
|
|
RUN if [ -f /opt/winfetch/redflag-desktop.exe ]; then \
|
|
mkdir -p ./binaries/windows-amd64 && \
|
|
mv /opt/winfetch/redflag-desktop.exe ./binaries/windows-amd64/redflag-desktop.exe && \
|
|
echo "[INFO] [build] [desktop] staged Windows tray into image"; \
|
|
else \
|
|
echo "[INFO] [build] [desktop] no Windows tray staged (none in latest release)"; \
|
|
fi; \
|
|
rm -rf /opt/winfetch
|
|
|
|
# Copy and setup entrypoint script
|
|
COPY server/docker-entrypoint.sh /usr/local/bin/
|
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
|
|
|
|
EXPOSE 8080
|
|
|
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
CMD ["./redflag-server"] |