Source-built drop-in replacement for nginx:1.25-bookworm with two CVE remediations using two different engineering approaches.
| Baseline | Fixed | |
|---|---|---|
| nginx | 1.25.5-1~bookworm |
1.25.5-1~bookworm+echo1 |
| FreeType | 2.12.1+dfsg-5 |
2.12.1+dfsg-5+deb12u4 |
| Compatibility | — | 4/4 tests passing |
| Image size | 71.0 MB | 39.4 MB |
| CVE | Component | Technique | Verification |
|---|---|---|---|
| CVE-2025-27363 | FreeType | Dependency version bump | No longer reported by Trivy or Grype |
| CVE-2026-60005 | nginx | Upstream source patch backport | Patch provenance + build verification + OpenVEX |
The nginx version intentionally remains 1.25.5. This demonstrates a real source-level security backport rather than hiding the remediation behind an upstream version upgrade.
make build
make testOr on Windows directly:
powershell -ExecutionPolicy Bypass -File .\build.ps1
powershell -ExecutionPolicy Bypass -File .\test.ps1Expected compatibility result:
Passed: 4/4
For security evidence:
make scan
make vexThe remainder of this README documents the build process, CVE evidence, VEX rationale, compatibility work, residual risk, and engineering tradeoffs.
.
├── README.md
├── Containerfile
├── build.ps1
├── test.ps1
│
├── build/
│ ├── nginx/
│ │ ├── Dockerfile.build
│ │ ├── build.sh
│ │ └── patches/
│ │ └── CVE-2026-60005.patch
│ │
│ └── freetype/
│ ├── Dockerfile.build
│ └── build.sh
│
├── artifacts/
│ ├── nginx/
│ │ └── nginx_1.25.5-1~bookworm+echo1_amd64.deb
│ └── freetype/
│ └── libfreetype6_2.12.1+dfsg-5+deb12u4_amd64.deb
│
├── test/
│ ├── compare.py
│ └── nginx.conf
│
├── vex/
│ └── openvex.json
│
├── reports/
│ ├── fixed-trivy.txt
│ ├── fixed-grype.txt
│ ├── fixed-trivy-vex.txt
│ └── compatibility.txt
│
├── docker-entrypoint.sh
└── docker-entrypoint.d/
Baseline image:
nginx:1.25-bookworm
Relevant baseline package versions:
nginx 1.25.5-1~bookworm
libfreetype6 2.12.1+dfsg-5
The baseline image was scanned independently with Trivy and Grype.
Trivy reported 651 vulnerabilities in the baseline image:
UNKNOWN 14
LOW 221
MEDIUM 261
HIGH 135
CRITICAL 20
TOTAL 651
The exercise intentionally focuses on two vulnerabilities rather than attempting to remediate the complete historical vulnerability set.
| CVE | Component | Severity observed | Fix method | Result |
|---|---|---|---|---|
| CVE-2025-27363 | FreeType / libfreetype6 | High | Dependency version bump | Fixed and no longer reported by Trivy or Grype |
| CVE-2026-60005 | nginx ngx_http_slice_module | High in scan | Upstream patch backport | Patch applied to nginx 1.25.5; VEX records the custom package as fixed |
The baseline contained:
libfreetype6 2.12.1+dfsg-5
Debian Bookworm provides the security-fixed version:
2.12.1+dfsg-5+deb12u4
Rather than downloading a pre-built replacement binary, the fixed Debian FreeType source package is retrieved and compiled inside a Debian Bookworm build environment.
The resulting package is then installed into the final container:
libfreetype6_2.12.1+dfsg-5+deb12u4_amd64.deb
After rebuilding the image, CVE-2025-27363 was absent from both the Trivy and Grype results.
Evidence:
- Debian Security Tracker — CVE-2025-27363
- Debian DSA-5880-1
reports/fixed-trivy.txtreports/fixed-grype.txt
The second remediation deliberately retains nginx 1.25.5 and backports the upstream security change instead of upgrading nginx.
The baseline nginx build includes:
--with-http_slice_module
making the affected module relevant to this image.
The upstream fix was backported as:
build/nginx/patches/CVE-2026-60005.patch
The patch modifies:
src/http/ngx_http_variables.c
The build performs a dry-run before applying the patch:
patch --dry-run -p1
and only then applies it to the nginx 1.25.5 source tree.
During the backport, patch reported an offset of 59 lines. This is expected because the security change originated from a newer upstream source tree. The surrounding source context matched successfully and the patch applied cleanly.
The resulting package remains:
nginx 1.25.5-1~bookworm+echo1
This is intentional: the goal is to demonstrate a security backport rather than hide the remediation behind a version upgrade.
Evidence:
- Official nginx security advisory for CVE-2026-60005
- Upstream security change used for the backport
build/nginx/patches/CVE-2026-60005.patch
Both Trivy and Grype continued to report CVE-2026-60005 after the backport.
This is expected for version-based vulnerability matching because the package continues to identify itself as nginx 1.25.5.
An OpenVEX document is therefore included:
vex/openvex.json
The statement identifies:
CVE-2026-60005
for the custom package and records its status as:
fixed
A VEX-aware Trivy scan successfully associates the finding with the supplied VEX document and reports:
CVE-2026-60005 HIGH fixed ... /vex/openvex.json
This provides machine-readable evidence that the vulnerability has been remediated even though version-only matching still associates nginx 1.25.5 with the CVE.
Requirements:
- Docker
- PowerShell
- Internet access for source/package retrieval during the build
Run the complete build from the repository root:
powershell -ExecutionPolicy Bypass -File .\build.ps1The build:
- Creates the nginx builder.
- Downloads nginx 1.25.5 source.
- Applies the CVE-2026-60005 backport.
- Compiles nginx.
- Produces the custom nginx
.deb. - Retrieves and builds the fixed Debian FreeType source package.
- Produces the fixed FreeType
.deb. - Builds the final container from
debian:bookworm-slim.
No pre-built nginx binary from the original image is copied into the final container.
The final image is built from:
debian:bookworm-slim
and installs the packages generated by this repository.
Final package versions:
nginx 1.25.5-1~bookworm+echo1
libfreetype6 2.12.1+dfsg-5+deb12u4
Container behavior is retained from the original image, including:
Entrypoint: /docker-entrypoint.sh
Cmd: nginx -g "daemon off;"
Exposed: 80/tcp
User: ""
WorkingDir: ""
The nginx service account is also preserved with UID/GID 101.
Image size was measured using:
docker image inspect <image> --format "{{.Size}}"
Results:
| Image | Bytes | Approx. size |
|---|---|---|
nginx:1.25-bookworm |
71,005,258 | 71.0 MB |
echo-nginx:1.25-bookworm |
39,394,813 | 39.4 MB |
The custom image is approximately:
31.6 MB smaller
or approximately:
44.5% smaller
than the original according to the same Docker image-size measurement.
Compatibility testing is implemented in:
test/compare.py
The test launches both:
nginx:1.25-bookworm
echo-nginx:1.25-bookworm
and compares their responses.
The following scenarios are covered:
- Default
GET / - Custom nginx configuration
- Large request body
- Malformed HTTP request
For HTTP responses, the test compares:
status
headers
body
The Date header value is normalized because requests to the two containers occur at slightly different times. Its presence is still checked.
Final compatibility result:
Passed: 4/4
All compatibility tests passed.
The complete output is stored in:
reports/compatibility.txt
Tests can be executed with:
powershell -ExecutionPolicy Bypass -File .\test.ps1Rebuilding nginx from source exposed two small but important compatibility differences.
With nginx configured using:
--prefix=/etc/nginx
the upstream make install process placed the default HTML files under:
/etc/nginx/html
The official container uses:
/usr/share/nginx/html
The packaging step therefore relocates the default HTML files and adjusts the default nginx document root to match the original container.
Although the rebuilt index.html contents were identical, its modification time initially differed from the original.
This changed nginx's generated:
Last-Modified
ETag
headers and caused the strict compatibility test to fail.
Instead of ignoring these headers in the test, the package build preserves the original source-file modification time.
After doing so, the default page matched in status, headers, and body.
This exercise remediates two selected CVEs rather than attempting to eliminate every finding in the Debian Bookworm image.
Residual findings remain in operating-system packages and potentially in nginx-related components.
A production hardening effort would continue by:
- Triaging remaining findings by exploitability and runtime reachability.
- Prioritizing critical/high vulnerabilities in components actually present and reachable in the final image.
- Applying supported Debian security updates where compatible.
- Removing unnecessary runtime packages where possible.
- Maintaining VEX only where there is strong evidence that version-based scanner results do not represent the actual code state.
- Re-running multiple scanners after every package or source update.
- Adding regression tests for security patches in addition to compatibility tests.
The VEX entry for CVE-2026-60005 is specifically backed by the patch present in this repository. It should not be treated as a generic mechanism for suppressing scanner findings.
The nginx build script initially failed inside the Linux builder because it had Windows CRLF line endings.
The builder now normalizes the script before execution.
The nginx patch applied with a 59-line offset because it originated from a newer upstream source tree.
The patch still matched the expected surrounding code and was validated with a dry-run before being applied.
Compiling nginx with the same configure flags was not sufficient to reproduce the complete behavior of the official image.
The compatibility test exposed differences in:
- default static-file location
- default document root
- file timestamps
- resulting HTTP cache-related headers
Those differences were fixed in the package/image rather than hidden by weakening the test.
With additional time I would:
- Automate baseline and post-build Trivy/Grype scans as part of the main workflow.
- Produce machine-readable JSON scan reports in addition to text reports.
- Add Linux shell/Make equivalents to the PowerShell helper scripts.
- Add CI that performs clean build, scan, VEX validation, and compatibility testing.
- Expand compatibility testing to additional nginx features and signals.
- Pin source artifacts with cryptographic hashes/signature verification for stronger supply-chain reproducibility.
AI assistance was used during the exercise for:
- Breaking the assignment into an implementation plan.
- Reviewing scanner findings and helping prioritize two CVEs.
- Researching upstream/Debian remediation information.
- Reviewing Docker and shell build logic.
- Diagnosing build and compatibility-test failures.
- Designing the automated compatibility test.
- Reviewing VEX structure and scanner behavior.
- Improving documentation.
AI-generated suggestions were not treated as proof that a vulnerability was fixed.
Security decisions were verified against scanner output, Debian/upstream information, the actual source patch, generated package contents, and runtime compatibility tests.
One area where AI assistance was less useful was making assumptions about filesystem paths during the initial nginx rebuild. The compatibility tests ultimately provided the authoritative answer and exposed the actual layout differences.