Fixes CrowdSec not starting automatically on container boot and LAPI binding failures due to permission issues. Changes: - Fix Dockerfile: Add charon:charon ownership for CrowdSec directories - Move reconciliation from routes.go goroutine to main.go initialization - Add mutex protection to prevent concurrent reconciliation - Increase LAPI startup timeout from 30s to 60s - Add config validation in entrypoint script Testing: - Backend coverage: 85.4% (✅ meets requirement) - Frontend coverage: 87.01% (✅ exceeds requirement) - Security: 0 Critical/High vulnerabilities (✅ Trivy + Go scans) - All CrowdSec-specific tests passing (✅ 100%) Technical Details: - Reconciliation now runs synchronously during app initialization (after DB migrations, before HTTP server starts) - Maintains "GUI-controlled" design philosophy per entrypoint docs - Follows principle of least privilege (charon user, not root) - No breaking changes to API or behavior Documentation: - Implementation guide: docs/implementation/crowdsec_startup_fix_COMPLETE.md - Migration guide: docs/implementation/crowdsec_startup_fix_MIGRATION.md - QA report: docs/reports/qa_report_crowdsec_startup_fix.md Related: #crowdsec-startup-timeout
381 lines
16 KiB
Docker
381 lines
16 KiB
Docker
# Multi-stage Dockerfile for Charon with integrated Caddy
|
|
# Single container deployment for simplified home user setup
|
|
|
|
# Build arguments for versioning
|
|
ARG VERSION=dev
|
|
ARG BUILD_DATE
|
|
ARG VCS_REF
|
|
|
|
# Allow pinning Caddy version - Renovate will update this
|
|
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
|
|
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
|
|
# avoid accidentally pulling a v3 major release. Renovate can still update
|
|
# this ARG to a specific v2.x tag when desired.
|
|
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
|
|
## If the requested tag isn't available, fall back to a known-good v2.10.2 build.
|
|
ARG CADDY_VERSION=2.10.2
|
|
## When an official caddy image tag isn't available on the host, use a
|
|
## plain Alpine base image and overwrite its caddy binary with our
|
|
## xcaddy-built binary in the later COPY step. This avoids relying on
|
|
## upstream caddy image tags while still shipping a pinned caddy binary.
|
|
# renovate: datasource=docker depName=alpine
|
|
ARG CADDY_IMAGE=alpine:3.23
|
|
|
|
# ---- Cross-Compilation Helpers ----
|
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0 AS xx
|
|
|
|
# ---- Frontend Builder ----
|
|
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
|
|
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine AS frontend-builder
|
|
WORKDIR /app/frontend
|
|
|
|
# Copy frontend package files
|
|
COPY frontend/package*.json ./
|
|
|
|
# Build-time project version (propagated from top-level build-arg)
|
|
ARG VERSION=dev
|
|
# Make version available to Vite as VITE_APP_VERSION during the frontend build
|
|
ENV VITE_APP_VERSION=${VERSION}
|
|
|
|
# Set environment to bypass native binary requirement for cross-arch builds
|
|
ENV npm_config_rollup_skip_nodejs_native=1 \
|
|
ROLLUP_SKIP_NODEJS_NATIVE=1
|
|
|
|
RUN npm ci
|
|
|
|
# Copy frontend source and build
|
|
COPY frontend/ ./
|
|
RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
|
|
npm run build
|
|
|
|
# ---- Backend Builder ----
|
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS backend-builder
|
|
# Copy xx helpers for cross-compilation
|
|
COPY --from=xx / /
|
|
|
|
WORKDIR /app/backend
|
|
|
|
# Install build dependencies
|
|
# xx-apk installs packages for the TARGET architecture
|
|
ARG TARGETPLATFORM
|
|
# hadolint ignore=DL3018
|
|
RUN apk add --no-cache clang lld
|
|
# hadolint ignore=DL3018,DL3059
|
|
RUN xx-apk add --no-cache gcc musl-dev sqlite-dev
|
|
|
|
# Install Delve (cross-compile for target)
|
|
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
|
|
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
|
|
# hadolint ignore=DL3059,DL4006
|
|
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest && \
|
|
DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
|
|
if [ -n "$DLV_PATH" ] && [ "$DLV_PATH" != "/go/bin/dlv" ]; then \
|
|
mv "$DLV_PATH" /go/bin/dlv; \
|
|
fi && \
|
|
xx-verify /go/bin/dlv
|
|
|
|
# Copy Go module files
|
|
COPY backend/go.mod backend/go.sum ./
|
|
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
|
|
|
# Copy backend source
|
|
COPY backend/ ./
|
|
|
|
# Build arguments passed from main build context
|
|
ARG VERSION=dev
|
|
ARG VCS_REF=unknown
|
|
ARG BUILD_DATE=unknown
|
|
|
|
# Build the Go binary with version information injected via ldflags
|
|
# xx-go handles CGO and cross-compilation flags automatically
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/go/pkg/mod \
|
|
CGO_ENABLED=1 xx-go build \
|
|
-ldflags "-s -w -X github.com/Wikid82/charon/backend/internal/version.Version=${VERSION} \
|
|
-X github.com/Wikid82/charon/backend/internal/version.GitCommit=${VCS_REF} \
|
|
-X github.com/Wikid82/charon/backend/internal/version.BuildTime=${BUILD_DATE}" \
|
|
-o charon ./cmd/api
|
|
|
|
# ---- Caddy Builder ----
|
|
# Build Caddy from source to ensure we use the latest Go version and dependencies
|
|
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
|
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS caddy-builder
|
|
ARG TARGETOS
|
|
ARG TARGETARCH
|
|
ARG CADDY_VERSION
|
|
|
|
# hadolint ignore=DL3018
|
|
RUN apk add --no-cache git
|
|
# hadolint ignore=DL3062
|
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
|
|
|
# Build Caddy for the target architecture with security plugins.
|
|
# Two-stage approach: xcaddy generates go.mod, we patch it, then build from scratch.
|
|
# This ensures the final binary is compiled with fully patched dependencies.
|
|
# hadolint ignore=SC2016
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/go/pkg/mod \
|
|
sh -c 'set -e; \
|
|
export XCADDY_SKIP_CLEANUP=1; \
|
|
echo "Stage 1: Generate go.mod with xcaddy..."; \
|
|
# Run xcaddy to generate the build directory and go.mod
|
|
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
|
|
--with github.com/greenpau/caddy-security \
|
|
--with github.com/corazawaf/coraza-caddy/v2 \
|
|
--with github.com/hslatman/caddy-crowdsec-bouncer \
|
|
--with github.com/zhangjiayin/caddy-geoip2 \
|
|
--with github.com/mholt/caddy-ratelimit \
|
|
--output /tmp/caddy-initial || true; \
|
|
# Find the build directory created by xcaddy
|
|
BUILDDIR=$(ls -td /tmp/buildenv_* 2>/dev/null | head -1); \
|
|
if [ ! -d "$BUILDDIR" ] || [ ! -f "$BUILDDIR/go.mod" ]; then \
|
|
echo "ERROR: Build directory not found or go.mod missing"; \
|
|
exit 1; \
|
|
fi; \
|
|
echo "Found build directory: $BUILDDIR"; \
|
|
cd "$BUILDDIR"; \
|
|
echo "Stage 2: Apply security patches to go.mod..."; \
|
|
# Patch ALL dependencies BEFORE building the final binary
|
|
# These patches fix CVEs in transitive dependencies
|
|
# Renovate tracks these via regex manager in renovate.json
|
|
# renovate: datasource=go depName=github.com/expr-lang/expr
|
|
go get github.com/expr-lang/expr@v1.17.7; \
|
|
# renovate: datasource=go depName=github.com/quic-go/quic-go
|
|
go get github.com/quic-go/quic-go@v0.57.1; \
|
|
# renovate: datasource=go depName=github.com/smallstep/certificates
|
|
go get github.com/smallstep/certificates@v0.29.0; \
|
|
# Clean up go.mod and ensure all dependencies are resolved
|
|
go mod tidy; \
|
|
echo "Dependencies patched successfully"; \
|
|
# Remove any temporary binaries from initial xcaddy run
|
|
rm -f /tmp/caddy-initial; \
|
|
echo "Stage 3: Build final Caddy binary with patched dependencies..."; \
|
|
# Build the final binary from scratch with the fully patched go.mod
|
|
# This ensures no vulnerable metadata is embedded
|
|
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/bin/caddy \
|
|
-ldflags "-w -s" -trimpath -tags "nobadger,nomysql,nopgx" .; \
|
|
echo "Build successful with patched dependencies"; \
|
|
# Verify the binary exists and is executable (no execution to avoid hang)
|
|
test -x /usr/bin/caddy || exit 1; \
|
|
echo "Caddy binary verified"; \
|
|
# Clean up temporary build directories
|
|
rm -rf /tmp/buildenv_* /tmp/caddy-initial'
|
|
|
|
# ---- CrowdSec Builder ----
|
|
# Build CrowdSec from source to ensure we use Go 1.25.5+ and avoid stdlib vulnerabilities
|
|
# (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729)
|
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS crowdsec-builder
|
|
COPY --from=xx / /
|
|
|
|
WORKDIR /tmp/crowdsec
|
|
|
|
ARG TARGETPLATFORM
|
|
ARG TARGETOS
|
|
ARG TARGETARCH
|
|
# CrowdSec version - Renovate can update this
|
|
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
|
|
ARG CROWDSEC_VERSION=1.7.4
|
|
|
|
# hadolint ignore=DL3018
|
|
RUN apk add --no-cache git clang lld
|
|
# hadolint ignore=DL3018,DL3059
|
|
RUN xx-apk add --no-cache gcc musl-dev
|
|
|
|
# Clone CrowdSec source
|
|
RUN git clone --depth 1 --branch "v${CROWDSEC_VERSION}" https://github.com/crowdsecurity/crowdsec.git .
|
|
|
|
# Build CrowdSec binaries for target architecture
|
|
# hadolint ignore=DL3059
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/go/pkg/mod \
|
|
CGO_ENABLED=1 xx-go build -o /crowdsec-out/crowdsec \
|
|
-ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v${CROWDSEC_VERSION}" \
|
|
./cmd/crowdsec && \
|
|
xx-verify /crowdsec-out/crowdsec
|
|
|
|
# hadolint ignore=DL3059
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/go/pkg/mod \
|
|
CGO_ENABLED=1 xx-go build -o /crowdsec-out/cscli \
|
|
-ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v${CROWDSEC_VERSION}" \
|
|
./cmd/crowdsec-cli && \
|
|
xx-verify /crowdsec-out/cscli
|
|
|
|
# Copy config files
|
|
RUN mkdir -p /crowdsec-out/config && \
|
|
cp -r config/* /crowdsec-out/config/ || true
|
|
|
|
# ---- CrowdSec Fallback (for architectures where build fails) ----
|
|
# renovate: datasource=docker depName=alpine
|
|
FROM alpine:3.23 AS crowdsec-fallback
|
|
|
|
WORKDIR /tmp/crowdsec
|
|
|
|
ARG TARGETARCH
|
|
# CrowdSec version - Renovate can update this
|
|
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
|
|
ARG CROWDSEC_VERSION=1.7.4
|
|
|
|
# hadolint ignore=DL3018
|
|
RUN apk add --no-cache curl tar
|
|
|
|
# Download static binaries as fallback (only available for amd64)
|
|
# For other architectures, create empty placeholder files so COPY doesn't fail
|
|
# hadolint ignore=DL3059,SC2015
|
|
RUN set -eux; \
|
|
mkdir -p /crowdsec-out/bin /crowdsec-out/config; \
|
|
if [ "$TARGETARCH" = "amd64" ]; then \
|
|
echo "Downloading CrowdSec binaries for amd64 (fallback)..."; \
|
|
curl -fSL "https://github.com/crowdsecurity/crowdsec/releases/download/v${CROWDSEC_VERSION}/crowdsec-release.tgz" \
|
|
-o /tmp/crowdsec.tar.gz && \
|
|
tar -xzf /tmp/crowdsec.tar.gz -C /tmp && \
|
|
cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec-cli/cscli" /crowdsec-out/bin/ && \
|
|
cp "/tmp/crowdsec-v${CROWDSEC_VERSION}/cmd/crowdsec/crowdsec" /crowdsec-out/bin/ && \
|
|
chmod +x /crowdsec-out/bin/* && \
|
|
if [ -d "/tmp/crowdsec-v${CROWDSEC_VERSION}/config" ]; then \
|
|
cp -r "/tmp/crowdsec-v${CROWDSEC_VERSION}/config/"* /crowdsec-out/config/; \
|
|
fi && \
|
|
echo "CrowdSec fallback binaries installed successfully"; \
|
|
else \
|
|
echo "CrowdSec binaries not available for $TARGETARCH - skipping"; \
|
|
touch /crowdsec-out/bin/.placeholder /crowdsec-out/config/.placeholder; \
|
|
fi
|
|
|
|
# ---- Final Runtime with Caddy ----
|
|
FROM ${CADDY_IMAGE}
|
|
WORKDIR /app
|
|
|
|
# Install runtime dependencies for Charon, including bash for maintenance scripts
|
|
# su-exec is used for dropping privileges after Docker socket group setup
|
|
# Explicitly upgrade c-ares to fix CVE-2025-62408
|
|
# hadolint ignore=DL3018
|
|
RUN apk --no-cache add bash ca-certificates sqlite-libs sqlite tzdata curl gettext su-exec \
|
|
&& apk --no-cache upgrade \
|
|
&& apk --no-cache upgrade c-ares
|
|
|
|
# Security: Create non-root user and group for running the application
|
|
# This follows the principle of least privilege (CIS Docker Benchmark 4.1)
|
|
RUN addgroup -g 1000 charon && \
|
|
adduser -D -u 1000 -G charon -h /app -s /sbin/nologin charon
|
|
|
|
# Download MaxMind GeoLite2 Country database
|
|
# Note: In production, users should provide their own MaxMind license key
|
|
# This uses the publicly available GeoLite2 database
|
|
RUN mkdir -p /app/data/geoip && \
|
|
curl -L "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
|
-o /app/data/geoip/GeoLite2-Country.mmdb
|
|
|
|
# Copy Caddy binary from caddy-builder (overwriting the one from base image)
|
|
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
|
|
|
|
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.25.5+)
|
|
# This ensures we don't have stdlib vulnerabilities from older Go versions
|
|
COPY --from=crowdsec-builder /crowdsec-out/crowdsec /usr/local/bin/crowdsec
|
|
COPY --from=crowdsec-builder /crowdsec-out/cscli /usr/local/bin/cscli
|
|
COPY --from=crowdsec-builder /crowdsec-out/config /etc/crowdsec.dist
|
|
|
|
# Verify CrowdSec binaries
|
|
RUN chmod +x /usr/local/bin/crowdsec /usr/local/bin/cscli 2>/dev/null || true; \
|
|
if [ -x /usr/local/bin/cscli ]; then \
|
|
echo "CrowdSec installed (built from source with Go 1.25):"; \
|
|
cscli version || echo "CrowdSec version check failed"; \
|
|
else \
|
|
echo "CrowdSec not available for this architecture"; \
|
|
fi
|
|
|
|
# Create required CrowdSec directories in runtime image
|
|
# NOTE: Do NOT create /etc/crowdsec here - it must be a symlink created at runtime by non-root user
|
|
RUN mkdir -p /var/lib/crowdsec/data /var/log/crowdsec /var/log/caddy \
|
|
/app/data/crowdsec/config /app/data/crowdsec/data && \
|
|
chown -R charon:charon /var/lib/crowdsec /var/log/crowdsec \
|
|
/app/data/crowdsec
|
|
|
|
# Generate CrowdSec default configs to .dist directory
|
|
RUN if command -v cscli >/dev/null; then \
|
|
mkdir -p /etc/crowdsec.dist && \
|
|
cscli config restore /etc/crowdsec.dist/ || \
|
|
cp -r /etc/crowdsec/* /etc/crowdsec.dist/ 2>/dev/null || true; \
|
|
fi
|
|
|
|
# Copy CrowdSec configuration templates from source
|
|
COPY configs/crowdsec/acquis.yaml /etc/crowdsec.dist/acquis.yaml
|
|
COPY configs/crowdsec/install_hub_items.sh /usr/local/bin/install_hub_items.sh
|
|
COPY configs/crowdsec/register_bouncer.sh /usr/local/bin/register_bouncer.sh
|
|
|
|
# Make CrowdSec scripts executable
|
|
RUN chmod +x /usr/local/bin/install_hub_items.sh /usr/local/bin/register_bouncer.sh
|
|
|
|
# Copy Go binary from backend builder
|
|
COPY --from=backend-builder /app/backend/charon /app/charon
|
|
RUN ln -s /app/charon /app/cpmp || true
|
|
# Copy Delve debugger (xx-go install places it in /go/bin)
|
|
COPY --from=backend-builder /go/bin/dlv /usr/local/bin/dlv
|
|
|
|
# Copy frontend build from frontend builder
|
|
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
|
|
|
# Copy startup script
|
|
COPY .docker/docker-entrypoint.sh /docker-entrypoint.sh
|
|
RUN chmod +x /docker-entrypoint.sh
|
|
|
|
# Copy utility scripts (used for DB recovery and maintenance)
|
|
COPY scripts/ /app/scripts/
|
|
RUN chmod +x /app/scripts/db-recovery.sh
|
|
|
|
# Set default environment variables
|
|
ENV CHARON_ENV=production \
|
|
CHARON_DB_PATH=/app/data/charon.db \
|
|
CHARON_FRONTEND_DIR=/app/frontend/dist \
|
|
CHARON_CADDY_ADMIN_API=http://localhost:2019 \
|
|
CHARON_CADDY_CONFIG_DIR=/app/data/caddy \
|
|
CHARON_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb \
|
|
CHARON_HTTP_PORT=8080 \
|
|
CHARON_CROWDSEC_CONFIG_DIR=/app/data/crowdsec
|
|
# Create necessary directories
|
|
RUN mkdir -p /app/data /app/data/caddy /config /app/data/crowdsec
|
|
|
|
# Security: Set ownership of all application directories to non-root charon user
|
|
# Security: Set ownership of all application directories to non-root charon user
|
|
# Note: /etc/crowdsec will be created as a symlink at runtime, not owned directly
|
|
RUN chown -R charon:charon /app /config /var/log/crowdsec /var/log/caddy && \
|
|
chown -R charon:charon /etc/crowdsec.dist 2>/dev/null || true && \
|
|
chown -R charon:charon /var/lib/crowdsec 2>/dev/null || true
|
|
|
|
# Re-declare build args for LABEL usage
|
|
ARG VERSION=dev
|
|
ARG BUILD_DATE
|
|
ARG VCS_REF
|
|
|
|
# OCI image labels for version metadata
|
|
LABEL org.opencontainers.image.title="Charon (CPMP legacy)" \
|
|
org.opencontainers.image.description="Web UI for managing Caddy reverse proxy configurations" \
|
|
org.opencontainers.image.version="${VERSION}" \
|
|
org.opencontainers.image.created="${BUILD_DATE}" \
|
|
org.opencontainers.image.revision="${VCS_REF}" \
|
|
org.opencontainers.image.source="https://github.com/Wikid82/charon" \
|
|
org.opencontainers.image.url="https://github.com/Wikid82/charon" \
|
|
org.opencontainers.image.vendor="charon" \
|
|
org.opencontainers.image.licenses="MIT"
|
|
|
|
# Expose ports
|
|
EXPOSE 80 443 443/udp 2019 8080
|
|
|
|
# Security: Add healthcheck to monitor container health
|
|
# Verifies the Charon API is responding correctly
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
|
CMD curl -f http://localhost:8080/api/v1/health || exit 1
|
|
|
|
# Create CrowdSec symlink as root before switching to non-root user
|
|
# This symlink allows CrowdSec to use persistent storage at /app/data/crowdsec/config
|
|
# while maintaining the expected /etc/crowdsec path for compatibility
|
|
RUN ln -sf /app/data/crowdsec/config /etc/crowdsec
|
|
|
|
# Security: Run as non-root user (CIS Docker Benchmark 4.1)
|
|
# NOTE: The entrypoint script starts as root to handle Docker socket permissions,
|
|
# then drops privileges to the charon user before starting applications.
|
|
# This is necessary for Docker integration while maintaining security.
|
|
|
|
# Use custom entrypoint to start both Caddy and Charon
|
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|