-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDockerfile
More file actions
1027 lines (947 loc) · 55.9 KB
/
Copy pathDockerfile
File metadata and controls
1027 lines (947 loc) · 55.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 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
# Set BUILD_DEBUG=1 to build with debug symbols (required for Delve debugging)
ARG BUILD_DEBUG=0
# ---- Prebuilt Caddy + CrowdSec toolchain image ----
# Built by .github/workflows/toolchain-image.yml from the caddy-inline /
# crowdsec-inline stages below (--target toolchain-runtime). Bumped by that
# workflow's bot PR when a security-relevant input moves OR the DAILY
# `--no-cache --pull` rebuild produces a new digest. The freshness-guard CI
# check (scripts/verify-toolchain-pin.sh) fails any PR where TAG/DIGEST is
# stale for the current pins.
ARG CHARON_TOOLCHAIN_IMAGE=ghcr.io/wikid82/charon-toolchain
# NOT Renovate-tracked (a content-hash tag has no series to follow, N7) — the
# toolchain-image.yml bot owns these two lines. DIGEST is the arch-independent
# manifest-list (OCI index) digest, so one pin covers linux/amd64 + linux/arm64.
ARG CHARON_TOOLCHAIN_TAG=caddy-crowdsec-2d54488fed53f771
ARG CHARON_TOOLCHAIN_DIGEST=sha256:aff00920fe9646cbcbc4dc92df0f67784daeb17c4b84f09e08cd4767aeff1c0d
# Stage selector — default consumes the prebuilt toolchain image (no compile).
# Fork PRs / bootstrap / offline builds pass
# --build-arg CADDY_BUILDER_SRC=caddy-inline --build-arg CROWDSEC_BUILDER_SRC=crowdsec-inline
# (e.g. `make build-offline`) to compile from source instead.
ARG CADDY_BUILDER_SRC=toolchain-prebuilt
ARG CROWDSEC_BUILDER_SRC=toolchain-prebuilt
# ---- Pinned Toolchain Versions ----
# renovate: datasource=docker depName=golang versioning=docker
ARG GO_VERSION=1.27.1
# renovate: datasource=docker depName=alpine versioning=docker
ARG ALPINE_IMAGE=alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
# ---- Shared CrowdSec Version ----
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.8.1
# ---- Shared Go Security Patches ----
# renovate: datasource=github-tags depName=expr-lang/expr extractVersion=^v(?<version>.+)$
ARG EXPR_LANG_VERSION=1.17.8
# renovate: datasource=go depName=golang.org/x/net
ARG XNET_VERSION=0.59.0
# Shared golang.org/x/crypto pin — consumed by BOTH the caddy-builder and the
# crowdsec-builder stages so the two never drift. v0.56.0 also carries the
# golang.org/x/crypto/ssh channel-flood deadlock DoS fixes (GO-2026-6354, GO-2026-6355).
# renovate: datasource=go depName=golang.org/x/crypto
ARG XCRYPTO_VERSION=0.57.0
# klauspost/compress DoS/resource-exhaustion fix, matching how golang.org/x/crypto
# is patched above: pinned here so the CrowdSec/cscli and Caddy binaries (which
# pull it in transitively) are patched immediately, ahead of upstream releases.
# renovate: datasource=go depName=github.com/klauspost/compress
ARG KLAUSPOST_COMPRESS_VERSION=1.20.0
# grpc-go HTTP/2 DATA-frame memory-exhaustion DoS fix (CVE-2026-84304), matching how
# golang.org/x/crypto and klauspost/compress are patched above: pinned here so the Caddy
# and CrowdSec/cscli binaries (which pull it in transitively) are patched immediately,
# ahead of upstream releases.
# renovate: datasource=go depName=google.golang.org/grpc
ARG GRPC_VERSION=1.83.2
# renovate: datasource=npm depName=npm
ARG NPM_VERSION=12.0.2
# 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.11.4 build.
# renovate: datasource=go depName=github.com/caddyserver/caddy/v2
ARG CADDY_VERSION=2.11.4
# renovate: datasource=go depName=github.com/caddyserver/caddy/v2
ARG CADDY_CANDIDATE_VERSION=2.11.4
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.64
# renovate: datasource=go depName=github.com/corazawaf/coraza-caddy/v2
ARG CORAZA_CADDY_VERSION=2.6.1
# xcaddy plugins that previously resolved "latest" at build time (B4). Pinned so
# a toolchain-key.sh input moves when the plugin does. All values are bare
# (no leading `v`); the `--with` lines add the `v`. Renovate tracks each via
# the datasource=go marker.
# renovate: datasource=go depName=github.com/zhangjiayin/caddy-geoip2
ARG CADDY_GEOIP2_VERSION=1.3.0
# renovate: datasource=go depName=github.com/mholt/caddy-ratelimit
ARG CADDY_RATELIMIT_VERSION=0.1.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
## xcaddy-built binary in the later COPY step. This avoids relying on
## upstream caddy image tags while still shipping a pinned caddy binary.
## Alpine 3.23 base to reduce glibc CVE exposure and image size.
# ---- Cross-Compilation Helpers ----
# renovate: datasource=docker depName=tonistiigi/xx
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
# ---- Gosu Builder ----
# Build gosu from source to avoid CVEs from Debian's pre-compiled version (Go 1.19.8)
# This fixes 22 HIGH/CRITICAL CVEs in stdlib embedded in Debian's gosu package
# CVEs fixed: CVE-2023-24531, CVE-2023-24540, CVE-2023-29402, CVE-2023-29404,
# CVE-2023-29405, CVE-2024-24790, CVE-2025-22871, and 15 more
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS gosu-builder
COPY --from=xx / /
WORKDIR /tmp/gosu
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
# renovate: datasource=github-releases depName=tianon/gosu
ARG GOSU_VERSION=1.17
# hadolint ignore=DL3018
RUN apk add --no-cache git clang lld
# hadolint ignore=DL3059
# hadolint ignore=DL3018
# Install both musl-dev (headers) and musl (runtime library) for cross-compilation linker
RUN xx-apk add --no-cache gcc musl-dev musl
# Clone and build gosu from source with modern Go
# Retry: survive transient network failures during clone.
RUN for _attempt in 1 2 3; do \
git clone --depth 1 --branch "${GOSU_VERSION}" https://github.com/tianon/gosu.git . && break; \
[ "${_attempt}" -lt 3 ] || exit 1; \
echo "git clone gosu attempt ${_attempt}/3 failed; retrying in $((_attempt * 15))s..." >&2; \
rm -rf ./.git ./*; \
sleep $((_attempt * 15)); \
done
# Pin golang.org/x/sys to a patched version for scanner hygiene (GO-2026-5024 / CVE-2026-39824).
# Upstream tianon/gosu@1.17's own go.sum resolves golang.org/x/sys to v0.13.0, which Grype/GitHub
# code scanning flags. The vulnerable code (NewNTUnicodeString overflow) lives only in
# golang.org/x/sys/windows; gosu is Unix-only and this stage only cross-compiles Linux targets
# (CGO_ENABLED=0 xx-go build below), so the flagged code path is never compiled into this binary.
# Fixed regardless, since it's cheap and keeps the scanner quiet. Pinned to v0.46.0 (above the
# advisory's v0.44.0 fix floor) to match the same x/sys version already used by the Delve debug
# stage below (Dockerfile:199) for this identical advisory. Do NOT bump GOSU_VERSION instead:
# upstream tag 1.19's go.mod actually requires an OLDER golang.org/x/sys v0.1.0.
RUN for _attempt in 1 2 3; do \
go get golang.org/x/sys@v0.46.0 && go mod tidy && go mod verify && break; \
[ "${_attempt}" -lt 3 ] || exit 1; \
echo "golang.org/x/sys pin attempt ${_attempt}/3 failed; retrying in $((_attempt * 15))s..." >&2; \
sleep $((_attempt * 15)); \
done
# Build gosu for target architecture with patched Go stdlib
# hadolint ignore=DL3059
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 xx-go build -v -ldflags '-s -w' -o /gosu-out/gosu . && \
xx-verify /gosu-out/gosu
# ---- Frontend Builder ----
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
# renovate: datasource=docker depName=node
FROM --platform=$BUILDPLATFORM node:24.21.0-alpine3.24@sha256:be80f76cf40ec8e42b9bec49f60a55e0660f30af58d3e5a25530785b30ea67e2 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}
# Vite 8: Rolldown native bindings auto-resolved per platform via optionalDependencies
ARG NPM_VERSION
# hadolint ignore=DL3017
RUN apk upgrade --no-cache && \
npm install -g npm@${NPM_VERSION} --no-fund --no-audit \
--fetch-retries=5 --fetch-retry-mintimeout=10000 && \
npm cache clean --force
# Patch CVE-2026-33671: picomatch ReDoS (fixed in 4.0.4) — bundled in Node.js 24.15.0 npm toolchain.
# Remove when a patched Node.js 24 image is available.
# hadolint ignore=DL3059
RUN npm install -g picomatch@4.0.4 --no-fund --no-audit \
--fetch-retries=5 --fetch-retry-mintimeout=10000
# Patch CVE-2026-12151: undici DoS via unbounded memory (fixed in 6.27.0) — bundled in Node.js 24.17.0 npm.
# Remove when a patched Node.js 24 image ships undici >=6.27.0.
# hadolint ignore=DL3059
RUN npm install -g undici@6.27.0 --no-fund --no-audit \
--fetch-retries=5 --fetch-retry-mintimeout=10000
RUN npm ci --ignore-scripts --fetch-retries=5 --fetch-retry-mintimeout=10000
# Copy frontend source and build.
# Sync package.json to the release version for semver builds; skip for
# non-semver values like "dev" or branch names, which npm version rejects.
COPY frontend/ ./
RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
if echo "${VERSION}" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+'; then \
npm version "${VERSION#v}" --no-git-tag-version --allow-same-version; \
fi && \
npm run build
# ---- Backend Builder ----
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS backend-builder
# Copy xx helpers for cross-compilation
COPY --from=xx / /
WORKDIR /app/backend
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
# Install build dependencies
# xx-apk installs packages for the TARGET architecture
ARG TARGETPLATFORM
ARG TARGETARCH
# hadolint ignore=DL3018
RUN apk add --no-cache git clang lld
# hadolint ignore=DL3059
# hadolint ignore=DL3018
# Install musl (headers + runtime) and gcc for cross-compilation linker
# The musl runtime library and gcc crt/libgcc are required by the linker
RUN xx-apk add --no-cache gcc musl-dev musl sqlite-dev
# Ensure the ARM64 musl loader exists for qemu-aarch64 cross-linking
# Without this, the linker fails with: qemu-aarch64: Could not open '/lib/ld-musl-aarch64.so.1'
RUN set -eux; \
if [ "$TARGETARCH" = "arm64" ]; then \
LOADER="/lib/ld-musl-aarch64.so.1"; \
LOADER_PATH="$LOADER"; \
if [ ! -e "$LOADER" ]; then \
FOUND="$(find / -path '*/ld-musl-aarch64.so.1' -type f 2>/dev/null | head -n 1)"; \
if [ -n "$FOUND" ]; then \
mkdir -p /lib; \
ln -sf "$FOUND" "$LOADER"; \
LOADER_PATH="$FOUND"; \
fi; \
fi; \
echo "Using musl loader at: $LOADER_PATH"; \
test -e "$LOADER"; \
fi
# Install Delve (cross-compile for target) — debug builds only.
# Security: dlv is only installed when BUILD_DEBUG=1. Production images (BUILD_DEBUG=0,
# the default) receive a harmless stub so the unconditional COPY below still succeeds,
# but no Delve binary with an unpatched golang.org/x/sys/windows (NewNTUnicodeString
# string-length overflow, CVE-2026-39824 / GO-2026-5024, fixed in v0.44.0+) is shipped.
# When dlv IS needed, we build it inside a temporary module that pins
# golang.org/x/sys to the patched version used by the rest of the project.
# renovate: datasource=go depName=github.com/go-delve/delve
ARG DLV_VERSION=1.27.2
# hadolint ignore=DL3059,DL4006
RUN if [ "$BUILD_DEBUG" = "1" ]; then \
echo "DEBUG build: installing Delve v${DLV_VERSION} with patched golang.org/x/sys..."; \
mkdir -p /tmp/dlv-install && cd /tmp/dlv-install && \
go mod init dlv_install && \
go get golang.org/x/sys@v0.46.0 && \
CGO_ENABLED=0 GOFLAGS="-mod=mod" xx-go install github.com/go-delve/delve/cmd/dlv@v${DLV_VERSION} && \
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 && \
cd / && rm -rf /tmp/dlv-install; \
else \
echo "Production build: skipping Delve install (GO-2026-5024 mitigation)"; \
printf '#!/bin/sh\necho "Delve not available in production builds. Rebuild with BUILD_DEBUG=1." >&2\nexit 1\n' \
> /go/bin/dlv && chmod +x /go/bin/dlv; \
fi
# Copy Go module files
COPY backend/go.mod backend/go.sum ./
# Retry: survive transient module proxy failures (e.g. stream INTERNAL_ERROR).
RUN --mount=type=cache,target=/go/pkg/mod \
for _attempt in 1 2 3; do \
go mod download && break; \
[ "${_attempt}" -lt 3 ] || exit 1; \
echo "go mod download attempt ${_attempt}/3 failed; retrying in $((_attempt * 15))s..." >&2; \
sleep $((_attempt * 15)); \
done
# Copy backend source
COPY backend/ ./
# Build arguments passed from main build context
ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE=unknown
ARG BUILD_DEBUG=0
# Build the Go binary with version information injected via ldflags
# xx-go handles CGO and cross-compilation flags automatically
# Note: Go 1.26 defaults to gold linker for ARM64, but clang doesn't support -fuse-ld=gold
# Use lld for ARM64 cross-linking; keep bfd for amd64 to preserve prior behavior
# PIE is required for arm64 cross-linking with lld to avoid relocation conflicts under
# QEMU emulation and improves security posture.
# When BUILD_DEBUG=1, we preserve debug symbols (no -s -w) and disable optimizations
# for Delve debugging. Otherwise, strip symbols for smaller production binaries.
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
EXT_LD_FLAGS="-fuse-ld=bfd"; \
BUILD_MODE=""; \
if [ "$TARGETARCH" = "arm64" ]; then \
EXT_LD_FLAGS="-fuse-ld=lld"; \
BUILD_MODE="-buildmode=pie"; \
fi; \
if [ "$BUILD_DEBUG" = "1" ]; then \
echo "Building with debug symbols for Delve..."; \
CGO_ENABLED=1 CC=xx-clang CXX=xx-clang++ xx-go build ${BUILD_MODE} \
-gcflags="all=-N -l" \
-ldflags "-extldflags=${EXT_LD_FLAGS} \
-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; \
else \
echo "Building optimized production binary..."; \
CGO_ENABLED=1 CC=xx-clang CXX=xx-clang++ xx-go build ${BUILD_MODE} \
-ldflags "-s -w -extldflags=${EXT_LD_FLAGS} \
-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; \
fi
# ---- Caddy Builder (inline / from-source) ----
# 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)
#
# This stage is the single source of truth for the Caddy build recipe. On the
# default app-build path its output is NOT recompiled — it is COPY --from'd out
# of the digest-pinned toolchain image (see the toolchain-prebuilt / caddy-builder
# selector stages further down). It is compiled here only by
# .github/workflows/toolchain-image.yml (--target toolchain-runtime) and on the
# fork / bootstrap / offline fallback path.
#
# N4: the golang:${GO_VERSION}-alpine tag is a moving reference; digest-pin it so
# a silent upstream base rebuild is caught by toolchain-key.sh. The pinned digest
# is refreshed by the daily toolchain rebuild's `--pull` + Renovate.
# renovate: datasource=docker depName=golang
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine@sha256:cf6fca6641884b8433441b2b0652976f975e1d0fdd26d177eaaf8596087f3125 AS caddy-inline
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION
ARG CADDY_CANDIDATE_VERSION
ARG CADDY_USE_CANDIDATE
ARG CADDY_PATCH_SCENARIO
ARG CADDY_SECURITY_VERSION
ARG CORAZA_CADDY_VERSION
ARG CADDY_GEOIP2_VERSION
ARG CADDY_RATELIMIT_VERSION
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
ARG XCADDY_VERSION=0.4.7
ARG EXPR_LANG_VERSION
ARG XNET_VERSION
ARG XCRYPTO_VERSION
ARG KLAUSPOST_COMPRESS_VERSION
ARG GRPC_VERSION
ARG CROWDSEC_VERSION
# hadolint ignore=DL3018
RUN apk add --no-cache bash git
# hadolint ignore=DL3062
# Retry: survive transient module proxy / sum.golang.org network failures.
RUN --mount=type=cache,target=/go/pkg/mod \
for _attempt in 1 2 3; do \
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v${XCADDY_VERSION} && break; \
[ "${_attempt}" -lt 3 ] || exit 1; \
echo "go install xcaddy attempt ${_attempt}/3 failed; retrying in $((_attempt * 15))s..." >&2; \
sleep $((_attempt * 15)); \
done
# 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.
# NOTE: Keep patching deterministic and explicit. Avoid silent fallbacks.
# hadolint ignore=SC2016
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
bash -c 'set -e; \
# Retry helper: survive transient network failures (module proxy or
# sum.golang.org timeouts) without weakening checksum verification.
# 3 attempts with incremental backoff (15s, 30s).
_retry() { \
local _attempt; \
for _attempt in 1 2 3; do \
"$@" && return 0; \
if [ "${_attempt}" -lt 3 ]; then \
echo "Attempt ${_attempt}/3 failed: $*; retrying in $((_attempt * 15))s..." >&2; \
sleep $((_attempt * 15)); \
fi; \
done; \
echo "ERROR: command failed after 3 attempts: $*" >&2; \
return 1; \
}; \
# Defensively undo a stale IPEquals forward-patch that a PRE-2026-09
# Dockerfile revision may have left in the shared module cache (the live
# patch now lives on a /tmp copy, see the bouncer block below). Guarded on
# the bad form actually being present so steady-state builds never write
# the shared cache here — BuildKit runs the two arches concurrently over a
# shared `--mount=type=cache,target=/go/pkg/mod`, and an unconditional
# `sed -i` would race xcaddy Stage 1 on the other arch.
_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}" ] && grep -q "IPEquals: value," "${_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}"; \
fi; \
# NOTE: the cel-go v0.29 celmatcher.go patch is NOT applied to the shared
# module cache (that raced the other arch's xcaddy Stage 1 — see Stage 3).
# It is applied to a local copy + a go.mod `replace`, so nothing here needs
# to reverse an in-cache forward-patch.
echo "Using Caddy target version: v${CADDY_TARGET_VERSION}"; \
echo "Using Caddy patch scenario: ${CADDY_PATCH_SCENARIO}"; \
export XCADDY_SKIP_CLEANUP=1; \
echo "Stage 1: Generate go.mod with xcaddy..."; \
# Run xcaddy to generate the build directory and go.mod
_retry env GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
--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.12.1 \
--with github.com/zhangjiayin/caddy-geoip2@v${CADDY_GEOIP2_VERSION} \
--with github.com/mholt/caddy-ratelimit@v${CADDY_RATELIMIT_VERSION} \
--output /tmp/caddy-initial; \
# 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
_retry go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION}; \
# renovate: datasource=go depName=github.com/hslatman/ipstore
_retry go get github.com/hslatman/ipstore@v0.4.0; \
_retry go get golang.org/x/crypto@v${XCRYPTO_VERSION}; \
_retry go get golang.org/x/net@v${XNET_VERSION}; \
# klauspost/compress DoS/resource-exhaustion fix. Affects /usr/bin/caddy
# (transitive dependency). Fix available at v1.18.7.
_retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \
# grpc-go HTTP/2 DATA-frame memory-exhaustion DoS (CVE-2026-84304), plus the
# earlier GHSA-hrxh-6v49-42gf xDS RBAC / HTTP/2 fixes. Affects /usr/bin/caddy
# (transitive dependency). Fixed at v1.83.1.
_retry go get google.golang.org/grpc@v${GRPC_VERSION}; \
# CVE-2026-34986: go-jose JOSE/JWT validation bypass
# renovate: datasource=go depName=github.com/go-jose/go-jose/v3
_retry go get github.com/go-jose/go-jose/v3@v3.0.5; \
# renovate: datasource=go depName=github.com/go-jose/go-jose/v4
_retry go get github.com/go-jose/go-jose/v4@v4.1.4; \
# CVE-2026-39883: OTel SDK resource leak
# renovate: datasource=go depName=go.opentelemetry.io/otel/sdk
_retry go get go.opentelemetry.io/otel/sdk@v1.43.0; \
# CVE-2026-39882: OTel HTTP exporter request smuggling
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
_retry go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@v0.19.0; \
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
_retry go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp@v1.43.0; \
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
_retry go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.43.0; \
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
# Fix available at v1.6.0. Pin here so the Caddy binary is patched immediately;
# remove once caddy-security ships a release built with goxmldsig >= v1.6.0.
# renovate: datasource=go depName=github.com/russellhaering/goxmldsig
_retry go get github.com/russellhaering/goxmldsig@v1.6.0; \
# CVE-2026-32952: go-ntlmssp DoS via malicious NTLM challenge response
# Affects /usr/bin/caddy (transitive dependency). Fix available at v0.1.1.
# renovate: datasource=go depName=github.com/Azure/go-ntlmssp
_retry go get github.com/Azure/go-ntlmssp@v0.1.1; \
# buger/jsonparser Delete() panic via negative slice index on malformed JSON.
# Affects /usr/bin/caddy (transitive via caddy-crowdsec-bouncer -> crowdsec). Fix available at v1.2.0.
# renovate: datasource=go depName=github.com/buger/jsonparser
_retry go get github.com/buger/jsonparser@v1.2.0; \
# GHSA-gcjh-h69q-9w9g (MEDIUM, /usr/bin/caddy): cel-go is pinned to the fixed
# v0.29.2 here, AND Caddy v2.11.4's modules/caddyhttp/celmatcher.go is source-patched
# on a local copy of the Caddy module + a go.mod `replace` (Stage 3 below) with the
# matching 2-line []interpreter.Interpretable -> []interpreter.InterpretableV2 change.
# Together this replicates upstream Caddy commit b2693fb / PR #7872 ("bump cel-go from
# v0.28.1 to v0.29.2"), which is not yet in any tagged Caddy release. Remove this pin
# and the Stage 3 celmatcher.go source patch (local copy + replace) once
# CADDY_VERSION >= 2.11.5, the first release expected to contain b2693fb.
# renovate: datasource=go depName=github.com/google/cel-go
_retry go get github.com/google/cel-go@v0.29.2; \
# 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.
_retry go get github.com/crowdsecurity/crowdsec@v${CROWDSEC_VERSION}; \
# CVE-2026-56864 / CVE-2026-56865: golang.org/x/mod/sumdb GOSUMDB tile-verification bypass
# (a colluding GOPROXY+GOSUMDB pair could forge sumdb tiles / serve module content outside
# the transparency log). Affects /usr/bin/caddy — go mod tidy's MVS resolution otherwise
# lands on an older, vulnerable version. Fix available at v0.40.0. Same pattern as the
# crowdsec-builder pin below.
# renovate: datasource=go depName=golang.org/x/mod
_retry go get golang.org/x/mod@v0.40.0; \
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
# uses legacy nebula APIs removed in nebula v1.10+, which causes compile
# failures in authority/provisioner. Keep this pinned to a known-compatible
# v1.9.x release until upstream stack supports nebula v1.10+.
# renovate: datasource=go depName=github.com/slackhq/nebula
_retry go get github.com/slackhq/nebula@v1.9.7; \
elif [ "${CADDY_PATCH_SCENARIO}" = "B" ] || [ "${CADDY_PATCH_SCENARIO}" = "C" ]; then \
# Default PR-2 posture: retire explicit nebula pin and use upstream resolution.
echo "Skipping nebula pin for scenario ${CADDY_PATCH_SCENARIO}"; \
else \
echo "Unsupported CADDY_PATCH_SCENARIO=${CADDY_PATCH_SCENARIO}"; \
exit 1; \
fi; \
# Final re-pin: enforce requested Caddy core version after plugin/security updates.
_retry go get github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION}; \
# Final re-pin: grpc-go (CVE-2026-84304). MUST come after the OpenTelemetry
# go.get block above: `go get .../otlp*http@v0.19.0 / v1.43.0` is a *downgrade*,
# and go get's downgrade cascade drags google.golang.org/grpc back down to the
# v1.83.0-dev that otel v1.43.0 requires (v1.83.1 => v1.83.0-dev => v1.83.0 in
# the build log), silently undoing the earlier pin. Re-pinning here — with grpc
# named on the command line so it is held fixed — and letting `go mod tidy`
# settle MVS keeps the shipped /usr/bin/caddy on the fixed v1.83.1. Same
# "final re-pin after plugin updates" pattern as the Caddy-core line above.
_retry go get google.golang.org/grpc@v${GRPC_VERSION}; \
# Clean up go.mod and ensure all dependencies are resolved
_retry 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).
_retry 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 \
echo "ERROR: Resolved Caddy version ${ACTUAL_CADDY_VERSION} does not match target v${CADDY_TARGET_VERSION}"; \
exit 1; \
fi; \
echo "Verified Caddy module version: ${ACTUAL_CADDY_VERSION}"; \
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..."; \
# GHSA-gcjh-h69q-9w9g: with cel-go now resolved to v0.29.2 (pinned above),
# forward-patch Caddy v2.11.4's celmatcher.go to the v0.29 interpreter.NewCall
# signature ([]interpreter.InterpretableV2) — the exact 2-line change from upstream
# Caddy commit b2693fb / PR #7872. reqAttr's concrete type already satisfies
# interpreter.InterpretableV2 in cel-go v0.29.2.
#
# The patch is applied to a LOCAL COPY of the Caddy module + a go.mod `replace`,
# never to the shared BuildKit module cache (${_GOMC}). BuildKit builds
# linux/amd64 and linux/arm64 concurrently over a shared
# `--mount=type=cache,target=/go/pkg/mod`; an in-cache `sed -i` here raced the
# other arch's xcaddy Stage 1 (still compiling Caddy against its native cel-go
# v0.28.1) and broke it with `undefined: interpreter.InterpretableV2`. This is the
# same local-copy + `go mod edit -replace` pattern used for caddy-crowdsec-bouncer
# / go-cs-bouncer above: order-independent and arch-isolated (/tmp is per-arch,
# the module cache is read-only).
_retry go mod download github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION}; \
CADDY_CACHE="${_GOMC}/github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION}"; \
CADDY_LOCAL="/tmp/caddy-patched"; \
rm -rf "${CADDY_LOCAL}"; \
cp -r "${CADDY_CACHE}/." "${CADDY_LOCAL}/"; \
chmod -R +w "${CADDY_LOCAL}"; \
CELM="${CADDY_LOCAL}/modules/caddyhttp/celmatcher.go"; \
if [ ! -f "$CELM" ]; then \
echo "ERROR: celmatcher.go not found at $CELM"; exit 1; \
fi; \
sed -i "s#\[\]interpreter\.Interpretable{reqAttr}#[]interpreter.InterpretableV2{reqAttr}#g" "$CELM"; \
grep -qF "InterpretableV2{reqAttr}" "$CELM" || { echo "ERROR: celmatcher.go cel-go v0.29 patch did not apply"; exit 1; }; \
if grep -qF "[]interpreter.Interpretable{reqAttr}" "$CELM"; then \
echo "ERROR: celmatcher.go still contains the pre-patch cel-go v0.28 form"; exit 1; \
fi; \
go mod edit -replace "github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION}=${CADDY_LOCAL}"; \
echo "Patched Caddy celmatcher.go for cel-go v0.29 InterpretableV2 API (local replace -> ${CADDY_LOCAL})"; \
# 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"; \
# Assert the shipped binary embeds the fixed cel-go (GHSA-gcjh-h69q-9w9g).
go version -m /usr/bin/caddy | grep -E "github.com/google/cel-go[[:space:]]+v0\.29\." || { echo "ERROR: /usr/bin/caddy did not embed cel-go v0.29.x"; exit 1; }; \
echo "Verified /usr/bin/caddy embeds cel-go v0.29.x"; \
# Assert the shipped binary embeds the fixed grpc-go (CVE-2026-84304). The
# OpenTelemetry downgrade block is prone to dragging grpc back to v1.83.0; fail
# the build loudly rather than ship a silently-regressed binary.
go version -m /usr/bin/caddy | grep -E "google\.golang\.org/grpc[[:space:]]+v${GRPC_VERSION}[[:space:]]" || { echo "ERROR: /usr/bin/caddy did not embed grpc-go v${GRPC_VERSION} (CVE-2026-84304)"; go version -m /usr/bin/caddy | grep "google.golang.org/grpc" || true; exit 1; }; \
echo "Verified /usr/bin/caddy embeds grpc-go v${GRPC_VERSION}"; \
# Clean up temporary build directories
rm -rf /tmp/buildenv_* /tmp/caddy-initial'
# ---- CrowdSec Builder (inline / from-source) ----
# Build CrowdSec from source to ensure we use Go 1.26.3+ and avoid stdlib vulnerabilities
# (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729)
#
# Like caddy-inline, this is the single source of truth for the CrowdSec build
# recipe. Compiled by toolchain-image.yml and the fork/offline fallback only; the
# default app build COPY --from's its output out of the pinned toolchain image.
# renovate: datasource=docker depName=golang
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine@sha256:cf6fca6641884b8433441b2b0652976f975e1d0fdd26d177eaaf8596087f3125 AS crowdsec-inline
COPY --from=xx / /
WORKDIR /tmp/crowdsec
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG CROWDSEC_VERSION
ARG EXPR_LANG_VERSION
ARG XNET_VERSION
ARG XCRYPTO_VERSION
ARG KLAUSPOST_COMPRESS_VERSION
ARG GRPC_VERSION
# hadolint ignore=DL3018
RUN apk add --no-cache git clang lld
# hadolint ignore=DL3059
# hadolint ignore=DL3018
# Install both musl-dev (headers) and musl (runtime library) for cross-compilation linker
RUN xx-apk add --no-cache gcc musl-dev musl
# Clone CrowdSec source
# Retry: survive transient network failures during clone.
RUN for _attempt in 1 2 3; do \
git clone --depth 1 --branch "v${CROWDSEC_VERSION}" https://github.com/crowdsecurity/crowdsec.git . && break; \
[ "${_attempt}" -lt 3 ] || exit 1; \
echo "git clone crowdsec attempt ${_attempt}/3 failed; retrying in $((_attempt * 15))s..." >&2; \
rm -rf ./.git ./*; \
sleep $((_attempt * 15)); \
done
# Patch dependencies to fix CVEs in transitive dependencies
# This follows the same pattern as Caddy's dependency patches
# Each fetch is retried to survive transient module proxy / sum.golang.org
# network failures without weakening checksum verification.
RUN set -e; \
_retry() { \
for _attempt in 1 2 3; do \
"$@" && return 0; \
if [ "${_attempt}" -lt 3 ]; then \
echo "Attempt ${_attempt}/3 failed: $*; retrying in $((_attempt * 15))s..." >&2; \
sleep $((_attempt * 15)); \
fi; \
done; \
echo "ERROR: command failed after 3 attempts: $*" >&2; \
return 1; \
}; \
_retry go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION}; \
# golang.org/x/crypto/ssh channel-flood deadlock DoS (GO-2026-6354 / CVE-2026-78662
# and GO-2026-6355 / CVE-2026-56855). Affects /usr/local/bin/crowdsec and
# /usr/local/bin/cscli (transitive dependency). Fixed at v0.56.0. Pinned via the
# shared XCRYPTO_VERSION build-arg so this stays aligned with the caddy-builder pin
# instead of drifting behind on a hard-coded literal.
# renovate: datasource=go depName=golang.org/x/crypto
_retry go get golang.org/x/crypto@v${XCRYPTO_VERSION}; \
_retry go get golang.org/x/net@v${XNET_VERSION}; \
# klauspost/compress DoS/resource-exhaustion fix. Affects /usr/local/bin/crowdsec
# and /usr/local/bin/cscli (transitive dependency). Fix available at v1.18.7.
_retry go get github.com/klauspost/compress@v${KLAUSPOST_COMPRESS_VERSION}; \
# grpc-go HTTP/2 DATA-frame memory-exhaustion DoS (CVE-2026-84304), plus the earlier
# GHSA-hrxh-6v49-42gf xDS RBAC / HTTP/2 fixes. Affects /usr/local/bin/crowdsec and
# /usr/local/bin/cscli (transitive dependency). Fixed at v1.83.1.
_retry go get google.golang.org/grpc@v${GRPC_VERSION}; \
# CVE-2026-32286: pgproto3/v2 buffer overflow (no v2 fix exists; bump pgx/v4 to latest patch)
# renovate: datasource=github-tags depName=jackc/pgx
_retry go get github.com/jackc/pgx/v4@v4.18.3; \
# CVE-2026-29181 (GHSA-mh2q-q3fh-2475): OpenTelemetry-Go baggage header multi-value DoS
# go.opentelemetry.io/otel >= 1.36.0 and <= 1.40.0 is vulnerable; fix available at v1.41.0.
# 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
_retry 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
_retry go get github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.14; \
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs
_retry go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.78.2; \
_retry go get github.com/aws/aws-sdk-go-v2/service/kinesis@v1.43.7; \
_retry 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
_retry go get github.com/Azure/go-ntlmssp@v0.1.1; \
# CVE-2026-40898 (GHSA-vvgj-x9jq-8cj9): quic-go HTTP/3 QPACK Trailer Expansion Memory Exhaustion.
# Affects /usr/local/bin/crowdsec and /usr/local/bin/cscli (CrowdSec embeds quic-go v0.57.0).
# Fix available at v0.59.1. Caddy already resolves v0.59.1 through its own graph.
# renovate: datasource=go depName=github.com/quic-go/quic-go
_retry go get github.com/quic-go/quic-go@v0.60.0; \
# buger/jsonparser Delete() panic via negative slice index on malformed JSON.
# Fix available at v1.2.0.
# renovate: datasource=go depName=github.com/buger/jsonparser
_retry go get github.com/buger/jsonparser@v1.2.0; \
# kin-openapi: CrowdSec v1.8.0 already ships v0.147.0 natively (its go.mod baseline),
# which is past the GHSA-r277-6w6q-xmqw (ValidationHandler.Load() fail-open auth bypass,
# fixed v0.144.0) and GHSA-jpcw-4wr7-c3vq / CVE-2026-73502 (DoS panic) fixes. This explicit
# pin is a defense-in-depth floor and a Renovate anchor so an accidental MVS downgrade or a
# CrowdSec version regression cannot reintroduce a pre-v0.147.0 (vulnerable) resolution.
# renovate: datasource=go depName=github.com/getkin/kin-openapi
_retry go get github.com/getkin/kin-openapi@v0.147.0; \
# CVE-2026-56864 / CVE-2026-56865: golang.org/x/mod/sumdb GOSUMDB tile-verification bypass
# (a colluding GOPROXY+GOSUMDB pair could forge sumdb tiles / serve module content outside
# the transparency log). Affects /usr/local/bin/crowdsec and /usr/local/bin/cscli — go mod
# tidy's MVS resolution otherwise lands on v0.38.0. Fix available at v0.40.0.
# renovate: datasource=go depName=golang.org/x/mod
_retry go get golang.org/x/mod@v0.40.0; \
_retry go mod tidy
# Fix compatibility issues with expr-lang v1.17.7
# In v1.17.7, program.Source() returns file.Source struct instead of string
# The upstream fix is in main branch but not yet released
RUN sed -i 's/string(program\.Source())/program.Source().String()/g' pkg/exprhelpers/debugger.go
# Build CrowdSec binaries for target architecture with patched dependencies
# 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
# ---- Toolchain image assembly target (built by toolchain-image.yml) ----
# NOT part of the app build graph — nothing FROMs it here. `docker buildx build
# --target toolchain-runtime` produces the publishable multi-arch image that the
# default app build then COPY --from's. The binaries land at the SAME paths the
# inline stages produce, so the final-stage COPY --from lines need no change.
FROM ${ALPINE_IMAGE} AS toolchain-runtime
ARG CHARON_TOOLCHAIN_TAG
COPY --from=caddy-inline /usr/bin/caddy /usr/bin/caddy
COPY --from=crowdsec-inline /crowdsec-out/crowdsec /crowdsec-out/crowdsec
COPY --from=crowdsec-inline /crowdsec-out/cscli /crowdsec-out/cscli
COPY --from=crowdsec-inline /crowdsec-out/config /crowdsec-out/config
# Provenance: `docker inspect` on the toolchain image shows the content key;
# image.source links the GHCR package to the repo so same-repo CI can pull it.
LABEL io.charon.toolchain.key="${CHARON_TOOLCHAIN_TAG}" \
org.opencontainers.image.source="https://github.com/Wikid82/charon"
# ---- Prebuilt toolchain (default source for caddy-builder / crowdsec-builder) ----
# Digest-pinned OCI index; BuildKit auto-selects the child matching $TARGETPLATFORM.
# Contains /usr/bin/caddy and /crowdsec-out/{crowdsec,cscli,config} at the SAME
# paths the inline stages produce, so the final-stage COPY --from lines are
# unchanged. Pruned from the graph (never pulled) when *_BUILDER_SRC=*-inline.
FROM ${CHARON_TOOLCHAIN_IMAGE}@${CHARON_TOOLCHAIN_DIGEST} AS toolchain-prebuilt
# ---- Effective builder stages: prebuilt image OR inline compile ----
# `FROM ${ARG} AS name` where the ARG resolves to a prior stage name is valid
# BuildKit; the unreferenced alternative is pruned and never built/pulled.
# On the default (prebuilt) path caddy-inline / crowdsec-inline are not in the
# graph, so the retargeted `--no-cache-filter caddy-inline,crowdsec-inline` in CI
# is a no-op there and live only on the fork/offline inline path (B5). The
# pin<->digest binding on the default path is enforced by
# scripts/verify-toolchain-pin.sh (wired as a required check in Commit 3).
FROM ${CADDY_BUILDER_SRC} AS caddy-builder
FROM ${CROWDSEC_BUILDER_SRC} AS crowdsec-builder
# ---- Final Runtime with Caddy ----
FROM ${ALPINE_IMAGE}
WORKDIR /app
# Install runtime dependencies for Charon, including bash for maintenance scripts
# Note: gosu is now built from source (see gosu-builder stage) to avoid CVEs from Debian's pre-compiled version
# Explicitly upgrade packages to fix security vulnerabilities
# hadolint ignore=DL3018
RUN apk add --no-cache \
bash ca-certificates sqlite-libs sqlite tzdata gettext libcap libcap-utils \
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 \
# 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
RUN chmod +x /usr/sbin/gosu
# 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 -S charon && \
adduser -u 1000 -S -G charon -h /app -s /sbin/nologin charon
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
# Download MaxMind GeoLite2 Country database
# 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=9616e64848d17f13f1ae5426f40b9f4d6022cef0e0a021bd81427cdd0e48abbc
RUN mkdir -p /app/data/geoip && \
if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \
echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
-T 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" 2>/dev/null \
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
echo "✅ GeoIP downloaded"; \
else \
echo "⚠️ GeoIP skipped"; \
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
fi; \
else \
echo "Local - full download (30s timeout, 3 retries)"; \
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
-T 30 -t 4 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
echo "✅ GeoIP downloaded"; \
else \
echo "⚠️ GeoIP download failed or empty — skipping"; \
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
fi; \
fi
# Copy Caddy binary from caddy-builder (overwriting the one from base image)
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
# Allow non-root to bind privileged ports (80/443) securely
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
# N5 — app-side sanity check on the toolchain-provided Caddy binary. On the
# prebuilt path the "does it embed the fixed cel-go / grpc-go" assertions ran
# only when the toolchain image was built, so a wrong / rolled-back
# CHARON_TOOLCHAIN_DIGEST (or a hand-edited pin) would sail through silently.
# The final stage has no Go toolchain, so instead assert the binary loads and
# exposes the four custom plugins the recipe adds — a wrong-arch or stale-recipe
# image fails here immediately. The authoritative embed-version checks remain in
# caddy-inline (run by toolchain-image.yml) and docker-build.yml's post-build step.
RUN set -e; \
mods="$(/usr/bin/caddy list-modules 2>/dev/null)"; \
for m in http.handlers.rate_limit http.handlers.crowdsec http.handlers.geoip2 http.handlers.waf; do \
printf '%s\n' "$mods" | grep -qx "$m" \
|| { echo "ERROR: toolchain caddy binary missing expected module: $m"; printf '%s\n' "$mods"; exit 1; }; \
done; \
echo "Verified toolchain caddy binary exposes rate_limit / crowdsec / geoip2 / waf(coraza)"
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.26.3+)
# 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
# N5 — app-side sanity check on the toolchain-provided cscli binary: it must run
# and emit its recognisable version block. (CrowdSec 1.8.x prints an empty
# `version:` field here regardless of the -X ldflag, so match a stable field
# instead.) A wrong-arch / stale-recipe image fails this immediately.
RUN set -e; \
/usr/local/bin/cscli version >/tmp/cscli-v.txt 2>&1 \
|| { echo "ERROR: toolchain cscli is not runnable"; cat /tmp/cscli-v.txt; exit 1; }; \
grep -q 'Constraint_api' /tmp/cscli-v.txt \
|| { echo "ERROR: toolchain cscli version output not recognised"; cat /tmp/cscli-v.txt; exit 1; }; \
rm -f /tmp/cscli-v.txt; \
echo "Verified toolchain cscli runs (GoVersion: $(/usr/local/bin/cscli version 2>&1 | sed -n 's/^GoVersion: //p'))"
# Copy CrowdSec configuration files to .dist directory (will be used at runtime)
COPY --from=crowdsec-builder /crowdsec-out/config /etc/crowdsec.dist
# Verify config files were copied successfully
RUN if [ ! -f /etc/crowdsec.dist/config.yaml ]; then \
echo "WARNING: config.yaml not found in /etc/crowdsec.dist"; \
echo "Available files in /etc/crowdsec.dist:"; \
ls -la /etc/crowdsec.dist/ 2>/dev/null || echo "Directory empty or missing"; \
else \
echo "✓ config.yaml found in /etc/crowdsec.dist"; \
fi
# Verify CrowdSec binaries and configuration
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.26):"; \
cscli version || echo "CrowdSec version check failed"; \
echo ""; \
echo "Configuration source: /etc/crowdsec.dist"; \
ls -la /etc/crowdsec.dist/ | head -10 || echo "ERROR: /etc/crowdsec.dist directory not found"; \
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
# Ensure config.yaml exists in .dist (required for runtime)
# Skip cscli config restore at build time (no valid /etc/crowdsec at this stage)
# The runtime entrypoint will handle config initialization from .dist
RUN if [ ! -f /etc/crowdsec.dist/config.yaml ]; then \
echo "⚠️ WARNING: config.yaml not in /etc/crowdsec.dist after builder COPY"; \
echo " This file is critical for CrowdSec initialization at runtime"; \
else \
echo "✓ /etc/crowdsec.dist/config.yaml verified"; \
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 stub/binary from backend-builder.
# Security (GO-2026-5024): production builds (BUILD_DEBUG=0) receive a harmless shell
# stub that prints an error and exits 1 — no golang.org/x/sys/windows binary vulnerable
# to CVE-2026-39824 (NewNTUnicodeString string-length overflow, fixed in v0.44.0+, GO-2026-5024)
# is present in production images. Debug builds (BUILD_DEBUG=1) receive the real dlv
# compiled against golang.org/x/sys v0.46.0 (patched).
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 \
CHARON_PLUGINS_DIR=/app/plugins
# Create necessary directories
RUN mkdir -p /app/data /app/data/caddy /config /app/data/crowdsec
# Security: Create plugins directory with secure permissions
# Mode 0755: owner rwx, group rx, other rx (NOT world-writable)
# This satisfies the PluginLoaderService security check (mode & 0002 == 0)
RUN mkdir -p /app/plugins && chmod 755 /app/plugins
# 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
# Note: /app/plugins has 755 permissions (NOT world-writable) for security
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)" \