Skip to content

Repository files navigation

Build It, Patch It, Ship It

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

Security Fixes

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.

Quick Start

make build
make test

Or on Windows directly:

powershell -ExecutionPolicy Bypass -File .\build.ps1
powershell -ExecutionPolicy Bypass -File .\test.ps1

Expected compatibility result:

Passed: 4/4

For security evidence:

make scan
make vex

The remainder of this README documents the build process, CVE evidence, VEX rationale, compatibility work, residual risk, and engineering tradeoffs.

Repository Structure

.
├── 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

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 Remediation

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

CVE-2025-27363 — Dependency Version Bump

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.txt
  • reports/fixed-grype.txt

CVE-2026-60005 — nginx Backport

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

VEX

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.

Building

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.ps1

The build:

  1. Creates the nginx builder.
  2. Downloads nginx 1.25.5 source.
  3. Applies the CVE-2026-60005 backport.
  4. Compiles nginx.
  5. Produces the custom nginx .deb.
  6. Retrieves and builds the fixed Debian FreeType source package.
  7. Produces the fixed FreeType .deb.
  8. Builds the final container from debian:bookworm-slim.

No pre-built nginx binary from the original image is copied into the final container.

Final Image

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

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

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:

  1. Default GET /
  2. Custom nginx configuration
  3. Large request body
  4. 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.ps1

Compatibility Fixes Discovered During Testing

Rebuilding nginx from source exposed two small but important compatibility differences.

Static File Layout

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.

Static File Timestamps

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.

Residual Risk

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.

Surprises / What I Would Improve With More Time

Windows CRLF

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.

Backport Offset

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.

Rebuilding Is Not Automatically Drop-In Compatible

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.

Further Work

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 Usage

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages