fix: set PORT environment variable for httpbin backend in integration scripts
This commit is contained in:
282
docs/plans/archive/cve_remediation_spec.md
Normal file
282
docs/plans/archive/cve_remediation_spec.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# CI Supply Chain CVE Remediation Plan
|
||||
|
||||
**Status:** Active
|
||||
**Created:** 2026-03-13
|
||||
**Branch:** `feature/beta-release`
|
||||
**Context:** Three HIGH vulnerabilities (CVE-2025-69650, CVE-2025-69649, CVE-2026-3805) in the Docker runtime image are blocking the CI supply-chain scan. Two Grype ignore-rule entries are also expired and require maintenance.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
| # | Action | Severity Reduction | Effort |
|
||||
|---|--------|--------------------|--------|
|
||||
| 1 | Remove `curl` from runtime image (replace with `wget`) | Eliminates 1 HIGH + ~7 MEDIUMs + 2 LOWs | ~30 min |
|
||||
| 2 | Remove `binutils` + `libc-utils` from runtime image | Eliminates 2 HIGH + 3 MEDIUMs | ~5 min |
|
||||
| 3 | Update expired Grype ignore rules | Prevents false scan failures at next run | ~10 min |
|
||||
|
||||
**Bottom line:** All three HIGH CVEs are eliminated at root rather than suppressed. After Phase 1 and Phase 2, `fail-on-severity: high` passes cleanly. Phase 3 is maintenance-only.
|
||||
|
||||
---
|
||||
|
||||
## 2. CVE Inventory
|
||||
|
||||
### Blocking HIGH CVEs
|
||||
|
||||
| CVE | Package | Version | CVSS | Fix State | Notes |
|
||||
|-----|---------|---------|------|-----------|-------|
|
||||
| CVE-2026-3805 | `curl` | 8.17.0-r1 | 7.5 | `unknown` | **New** — appeared in Grype DB 2026-03-13, published 2026-03-11. SMB protocol use-after-free. Charon uses HTTPS/HTTP only. |
|
||||
| CVE-2025-69650 | `binutils` | 2.45.1-r0 | 7.5 | `` (none) | Double-free in `readelf`. Charon never invokes `readelf`. |
|
||||
| CVE-2025-69649 | `binutils` | 2.45.1-r0 | 7.5 | `` (none) | Null-ptr deref in `readelf`. Charon never invokes `readelf`. |
|
||||
|
||||
### Associated MEDIUM/LOW CVEs eliminated as side-effects
|
||||
|
||||
| CVEs | Package | Count | Eliminated by |
|
||||
|------|---------|-------|---------------|
|
||||
| CVE-2025-14819, CVE-2025-15079, CVE-2025-14524, CVE-2025-13034, CVE-2025-14017 | `curl` | 5 × MEDIUM | Phase 1 |
|
||||
| CVE-2025-69652, CVE-2025-69644, CVE-2025-69651 | `binutils` | 3 × MEDIUM | Phase 2 |
|
||||
|
||||
### Expired Grype Ignore Rules
|
||||
|
||||
| Entry | Expiry | Status | Action |
|
||||
|-------|--------|--------|--------|
|
||||
| `CVE-2026-22184` (zlib) | 2026-03-14 | Expires tomorrow; underlying CVE already fixed via `apk upgrade --no-cache zlib` | **Remove entirely** |
|
||||
| `GHSA-69x3-g4r3-p962` (nebula) | 2026-03-05 | **Expired 8 days ago**; upstream fix still unavailable | **Extend to 2026-04-13** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 1 — Remove `curl` from Runtime Image
|
||||
|
||||
### Rationale
|
||||
|
||||
`curl` is present solely for:
|
||||
1. GeoLite2 DB download at build time (Dockerfile, runtime stage `RUN` block)
|
||||
2. HEALTHCHECK probe (Dockerfile `HEALTHCHECK` directive)
|
||||
3. Caddy admin API readiness poll (`.docker/docker-entrypoint.sh`)
|
||||
|
||||
`busybox` (already installed on Alpine as a transitive dependency of `busybox-extras`, which is explicitly installed) provides `wget` with sufficient functionality for all three uses.
|
||||
|
||||
### 3.1 `wget` Translation Reference
|
||||
|
||||
| `curl` invocation | `wget` equivalent | Notes |
|
||||
|-------------------|--------------------|-------|
|
||||
| `curl -fSL -m 10 "URL" -o FILE 2>/dev/null` | `wget -qO FILE -T 10 "URL" 2>/dev/null` | `-q` = quiet; `-T` = timeout (seconds); exits nonzero on failure |
|
||||
| `curl -fSL -m 30 --retry 3 "URL" -o FILE` | `wget -qO FILE -T 30 -t 4 "URL"` | `-t 4` = 4 total tries (1 initial + 3 retries); add `&& [ -s FILE ]` guard |
|
||||
| `curl -f http://HOST/path \|\| exit 1` | `wget -q -O /dev/null http://HOST/path \|\| exit 1` | HEALTHCHECK; wget exits nonzero on HTTP error |
|
||||
| `curl -sf http://HOST/path > /dev/null 2>&1` | `wget -qO /dev/null http://HOST/path 2>/dev/null` | Silent readiness probe |
|
||||
|
||||
**busybox wget notes:**
|
||||
- `-T N` is per-connection timeout in seconds (equivalent to `curl --max-time`).
|
||||
- `-t N` is total number of tries, not retries; `-t 4` = 3 retries.
|
||||
- On download failure, busybox wget may leave a zero-byte or partial file at the output path. The `[ -s FILE ]` guard (`-s` = non-empty) prevents a corrupted placeholder from passing the sha256 check.
|
||||
|
||||
### 3.2 Dockerfile Changes
|
||||
|
||||
**File:** `Dockerfile`
|
||||
|
||||
**Change A — Remove `curl`, `binutils`, `libc-utils` from `apk add` (runtime stage, line ~413):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
RUN apk add --no-cache \
|
||||
bash ca-certificates sqlite-libs sqlite tzdata curl gettext libcap libcap-utils \
|
||||
c-ares binutils libc-utils busybox-extras \
|
||||
&& apk upgrade --no-cache zlib
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
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
|
||||
```
|
||||
|
||||
*(This single edit covers both Phase 1 and Phase 2 removals.)*
|
||||
|
||||
**Change B — GeoLite2 download block, CI path (line ~437):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
if curl -fSL -m 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
||||
-o /app/data/geoip/GeoLite2-Country.mmdb 2>/dev/null; then
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
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; then
|
||||
```
|
||||
|
||||
**Change C — GeoLite2 download block, non-CI path (line ~445):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
if curl -fSL -m 30 --retry 3 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
||||
-o /app/data/geoip/GeoLite2-Country.mmdb; then
|
||||
if echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
|
||||
-T 30 -t 4 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb"; then
|
||||
if [ -s /app/data/geoip/GeoLite2-Country.mmdb ] && \
|
||||
echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then
|
||||
```
|
||||
|
||||
The `[ -s FILE ]` check is added before `sha256sum` to guard against wget leaving an empty file on partial failure.
|
||||
|
||||
**Change D — HEALTHCHECK directive (line ~581):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/api/v1/health || exit 1
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://localhost:8080/api/v1/health || exit 1
|
||||
```
|
||||
|
||||
### 3.3 Entrypoint Changes
|
||||
|
||||
**File:** `.docker/docker-entrypoint.sh`
|
||||
|
||||
**Change E — Caddy readiness poll (line ~368):**
|
||||
|
||||
Current:
|
||||
```sh
|
||||
if curl -sf http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
|
||||
```
|
||||
|
||||
New:
|
||||
```sh
|
||||
if wget -qO /dev/null http://127.0.0.1:2019/config/ 2>/dev/null; then
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 2 — Remove `binutils` and `libc-utils` from Runtime Image
|
||||
|
||||
### Rationale
|
||||
|
||||
`binutils` is installed solely for `objdump`, used in `.docker/docker-entrypoint.sh` to detect DWARF debug symbols when `CHARON_DEBUG=1`. The entrypoint already has a graceful fallback (lines ~401–404):
|
||||
|
||||
```sh
|
||||
else
|
||||
# objdump not available, try to run Delve anyway with a warning
|
||||
echo "Note: Cannot verify debug symbols (objdump not found). Attempting Delve..."
|
||||
run_as_charon /usr/local/bin/dlv exec "$bin_path" ...
|
||||
fi
|
||||
```
|
||||
|
||||
When `objdump` is absent the container functions correctly for all standard and debug-mode runs. The check is advisory.
|
||||
|
||||
`libc-utils` appears **only once** across the entire codebase (confirmed by grep across `*.sh`, `Dockerfile`, `*.yml`): as a sibling entry on the same `apk add` line as `binutils`. It provides glibc-compatible headers for musl-based Alpine and has no independent consumer in this image. It is safe to remove together with `binutils`.
|
||||
|
||||
### 4.1 Dockerfile Change
|
||||
|
||||
Already incorporated in Phase 1 Change A — the `apk add` line removes both `binutils` and `libc-utils` in a single edit. No additional changes are required.
|
||||
|
||||
### 4.2 Why Not Suppress Instead?
|
||||
|
||||
Suppressing in Grype requires two new ignore entries with expiry maintenance every 30 days indefinitely (no upstream Alpine fix exists). Removing the packages eliminates the CVEs permanently. There is no functional regression given the working fallback.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 3 — Update Expired Grype Ignore Rules
|
||||
|
||||
**File:** `.grype.yaml`
|
||||
|
||||
### 5.1 Remove `CVE-2026-22184` (zlib) Block
|
||||
|
||||
**Action:** Delete the entire `CVE-2026-22184` ignore entry.
|
||||
|
||||
**Reason:** The Dockerfile runtime stage already contains `&& apk upgrade --no-cache zlib`, which upgrades zlib from 1.3.1-r2 to 1.3.2-r0, resolving CVE-2026-22184. Suppressing a resolved CVE creates false confidence and obscures scan accuracy. The entry's own removal criteria have been met: Alpine released `zlib 1.3.2-r0`.
|
||||
|
||||
### 5.2 Extend `GHSA-69x3-g4r3-p962` (nebula) Expiry
|
||||
|
||||
**Action:** Update the `expiry` field and review comment in the nebula block.
|
||||
|
||||
Current:
|
||||
```yaml
|
||||
expiry: "2026-03-05" # Re-evaluate in 14 days (2026-02-19 + 14 days)
|
||||
```
|
||||
|
||||
New:
|
||||
```yaml
|
||||
expiry: "2026-04-13" # Re-evaluated 2026-03-13: smallstep/certificates stable still v0.27.5, no nebula v1.10+ requirement. Extended 30 days.
|
||||
```
|
||||
|
||||
Update the review comment line:
|
||||
```
|
||||
# - Next review: 2026-04-13.
|
||||
# - Reviewed 2026-03-13: smallstep stable still v0.27.5 (no nebula v1.10+ requirement). Extended 30 days.
|
||||
# - Remove suppression immediately once upstream fixes.
|
||||
```
|
||||
|
||||
**Reason:** As of 2026-03-13, `smallstep/certificates` has not released a stable version requiring nebula v1.10+. The constraint analysis from 2026-02-19 remains valid. Expiry extended 30 days to 2026-04-13.
|
||||
|
||||
---
|
||||
|
||||
## 6. File Change Summary
|
||||
|
||||
| File | Change | Scope |
|
||||
|------|--------|-------|
|
||||
| `Dockerfile` | Remove `curl`, `binutils`, `libc-utils` from `apk add` | Line ~413–415 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in GeoLite2 CI download path | Line ~437–441 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in GeoLite2 non-CI path; add `[ -s FILE ]` guard | Line ~445–452 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in HEALTHCHECK | Line ~581 |
|
||||
| `.docker/docker-entrypoint.sh` | Replace `curl` with `wget` in Caddy readiness poll | Line ~368 |
|
||||
| `.grype.yaml` | Delete `CVE-2026-22184` (zlib) ignore block entirely | zlib block |
|
||||
| `.grype.yaml` | Extend `GHSA-69x3-g4r3-p962` expiry to 2026-04-13; update review comment | nebula block |
|
||||
|
||||
---
|
||||
|
||||
## 7. Commit Slicing Strategy
|
||||
|
||||
**Single PR** — all changes are security-related and tightly coupled. Splitting curl removal from binutils removal would produce an intermediate commit with partially resolved HIGHs, offering no validation benefit and complicating rollback.
|
||||
|
||||
Suggested commit message:
|
||||
```
|
||||
fix(security): remove curl and binutils from runtime image
|
||||
|
||||
Replace curl with busybox wget for GeoLite2 downloads, HEALTHCHECK,
|
||||
and the Caddy readiness probe. Remove binutils and libc-utils from the
|
||||
runtime image; the entrypoint objdump check has a documented fallback
|
||||
for missing objdump. Eliminates CVE-2026-3805 (curl HIGH), CVE-2025-69650
|
||||
and CVE-2025-69649 (binutils HIGH), plus 8 associated MEDIUM findings.
|
||||
|
||||
Remove the now-resolved CVE-2026-22184 (zlib) suppression from
|
||||
.grype.yaml and extend GHSA-69x3-g4r3-p962 (nebula) expiry to
|
||||
2026-04-13 pending upstream smallstep/certificates update.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Expected Scan Results After Fix
|
||||
|
||||
| Metric | Before | After | Delta |
|
||||
|--------|--------|-------|-------|
|
||||
| HIGH count | 3 | **0** | −3 |
|
||||
| MEDIUM count | ~13 | ~5 | −8 |
|
||||
| LOW count | ~2 | ~0 | −2 |
|
||||
| `fail-on-severity: high` | ❌ FAIL | ✅ PASS | — |
|
||||
| CI supply-chain scan | ❌ BLOCKED | ✅ GREEN | — |
|
||||
|
||||
Remaining MEDIUMs after fix (~5):
|
||||
- `busybox` / `busybox-extras` / `ssl_client` — CVE-2025-60876 (CRLF injection in wget/ssl_client; no Alpine fix; Charon application code does not invoke `wget` directly at runtime)
|
||||
|
||||
---
|
||||
|
||||
## 9. Validation Steps
|
||||
|
||||
1. Rebuild Docker image: `docker build -t charon:test .`
|
||||
2. Run Grype scan: `grype charon:test` — confirm zero HIGH findings
|
||||
3. Confirm HEALTHCHECK probe passes: start container, check `docker inspect` for `healthy` status
|
||||
4. Confirm Caddy readiness: inspect entrypoint logs for `"Caddy is ready!"`
|
||||
5. Run E2E suite: `npx playwright test --project=firefox`
|
||||
6. Push branch and confirm CI supply-chain workflow exits green
|
||||
@@ -1,282 +1,290 @@
|
||||
# CI Supply Chain CVE Remediation Plan
|
||||
# Integration Workflow Fix Plan: go-httpbin Port Mismatch & curl-in-Container Remediation
|
||||
|
||||
**Status:** Active
|
||||
**Created:** 2026-03-13
|
||||
**Created:** 2026-03-14
|
||||
**Branch:** `feature/beta-release`
|
||||
**Context:** Three HIGH vulnerabilities (CVE-2025-69650, CVE-2025-69649, CVE-2026-3805) in the Docker runtime image are blocking the CI supply-chain scan. Two Grype ignore-rule entries are also expired and require maintenance.
|
||||
**Context:** All four integration workflows (cerberus, crowdsec, rate-limit, waf) are failing with "httpbin not starting." Root cause is a compounding port mismatch introduced when `kennethreitz/httpbin` (port 80) was swapped for `mccutchen/go-httpbin` (port 8080) without updating port configuration. A secondary issue exists where two scripts still use `curl` via `docker exec` inside an Alpine container that only has `wget`.
|
||||
**Previous Plan:** Backed up to `docs/plans/cve_remediation_spec.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
## 1. Root Cause Analysis
|
||||
|
||||
| # | Action | Severity Reduction | Effort |
|
||||
|---|--------|--------------------|--------|
|
||||
| 1 | Remove `curl` from runtime image (replace with `wget`) | Eliminates 1 HIGH + ~7 MEDIUMs + 2 LOWs | ~30 min |
|
||||
| 2 | Remove `binutils` + `libc-utils` from runtime image | Eliminates 2 HIGH + 3 MEDIUMs | ~5 min |
|
||||
| 3 | Update expired Grype ignore rules | Prevents false scan failures at next run | ~10 min |
|
||||
### 1.1 Primary: go-httpbin Port Mismatch
|
||||
|
||||
**Bottom line:** All three HIGH CVEs are eliminated at root rather than suppressed. After Phase 1 and Phase 2, `fail-on-severity: high` passes cleanly. Phase 3 is maintenance-only.
|
||||
**Commit `042c5ec6`** replaced `kennethreitz/httpbin` with `mccutchen/go-httpbin` across all four integration scripts. However, it **only changed the image name** — no port configuration was updated.
|
||||
|
||||
| Property | `kennethreitz/httpbin` | `mccutchen/go-httpbin` |
|
||||
|----------|----------------------|----------------------|
|
||||
| Default listen port | **80** | **8080** |
|
||||
| Port env var | N/A | `PORT` |
|
||||
| Exposed port (Dockerfile) | `80/tcp` | `8080/tcp` |
|
||||
|
||||
**Evidence:** `docker inspect mccutchen/go-httpbin --format='{{json .Config.ExposedPorts}}'` returns `{"8080/tcp":{}}`. The [go-httpbin README](https://github.com/mccutchen/go-httpbin) confirms the default port is `8080`, configurable via `-port` flag or `PORT` environment variable.
|
||||
|
||||
**Failure mechanism:** Each integration script has a health check loop like:
|
||||
|
||||
```bash
|
||||
for i in {1..45}; do
|
||||
if docker exec ${CONTAINER_NAME} sh -c "wget -qO /dev/null http://${BACKEND_CONTAINER}/get" >/dev/null 2>&1; then
|
||||
echo "✓ httpbin backend is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 45 ]; then
|
||||
echo "✗ httpbin backend failed to start"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
|
||||
The `wget` URL `http://${BACKEND_CONTAINER}/get` resolves to port 80 (HTTP default). go-httpbin is listening on port 8080. The connection is refused on port 80 for 45 iterations, then the script prints "httpbin backend failed to start" and exits 1.
|
||||
|
||||
Additionally, all four scripts create proxy hosts with `"forward_port": 80`, which would also fail at the Caddy reverse proxy level even if the health check passed.
|
||||
|
||||
### 1.2 Secondary: curl Not Available Inside Container
|
||||
|
||||
**Commit `58b087bc`** correctly migrated the four httpbin health checks from `curl` to `wget` (busybox). However, two other scripts still use `docker exec ... curl` to probe services inside the Alpine container, which does not have `curl` installed (removed as part of CVE remediation):
|
||||
|
||||
1. **`scripts/crowdsec_startup_test.sh` L179** — LAPI health check uses `docker exec ${CONTAINER_NAME} curl -sf http://127.0.0.1:8085/health`. Always returns "FAILED" (soft-pass, not blocking CI, but wrong).
|
||||
2. **`scripts/diagnose-test-env.sh` L104** — CrowdSec LAPI check uses `docker exec charon-e2e curl -sf http://localhost:8090/health`. Always fails silently.
|
||||
|
||||
### 1.3 Timeline of Changes
|
||||
|
||||
| Commit | Change | Effect |
|
||||
|--------|--------|--------|
|
||||
| `042c5ec6` | Swap `kennethreitz/httpbin` → `mccutchen/go-httpbin` | **Introduced port mismatch** (8080 vs 80). Not caught because the prior curl-based health checks were already broken by the missing curl. |
|
||||
| `58b087bc` | Replace `curl` with `wget` in `docker exec` httpbin health checks | **Correct tool fix** for 4 scripts. But exposed the hidden port mismatch — now wget correctly tries port 80 and correctly fails (connection refused), producing the visible "httpbin not starting" error. |
|
||||
| `4b896c2e` | Replace `curl` with `wget` in Docker compose healthchecks | Correct, no issues. |
|
||||
|
||||
**Key insight:** The curl→wget migration was a correct fix for a real problem. It was applied on top of an earlier, unnoticed bug (port mismatch from the image swap). The wget migration made the port bug visible because wget actually runs inside the container, whereas curl was silently failing (not found) and the health check was timing out for a different reason.
|
||||
|
||||
---
|
||||
|
||||
## 2. CVE Inventory
|
||||
## 2. Impact Assessment
|
||||
|
||||
### Blocking HIGH CVEs
|
||||
### 2.1 Affected Scripts — Port Mismatch (PRIMARY)
|
||||
|
||||
| CVE | Package | Version | CVSS | Fix State | Notes |
|
||||
|-----|---------|---------|------|-----------|-------|
|
||||
| CVE-2026-3805 | `curl` | 8.17.0-r1 | 7.5 | `unknown` | **New** — appeared in Grype DB 2026-03-13, published 2026-03-11. SMB protocol use-after-free. Charon uses HTTPS/HTTP only. |
|
||||
| CVE-2025-69650 | `binutils` | 2.45.1-r0 | 7.5 | `` (none) | Double-free in `readelf`. Charon never invokes `readelf`. |
|
||||
| CVE-2025-69649 | `binutils` | 2.45.1-r0 | 7.5 | `` (none) | Null-ptr deref in `readelf`. Charon never invokes `readelf`. |
|
||||
| File | Docker Run Line | Health Check Line | Forward Port Line | Status |
|
||||
|------|----------------|-------------------|-------------------|--------|
|
||||
| `scripts/cerberus_integration.sh` | L174 | L215 | L255 | **BROKEN** |
|
||||
| `scripts/waf_integration.sh` | L167 | L206 | L246 | **BROKEN** |
|
||||
| `scripts/rate_limit_integration.sh` | L188 | L191 | L231 | **BROKEN** |
|
||||
| `scripts/coraza_integration.sh` | L159 | L163 | L191 | **BROKEN** |
|
||||
|
||||
### Associated MEDIUM/LOW CVEs eliminated as side-effects
|
||||
### 2.2 Affected Scripts — curl Inside Container (SECONDARY)
|
||||
|
||||
| CVEs | Package | Count | Eliminated by |
|
||||
|------|---------|-------|---------------|
|
||||
| CVE-2025-14819, CVE-2025-15079, CVE-2025-14524, CVE-2025-13034, CVE-2025-14017 | `curl` | 5 × MEDIUM | Phase 1 |
|
||||
| CVE-2025-69652, CVE-2025-69644, CVE-2025-69651 | `binutils` | 3 × MEDIUM | Phase 2 |
|
||||
| File | Line | Command | Status |
|
||||
|------|------|---------|--------|
|
||||
| `scripts/crowdsec_startup_test.sh` | L179 | `docker exec ${CONTAINER_NAME} curl -sf http://127.0.0.1:8085/health` | **SILENT FAIL** |
|
||||
| `scripts/diagnose-test-env.sh` | L104 | `docker exec charon-e2e curl -sf http://localhost:8090/health` | **SILENT FAIL** |
|
||||
|
||||
### Expired Grype Ignore Rules
|
||||
### 2.3 NOT Affected
|
||||
|
||||
| Entry | Expiry | Status | Action |
|
||||
|-------|--------|--------|--------|
|
||||
| `CVE-2026-22184` (zlib) | 2026-03-14 | Expires tomorrow; underlying CVE already fixed via `apk upgrade --no-cache zlib` | **Remove entirely** |
|
||||
| `GHSA-69x3-g4r3-p962` (nebula) | 2026-03-05 | **Expired 8 days ago**; upstream fix still unavailable | **Extend to 2026-04-13** |
|
||||
| Component | Reason |
|
||||
|-----------|--------|
|
||||
| Dockerfile HEALTHCHECK | Already uses `wget` — targets Charon API `:8080`, not httpbin |
|
||||
| `.docker/docker-entrypoint.sh` | Already uses `wget` — Caddy readiness on `:2019` |
|
||||
| All `docker-compose` healthchecks | Already use `wget` |
|
||||
| Workflow YAML files | Use host-side `curl` (installed on `ubuntu-latest`), not `docker exec` |
|
||||
| `scripts/crowdsec_integration.sh` | Uses only host-side `curl`; does not use httpbin at all |
|
||||
| `scripts/integration-test.sh` | Uses `whoami` image (port 80), not go-httpbin |
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 1 — Remove `curl` from Runtime Image
|
||||
## 3. Remediation Plan
|
||||
|
||||
### Rationale
|
||||
### 3.1 Fix Strategy: Add `-e PORT=80` to go-httpbin Container
|
||||
|
||||
`curl` is present solely for:
|
||||
1. GeoLite2 DB download at build time (Dockerfile, runtime stage `RUN` block)
|
||||
2. HEALTHCHECK probe (Dockerfile `HEALTHCHECK` directive)
|
||||
3. Caddy admin API readiness poll (`.docker/docker-entrypoint.sh`)
|
||||
The **least invasive** fix is to set the `PORT` environment variable on the go-httpbin container so it listens on port 80, matching all existing health check URLs and proxy host `forward_port` values. This avoids cascading changes to URLs and Caddy configurations.
|
||||
|
||||
`busybox` (already installed on Alpine as a transitive dependency of `busybox-extras`, which is explicitly installed) provides `wget` with sufficient functionality for all three uses.
|
||||
**Alternative considered:** Change all health check URLs and `forward_port` values to 8080. Rejected because:
|
||||
- Requires more changes (health check URLs, forward_port, potentially Caddy route expectations)
|
||||
- The proxy host `forward_port` is the user-facing API field; port 80 is the natural default for HTTP backends
|
||||
|
||||
### 3.1 `wget` Translation Reference
|
||||
### 3.2 Exact Changes — Port Fix (4 files)
|
||||
|
||||
| `curl` invocation | `wget` equivalent | Notes |
|
||||
|-------------------|--------------------|-------|
|
||||
| `curl -fSL -m 10 "URL" -o FILE 2>/dev/null` | `wget -qO FILE -T 10 "URL" 2>/dev/null` | `-q` = quiet; `-T` = timeout (seconds); exits nonzero on failure |
|
||||
| `curl -fSL -m 30 --retry 3 "URL" -o FILE` | `wget -qO FILE -T 30 -t 4 "URL"` | `-t 4` = 4 total tries (1 initial + 3 retries); add `&& [ -s FILE ]` guard |
|
||||
| `curl -f http://HOST/path \|\| exit 1` | `wget -q -O /dev/null http://HOST/path \|\| exit 1` | HEALTHCHECK; wget exits nonzero on HTTP error |
|
||||
| `curl -sf http://HOST/path > /dev/null 2>&1` | `wget -qO /dev/null http://HOST/path 2>/dev/null` | Silent readiness probe |
|
||||
#### `scripts/cerberus_integration.sh` — Line 174
|
||||
|
||||
**busybox wget notes:**
|
||||
- `-T N` is per-connection timeout in seconds (equivalent to `curl --max-time`).
|
||||
- `-t N` is total number of tries, not retries; `-t 4` = 3 retries.
|
||||
- On download failure, busybox wget may leave a zero-byte or partial file at the output path. The `[ -s FILE ]` guard (`-s` = non-empty) prevents a corrupted placeholder from passing the sha256 check.
|
||||
|
||||
### 3.2 Dockerfile Changes
|
||||
|
||||
**File:** `Dockerfile`
|
||||
|
||||
**Change A — Remove `curl`, `binutils`, `libc-utils` from `apk add` (runtime stage, line ~413):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
RUN apk add --no-cache \
|
||||
bash ca-certificates sqlite-libs sqlite tzdata curl gettext libcap libcap-utils \
|
||||
c-ares binutils libc-utils busybox-extras \
|
||||
&& apk upgrade --no-cache zlib
|
||||
```diff
|
||||
-docker run -d --name ${BACKEND_CONTAINER} --network containers_default mccutchen/go-httpbin
|
||||
+docker run -d --name ${BACKEND_CONTAINER} --network containers_default -e PORT=80 mccutchen/go-httpbin
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
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
|
||||
#### `scripts/waf_integration.sh` — Line 167
|
||||
|
||||
```diff
|
||||
-docker run -d --name ${BACKEND_CONTAINER} --network containers_default mccutchen/go-httpbin
|
||||
+docker run -d --name ${BACKEND_CONTAINER} --network containers_default -e PORT=80 mccutchen/go-httpbin
|
||||
```
|
||||
|
||||
*(This single edit covers both Phase 1 and Phase 2 removals.)*
|
||||
#### `scripts/rate_limit_integration.sh` — Line 188
|
||||
|
||||
**Change B — GeoLite2 download block, CI path (line ~437):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
if curl -fSL -m 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
||||
-o /app/data/geoip/GeoLite2-Country.mmdb 2>/dev/null; then
|
||||
```diff
|
||||
-docker run -d --name ${BACKEND_CONTAINER} --network containers_default mccutchen/go-httpbin
|
||||
+docker run -d --name ${BACKEND_CONTAINER} --network containers_default -e PORT=80 mccutchen/go-httpbin
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
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; then
|
||||
#### `scripts/coraza_integration.sh` — Line 159
|
||||
|
||||
```diff
|
||||
-docker run -d --name coraza-backend --network containers_default mccutchen/go-httpbin
|
||||
+docker run -d --name coraza-backend --network containers_default -e PORT=80 mccutchen/go-httpbin
|
||||
```
|
||||
|
||||
**Change C — GeoLite2 download block, non-CI path (line ~445):**
|
||||
### 3.3 Exact Changes — curl→wget Inside Container (2 files)
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
if curl -fSL -m 30 --retry 3 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
||||
-o /app/data/geoip/GeoLite2-Country.mmdb; then
|
||||
if echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then
|
||||
#### `scripts/crowdsec_startup_test.sh` — Line 179
|
||||
|
||||
```diff
|
||||
-LAPI_HEALTH=$(docker exec ${CONTAINER_NAME} curl -sf http://127.0.0.1:8085/health 2>/dev/null || echo "FAILED")
|
||||
+LAPI_HEALTH=$(docker exec ${CONTAINER_NAME} wget -qO - http://127.0.0.1:8085/health 2>/dev/null || echo "FAILED")
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
|
||||
-T 30 -t 4 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb"; then
|
||||
if [ -s /app/data/geoip/GeoLite2-Country.mmdb ] && \
|
||||
echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then
|
||||
```
|
||||
Note: Using `wget -qO -` (output to stdout) instead of `wget -qO /dev/null` because the response body is captured in `$LAPI_HEALTH` and checked.
|
||||
|
||||
The `[ -s FILE ]` check is added before `sha256sum` to guard against wget leaving an empty file on partial failure.
|
||||
#### `scripts/diagnose-test-env.sh` — Line 104
|
||||
|
||||
**Change D — HEALTHCHECK directive (line ~581):**
|
||||
|
||||
Current:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/api/v1/health || exit 1
|
||||
```
|
||||
|
||||
New:
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://localhost:8080/api/v1/health || exit 1
|
||||
```
|
||||
|
||||
### 3.3 Entrypoint Changes
|
||||
|
||||
**File:** `.docker/docker-entrypoint.sh`
|
||||
|
||||
**Change E — Caddy readiness poll (line ~368):**
|
||||
|
||||
Current:
|
||||
```sh
|
||||
if curl -sf http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
|
||||
```
|
||||
|
||||
New:
|
||||
```sh
|
||||
if wget -qO /dev/null http://127.0.0.1:2019/config/ 2>/dev/null; then
|
||||
```diff
|
||||
-if docker exec charon-e2e curl -sf http://localhost:8090/health > /dev/null 2>&1; then
|
||||
+if docker exec charon-e2e wget -qO /dev/null http://localhost:8090/health 2>/dev/null; then
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 2 — Remove `binutils` and `libc-utils` from Runtime Image
|
||||
## 4. wget vs curl Flag Mapping Reference
|
||||
|
||||
### Rationale
|
||||
|
||||
`binutils` is installed solely for `objdump`, used in `.docker/docker-entrypoint.sh` to detect DWARF debug symbols when `CHARON_DEBUG=1`. The entrypoint already has a graceful fallback (lines ~401–404):
|
||||
|
||||
```sh
|
||||
else
|
||||
# objdump not available, try to run Delve anyway with a warning
|
||||
echo "Note: Cannot verify debug symbols (objdump not found). Attempting Delve..."
|
||||
run_as_charon /usr/local/bin/dlv exec "$bin_path" ...
|
||||
fi
|
||||
```
|
||||
|
||||
When `objdump` is absent the container functions correctly for all standard and debug-mode runs. The check is advisory.
|
||||
|
||||
`libc-utils` appears **only once** across the entire codebase (confirmed by grep across `*.sh`, `Dockerfile`, `*.yml`): as a sibling entry on the same `apk add` line as `binutils`. It provides glibc-compatible headers for musl-based Alpine and has no independent consumer in this image. It is safe to remove together with `binutils`.
|
||||
|
||||
### 4.1 Dockerfile Change
|
||||
|
||||
Already incorporated in Phase 1 Change A — the `apk add` line removes both `binutils` and `libc-utils` in a single edit. No additional changes are required.
|
||||
|
||||
### 4.2 Why Not Suppress Instead?
|
||||
|
||||
Suppressing in Grype requires two new ignore entries with expiry maintenance every 30 days indefinitely (no upstream Alpine fix exists). Removing the packages eliminates the CVEs permanently. There is no functional regression given the working fallback.
|
||||
| curl Flag | wget Equivalent | Meaning |
|
||||
|-----------|----------------|---------|
|
||||
| `-s` (silent) | `-q` (quiet) | Suppress progress output |
|
||||
| `-f` (fail silently on HTTP errors) | *(default behavior)* | wget exits nonzero on HTTP 4xx/5xx |
|
||||
| `-o FILE` (output to file) | `-O FILE` | Write output to specified file |
|
||||
| `-o /dev/null` | `-O /dev/null` | Discard response body |
|
||||
| `> /dev/null 2>&1` | `2>/dev/null` | Suppress stderr (wget is quiet with `-q`, only stderr needs redirect) |
|
||||
| `-m SECS` / `--max-time` | `-T SECS` | Connection timeout |
|
||||
| `--retry N` | `-t N+1` | Total attempts (wget counts initial try) |
|
||||
| `-S` (show error) | *(no equivalent)* | wget shows errors by default without `-q` |
|
||||
| `-L` (follow redirects) | *(default behavior)* | wget follows redirects by default |
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 3 — Update Expired Grype Ignore Rules
|
||||
## 5. Implementation Plan
|
||||
|
||||
**File:** `.grype.yaml`
|
||||
### Phase 1: Playwright Tests (Verification of Expected Behavior)
|
||||
|
||||
### 5.1 Remove `CVE-2026-22184` (zlib) Block
|
||||
No new Playwright tests needed. The integration scripts are bash-based CI workflows, not UI features. Existing workflow YAML files already serve as the test harness.
|
||||
|
||||
**Action:** Delete the entire `CVE-2026-22184` ignore entry.
|
||||
### Phase 2: Backend/Script Implementation
|
||||
|
||||
**Reason:** The Dockerfile runtime stage already contains `&& apk upgrade --no-cache zlib`, which upgrades zlib from 1.3.1-r2 to 1.3.2-r0, resolving CVE-2026-22184. Suppressing a resolved CVE creates false confidence and obscures scan accuracy. The entry's own removal criteria have been met: Alpine released `zlib 1.3.2-r0`.
|
||||
| Task | File | Change | Complexity |
|
||||
|------|------|--------|------------|
|
||||
| T1 | `scripts/cerberus_integration.sh` | Add `-e PORT=80` to docker run | Trivial |
|
||||
| T2 | `scripts/waf_integration.sh` | Add `-e PORT=80` to docker run | Trivial |
|
||||
| T3 | `scripts/rate_limit_integration.sh` | Add `-e PORT=80` to docker run | Trivial |
|
||||
| T4 | `scripts/coraza_integration.sh` | Add `-e PORT=80` to docker run | Trivial |
|
||||
| T5 | `scripts/crowdsec_startup_test.sh` | Replace `curl -sf` with `wget -qO -` | Trivial |
|
||||
| T6 | `scripts/diagnose-test-env.sh` | Replace `curl -sf` with `wget -qO /dev/null` | Trivial |
|
||||
|
||||
### 5.2 Extend `GHSA-69x3-g4r3-p962` (nebula) Expiry
|
||||
### Phase 3: Frontend Implementation
|
||||
|
||||
**Action:** Update the `expiry` field and review comment in the nebula block.
|
||||
N/A — no frontend changes required.
|
||||
|
||||
Current:
|
||||
```yaml
|
||||
expiry: "2026-03-05" # Re-evaluate in 14 days (2026-02-19 + 14 days)
|
||||
```
|
||||
### Phase 4: Integration and Testing
|
||||
|
||||
New:
|
||||
```yaml
|
||||
expiry: "2026-04-13" # Re-evaluated 2026-03-13: smallstep/certificates stable still v0.27.5, no nebula v1.10+ requirement. Extended 30 days.
|
||||
```
|
||||
1. **Local validation:** Run each integration script individually against a locally built `charon:local` image
|
||||
2. **CI validation:** Push branch and verify all four integration workflows pass:
|
||||
- `.github/workflows/cerberus-integration.yml`
|
||||
- `.github/workflows/waf-integration.yml`
|
||||
- `.github/workflows/rate-limit-integration.yml`
|
||||
- `.github/workflows/crowdsec-integration.yml`
|
||||
3. **Regression check:** Confirm E2E Playwright tests still pass (they don't touch these scripts, but verify no accidental breakage)
|
||||
|
||||
Update the review comment line:
|
||||
```
|
||||
# - Next review: 2026-04-13.
|
||||
# - Reviewed 2026-03-13: smallstep stable still v0.27.5 (no nebula v1.10+ requirement). Extended 30 days.
|
||||
# - Remove suppression immediately once upstream fixes.
|
||||
```
|
||||
### Phase 5: Documentation and Deployment
|
||||
|
||||
**Reason:** As of 2026-03-13, `smallstep/certificates` has not released a stable version requiring nebula v1.10+. The constraint analysis from 2026-02-19 remains valid. Expiry extended 30 days to 2026-04-13.
|
||||
No documentation changes needed. The fix is internal to CI scripts.
|
||||
|
||||
---
|
||||
|
||||
## 6. File Change Summary
|
||||
|
||||
| File | Change | Scope |
|
||||
| File | Change | Lines |
|
||||
|------|--------|-------|
|
||||
| `Dockerfile` | Remove `curl`, `binutils`, `libc-utils` from `apk add` | Line ~413–415 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in GeoLite2 CI download path | Line ~437–441 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in GeoLite2 non-CI path; add `[ -s FILE ]` guard | Line ~445–452 |
|
||||
| `Dockerfile` | Replace `curl` with `wget` in HEALTHCHECK | Line ~581 |
|
||||
| `.docker/docker-entrypoint.sh` | Replace `curl` with `wget` in Caddy readiness poll | Line ~368 |
|
||||
| `.grype.yaml` | Delete `CVE-2026-22184` (zlib) ignore block entirely | zlib block |
|
||||
| `.grype.yaml` | Extend `GHSA-69x3-g4r3-p962` expiry to 2026-04-13; update review comment | nebula block |
|
||||
| `scripts/cerberus_integration.sh` | Add `-e PORT=80` to go-httpbin `docker run` | L174 |
|
||||
| `scripts/waf_integration.sh` | Add `-e PORT=80` to go-httpbin `docker run` | L167 |
|
||||
| `scripts/rate_limit_integration.sh` | Add `-e PORT=80` to go-httpbin `docker run` | L188 |
|
||||
| `scripts/coraza_integration.sh` | Add `-e PORT=80` to go-httpbin `docker run` | L159 |
|
||||
| `scripts/crowdsec_startup_test.sh` | Replace `curl -sf` with `wget -qO -` in docker exec | L179 |
|
||||
| `scripts/diagnose-test-env.sh` | Replace `curl -sf` with `wget -qO /dev/null` in docker exec | L104 |
|
||||
|
||||
**Total: 6 one-line changes across 6 files.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Commit Slicing Strategy
|
||||
|
||||
**Single PR** — all changes are security-related and tightly coupled. Splitting curl removal from binutils removal would produce an intermediate commit with partially resolved HIGHs, offering no validation benefit and complicating rollback.
|
||||
### Decision: Single PR
|
||||
|
||||
Suggested commit message:
|
||||
**Reasoning:**
|
||||
- All 6 changes are trivial one-line fixes
|
||||
- Total diff is ~12 lines (6 deletions + 6 additions)
|
||||
- All changes are logically related (integration test infrastructure fix)
|
||||
- No cross-domain concerns (all bash scripts in `scripts/`)
|
||||
- Review size is well under the 200-line threshold for splitting
|
||||
- No risk of partial deployment causing issues
|
||||
|
||||
### PR-1: Fix integration workflow httpbin startup and curl-in-container issues
|
||||
|
||||
**Scope:** All 6 file changes
|
||||
**Files:** `scripts/{cerberus,waf,rate_limit,coraza}_integration.sh`, `scripts/crowdsec_startup_test.sh`, `scripts/diagnose-test-env.sh`
|
||||
**Dependencies:** None
|
||||
**Validation gate:** All four integration workflows pass in CI
|
||||
|
||||
**Suggested commit message:**
|
||||
```
|
||||
fix(security): remove curl and binutils from runtime image
|
||||
fix(ci): add PORT=80 to go-httpbin containers and replace curl with wget in docker exec
|
||||
|
||||
Replace curl with busybox wget for GeoLite2 downloads, HEALTHCHECK,
|
||||
and the Caddy readiness probe. Remove binutils and libc-utils from the
|
||||
runtime image; the entrypoint objdump check has a documented fallback
|
||||
for missing objdump. Eliminates CVE-2026-3805 (curl HIGH), CVE-2025-69650
|
||||
and CVE-2025-69649 (binutils HIGH), plus 8 associated MEDIUM findings.
|
||||
mccutchen/go-httpbin defaults to port 8080, but all integration scripts
|
||||
expected port 80 from the previous kennethreitz/httpbin image. Add
|
||||
-e PORT=80 to the docker run commands in cerberus, waf, rate_limit,
|
||||
and coraza integration scripts.
|
||||
|
||||
Remove the now-resolved CVE-2026-22184 (zlib) suppression from
|
||||
.grype.yaml and extend GHSA-69x3-g4r3-p962 (nebula) expiry to
|
||||
2026-04-13 pending upstream smallstep/certificates update.
|
||||
Also replace remaining docker exec curl commands with wget in
|
||||
crowdsec_startup_test.sh and diagnose-test-env.sh, since curl is not
|
||||
installed in the Alpine runtime container.
|
||||
|
||||
Fixes: httpbin health check timeout ("httpbin not starting") in all
|
||||
four integration workflows.
|
||||
```
|
||||
|
||||
**Rollback:** `git revert <commit>` — safe and instant. No database migrations, no API changes, no user-facing impact.
|
||||
|
||||
---
|
||||
|
||||
## 8. Expected Scan Results After Fix
|
||||
## 8. Supporting Files Review
|
||||
|
||||
| Metric | Before | After | Delta |
|
||||
|--------|--------|-------|-------|
|
||||
| HIGH count | 3 | **0** | −3 |
|
||||
| MEDIUM count | ~13 | ~5 | −8 |
|
||||
| LOW count | ~2 | ~0 | −2 |
|
||||
| `fail-on-severity: high` | ❌ FAIL | ✅ PASS | — |
|
||||
| CI supply-chain scan | ❌ BLOCKED | ✅ GREEN | — |
|
||||
|
||||
Remaining MEDIUMs after fix (~5):
|
||||
- `busybox` / `busybox-extras` / `ssl_client` — CVE-2025-60876 (CRLF injection in wget/ssl_client; no Alpine fix; Charon application code does not invoke `wget` directly at runtime)
|
||||
| File | Review Result | Action Needed |
|
||||
|------|--------------|---------------|
|
||||
| `.gitignore` | No changes needed — no new files or artifacts | None |
|
||||
| `codecov.yml` | No changes needed — integration scripts are not coverage-tracked | None |
|
||||
| `.dockerignore` | No changes needed — scripts are not copied into Docker image | None |
|
||||
| `Dockerfile` | Already correct — HEALTHCHECK uses wget, no httpbin references | None |
|
||||
| `.docker/docker-entrypoint.sh` | Already correct — uses wget for Caddy readiness | None |
|
||||
| `docker-compose*.yml` | Already correct — all healthchecks use wget | None |
|
||||
| Workflow YAML files | Already correct — use host-side curl for debug dumps | None |
|
||||
|
||||
---
|
||||
|
||||
## 9. Validation Steps
|
||||
## 9. Acceptance Criteria
|
||||
|
||||
1. Rebuild Docker image: `docker build -t charon:test .`
|
||||
2. Run Grype scan: `grype charon:test` — confirm zero HIGH findings
|
||||
3. Confirm HEALTHCHECK probe passes: start container, check `docker inspect` for `healthy` status
|
||||
4. Confirm Caddy readiness: inspect entrypoint logs for `"Caddy is ready!"`
|
||||
5. Run E2E suite: `npx playwright test --project=firefox`
|
||||
6. Push branch and confirm CI supply-chain workflow exits green
|
||||
- [ ] `scripts/cerberus_integration.sh` starts go-httpbin with `-e PORT=80`
|
||||
- [ ] `scripts/waf_integration.sh` starts go-httpbin with `-e PORT=80`
|
||||
- [ ] `scripts/rate_limit_integration.sh` starts go-httpbin with `-e PORT=80`
|
||||
- [ ] `scripts/coraza_integration.sh` starts go-httpbin with `-e PORT=80`
|
||||
- [ ] `scripts/crowdsec_startup_test.sh` uses `wget` instead of `curl` for LAPI health check
|
||||
- [ ] `scripts/diagnose-test-env.sh` uses `wget` instead of `curl` for CrowdSec LAPI check
|
||||
- [ ] CI workflow `cerberus-integration` passes
|
||||
- [ ] CI workflow `waf-integration` passes
|
||||
- [ ] CI workflow `rate-limit-integration` passes
|
||||
- [ ] CI workflow `crowdsec-integration` passes
|
||||
- [ ] No regression in E2E Playwright tests
|
||||
- [ ] `grep -r "docker exec.*curl" scripts/` returns zero matches (excluding comments/echo hints)
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
# QA/Security Audit Report — CVE Remediation (curl / binutils / libc-utils)
|
||||
# QA Report: Integration Script Port Fix & curl→wget Remediation
|
||||
|
||||
**Date**: 2026-03-13
|
||||
**Branch**: `feature/beta-release`
|
||||
**Scope**: Full audit after removing `curl`, `binutils`, `libc-utils` from runtime image; substituting `wget`; updating `.grype.yaml`
|
||||
**Auditor**: QA Security Agent
|
||||
**Date:** 2026-03-14
|
||||
**Branch:** `feature/beta-release`
|
||||
**Scope:** 6 shell scripts in `scripts/` — one-line changes each
|
||||
**Reviewer:** QA Security Agent
|
||||
|
||||
---
|
||||
|
||||
## Overall Verdict: PASS
|
||||
|
||||
All blocking gates cleared. The three HIGH CVEs targeted by this remediation are confirmed absent from the runtime image. One additional critical gap (docker-compose health checks still referencing the removed `curl` binary) was discovered and corrected during Step 1.
|
||||
All 6 modified scripts pass syntax validation, ShellCheck, pre-commit hooks, verification greps, security review, and Trivy scanning. No new issues were introduced. The changes are minimal, correct, and safe for merge.
|
||||
|
||||
---
|
||||
|
||||
## CVE Remediation Verification
|
||||
## Change Summary
|
||||
|
||||
### Confirmed Eliminated
|
||||
|
||||
| CVE | Package | Method | Verified |
|
||||
|-----|---------|--------|---------|
|
||||
| CVE-2026-3805 (HIGH) | `curl` 8.17.0-r1 | Removed from `apk add` in runtime stage | ✅ |
|
||||
| CVE-2025-69650 (HIGH) | `binutils` 2.45.1-r0 | Removed from `apk add` in runtime stage | ✅ |
|
||||
| CVE-2025-69649 (HIGH) | `binutils` 2.45.1-r0 | Removed from `apk add` in runtime stage | ✅ |
|
||||
|
||||
Side-effect MEDIUMs eliminated: 8 (5× curl MEDIUMs, 3× binutils MEDIUMs).
|
||||
|
||||
### .grype.yaml State
|
||||
|
||||
| Entry | Status |
|
||||
|-------|--------|
|
||||
| `CVE-2026-22184` (zlib) | Removed — resolved by upstream Alpine fix |
|
||||
| `GHSA-69x3-g4r3-p962` (nebula in caddy) | Retained — extended to 2026-04-12; upstream still pinned to old nebula |
|
||||
| File | Change | Line |
|
||||
|------|--------|------|
|
||||
| `scripts/cerberus_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L174 |
|
||||
| `scripts/waf_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L167 |
|
||||
| `scripts/rate_limit_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L187 |
|
||||
| `scripts/coraza_integration.sh` | Add `-e PORT=80` to `docker run ... mccutchen/go-httpbin` | L158 |
|
||||
| `scripts/crowdsec_startup_test.sh` | Replace `curl -sf` with `wget -qO -` in `docker exec` | L179 |
|
||||
| `scripts/diagnose-test-env.sh` | Replace `curl -sf` with `wget -qO /dev/null` in `docker exec` | L104 |
|
||||
|
||||
---
|
||||
|
||||
@@ -38,180 +30,152 @@ Side-effect MEDIUMs eliminated: 8 (5× curl MEDIUMs, 3× binutils MEDIUMs).
|
||||
|
||||
| # | Gate | Result | Details |
|
||||
|---|------|--------|---------|
|
||||
| 1 | E2E Container Rebuild | **PASS** | Built fresh; healthy in <5s after fixing compose health checks |
|
||||
| 2 | E2E Playwright Tests | **PASS** | 868 passed, 1 pre-existing failure, 0 flaky |
|
||||
| 3 | Local Patch Coverage Preflight | **PASS** | 93.1% overall (threshold: 90%) |
|
||||
| 4 | Backend Unit Tests & Coverage | **PASS** | 88.2% line coverage (gate: ≥87%) |
|
||||
| 5 | Frontend Unit Tests & Coverage | **PASS** | 89.73% line coverage |
|
||||
| 6 | TypeScript Type Check | **PASS** | 0 errors |
|
||||
| 7 | Pre-commit Hooks | **SKIP** | No `.pre-commit-config.yaml` in project |
|
||||
| 8 | Trivy Filesystem Scan | **PASS** | 0 CRITICAL, 0 HIGH, 0 total |
|
||||
| 9 | Docker Image Scan (Grype) | **PASS** | 0 CRITICAL, 0 HIGH |
|
||||
| 10 | CodeQL (Go + JavaScript) | **PASS** | 0 errors, 0 warnings |
|
||||
| 11 | Linting (ESLint + golangci-lint) | **PASS** | 0 errors; pre-existing warnings only |
|
||||
| 1 | Syntax Validation (`bash -n`) | **PASS** | All 6 scripts parse cleanly |
|
||||
| 2 | ShellCheck (error severity) | **PASS** | 0 errors; matches lefthook `--severity=error` |
|
||||
| 3 | ShellCheck (all severities) | **PASS** | No findings on any modified line; all findings pre-existing |
|
||||
| 4 | Pre-commit Hooks (lefthook) | **PASS** | All 6 hooks passed (shellcheck, actionlint, yaml, whitespace, eof, dockerfile) |
|
||||
| 5 | Verification: go-httpbin PORT | **PASS** | 4/4 `docker run` lines have `-e PORT=80` |
|
||||
| 6 | Verification: docker exec curl | **PASS** | 0 executed curl calls; 2 echo-only references (hints) |
|
||||
| 7 | Security Review | **PASS** | No secrets, credentials, injection vectors, or Gotify tokens |
|
||||
| 8 | Trivy Filesystem Scan | **PASS** | 0 secrets, 0 misconfigurations |
|
||||
|
||||
---
|
||||
|
||||
## Step Details
|
||||
## 1. Syntax Validation (`bash -n`)
|
||||
|
||||
### 1. E2E Container Rebuild
|
||||
|
||||
Image rebuilt from scratch (212s build time). Container reached `healthy` in <5s. Confirmed HEALTHCHECK passes against `/api/v1/health` using `wget`. Image SHA: `ae066857e8c0`.
|
||||
|
||||
> **See [Incidental Findings](#incidental-findings)** — all five docker-compose files still had `curl` in their health check definitions; corrected before rebuild was confirmed healthy.
|
||||
|
||||
### 2. E2E Playwright Tests (Chromium + Firefox + WebKit)
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Passed | 868 |
|
||||
| Failed | 1 |
|
||||
| Skipped | 1007 |
|
||||
| Flaky | 0 |
|
||||
| Duration | ~30 min |
|
||||
|
||||
**Failure:**
|
||||
```
|
||||
core/multi-component-workflows.spec.ts > Multi-Component Workflows
|
||||
› User with proxy creation role is configured for proxy management [firefox]
|
||||
Error: invalid credentials
|
||||
```
|
||||
Pre-existing test-isolation flakiness with dynamically-created users. Not related to CVE changes. No regression introduced.
|
||||
|
||||
### 3. Local Patch Coverage Preflight
|
||||
|
||||
| Scope | Changed Lines | Covered Lines | Coverage |
|
||||
|-------|--------------|---------------|---------|
|
||||
| Overall | 58 | 54 | **93.1%** ✅ |
|
||||
| Backend | 52 | 48 | **92.3%** |
|
||||
| Frontend | 6 | 6 | **100.0%** |
|
||||
|
||||
Uncovered: `notification_service.go` L462–463, L466–467 (dead code paths, accepted).
|
||||
|
||||
### 4. Backend Unit Tests & Coverage
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Statement coverage | 87.9% |
|
||||
| Line coverage | **88.2%** |
|
||||
| Gate (min) | 87% |
|
||||
| Status | ✅ Met |
|
||||
|
||||
### 5. Frontend Unit Tests & Coverage
|
||||
|
||||
Data from `frontend/coverage/coverage-summary.json` (2026-03-13 06:05). No frontend files were modified by this remediation.
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Lines | **89.73%** |
|
||||
| Statements | 89.01% |
|
||||
| Functions | 86.18% |
|
||||
| Branches | 81.21% |
|
||||
|
||||
### 6. TypeScript Type Check
|
||||
|
||||
```
|
||||
tsc --noEmit → exit 0 (0 errors)
|
||||
```
|
||||
|
||||
### 7. Pre-commit Hooks
|
||||
|
||||
**SKIP** — No `.pre-commit-config.yaml` exists in the project. Git hooks are managed via lefthook; ESLint and golangci-lint run explicitly in Step 11.
|
||||
|
||||
### 8. Trivy Filesystem Scan
|
||||
|
||||
| Target | Type | CRITICAL | HIGH | MEDIUM | LOW |
|
||||
|--------|------|---------|------|--------|-----|
|
||||
| `backend/go.mod` | gomod | 0 | 0 | 0 | 0 |
|
||||
| `frontend/package-lock.json` | npm | 0 | 0 | 0 | 0 |
|
||||
| `package-lock.json` | npm | 0 | 0 | 0 | 0 |
|
||||
|
||||
Scanners: `vuln,secret`. Zero findings.
|
||||
|
||||
### 9. Docker Image Scan (Grype)
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| 🔴 CRITICAL | **0** |
|
||||
| 🟠 HIGH | **0** |
|
||||
| 🟡 MEDIUM | 4 |
|
||||
| 🟢 LOW | 2 |
|
||||
|
||||
**MEDIUM (non-blocking, no Alpine fix available):**
|
||||
|
||||
| CVE | Package(s) | Version |
|
||||
|-----|-----------|---------|
|
||||
| CVE-2025-60876 | `busybox`, `busybox-binsh`, `busybox-extras`, `ssl_client` | 1.37.0-r30 |
|
||||
|
||||
**LOW (non-blocking):**
|
||||
|
||||
| ID | Package | Notes |
|
||||
|----|---------|-------|
|
||||
| GHSA-fw7p-63qq-7hpr | `filippo.io/edwards25519` v1.1.0 | 2 instances; fixed in v1.1.1 |
|
||||
|
||||
**Suppressed (documented in `.grype.yaml`):**
|
||||
|
||||
| ID | Package | Expiry | Justification |
|
||||
|----|---------|--------|---------------|
|
||||
| GHSA-69x3-g4r3-p962 | nebula (embedded in caddy) | 2026-04-12 | smallstep/certificates still requires nebula v1.9.x; reviewed 2026-03-13 |
|
||||
|
||||
### 10. CodeQL Static Analysis
|
||||
|
||||
| Language | Errors | Warnings | Files Scanned |
|
||||
|----------|--------|---------|---------------|
|
||||
| Go | 0 | 0 | Full backend |
|
||||
| JavaScript/TypeScript | 0 | 0 | 354/354 files |
|
||||
|
||||
### 11. Linting
|
||||
|
||||
**ESLint (frontend):**
|
||||
- 0 errors, 857 warnings (all pre-existing non-blocking patterns)
|
||||
- Exit 0
|
||||
|
||||
**golangci-lint (backend):**
|
||||
- 0 errors, 53 warnings (all pre-existing)
|
||||
- gocritic×50, gosec×2, bodyclose×1
|
||||
- Pre-existing gosec findings (not introduced by this change):
|
||||
- `mail_service.go:195` G203 — `template.HTML()` cast (no XSS vector in current usage)
|
||||
- `docker_service_test.go:231` G306 — `os.WriteFile(0o660)` in test fixture
|
||||
- Exit 0
|
||||
| Script | Result |
|
||||
|--------|--------|
|
||||
| `scripts/cerberus_integration.sh` | PASS |
|
||||
| `scripts/waf_integration.sh` | PASS |
|
||||
| `scripts/rate_limit_integration.sh` | PASS |
|
||||
| `scripts/coraza_integration.sh` | PASS |
|
||||
| `scripts/crowdsec_startup_test.sh` | PASS |
|
||||
| `scripts/diagnose-test-env.sh` | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Incidental Findings
|
||||
## 2. ShellCheck
|
||||
|
||||
### CRITICAL — Corrected During Audit
|
||||
### At error severity (`--severity=error`, matching lefthook pre-commit)
|
||||
|
||||
**docker-compose health checks still referenced `curl` after CVE remediation**
|
||||
**Result: PASS** — Zero errors across all 6 scripts. Exit code 0.
|
||||
|
||||
All five docker-compose files retained `curl`-based `healthcheck.test` definitions. Since `curl` is no longer present in the runtime image, any container started from these files would enter and remain in the `unhealthy` state. This was confirmed during Step 1 (container failed health checks immediately after first rebuild).
|
||||
### At default severity (full informational audit)
|
||||
|
||||
**Root cause**: The Dockerfile `HEALTHCHECK` and `.docker/docker-entrypoint.sh` were correctly migrated to `wget`, but the compose `healthcheck` overrides were not updated in the same commit.
|
||||
Exit code 1 (findings present, all `note` or `warning` severity).
|
||||
|
||||
**Files corrected:**
|
||||
| Script | Findings | Severity | On Modified Lines? |
|
||||
|--------|----------|----------|--------------------|
|
||||
| `cerberus_integration.sh` | 2× SC2086 (unquoted variable) | note | No (L204, L219) |
|
||||
| `waf_integration.sh` | ~30× SC2317 (unreachable code in trap), 3× SC2086 | note | No |
|
||||
| `rate_limit_integration.sh` | 9× SC2086 | note | No |
|
||||
| `coraza_integration.sh` | 10× SC2086, 2× SC2034 (unused variable) | note/warning | No |
|
||||
| `crowdsec_startup_test.sh` | ~10× SC2317, 1× SC2086 | note | No |
|
||||
| `diagnose-test-env.sh` | 1× SC2034 (unused variable) | warning | No |
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `.docker/compose/docker-compose.playwright-local.yml` | `curl -fsS` → `wget -qO /dev/null` |
|
||||
| `.docker/compose/docker-compose.playwright-ci.yml` | `curl -sf` → `wget -qO /dev/null` |
|
||||
| `.docker/compose/docker-compose.test.yml` | `["CMD","curl","-f",...]` → `["CMD-SHELL","wget -qO /dev/null ... || exit 1"]` |
|
||||
| `.docker/compose/docker-compose.local.yml` | `curl -fsS` → `wget -qO /dev/null` |
|
||||
| `.docker/compose/docker-compose.yml` | `curl -fsS` → `wget -qO /dev/null` |
|
||||
|
||||
### Minor — Corrected During Audit
|
||||
|
||||
**Stale comment in Dockerfile**
|
||||
|
||||
Removed comment `# binutils provides objdump for debug symbol detection in docker-entrypoint.sh` from Dockerfile — `binutils` is no longer installed; the comment was stale and misleading.
|
||||
**No ShellCheck findings on any of the 6 modified lines.** All findings are pre-existing.
|
||||
|
||||
---
|
||||
|
||||
## Remediation Confirmation
|
||||
## 3. Pre-commit Hooks (lefthook)
|
||||
|
||||
| Blocker | Status |
|
||||
|---------|--------|
|
||||
| CVE-2026-3805 (`curl` HIGH) | ✅ Eliminated — `curl` removed from runtime image |
|
||||
| CVE-2025-69650 (`binutils` HIGH) | ✅ Eliminated — `binutils` removed from runtime image |
|
||||
| CVE-2025-69649 (`binutils` HIGH) | ✅ Eliminated — `binutils` removed from runtime image |
|
||||
| `libc-utils` removed from runtime | ✅ Confirmed absent |
|
||||
| `wget` substituted everywhere `curl` was used | ✅ Dockerfile, entrypoint, all 5 compose files |
|
||||
Ran `lefthook run pre-commit`:
|
||||
|
||||
| Hook | Result | Duration |
|
||||
|------|--------|----------|
|
||||
| check-yaml | PASS | 1.93s |
|
||||
| actionlint | PASS | 4.36s |
|
||||
| end-of-file-fixer | PASS | 9.23s |
|
||||
| trailing-whitespace | PASS | 9.49s |
|
||||
| dockerfile-check | PASS | 10.41s |
|
||||
| shellcheck | PASS | 11.24s |
|
||||
|
||||
Hooks for Go, TypeScript, and Semgrep correctly skipped (no matching files).
|
||||
|
||||
---
|
||||
|
||||
## 4. Verification Greps
|
||||
|
||||
### 4a. All `mccutchen/go-httpbin` `docker run` instances have `-e PORT=80`
|
||||
|
||||
```
|
||||
scripts/cerberus_integration.sh:174: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||||
scripts/waf_integration.sh:167: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||||
scripts/rate_limit_integration.sh:187:docker run ... -e PORT=80 mccutchen/go-httpbin
|
||||
scripts/coraza_integration.sh:158: docker run ... -e PORT=80 mccutchen/go-httpbin
|
||||
```
|
||||
|
||||
Remaining `mccutchen/go-httpbin` matches are `docker pull` lines (no `-e PORT` needed).
|
||||
|
||||
**Result: PASS** — 4/4 confirmed.
|
||||
|
||||
### 4b. Zero executed `docker exec ... curl` calls
|
||||
|
||||
Only 2 matches found in `scripts/verify_crowdsec_app_config.sh` (L94–95) — both inside `echo` statements (user hint text, not executed). Confirmed by manual review.
|
||||
|
||||
**Result: PASS** — 0 executed `docker exec ... curl` calls.
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Review
|
||||
|
||||
| Check | Result | Notes |
|
||||
|-------|--------|-------|
|
||||
| Secrets/credentials in diff | PASS | `git diff | grep -iE "password\|secret\|key\|token\|credential\|auth"` — no matches |
|
||||
| Gotify tokens | PASS | `grep -rn "Gotify\|gotify\|token="` across all 6 scripts — no matches |
|
||||
| Injection vectors | PASS | `-e PORT=80` is a static literal; no user-controlled input flows into new code |
|
||||
| Command injection | PASS | `wget -qO` flags are hardcoded; no interpolated user input |
|
||||
| SSRF | N/A | URLs are internal container addresses (127.0.0.1, localhost) in CI-only scripts |
|
||||
| Sensitive data in logs | PASS | No new log/echo statements added |
|
||||
| URL query parameters | PASS | No tokenized URLs (e.g., `?token=...`) in changed or adjacent code |
|
||||
|
||||
---
|
||||
|
||||
## 6. Trivy Filesystem Scan
|
||||
|
||||
Scanners: `secret,misconfig`. Severity filter: `CRITICAL,HIGH,MEDIUM`.
|
||||
|
||||
| Target | Type | Secrets | Misconfigurations |
|
||||
|--------|------|---------|-------------------|
|
||||
| `backend/go.mod` | gomod | — | — |
|
||||
| `frontend/package-lock.json` | npm | — | — |
|
||||
| `package-lock.json` | npm | — | — |
|
||||
| `Dockerfile` | dockerfile | — | 0 |
|
||||
| `playwright/.auth/user.json` | text | 0 | — |
|
||||
|
||||
**Result: 0 findings. Exit code 0.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Scope Exclusions
|
||||
|
||||
| Check | Excluded? | Justification |
|
||||
|-------|-----------|---------------|
|
||||
| E2E Playwright tests | Yes | Scripts are CI-only; no UI changes |
|
||||
| Backend unit coverage | Yes | No Go code changes |
|
||||
| Frontend unit coverage | Yes | No TypeScript/React changes |
|
||||
| Docker image scan | Yes | No Dockerfile or image changes |
|
||||
| CodeQL | Yes | No Go or JavaScript changes |
|
||||
| GORM security scan | Yes | No model/database changes |
|
||||
| Local patch coverage report | Yes | No application code; scripts not coverage-tracked |
|
||||
|
||||
---
|
||||
|
||||
## 8. Pre-existing Issues (Not Introduced by This Change)
|
||||
|
||||
| Category | Count | Scripts Affected | Risk |
|
||||
|----------|-------|-----------------|------|
|
||||
| SC2086 (unquoted variables) | ~25 | All 6 | Low — CI-controlled variables |
|
||||
| SC2317 (unreachable code) | ~40 | waf, crowdsec | None — trap cleanup functions (ShellCheck false positive) |
|
||||
| SC2034 (unused variables) | 3 | coraza, diagnose | Low — may be planned for future use |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Validation (CI)
|
||||
|
||||
The integration scripts cannot be executed locally without a built `charon:local` image and Docker network. Full end-to-end validation will occur when the PR triggers CI:
|
||||
|
||||
- `.github/workflows/cerberus-integration.yml`
|
||||
- `.github/workflows/waf-integration.yml`
|
||||
- `.github/workflows/rate-limit-integration.yml`
|
||||
- `.github/workflows/crowdsec-integration.yml`
|
||||
|
||||
Reference in New Issue
Block a user