diff --git a/.github/renovate.json b/.github/renovate.json index 92f8c882b..a92c07cee 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -269,6 +269,74 @@ "datasourceTemplate": "github-releases", "versioningTemplate": "semver", "extractVersionTemplate": "^v(?.*)" + }, + { + "customType": "regex", + "description": "Track CrowdSec version ARG in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=github-releases\\s+depName=crowdsecurity/crowdsec\\s*\\nARG CROWDSEC_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "crowdsecurity/crowdsec", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver", + "extractVersionTemplate": "^v?(?.*)$" + }, + { + "customType": "regex", + "description": "Track Caddy version ARGs in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/caddyserver/caddy[^\\s]*\\s*\\nARG CADDY_VERSION=(?[^\\s]+)", + "#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/caddyserver/caddy[^\\s]*\\s*\\nARG CADDY_CANDIDATE_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "github.com/caddyserver/caddy/v2", + "datasourceTemplate": "go", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Track gosu version ARG in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=github-releases\\s+depName=tianon/gosu\\s*\\nARG GOSU_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "tianon/gosu", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Track npm version ARG in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=npm\\s+depName=npm\\s*\\nARG NPM_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "npm", + "datasourceTemplate": "npm", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Track golang.org/x/crypto version ARG in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=go\\s+depName=golang\\.org/x/crypto\\s*\\nARG XCRYPTO_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "golang.org/x/crypto", + "datasourceTemplate": "go", + "versioningTemplate": "semver" + }, + { + "customType": "regex", + "description": "Track coraza-caddy version ARG in Dockerfile", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/corazawaf/coraza-caddy[^\\s]*\\s*\\nARG CORAZA_CADDY_VERSION=(?[^\\s]+)" + ], + "depNameTemplate": "github.com/corazawaf/coraza-caddy/v2", + "datasourceTemplate": "go", + "versioningTemplate": "semver" } ], @@ -280,17 +348,70 @@ "packageRules": [ { - "description": "THE MEGAZORD: Group ALL non-major updates (NPM, Docker, Go, Actions) into one PR", + "description": "Group GitHub Actions non-major updates into one PR", + "matchManagers": [ + "github-actions" + ], "matchUpdateTypes": [ "minor", "patch", "pin", "digest" ], - "groupName": "non-major-updates", + "groupName": "github-actions-non-major", + "groupSlug": "github-actions-non-major" + }, + { + "description": "Group Go non-major updates into one PR", + "matchDatasources": [ + "go", + "golang-version" + ], + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "groupName": "go-non-major", + "groupSlug": "go-non-major" + }, + { + "description": "Group Go github-tags fallback updates from Dockerfile custom manager into Go non-major PR", + "matchDatasources": [ + "github-tags" + ], + "matchManagers": [ + "custom.regex" + ], + "matchFileNames": [ + "Dockerfile" + ], "matchPackageNames": [ - "*" - ] + "jackc/pgx" + ], + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "groupName": "go-non-major", + "groupSlug": "go-non-major" + }, + { + "description": "Group NPM non-major updates into one PR", + "matchDatasources": [ + "npm" + ], + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "groupName": "npm-non-major", + "groupSlug": "npm-non-major" }, { "description": "Development branch: Auto-merge non-major updates after proven stable", @@ -315,6 +436,18 @@ "matchPackageNames": ["caddy"], "allowedVersions": "<3.0.0" }, + { + "description": "Go: keep Caddy within v2 (no automatic jump to v3) - ARG tracking via custom manager", + "matchDatasources": ["go"], + "matchPackageNames": ["github.com/caddyserver/caddy/v2"], + "allowedVersions": "<3.0.0" + }, + { + "description": "Label CrowdSec updates as security-relevant", + "matchDatasources": ["github-releases"], + "matchPackageNames": ["crowdsecurity/crowdsec"], + "labels": ["security", "dependencies"] + }, { "description": "Go: keep pgx within v4 (CrowdSec requires pgx/v4 module path) - applies to go.mod lookups", "matchDatasources": ["go"], diff --git a/.github/workflows/auto-changelog.yml b/.github/workflows/auto-changelog.yml index cd3fc914f..a91290834 100644 --- a/.github/workflows/auto-changelog.yml +++ b/.github/workflows/auto-changelog.yml @@ -24,6 +24,6 @@ jobs: with: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Draft Release - uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7 + uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6a5d126c7..5b38daad4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -168,7 +168,7 @@ jobs: - name: Set up QEMU if: steps.skip.outputs.skip_build != 'true' - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx if: steps.skip.outputs.skip_build != 'true' uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 30f58c5e1..75e34444d 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -154,6 +154,17 @@ jobs: digest: ${{ steps.resolve_digest.outputs.digest }} steps: + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true + tool-cache: false + - name: Checkout nightly branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -164,7 +175,7 @@ jobs: run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV" - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -224,9 +235,10 @@ jobs: VCS_REF=${{ github.sha }} BUILD_DATE=${{ github.event.repository.pushed_at }} ALPINE_IMAGE=${{ steps.alpine.outputs.image }} + CROWDSEC_VERSION=1.7.8 cache-from: type=gha cache-to: type=gha,mode=max - no-cache-filters: caddy-builder + no-cache-filters: caddy-builder,crowdsec-builder provenance: true sbom: true @@ -396,6 +408,17 @@ jobs: packages: write steps: + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true + tool-cache: false + - name: Checkout nightly branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -406,7 +429,7 @@ jobs: run: echo "ORTHRUS_IMAGE_NAME_LC=${ORTHRUS_IMAGE_NAME,,}" >> "$GITHUB_ENV" - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/orthrus-build.yml b/.github/workflows/orthrus-build.yml index aeff1197d..6990efe86 100644 --- a/.github/workflows/orthrus-build.yml +++ b/.github/workflows/orthrus-build.yml @@ -99,7 +99,7 @@ jobs: fi - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index 9fc663dc7..740eed70b 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -53,7 +53,7 @@ jobs: echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index 23dae8407..7d76ca842 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -282,7 +282,7 @@ jobs: echo "component_count=${COMPONENT_COUNT}" >> "$GITHUB_OUTPUT" echo "✅ SBOM generated with ${COMPONENT_COUNT} components" - # Scan for vulnerabilities using manual Grype installation (pinned to v0.110.0) + # Scan for vulnerabilities using manual Grype installation (pinned to v0.112.0) - name: Install Grype if: steps.set-target.outputs.image_name != '' run: | @@ -362,7 +362,7 @@ jobs: fi - name: Upload SARIF to GitHub Security - if: steps.check-artifact.outputs.artifact_found == 'true' + if: steps.set-target.outputs.image_name != '' uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4 continue-on-error: true with: diff --git a/.gitignore b/.gitignore index aff059433..5d69345e3 100644 --- a/.gitignore +++ b/.gitignore @@ -328,4 +328,5 @@ backend/***_coverage.txt backend/***_cov.txt .tmp/caddy-binary-pin-cleanup .tmp/caddy-binary-pin-cleanup-local.tar -.tmp/*** \ No newline at end of file +.tmp/*** +charon-scan.tar diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fcd02d32..e00c76962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- **CVE-2026-44982 / GHSA-rw47-hm26-6wr7**: Resolved high-severity CrowdSec AppSec vulnerability where HTTP request bodies were silently dropped for chunked/HTTP-2 requests, allowing WAF bypass + - Upgraded `CROWDSEC_VERSION` to `v1.7.8` in the Dockerfile + - Upgraded `caddy-crowdsec-bouncer` to `v0.12.1` to align with the updated crowdsec API + - Applied build-time source patches for two breaking API changes in crowdsec v1.7.8 (`DecisionsListOpts` field pointer types, `version.DetectOS()` return arity) + - **CVE-2026-34040**: Remediated high-severity vulnerability by migrating from `github.com/docker/docker` to `github.com/moby/moby/client v0.4.1` - Affected component: Docker client SDK used for container management features - Resolution: Updated `go.mod` to reference the actively maintained `moby/moby` module diff --git a/Dockerfile b/Dockerfile index 1714dfd3c..f45eab439 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG ALPINE_IMAGE=alpine:3.23.4@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc911 # ---- Shared CrowdSec Version ---- # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.7 +ARG CROWDSEC_VERSION=1.7.8 # CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION}) ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd @@ -29,7 +29,7 @@ ARG XNET_VERSION=0.55.0 # renovate: datasource=go depName=golang.org/x/crypto ARG XCRYPTO_VERSION=0.52.0 # renovate: datasource=npm depName=npm -ARG NPM_VERSION=11.11.1 +ARG NPM_VERSION=11.16.0 # Allow pinning Caddy version - Renovate will update this # Build the most recent Caddy 2.x release (keeps major pinned under v3). @@ -38,15 +38,15 @@ ARG NPM_VERSION=11.11.1 # 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.11.3 build. -# renovate: datasource=go depName=https://github.com/caddyserver/caddy +# renovate: datasource=go depName=github.com/caddyserver/caddy/v2 ARG CADDY_VERSION=2.11.3 -# renovate: datasource=go depName=https://github.com/caddyserver/caddy +# renovate: datasource=go depName=github.com/caddyserver/caddy/v2 ARG CADDY_CANDIDATE_VERSION=2.11.3 ARG CADDY_USE_CANDIDATE=0 ARG CADDY_PATCH_SCENARIO=B # renovate: datasource=go depName=github.com/greenpau/caddy-security ARG CADDY_SECURITY_VERSION=1.1.62 -# renovate: datasource=go depName=github.com/corazawaf/coraza-caddy +# renovate: datasource=go depName=github.com/corazawaf/coraza-caddy/v2 ARG CORAZA_CADDY_VERSION=2.5.0 ## 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 @@ -242,6 +242,7 @@ ARG XCADDY_VERSION=0.4.6 ARG EXPR_LANG_VERSION ARG XNET_VERSION ARG XCRYPTO_VERSION +ARG CROWDSEC_VERSION # hadolint ignore=DL3018 RUN apk add --no-cache bash git @@ -257,6 +258,18 @@ RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ bash -c 'set -e; \ + # Restore any module cache files patched by a previous build run. + # xcaddy Stage 1 resolves crowdsec to its native version (v1.6.x, IPEquals *string). + # If a prior build left IPEquals: value, (plain string) in the cache, xcaddy fails. + _GOMC="$(go env GOMODCACHE)"; \ + for _PF in \ + "${_GOMC}/github.com/hslatman/caddy-crowdsec-bouncer@v0.12.1/internal/bouncer/live.go" \ + "${_GOMC}/github.com/crowdsecurity/go-cs-bouncer@v0.0.14/live_bouncer.go"; do \ + if [ -f "${_PF}" ]; then \ + chmod +w "${_PF}"; \ + sed -i "s/IPEquals: value,/IPEquals: \&value,/g" "${_PF}"; \ + fi; \ + done; \ CADDY_TARGET_VERSION="${CADDY_VERSION}"; \ if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \ CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \ @@ -270,7 +283,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ --with github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION} \ --with github.com/greenpau/caddy-security@v${CADDY_SECURITY_VERSION} \ --with github.com/corazawaf/coraza-caddy/v2@v${CORAZA_CADDY_VERSION} \ - --with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \ + --with github.com/hslatman/caddy-crowdsec-bouncer@v0.12.1 \ --with github.com/zhangjiayin/caddy-geoip2 \ --with github.com/mholt/caddy-ratelimit \ --output /tmp/caddy-initial; \ @@ -321,6 +334,13 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ # Affects /usr/bin/caddy (transitive dependency). Fix available at v0.1.1. # renovate: datasource=go depName=github.com/Azure/go-ntlmssp go get github.com/Azure/go-ntlmssp@v0.1.1; \ + # CVE-2026-44982 (GHSA-rw47-hm26-6wr7): CrowdSec AppSec silently drops HTTP request + # body for chunked/HTTP-2 requests, bypassing WAF body inspection rules. + # caddy-crowdsec-bouncer@v0.12.1 was built against crowdsec v1.6.3 whose + # DecisionsListOpts fields were *string; v1.7.8 changed them to plain string. + # The source-level incompatibility is patched below via local copy + go.mod replace. + # Remove once bouncer ships against crowdsec >= v1.7.8. + go get github.com/crowdsecurity/crowdsec@v${CROWDSEC_VERSION}; \ if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \ # Rollback scenario: keep explicit nebula pin if upstream compatibility regresses. # NOTE: smallstep/certificates (pulled by caddy-security stack) currently @@ -340,6 +360,36 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ go get github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION}; \ # Clean up go.mod and ensure all dependencies are resolved go mod tidy; \ + # Patch DecisionsListOpts API: crowdsec v1.7.8 changed fields (IPEquals, ScopeEquals, + # etc.) from *string to plain string. caddy-crowdsec-bouncer@v0.12.1 and its transitive + # dep go-cs-bouncer@v0.0.14 still use the old pointer form. + # Strategy: copy modules to ephemeral /tmp dirs and use go.mod replace directives. + # This avoids modifying the shared BuildKit module cache, which would corrupt xcaddy + # Stage 1 of subsequent builds (where these modules are compiled with crowdsec v1.6.x). + go mod download github.com/hslatman/caddy-crowdsec-bouncer@v0.12.1; \ + BOUNCER_CACHE="${_GOMC}/github.com/hslatman/caddy-crowdsec-bouncer@v0.12.1"; \ + BOUNCER_LOCAL="/tmp/bouncer-patched"; \ + rm -rf "${BOUNCER_LOCAL}"; \ + cp -r "${BOUNCER_CACHE}/." "${BOUNCER_LOCAL}/"; \ + chmod -R +w "${BOUNCER_LOCAL}"; \ + sed -i "s/IPEquals: &value,/IPEquals: value,/g" "${BOUNCER_LOCAL}/internal/bouncer/live.go"; \ + echo "Patched caddy-crowdsec-bouncer at ${BOUNCER_LOCAL}"; \ + go mod edit -replace "github.com/hslatman/caddy-crowdsec-bouncer@v0.12.1=${BOUNCER_LOCAL}"; \ + GO_CS_CACHE="${_GOMC}/github.com/crowdsecurity/go-cs-bouncer@v0.0.14"; \ + if [ -d "${GO_CS_CACHE}" ]; then \ + GO_CS_LOCAL="/tmp/go-cs-bouncer-patched"; \ + rm -rf "${GO_CS_LOCAL}"; \ + cp -r "${GO_CS_CACHE}/." "${GO_CS_LOCAL}/"; \ + chmod -R +w "${GO_CS_LOCAL}"; \ + sed -i "s/IPEquals: &value,/IPEquals: value,/g" "${GO_CS_LOCAL}/live_bouncer.go"; \ + sed -i "s/ScopeEquals: &value,/ScopeEquals: value,/g" "${GO_CS_LOCAL}/live_bouncer.go"; \ + sed -i "s/ValueEquals: &value,/ValueEquals: value,/g" "${GO_CS_LOCAL}/live_bouncer.go"; \ + sed -i "s/TypeEquals: &value,/TypeEquals: value,/g" "${GO_CS_LOCAL}/live_bouncer.go"; \ + sed -i "s/RangeEquals: &value,/RangeEquals: value,/g" "${GO_CS_LOCAL}/live_bouncer.go"; \ + sed -i "s/osName, osVersion := version.DetectOS()/osName, osVersion, _ := version.DetectOS()/g" "${GO_CS_LOCAL}/metrics.go"; \ + echo "Patched go-cs-bouncer at ${GO_CS_LOCAL}"; \ + go mod edit -replace "github.com/crowdsecurity/go-cs-bouncer@v0.0.14=${GO_CS_LOCAL}"; \ + fi; \ # Hard assertion: fail if module graph resolves to a different Caddy core version. ACTUAL_CADDY_VERSION="$(go list -m -f "{{.Version}}" github.com/caddyserver/caddy/v2)"; \ if [ "$ACTUAL_CADDY_VERSION" != "v${CADDY_TARGET_VERSION}" ]; then \ @@ -407,15 +457,14 @@ RUN go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION} && \ # Pin here so the CrowdSec binary is patched immediately; # remove once CrowdSec ships a release built with go.opentelemetry.io/otel >= v1.41.0. # renovate: datasource=go depName=go.opentelemetry.io/otel - go get go.opentelemetry.io/otel@v1.43.0 && \ + go get go.opentelemetry.io/otel@v1.44.0 && \ # GHSA-xmrv-pmrh-hhx2: AWS SDK v2 event stream injection # renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream go get github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.10 && \ # renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs - go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.74.0 && \ + go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.74.1 && \ go get github.com/aws/aws-sdk-go-v2/service/kinesis@v1.43.7 && \ - # renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/s3 - go get github.com/aws/aws-sdk-go-v2/service/s3@v1.101.0 && \ + go get github.com/aws/aws-sdk-go-v2/service/s3@v1.102.1 && \ # CVE-2026-32952: go-ntlmssp DoS via malicious NTLM challenge response # Affects /usr/local/bin/cscli (transitive dependency). Fix available at v0.1.1. # renovate: datasource=go depName=github.com/Azure/go-ntlmssp @@ -498,7 +547,9 @@ RUN apk add --no-cache \ c-ares busybox-extras \ && apk upgrade --no-cache zlib libcrypto3 libssl3 musl musl-utils \ # CVE-2026-34743: xz-libs DoS via buffer overflow in index decoding (fixed in 5.8.3-r0) - xz-libs + xz-libs \ + # CVE-2026-6732: libxml2 HIGH vulnerability (fixed in 2.13.9-r1) + libxml2 # Copy gosu binary from gosu-builder (built with Go 1.26+ to avoid stdlib CVEs) COPY --from=gosu-builder /gosu-out/gosu /usr/sbin/gosu @@ -515,7 +566,7 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"] # Note: In production, users should provide their own MaxMind license key # This uses the publicly available GeoLite2 database # In CI, timeout quickly rather than retrying to save build time -ARG GEOLITE2_COUNTRY_SHA256=d074a873c0db6755c0d7f22efe8c76d14fd5d4bcdaa5fc5e940508e8517e99ba +ARG GEOLITE2_COUNTRY_SHA256=c77ac1d7e64b3fcd1447045615fc3aefb3ed886e176608c568b01f29f955e21a RUN mkdir -p /app/data/geoip && \ if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \ echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \ @@ -660,7 +711,7 @@ 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=10s --start-period=15s --retries=3 \ +HEALTHCHECK --interval=30s --timeout=10s --start-period=4m --retries=3 \ CMD wget -q -O /dev/null http://localhost:8080/api/v1/health || exit 1 # Create CrowdSec symlink as root before switching to non-root user diff --git a/SECURITY.md b/SECURITY.md index e20a57db8..1fde07e29 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,7 +27,51 @@ public disclosure. ## Known Vulnerabilities -Last reviewed: 2026-05-25 +Last reviewed: 2026-05-27 + +### [RESOLVED] GHSA-rw47-hm26-6wr7 / CVE-2026-44982 · CrowdSec AppSec Drops HTTP Request Body + +| Field | Value | +|--------------|-------| +| **ID** | GHSA-rw47-hm26-6wr7 / CVE-2026-44982 | +| **Severity** | High | +| **Status** | Resolved — crowdsec upgraded to v1.7.8 | + +**What** +The CrowdSec AppSec component silently dropped the HTTP request body for chunked-encoded or +HTTP/2 requests, causing the Web Application Firewall rules to operate on an empty body. This +allowed malicious payloads in those request types to bypass WAF inspection. + +**Who** + +- Discovered by: CrowdSec security team +- Reported: 2026-05-27 (via GHSA advisory) +- Affects: Charon deployments with the AppSec/WAF security module enabled + +**Where** + +- Component: `github.com/crowdsecurity/crowdsec` (via `caddy-crowdsec-bouncer`) +- Versions affected: crowdsec < v1.7.8 + +**When** + +- Discovered: 2026-05-27 +- Fixed upstream: crowdsec v1.7.8 +- Resolved in Charon: 2026-05-27 + +**How** +The body reader in the AppSec engine did not correctly buffer chunked or HTTP/2 request bodies +before passing them to the WAF rule evaluation pipeline. Requests with these transfer encodings +would present an empty body to inspection rules, meaning payload-based WAF rules had no effect. + +**Resolution** +Upgraded `CROWDSEC_VERSION` to `v1.7.8` in the Dockerfile. The `caddy-crowdsec-bouncer` module +(upgraded to `v0.12.1`) now builds against crowdsec v1.7.8 which contains the body-reader fix. +Two source-level compatibility patches are applied at build time to handle breaking API changes +introduced between v1.6.x and v1.7.8 (`DecisionsListOpts` field types and +`version.DetectOS()` return signature). + +--- ### [HIGH] CVE-2026-31790 · OpenSSL Vulnerability in Alpine Base Image diff --git a/backend/go.mod b/backend/go.mod index cbc870e44..fa9bfe131 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -51,7 +51,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-playground/validator/v10 v10.30.3 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -73,7 +73,7 @@ require ( github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/common v0.68.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.1 // indirect @@ -83,16 +83,15 @@ require ( github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect golang.org/x/arch v0.27.0 // indirect golang.org/x/sys v0.45.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - modernc.org/libc v1.72.3 // indirect + modernc.org/libc v1.72.5 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.50.1 // indirect + modernc.org/sqlite v1.51.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index df44ff3fb..a7da71d02 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -52,8 +52,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= -github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -126,8 +126,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA= +github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -163,18 +163,18 @@ go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/ go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -217,18 +217,18 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= -modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo= +modernc.org/ccgo/v4 v4.34.2/go.mod h1:1L7us56+kAKu04p25EATpmBBvhbcqqZ85ibqWVwVgog= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= -modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U= +modernc.org/libc v1.72.5/go.mod h1:np0N7KDJ7eUtMZmOqVZNldrZyG+DHLl2B5pg8Hbar3U= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -237,8 +237,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w= -modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U= +modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/backend/internal/hecate/providers/cloudflare/coverage_test.go b/backend/internal/hecate/providers/cloudflare/coverage_test.go index 139fe46c2..69e0efe39 100644 --- a/backend/internal/hecate/providers/cloudflare/coverage_test.go +++ b/backend/internal/hecate/providers/cloudflare/coverage_test.go @@ -2,11 +2,13 @@ package cloudflare import ( "context" + "errors" "fmt" "net/http" "net/http/httptest" "os" "os/exec" + "path/filepath" "testing" "time" @@ -207,3 +209,96 @@ func TestListTunnels_RequestBuildError(t *testing.T) { _, err := c.ListTunnels(context.Background()) require.Error(t, err) } + +// TestStart_StdoutPipeError covers the true branch of the stdout pipe error guard (lines 132–133) +// by injecting osPipe to fail on the first call. +func TestStart_StdoutPipeError(t *testing.T) { + dir := t.TempDir() + fakeBin := filepath.Join(dir, "cloudflared") + require.NoError(t, os.WriteFile(fakeBin, []byte("not elf"), 0o755)) //nolint:gosec + + p := &CloudflareTunnelProvider{ + binaryPath: fakeBin, + creds: cfCredentials{TunnelToken: "tok"}, + buf: hecate.NewRingBuffer(1000), + } + + orig := osPipe + t.Cleanup(func() { osPipe = orig }) + osPipe = func() (*os.File, *os.File, error) { + return nil, nil, errors.New("simulated stdout pipe failure") + } + + err := p.Start(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "stdout pipe") + assert.Equal(t, hecate.TunnelStateConnecting, p.Status()) +} + +// TestStart_StderrPipeError covers the true branch of the stderr pipe error guard (lines 136–139) +// by injecting osPipe to succeed on the first call and fail on the second. +func TestStart_StderrPipeError(t *testing.T) { + dir := t.TempDir() + fakeBin := filepath.Join(dir, "cloudflared") + require.NoError(t, os.WriteFile(fakeBin, []byte("not elf"), 0o755)) //nolint:gosec + + p := &CloudflareTunnelProvider{ + binaryPath: fakeBin, + creds: cfCredentials{TunnelToken: "tok"}, + buf: hecate.NewRingBuffer(1000), + } + + calls := 0 + origPipe := osPipe + t.Cleanup(func() { osPipe = origPipe }) + osPipe = func() (*os.File, *os.File, error) { + calls++ + if calls == 1 { + return origPipe() + } + return nil, nil, errors.New("simulated stderr pipe failure") + } + + err := p.Start(context.Background()) + + require.Error(t, err) + assert.Contains(t, err.Error(), "stderr pipe") + assert.Equal(t, hecate.TunnelStateConnecting, p.Status()) +} + +// TestStart_WriteEndCloseErrors covers the error-log branches for closeWriteFile (lines 161, 163, 166, 168) +// by injecting closeWriteFile to physically close the file (unblocking scanners) but also return an error. +func TestStart_WriteEndCloseErrors(t *testing.T) { + trueBin, err := exec.LookPath("true") + if err != nil { + t.Skip("true binary not available") + } + + p := &CloudflareTunnelProvider{ + binaryPath: trueBin, + creds: cfCredentials{TunnelToken: "tok"}, + buf: hecate.NewRingBuffer(1000), + } + + origClose := closeWriteFile + t.Cleanup(func() { closeWriteFile = origClose }) + closeWriteFile = func(f *os.File) error { + _ = f.Close() + return errors.New("simulated write-end close error") + } + + startErr := p.Start(context.Background()) + + require.NoError(t, startErr, "close errors are logged, not returned from Start()") + + p.mu.RLock() + done := p.done + p.mu.RUnlock() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for cloudflared goroutines to exit") + } +} diff --git a/backend/internal/hecate/providers/cloudflare/provider.go b/backend/internal/hecate/providers/cloudflare/provider.go index 04e801150..57720acec 100644 --- a/backend/internal/hecate/providers/cloudflare/provider.go +++ b/backend/internal/hecate/providers/cloudflare/provider.go @@ -12,7 +12,18 @@ import ( "time" "github.com/Wikid82/charon/backend/internal/hecate" + "github.com/Wikid82/charon/backend/internal/logger" "github.com/Wikid82/charon/backend/internal/models" + "github.com/sirupsen/logrus" +) + +// Test hooks to allow overriding OS functions in unit tests. +var ( + // osPipe wraps os.Pipe to allow simulating pipe-creation failures. + osPipe = os.Pipe + // closeWriteFile wraps (*os.File).Close for the pipe write-ends closed + // after cmd.Start() succeeds. Allows simulating close errors in tests. + closeWriteFile = func(f *os.File) error { return f.Close() } ) // cfCredentials holds the decrypted JSON credentials for the Cloudflare provider. @@ -126,16 +137,25 @@ func (p *CloudflareTunnelProvider) Start(ctx context.Context) error { cmd := exec.CommandContext(ctx, binaryPath, "tunnel", "run") //nolint:gosec cmd.Env = append(os.Environ(), "TUNNEL_TOKEN="+p.creds.TunnelToken) - stdoutPipe, err := cmd.StdoutPipe() + stdoutR, stdoutW, err := osPipe() if err != nil { return fmt.Errorf("cloudflare: stdout pipe: %w", err) } - stderrPipe, err := cmd.StderrPipe() + stderrR, stderrW, err := osPipe() if err != nil { + _ = stdoutR.Close() + _ = stdoutW.Close() return fmt.Errorf("cloudflare: stderr pipe: %w", err) } + cmd.Stdout = stdoutW + cmd.Stderr = stderrW + if err := cmd.Start(); err != nil { + _ = stdoutR.Close() + _ = stdoutW.Close() + _ = stderrR.Close() + _ = stderrW.Close() p.mu.Lock() p.state = hecate.TunnelStateError close(p.done) @@ -143,6 +163,20 @@ func (p *CloudflareTunnelProvider) Start(ctx context.Context) error { return fmt.Errorf("cloudflare: start cloudflared: %w", err) } + // Close the write ends in the parent process. The child holds its own + // copies via exec.Cmd; keeping parent write ends open would prevent the + // read ends from reaching EOF when the child exits. + if err := closeWriteFile(stdoutW); err != nil { + logger.Log().WithFields(logrus.Fields{ + "error": err, + }).Error("cloudflare: failed to close stdout write end") + } + if err := closeWriteFile(stderrW); err != nil { + logger.Log().WithFields(logrus.Fields{ + "error": err, + }).Error("cloudflare: failed to close stderr write end") + } + p.mu.Lock() p.cmd = cmd p.state = hecate.TunnelStateConnected @@ -154,7 +188,8 @@ func (p *CloudflareTunnelProvider) Start(ctx context.Context) error { scanWg.Add(1) go func() { defer scanWg.Done() - s := bufio.NewScanner(stdoutPipe) + defer stdoutR.Close() //nolint:errcheck + s := bufio.NewScanner(stdoutR) for s.Scan() { p.buf.Write(s.Text()) } @@ -164,7 +199,8 @@ func (p *CloudflareTunnelProvider) Start(ctx context.Context) error { scanWg.Add(1) go func() { defer scanWg.Done() - s := bufio.NewScanner(stderrPipe) + defer stderrR.Close() //nolint:errcheck + s := bufio.NewScanner(stderrR) for s.Scan() { p.buf.Write(s.Text()) } diff --git a/backend/internal/orthrus/server.go b/backend/internal/orthrus/server.go index d1a063c5c..0ac63599d 100644 --- a/backend/internal/orthrus/server.go +++ b/backend/internal/orthrus/server.go @@ -113,8 +113,6 @@ func (s *OrthrusServer) HandleWebSocket(c *gin.Context) { } } - s.sessions.Store(agent.UUID, session) - now := time.Now() if err := s.db.Model(agent).Updates(map[string]interface{}{ "status": models.OrthrusStatusOnline, @@ -133,6 +131,8 @@ func (s *OrthrusServer) HandleWebSocket(c *gin.Context) { defer s.wg.Done() s.watchHeartbeat(agent.UUID, session) }() + + s.sessions.Store(agent.UUID, session) } // GetExternalProxyStatus returns the external proxy status for a connected agent. diff --git a/configs/crowdsec/install_hub_items.sh b/configs/crowdsec/install_hub_items.sh index 87c7ee16d..b7687962a 100644 --- a/configs/crowdsec/install_hub_items.sh +++ b/configs/crowdsec/install_hub_items.sh @@ -10,43 +10,56 @@ echo "Installing CrowdSec hub items for Charon..." # Hub index update is handled by the entrypoint before this script is called. # Do not duplicate it here — a redundant update adds ~3s to startup for no benefit. +# Helper: only install if not already present (avoids 5-10s per cscli call on rebuilds) +install_if_missing() { + type="$1" # parsers | scenarios | collections + name="$2" + label="${3:-$name}" + if cscli "${type}" inspect "${name}" >/dev/null 2>&1; then + echo " ✓ ${label} already installed, skipping" + else + echo " Installing ${label}..." + cscli "${type}" install "${name}" || echo "⚠️ Failed to install ${name}" + fi +} + # Install Caddy log parser (if available) # Note: crowdsecurity/caddy-logs may not exist yet - check hub if cscli parsers inspect crowdsecurity/caddy-logs >/dev/null 2>&1; then echo "Installing Caddy log parser..." - cscli parsers install crowdsecurity/caddy-logs --force || echo "⚠️ Failed to install crowdsecurity/caddy-logs" + install_if_missing parsers crowdsecurity/caddy-logs "Caddy log parser" else echo "Caddy-specific parser not available, using HTTP parser..." fi # Install base HTTP parsers (always needed) echo "Installing base parsers..." -cscli parsers install crowdsecurity/http-logs --force || echo "⚠️ Failed to install crowdsecurity/http-logs" -cscli parsers install crowdsecurity/syslog-logs --force || echo "⚠️ Failed to install crowdsecurity/syslog-logs" -timeout 60 cscli parsers install crowdsecurity/geoip-enrich --force || echo "⚠️ Failed or timed out installing crowdsecurity/geoip-enrich (GeoLite2-City.mmdb download may be slow)" -cscli parsers install crowdsecurity/whitelists --force || echo "⚠️ Failed to install crowdsecurity/whitelists" +install_if_missing parsers crowdsecurity/http-logs "http-logs" +install_if_missing parsers crowdsecurity/syslog-logs "syslog-logs" +install_if_missing parsers crowdsecurity/geoip-enrich "geoip-enrich" +install_if_missing parsers crowdsecurity/whitelists "whitelists" # Install HTTP scenarios for attack detection echo "Installing HTTP scenarios..." -cscli scenarios install crowdsecurity/http-probing --force || echo "⚠️ Failed to install crowdsecurity/http-probing" -cscli scenarios install crowdsecurity/http-sensitive-files --force || echo "⚠️ Failed to install crowdsecurity/http-sensitive-files" -cscli scenarios install crowdsecurity/http-backdoors-attempts --force || echo "⚠️ Failed to install crowdsecurity/http-backdoors-attempts" -cscli scenarios install crowdsecurity/http-path-traversal-probing --force || echo "⚠️ Failed to install crowdsecurity/http-path-traversal-probing" -cscli scenarios install crowdsecurity/http-xss-probing --force || echo "⚠️ Failed to install crowdsecurity/http-xss-probing" -cscli scenarios install crowdsecurity/http-sqli-probing --force || echo "⚠️ Failed to install crowdsecurity/http-sqli-probing" -cscli scenarios install crowdsecurity/http-generic-bf --force || echo "⚠️ Failed to install crowdsecurity/http-generic-bf" +install_if_missing scenarios crowdsecurity/http-probing "http-probing" +install_if_missing scenarios crowdsecurity/http-sensitive-files "http-sensitive-files" +install_if_missing scenarios crowdsecurity/http-backdoors-attempts "http-backdoors-attempts" +install_if_missing scenarios crowdsecurity/http-path-traversal-probing "http-path-traversal-probing" +install_if_missing scenarios crowdsecurity/http-xss-probing "http-xss-probing" +install_if_missing scenarios crowdsecurity/http-sqli-probing "http-sqli-probing" +install_if_missing scenarios crowdsecurity/http-generic-bf "http-generic-bf" # Install CVE collection for known vulnerabilities echo "Installing CVE collection..." -cscli collections install crowdsecurity/http-cve --force || echo "⚠️ Failed to install crowdsecurity/http-cve" +install_if_missing collections crowdsecurity/http-cve "http-cve collection" # Install base HTTP collection (bundles common scenarios) echo "Installing base HTTP collection..." -cscli collections install crowdsecurity/base-http-scenarios --force || echo "⚠️ Failed to install crowdsecurity/base-http-scenarios" +install_if_missing collections crowdsecurity/base-http-scenarios "base-http-scenarios collection" # Install Caddy collection (parser + scenarios for Caddy access logs) echo "Installing Caddy collection..." -cscli collections install crowdsecurity/caddy --force || echo "⚠️ Failed to install crowdsecurity/caddy" +install_if_missing collections crowdsecurity/caddy "caddy collection" # Verify installation echo "" diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 73ed04c7c..2d247dc60 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,711 +1,327 @@ -# Plan: Orthrus/Hecate Docs + FeedbackWidget Docs Link - -**Status:** Draft — Pending Review -**Date:** 2025-06 -**Scope:** Documentation authoring (ELI5 feature pages) + Frontend widget enhancement - ---- +# Fix: Add Disk Space Reclamation to Nightly Build Jobs ## 1. Introduction ### Overview -This plan covers two independent, non-blocking deliverables: - -1. **Docs Deliverable** — Write ELI5-level documentation files for two undocumented features (Orthrus and Hecate) and a remote Docker setup guide, fix a broken link, and update index/nav entries to surface these pages. -2. **Widget Deliverable** — Add a "View Documentation" third link to the floating `FeedbackWidget` React component so users can navigate to the docs site directly from anywhere in the UI. +The `build-and-push-nightly` and `build-and-push-nightly-orthrus` jobs in +`.github/workflows/nightly-build.yml` crash with `System.IO.IOException: No space left on +device` during multi-platform Docker builds (`linux/amd64,linux/arm64`). The `ubuntu-latest` +GitHub Actions runner starts with approximately 14 GB of free disk space, but pre-installed +toolchains (Android SDK ~8 GB, .NET ~2 GB, Haskell ~2 GB) consume most of it before any +build step executes. When the disk fills mid-build, the runner process dies without sending +terminal step statuses, leaving GitHub's UI showing the job as simultaneously "failed" and +"in progress". ### Objectives -- Close the broken `features/hecate.md` reference in `docs/features.md` line 226. -- Create `docs/features/orthrus.md` — dedicated ELI5 explainer for the Orthrus tunnel agent. -- Create `docs/features/hecate.md` — ELI5 explainer for the Hecate Tunnel & Pathway Manager. -- Create `docs/guides/remote-docker-setup.md` — step-by-step guide for connecting a remote HomeLab/server via Orthrus. -- Update `docs/index.md` to surface these three new pages. -- Update `docs/features.md` to add an Orthrus entry and fix the Hecate link. -- Add a third "View Docs" link to `FeedbackWidget.tsx` with full i18n and accessibility support. -- Update `frontend/src/components/__tests__/FeedbackWidget.test.tsx` to cover the new link. -- Update `frontend/src/locales/en/translation.json` with the new i18n keys. +1. Reclaim 10–15 GB of disk space on both build jobs before any Docker-related step runs. +2. Insert a single `Free disk space` step as the **first step** in each affected job. +3. Pin the action to commit SHA per the project's existing SHA-pinning convention. +4. Preserve Docker images already present on the runner (`docker-images: false`) so Buildx + can operate normally. --- ## 2. Research Findings -### 2.1 Docs Site Architecture - -- **Framework:** No `mkdocs.yml` was found anywhere in the repository. The docs site is authored as raw Markdown under `docs/` and served via GitHub Pages. -- **Base URL:** `https://wikid82.github.io/Charon/` (from `README.md` line 134). -- **Navigation:** Purely file-system-based relative links; there is no central nav config file to update. -- **Existing docs gaps:** - - `docs/features/hecate.md` — **does not exist** but is linked from `docs/features.md` line 226 — this is an active broken link (bug fix). - - `docs/features/orthrus.md` — does not exist, no link yet. - - `docs/guides/remote-docker-setup.md` — does not exist, no link yet. - -### 2.2 Orthrus System - -- **Package:** `backend/internal/orthrus/` -- **What it is:** A reverse-WebSocket tunnel agent system. An `OrthrusAgent` binary runs on the remote machine, connects outbound via WebSocket to Charon's management interface, and multiplexes streams over yamux. Charon uses these multiplexed streams to talk to Docker on the remote machine. -- **Why it exists:** Remote Docker hosts behind NAT/firewalls cannot accept inbound TCP connections. Orthrus flips the direction — the remote agent dials outward to Charon. -- **Muzzle filter (`muzzle.go`):** Restricts Docker API access to a read-only allowlist (`/containers/json`, `/images/json`, `/_ping`, `/info`, `/version`, `/events`, `/volumes`, `/networks`, `/system/df`). Dynamic read-only patterns: `/containers/*/json`, `/containers/*/logs`, `/containers/*/stats`, `/containers/*/top`. All non-GET methods blocked (except HEAD `/_ping`). HTTP 403 for disallowed paths. -- **Key model fields (`models/orthrus_agent.go`):** - - `UUID`, `Name`, `Status` — `OrthrusStatus`: "online" / "offline" / "pending" - - `AuthKeyHash` — bcrypt hash; `json:"-"` (never exposed); plain key shown once at provisioning, prefixed `ch_orthrus_` - - `Capabilities` — JSON array, e.g. `["docker", "tcp:5432"]` - - `AgentCertPEM` — mTLS cert from Charon's internal CA - - `HecateTunnelUUID` — links agent to a Hecate tunnel provider - - `ResolvedAddress` — cached connectivity address - - `ExternalProxyPort` — TCP port for inter-container Docker API access (0 = disabled) - - `LastHeartbeat`, `LastSeen` -- **Install surfaces (`snippets.go`):** Docker Compose, systemd, tarball, Homebrew, Kubernetes DaemonSet — delivered via `GET /orthrus/agents/:uuid/snippets`. -- **REST API (`orthrus_handler.go`):** - - `GET /management/orthrus/agents` — list agents - - `POST /management/orthrus/agents` — provision (returns one-time auth key) - - `GET /management/orthrus/agents/:uuid` — get one agent - - `PATCH /management/orthrus/agents/:uuid` — update - - `DELETE /management/orthrus/agents/:uuid` — delete - - `POST /management/orthrus/agents/:uuid/revoke` — revoke auth key - - `GET /management/orthrus/agents/:uuid/snippets` — install instructions - - `GET /management/orthrus/agents/:uuid/proxy-status` — live external proxy state -- **WebSocket endpoint:** `GET /api/v1/ws/orthrus/connect` — Bearer token auth (bcrypt), HeartbeatTimeout 10 seconds. -- **`RemoteServer` linkage:** `ConnectionTypeOrthrus = "orthrus"` in `models/remote_server.go`; `OrthrusAgentUUID *string` field links a host config to its agent. - -### 2.3 Hecate System - -- **Package:** `backend/internal/hecate/` -- **What it is:** The Tunnel & Pathway Manager. Manages third-party tunneling providers (Cloudflare, Tailscale, ZeroTier, NetBird) and integrates the Orthrus agent protocol. `TunnelManager` supervises lifecycle of all active tunnel providers with exponential backoff restart (5s → 10s → 30s → 60s). -- **Currently registered provider:** `netbird` (`NewHecateService` in `services/hecate_service.go`). Architecture supports cloudflare, tailscale, zerotier via `RegisterFactory()`. -- **`HecateService`:** CRUD for `TunnelConfig` records; delegates start/stop to `TunnelManager`. Credentials encrypted AES-GCM before DB storage. If `IsActive=true` at creation, tunnel starts immediately. -- **Connection modes (from `docs/features.md`):** - - **Direct** — manual hostname/IP - - **Agent** — pick an Orthrus agent; address resolved from `OrthrusAgent.ResolvedAddress` - - **Provider** — pick a VPN tunnel device directly (no agent required) -- **Relationship to Orthrus:** Each Orthrus agent can be assigned a `HecateTunnelUUID` pointing to a provider tunnel, giving it a `ResolvedAddress`. Remote Servers then use `ConnectionTypeOrthrus`. - -### 2.4 FeedbackWidget - -- **File:** `frontend/src/components/FeedbackWidget.tsx` -- **Current links:** 2 — "Report a Bug" (`GITHUB_BUG_URL`) and "Request a Feature" (`GITHUB_FEATURE_URL`) -- **Structure:** `