- Updated WafConfig.tsx to correct regex for common bad bots. - Modified cerberus_integration.sh to use curl instead of wget for backend readiness check. - Changed coraza_integration.sh to utilize curl for checking httpbin backend status. - Updated crowdsec_startup_test.sh to use curl for LAPI health check. - Replaced wget with curl in install-go-1.25.5.sh for downloading Go. - Modified rate_limit_integration.sh to use curl for backend readiness check. - Updated waf_integration.sh to replace wget with curl for checking httpbin backend status.
23 KiB
Alpine to Debian Slim Migration Specification
Version: 1.0.0 Created: 2026-01-18 Status: PLANNING Author: Planning Agent
Executive Summary
Security Rationale
The user has identified a critical CVE in Alpine Linux that necessitates migrating the Docker base images from Alpine to Debian slim. This migration addresses:
- Critical CVE Mitigation: Immediate resolution of the identified Alpine Linux vulnerability
- Broader Security Posture: Debian's larger security team and faster CVE response times
- glibc vs musl Compatibility: Eliminates potential musl libc edge cases that can cause subtle bugs in Go binaries with CGO
- Long-term Maintainability: Debian slim provides a battle-tested, stable base with predictable security update cycles
Key Benefits of Debian Slim
| Aspect | Alpine | Debian Slim | Advantage |
|---|---|---|---|
| Security Updates | Community-driven | Dedicated security team (Debian Security Team) | Faster CVE patches |
| C Library | musl libc | glibc | Better compatibility with CGO |
| Package Availability | ~10k packages | ~60k packages | More comprehensive |
| DNS Resolution | musl DNS bugs known | glibc mature DNS | More reliable |
| Image Size | ~5MB base | ~25MB base | Alpine smaller, but acceptable trade-off |
Current State Analysis
Dockerfile Structure Overview
The current Dockerfile is a multi-stage build with the following Alpine-based stages:
Builder Stages (Alpine-based)
| Stage | Base Image | Purpose |
|---|---|---|
xx |
tonistiigi/xx:1.9.0 |
Cross-compilation helpers (unchanged) |
frontend-builder |
node:24.13.0-alpine |
Build React frontend |
backend-builder |
golang:1.25-alpine |
Build Go backend with CGO |
caddy-builder |
golang:1.25-alpine |
Build Caddy with plugins |
crowdsec-builder |
golang:1.25.6-alpine |
Build CrowdSec from source |
crowdsec-fallback |
alpine:3.23 |
Fallback binary download |
Runtime Stage (Alpine-based)
| Stage | Base Image | Purpose |
|---|---|---|
| Final runtime | alpine:3.23 (via CADDY_IMAGE ARG) |
Production runtime |
Alpine Packages Currently Installed
Builder Stage Packages (apk)
# backend-builder
apk add --no-cache clang lld
xx-apk add --no-cache gcc musl-dev sqlite-dev
# caddy-builder
apk add --no-cache git
# crowdsec-builder
apk add --no-cache git clang lld
xx-apk add --no-cache gcc musl-dev
# crowdsec-fallback
apk add --no-cache curl tar
Runtime Stage Packages (apk)
# Final runtime image
apk --no-cache add bash ca-certificates sqlite-libs sqlite tzdata curl gettext su-exec libcap-utils
apk --no-cache upgrade
apk --no-cache upgrade c-ares
Alpine-Specific Commands in Dockerfile
-
User/Group Creation:
RUN addgroup -g 1000 charon && \ adduser -D -u 1000 -G charon -h /app -s /sbin/nologin charon -
Package Management:
apk add --no-cacheapk --no-cache upgradexx-apk add --no-cache(cross-compilation)
-
Privilege Dropping:
- Uses
su-exec(Alpine-specific lightweight sudo replacement)
- Uses
Alpine-Specific Commands in docker-entrypoint.sh
-
User Group Management:
addgroup -g "$DOCKER_SOCK_GID" docker 2>/dev/null || true addgroup charon docker 2>/dev/null || true addgroup charon "$GROUP_NAME" 2>/dev/null || true -
File Statistics:
stat -c '%a' "$PLUGINS_DIR" # Alpine stat syntax stat -c '%g' /var/run/docker.sock
CI/CD Workflow References
Files referencing Alpine that need updates:
| File | Line | Reference |
|---|---|---|
.github/workflows/docker-build.yml |
103-104 | caddy:2-alpine image pull |
.github/workflows/security-weekly-rebuild.yml |
53-54 | caddy:2-alpine image pull |
.github/workflows/security-weekly-rebuild.yml |
127 | apk info command for package check |
Target State: Debian Slim Configuration
Recommended Base Images
| Current Alpine Image | Debian Slim Replacement | Notes |
|---|---|---|
node:24.13.0-alpine |
node:24.13.0-slim |
Node.js official slim variant |
golang:1.25-alpine |
golang:1.25-bookworm |
Go official Debian variant |
golang:1.25.6-alpine |
golang:1.25.6-bookworm |
CrowdSec builder |
alpine:3.23 |
debian:bookworm-slim |
Runtime image |
caddy:2-alpine |
Build Caddy ourselves | Already building from source |
Package Mapping: Alpine → Debian
| Alpine Package | Debian Equivalent | Notes |
|---|---|---|
bash |
bash |
Same |
ca-certificates |
ca-certificates |
Same |
sqlite-libs |
libsqlite3-0 |
Runtime library |
sqlite |
sqlite3 |
CLI tool |
sqlite-dev |
libsqlite3-dev |
Build dependency |
tzdata |
tzdata |
Same |
curl |
curl |
Same |
gettext |
gettext-base |
Smaller variant with envsubst |
su-exec |
gosu |
Debian equivalent |
libcap-utils |
libcap2-bin |
Contains setcap |
clang |
clang |
Same |
lld |
lld |
Same |
gcc |
gcc |
Same (may need build-essential) |
musl-dev |
libc6-dev |
glibc development files |
git |
git |
Same |
tar |
tar |
Usually pre-installed |
c-ares |
libc-ares2 |
Async DNS library |
User/Group Creation Syntax Changes
| Operation | Alpine | Debian |
|---|---|---|
| Create group | addgroup -g 1000 charon |
groupadd -g 1000 charon |
| Create user | adduser -D -u 1000 -G charon -h /app -s /sbin/nologin charon |
useradd -u 1000 -g charon -d /app -s /usr/sbin/nologin -M charon |
| Add to group | addgroup charon docker |
usermod -aG docker charon |
Note
: Debian uses
/usr/sbin/nologininstead of Alpine's/sbin/nologin
Entrypoint Script Changes
The docker-entrypoint.sh requires these changes:
- Replace
addgroup/adduserwithgroupadd/useradd - Replace
su-execwithgosu - Update stat command syntax (BSD vs GNU - Debian uses GNU which is same)
Detailed Migration Steps
Phase 1: Builder Stage Migrations
Step 1.1: Frontend Builder
File: Dockerfile
Lines: 27-47
# BEFORE (Alpine)
FROM --platform=$BUILDPLATFORM node:24.13.0-alpine AS frontend-builder
# AFTER (Debian slim)
FROM --platform=$BUILDPLATFORM node:24.13.0-slim AS frontend-builder
Notes:
- No package installation changes needed (npm handles dependencies)
- Environment variables remain the same
Step 1.2: Backend Builder
File: Dockerfile
Lines: 49-143
# BEFORE (Alpine)
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS backend-builder
# ...
RUN apk add --no-cache clang lld
RUN xx-apk add --no-cache gcc musl-dev sqlite-dev
# AFTER (Debian)
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS backend-builder
# ...
RUN apt-get update && apt-get install -y --no-install-recommends \
clang lld \
&& rm -rf /var/lib/apt/lists/*
Critical Change - CGO with glibc:
- Remove the clang wrapper workaround for ARM64 gold linker (lines 67-91)
- glibc environments handle this natively
- Change
xx-apkto cross-compilation apt packages or useTARGETPLATFORMspecific installs
xx-go Cross Compilation Notes:
- The
xxhelper supports Debian-based images - Replace
xx-apkwith appropriate Debian cross-compilation setup
Step 1.3: Caddy Builder
File: Dockerfile
Lines: 145-202
# BEFORE (Alpine)
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS caddy-builder
# ...
RUN apk add --no-cache git
# AFTER (Debian)
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS caddy-builder
# ...
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
Step 1.4: CrowdSec Builder
File: Dockerfile
Lines: 204-256
# BEFORE (Alpine)
FROM --platform=$BUILDPLATFORM golang:1.25.6-alpine AS crowdsec-builder
# ...
RUN apk add --no-cache git clang lld
RUN xx-apk add --no-cache gcc musl-dev
# AFTER (Debian)
FROM --platform=$BUILDPLATFORM golang:1.25.6-bookworm AS crowdsec-builder
# ...
RUN apt-get update && apt-get install -y --no-install-recommends \
git clang lld gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
Step 1.5: CrowdSec Fallback
File: Dockerfile
Lines: 258-293
# BEFORE (Alpine)
FROM alpine:3.23 AS crowdsec-fallback
# ...
RUN apk add --no-cache curl tar
# AFTER (Debian)
FROM debian:bookworm-slim AS crowdsec-fallback
# ...
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates tar \
&& rm -rf /var/lib/apt/lists/*
⚠️ IMPORTANT: Debian slim does NOT include
tarby default. It must be explicitly installed for CrowdSec binary extraction.
Phase 2: Runtime Stage Migration
Step 2.1: Base Image Change
File: Dockerfile
Lines: 23, 295-303
# BEFORE (Alpine)
ARG CADDY_IMAGE=alpine:3.23
# ...
FROM ${CADDY_IMAGE}
# ...
RUN apk --no-cache add bash ca-certificates sqlite-libs sqlite tzdata curl gettext su-exec libcap-utils \
&& apk --no-cache upgrade \
&& apk --no-cache upgrade c-ares
# AFTER (Debian)
ARG CADDY_IMAGE=debian:bookworm-slim
# ...
FROM ${CADDY_IMAGE}
# ...
RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates libsqlite3-0 sqlite3 tzdata curl gettext-base gosu libcap2-bin libc-ares2 \
&& apt-get upgrade -y \
&& rm -rf /var/lib/apt/lists/*
Step 2.2: User Creation
File: Dockerfile
Lines: 307-308
# BEFORE (Alpine)
RUN addgroup -g 1000 charon && \
adduser -D -u 1000 -G charon -h /app -s /sbin/nologin charon
# AFTER (Debian)
RUN groupadd -g 1000 charon && \
useradd -u 1000 -g charon -d /app -s /usr/sbin/nologin -M charon
⚠️ PATH CHANGE: Debian uses
/usr/sbin/nologininstead of Alpine's/sbin/nologin.
Step 2.3: setcap Command
File: Dockerfile
Line: 318
# BEFORE (Alpine - same)
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
# AFTER (Debian - same, but requires libcap2-bin)
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
Phase 3: Entrypoint Script Migration
File: .docker/docker-entrypoint.sh
⚠️ CRITICAL: Debian slim does NOT include
curl. The entrypoint uses curl for the Caddy readiness check. Allcurlcalls must be replaced withcurlequivalents.
Step 3.0: Replace curl with curl for Caddy Readiness Check
# BEFORE (Alpine - uses curl)
curl -q --spider http://localhost:2019/config/ || exit 1
# AFTER (Debian - uses curl)
curl -sf http://localhost:2019/config/ > /dev/null || exit 1
Step 3.1: Replace su-exec with gosu
# BEFORE (Alpine)
run_as_charon() {
if is_root; then
su-exec charon "$@"
else
"$@"
fi
}
# AFTER (Debian)
run_as_charon() {
if is_root; then
gosu charon "$@"
else
"$@"
fi
}
Step 3.2: Replace addgroup/adduser with groupadd/usermod
# BEFORE (Alpine)
addgroup -g "$DOCKER_SOCK_GID" docker 2>/dev/null || true
addgroup charon docker 2>/dev/null || true
# AFTER (Debian)
groupadd -g "$DOCKER_SOCK_GID" docker 2>/dev/null || true
usermod -aG docker charon 2>/dev/null || true
# BEFORE (Alpine)
addgroup charon "$GROUP_NAME" 2>/dev/null || true
# AFTER (Debian)
usermod -aG "$GROUP_NAME" charon 2>/dev/null || true
Step 3.3: stat Command (No Change Required)
Both Alpine and Debian use GNU coreutils stat, so the syntax remains:
stat -c '%a' "$PLUGINS_DIR"
stat -c '%g' /var/run/docker.sock
Phase 4: CI/CD Workflow Updates
Step 4.1: docker-build.yml
File: .github/workflows/docker-build.yml
Lines: 103-104
# BEFORE
run: |
docker pull caddy:2-alpine
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
# AFTER
# Remove this step entirely - we build Caddy from source
# Or update to pull debian:bookworm-slim for digest verification
run: |
docker pull debian:bookworm-slim
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' debian:bookworm-slim)
Step 4.2: security-weekly-rebuild.yml
File: .github/workflows/security-weekly-rebuild.yml
Lines 53-54 (Caddy digest):
# Remove or update similar to docker-build.yml
Lines 127-133 (Package version check):
# BEFORE
- name: Check Alpine package versions
run: |
echo "Checking key security packages:" >> $GITHUB_STEP_SUMMARY
docker run --rm --entrypoint "" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} \
sh -c "apk update >/dev/null 2>&1 && apk info c-ares curl libcurl openssl" >> $GITHUB_STEP_SUMMARY
# AFTER
- name: Check Debian package versions
run: |
echo "Checking key security packages:" >> $GITHUB_STEP_SUMMARY
docker run --rm --entrypoint "" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} \
sh -c "dpkg -l | grep -E 'libc-ares|curl|libcurl|openssl|libssl'" >> $GITHUB_STEP_SUMMARY
Phase 5: Cross-Compilation Considerations
xx Helper Compatibility
The tonistiigi/xx project supports both Alpine and Debian. Key changes:
- Remove xx-apk usage: Replace with native apt-get for the target architecture
- CGO Cross-Compilation: Debian has better cross-compilation toolchain support
- Remove Gold Linker Workaround: The clang wrapper hack (lines 67-91) for Go 1.25 ARM64 can be removed
# Debian cross-compilation setup (replaces xx-apk)
ARG TARGETARCH
RUN dpkg --add-architecture ${TARGETARCH} && \
apt-get update && \
apt-get install -y --no-install-recommends \
gcc-$(dpkg-architecture -A ${TARGETARCH} -qDEB_TARGET_GNU_TYPE) \
libc6-dev:${TARGETARCH} \
libsqlite3-dev:${TARGETARCH} \
&& rm -rf /var/lib/apt/lists/*
Alternative: Continue using xx helper which has Debian support:
COPY --from=xx / /
RUN xx-apt install -y libc6-dev libsqlite3-dev
Testing Strategy
Phase 1: Local Build Verification
-
Build All Architectures:
docker buildx build --platform linux/amd64,linux/arm64 -t charon:debian-test . -
Verify Binary Execution:
docker run --rm charon:debian-test /app/charon --version docker run --rm charon:debian-test caddy version docker run --rm charon:debian-test cscli version -
Verify Package Installation:
docker run --rm --entrypoint bash charon:debian-test -c "which gosu setcap curl sqlite3"
Phase 2: Functional Testing
-
Run E2E Playwright Tests:
npx playwright test --project=chromium -
Run Backend Unit Tests:
make test-backend -
Run Docker Compose Stack:
docker compose -f .docker/compose/docker-compose.yml up -d # Verify all services start correctly curl http://localhost:8080/api/v1/health
Phase 3: Security Verification
-
Trivy Vulnerability Scan:
trivy image charon:debian-test --severity CRITICAL,HIGH -
Verify No Alpine CVE Present:
trivy image charon:debian-test | grep -i alpine # Should return nothing -
Verify User Permissions:
docker run --rm charon:debian-test id # Should show: uid=1000(charon) gid=1000(charon)
Phase 4: Performance Validation
-
Compare Image Sizes:
docker images | grep charon # Alpine: ~150MB, Debian: ~200MB (acceptable) -
Startup Time Comparison:
time docker run --rm charon:debian-test /app/charon --version -
Memory Usage Comparison:
docker stats --no-stream charon-container
Rollback Plan
Immediate Rollback
-
Revert Dockerfile Changes:
git checkout main -- Dockerfile git checkout main -- .docker/docker-entrypoint.sh -
Rebuild with Alpine:
docker buildx build --no-cache -t charon:alpine-rollback .
Staged Rollback
If issues are discovered post-deployment:
-
Tag Current (Debian) Image:
docker tag ghcr.io/wikid82/charon:latest ghcr.io/wikid82/charon:debian-v1 -
Push Previous Alpine Image:
docker tag ghcr.io/wikid82/charon:v{previous} ghcr.io/wikid82/charon:latest docker push ghcr.io/wikid82/charon:latest -
Document Rollback:
- Create GitHub issue documenting the reason
- Update CHANGELOG.md with rollback notice
Rollback Criteria
Trigger rollback if any of these occur:
- Critical security vulnerability in Debian base
- Application crashes on startup
- E2E tests fail > 10%
- Memory usage increases > 50%
- Build times increase > 3x
Security Considerations
Security Features to Maintain
| Feature | Alpine Implementation | Debian Implementation | Status |
|---|---|---|---|
| Non-root user | adduser -D |
useradd -M |
✅ Maintain |
| Privilege dropping | su-exec |
gosu |
✅ Maintain |
| Capability binding | setcap via libcap-utils |
setcap via libcap2-bin |
✅ Maintain |
| Read-only filesystem | N/A | N/A | N/A |
| Minimal packages | --no-cache |
--no-install-recommends |
✅ Maintain |
| Security upgrades | apk upgrade |
apt-get upgrade |
✅ Maintain |
| HEALTHCHECK | Present | Present | ✅ Maintain |
Security Enhancements with Debian
- glibc Security: Better Address Space Layout Randomization (ASLR)
- Faster CVE Patches: Debian Security Team is larger and faster
- No musl Edge Cases: Eliminates subtle bugs in Go binaries with CGO
- SELinux Compatibility: Debian has better SELinux support if needed
Security Scanning Updates
Update Trivy configuration to scan for Debian-specific vulnerabilities:
# .github/workflows/security-weekly-rebuild.yml
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@...
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
Implementation Checklist
Pre-Migration
- Create feature branch:
feature/debian-migration - Document current Alpine image hashes for comparison
- Run full E2E test suite as baseline
- Create backup of working Dockerfile
- Backup current production image for rollback:
docker tag ghcr.io/wikid82/charon:latest ghcr.io/wikid82/charon:pre-debian-migration docker push ghcr.io/wikid82/charon:pre-debian-migration
Dockerfile Changes
- Update
frontend-builderstage to Debian slim - Update
backend-builderstage to Debian bookworm - Update
caddy-builderstage to Debian bookworm - Update
crowdsec-builderstage to Debian bookworm - Update
crowdsec-fallbackstage to Debian slim - Update final runtime stage to Debian slim
- Update
CADDY_IMAGEARG default - Replace all
apkcommands withapt-get - Update user/group creation commands
- Replace
su-execwithgosu - Remove ARM64 clang wrapper workaround
- Update cross-compilation setup for xx helper
Entrypoint Changes
- Replace
su-execwithgosu - Replace
addgroupwithgroupadd - Replace
adduserwithusermod -aG
CI/CD Changes
- Update
docker-build.ymlCaddy digest step - Update
security-weekly-rebuild.ymlpackage check - Update any other workflows referencing Alpine
- Update Renovate configuration (
renovate.json) to track Debian base image updates (see Appendix B)
Testing
- Build multi-architecture image (amd64, arm64)
- Run all E2E Playwright tests
- Run all backend unit tests
- Run Trivy vulnerability scan
- Verify non-root user execution
- Verify CrowdSec initialization
- Verify Caddy startup
- gosu functionality test: Verify privilege dropping works correctly
docker run --rm charon:debian-test gosu charon id # Expected: uid=1000(charon) gid=1000(charon) groups=1000(charon) docker run --rm charon:debian-test gosu charon whoami # Expected: charon - Docker socket integration test: Verify socket group mapping works
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock charon:debian-test \ bash -c "stat -c '%g' /var/run/docker.sock && groups charon" # Verify charon user is added to the docker socket's group
Documentation
- Update README.md if any user-facing changes
- Update CHANGELOG.md with migration details
- Update
docs/DOCKER.mdwith Debian-specific instructions - Update
docs/features.mdto reflect base image change - Archive this plan to
docs/implementation/
Post-Migration
- Monitor production for 48 hours
- Verify no regression in vulnerability reports
- Close related security issues/CVEs
- Remove any Alpine-specific workarounds from codebase
Appendix A: Complete Debian Package List
# Runtime packages
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
gettext-base \
gosu \
libc-ares2 \
libcap2-bin \
libsqlite3-0 \
sqlite3 \
tzdata \
&& apt-get upgrade -y \
&& rm -rf /var/lib/apt/lists/*
Appendix B: Renovate Configuration Updates
If using Renovate for dependency management, update renovate.json:
{
"regexManagers": [
{
"fileMatch": ["^Dockerfile$"],
"matchStrings": ["ARG CADDY_IMAGE=debian:(?<currentValue>[\\w.-]+)"],
"depNameTemplate": "debian",
"datasourceTemplate": "docker"
}
]
}
Appendix C: Image Size Comparison (Expected)
| Component | Alpine Size | Debian Size | Delta |
|---|---|---|---|
| Base image | 5 MB | 25 MB | +20 MB |
| Runtime packages | 45 MB | 55 MB | +10 MB |
| Go binary (Charon) | 30 MB | 30 MB | 0 |
| Caddy binary | 45 MB | 45 MB | 0 |
| CrowdSec binaries | 25 MB | 25 MB | 0 |
| Frontend assets | 10 MB | 10 MB | 0 |
| Total | ~160 MB | ~190 MB | +30 MB |
Note: Actual sizes may vary. The ~30MB increase is an acceptable trade-off for improved security.