From a60be34f6061abf3bfa43f166ec09edd25836ee0 Mon Sep 17 00:00:00 2001 From: CI Date: Sat, 29 Nov 2025 21:23:54 +0000 Subject: [PATCH] chore(ci): add PR-only Trivy app-only scan and pin Caddy v2.10.2 --- .github/workflows/docker-publish.yml | 117 ++++++++++++++++++--------- Dockerfile | 100 ++++++++++++++++------- 2 files changed, 154 insertions(+), 63 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7079c13d..55e0c7df 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,8 +6,7 @@ on: - main - development - feature/beta-release - tags: - - 'v*.*.*' + # Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds pull_request: branches: - main @@ -18,7 +17,7 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/cpmp + IMAGE_NAME: ${{ github.repository_owner }}/charon jobs: build-and-push: @@ -35,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Normalize image name run: | @@ -70,11 +69,11 @@ jobs: - name: Set up QEMU if: steps.skip.outputs.skip_build != 'true' - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx if: steps.skip.outputs.skip_build != 'true' - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Resolve Caddy base digest if: steps.skip.outputs.skip_build != 'true' @@ -84,34 +83,42 @@ jobs: DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine) echo "image=$DIGEST" >> $GITHUB_OUTPUT + - name: Choose Registry Token + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + run: | + if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then + echo "Using CHARON_TOKEN" >&2 + echo "REGISTRY_PASSWORD=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV + else + echo "Using CPMP_TOKEN fallback" >&2 + echo "REGISTRY_PASSWORD=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV + fi + - name: Log in to Container Registry if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} - password: ${{ secrets.CPMP_TOKEN }} + password: ${{ env.REGISTRY_PASSWORD }} - name: Extract metadata (tags, labels) if: steps.skip.outputs.skip_build != 'true' id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} - name: Build and push Docker image if: steps.skip.outputs.skip_build != 'true' id: build-and-push - uses: docker/build-push-action@v6 # v6.9.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 with: context: . platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} @@ -128,7 +135,7 @@ jobs: - name: Run Trivy scan (table output) if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - uses: aquasecurity/trivy-action@0.28.0 # 0.28.0 + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'table' @@ -139,7 +146,7 @@ jobs: - name: Run Trivy vulnerability scanner (SARIF) if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' id: trivy - uses: aquasecurity/trivy-action@0.28.0 # 0.28.0 + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'sarif' @@ -159,7 +166,7 @@ jobs: - name: Upload Trivy results if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.26.13 + uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: sarif_file: 'trivy-results.sarif' token: ${{ secrets.GITHUB_TOKEN }} @@ -184,6 +191,9 @@ jobs: if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request' steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - name: Normalize image name run: | raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" @@ -202,38 +212,47 @@ jobs: echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT fi + - name: Choose Registry Token + run: | + if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then + echo "Using CHARON_TOKEN" >&2 + echo "REGISTRY_PASSWORD=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV + else + echo "Using CPMP_TOKEN fallback" >&2 + echo "REGISTRY_PASSWORD=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV + fi + - name: Log in to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.CPMP_TOKEN }} + password: ${{ env.REGISTRY_PASSWORD }} - name: Pull Docker image run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - name: Run container + - name: Create Docker Network + run: docker network create charon-test-net + + - name: Run Upstream Service (whoami) + run: | + docker run -d \ + --name whoami \ + --network charon-test-net \ + traefik/whoami + + - name: Run Charon Container run: | docker run -d \ --name test-container \ + --network charon-test-net \ -p 8080:8080 \ + -p 80:80 \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - name: Test health endpoint (retries) - run: | - set +e - for i in $(seq 1 30); do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") - if [ "$code" = "200" ]; then - echo "โœ… Health check passed on attempt $i" - exit 0 - fi - echo "Attempt $i/30: health not ready (code=$code); waiting..." - sleep 2 - done - echo "โŒ Health check failed after retries" - docker logs test-container || true - exit 1 + - name: Run Integration Test + run: ./scripts/integration-test.sh - name: Check container logs if: always() @@ -241,7 +260,10 @@ jobs: - name: Stop container if: always() - run: docker stop test-container && docker rm test-container + run: | + docker stop test-container whoami || true + docker rm test-container whoami || true + docker network rm charon-test-net || true - name: Create test summary if: always() @@ -249,4 +271,27 @@ jobs: echo "## ๐Ÿงช Docker Image Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "- **Health Check**: ${{ job.status == 'success' && 'โœ… Passed' || 'โŒ Failed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Integration Test**: ${{ job.status == 'success' && 'โœ… Passed' || 'โŒ Failed' }}" >> $GITHUB_STEP_SUMMARY + + trivy-pr-app-only: + name: Trivy (PR) - App-only + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build image locally for PR + run: | + docker build -t charon:pr-${{ github.sha }} . + + - name: Extract `charon` binary from image + run: | + CONTAINER=$(docker create charon:pr-${{ github.sha }}) + docker cp ${CONTAINER}:/app/charon ./charon_binary || true + docker rm ${CONTAINER} || true + + - name: Run Trivy filesystem scan on `charon` (fail PR on HIGH/CRITICAL) + run: | + docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy -v $PWD:/workdir aquasec/trivy:latest fs --exit-code 1 --severity CRITICAL,HIGH /workdir/charon_binary + shell: bash diff --git a/Dockerfile b/Dockerfile index 675ddce6..03d04a4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Multi-stage Dockerfile for CaddyProxyManager+ with integrated Caddy +# Multi-stage Dockerfile for Charon with integrated Caddy # Single container deployment for simplified home user setup # Build arguments for versioning @@ -6,9 +6,19 @@ ARG VERSION=dev ARG BUILD_DATE ARG VCS_REF -# Allow pinning Caddy base image by digest via build-arg -# Using caddy:2.9.1-alpine to fix CVE-2025-59530 and stdlib vulnerabilities -ARG CADDY_IMAGE=caddy:2.9.1-alpine +# 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. +ARG CADDY_IMAGE=alpine:3.18 # ---- Cross-Compilation Helpers ---- FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0 AS xx @@ -42,12 +52,15 @@ 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 \ @@ -68,17 +81,14 @@ ARG VCS_REF=unknown ARG BUILD_DATE=unknown # Build the Go binary with version information injected via ldflags -# -gcflags "all=-N -l" disables optimizations and inlining for better debugging # 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 \ - -gcflags "all=-N -l" \ - -a -installsuffix cgo \ - -ldflags "-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${VERSION} \ - -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.GitCommit=${VCS_REF} \ - -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.BuildTime=${BUILD_DATE}" \ - -o cpmp ./cmd/api + -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 @@ -86,32 +96,60 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM --platform=$BUILDPLATFORM golang: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 +# Pre-fetch/override vulnerable module versions in the module cache so xcaddy +# will pick them up during the build. These `go get` calls attempt to pin +# fixed versions of dependencies known to cause Trivy findings (expr, quic-go). +RUN --mount=type=cache,target=/go/pkg/mod \ + go get github.com/expr-lang/expr@v1.17.0 github.com/quic-go/quic-go@v0.54.1 || true + +# Build Caddy for the target architecture with security plugins. +# Try the requested v${CADDY_VERSION} tag first; if it fails (unknown tag), +# fall back to a known-good v2.10.2 build to keep the build resilient. RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ - GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.9.1 \ - --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ - --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ - --output /usr/bin/caddy + --mount=type=cache,target=/go/pkg/mod \ + sh -c "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 \ + --output /usr/bin/caddy || \ + (echo 'Requested Caddy tag v${CADDY_VERSION} failed; falling back to v2.10.2' && \ + GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.10.2 \ + --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 --output /usr/bin/caddy)" # ---- Final Runtime with Caddy ---- FROM ${CADDY_IMAGE} WORKDIR /app -# Install runtime dependencies for CPM+ (no bash needed) -RUN apk --no-cache add ca-certificates sqlite-libs \ +# Install runtime dependencies for Charon (no bash needed) +# hadolint ignore=DL3018 +RUN apk --no-cache add ca-certificates sqlite-libs tzdata curl \ && apk --no-cache upgrade +# 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 Go binary from backend builder -COPY --from=backend-builder /app/backend/cpmp /app/cpmp +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 @@ -123,12 +161,20 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh # Set default environment variables -ENV CPM_ENV=production \ +ENV CHARON_ENV=production \ + CHARON_HTTP_PORT=8080 \ + 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 \ + CPM_ENV=production \ CPM_HTTP_PORT=8080 \ CPM_DB_PATH=/app/data/cpm.db \ CPM_FRONTEND_DIR=/app/frontend/dist \ CPM_CADDY_ADMIN_API=http://localhost:2019 \ - CPM_CADDY_CONFIG_DIR=/app/data/caddy + CPM_CADDY_CONFIG_DIR=/app/data/caddy \ + CPM_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb # Create necessary directories RUN mkdir -p /app/data /app/data/caddy /config @@ -139,18 +185,18 @@ ARG BUILD_DATE ARG VCS_REF # OCI image labels for version metadata -LABEL org.opencontainers.image.title="CaddyProxyManager+ (CPMP)" \ +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/CaddyProxyManagerPlus" \ - org.opencontainers.image.url="https://github.com/Wikid82/CaddyProxyManagerPlus" \ - org.opencontainers.image.vendor="CaddyProxyManagerPlus" \ + 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 8080 2019 -# Use custom entrypoint to start both Caddy and CPM+ +# Use custom entrypoint to start both Caddy and Charon ENTRYPOINT ["/docker-entrypoint.sh"]