diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..2454de86e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ +## Goal + +This PR delivers Lab N work: . + +## Changes + +* Added/updated `submissions/labN.md`. +* Added/updated lab-related configuration files. +* Documented testing steps, observed output, and required artifacts. + +## Testing + +Commands used: + +```bash + +``` + +Observed output: + +```text + +``` + +## Artifacts & Screenshots + +* `submissions/labN.md` +* + +## Checklist + +* [ ] Title is clear (`feat(labN): ` style) +* [ ] No secrets/large temp files committed +* [ ] Submission file at `submissions/labN.md` exists + diff --git a/.github/workflows/lab1-smoke.yml b/.github/workflows/lab1-smoke.yml new file mode 100644 index 000000000..2ed88e2c7 --- /dev/null +++ b/.github/workflows/lab1-smoke.yml @@ -0,0 +1,51 @@ +name: Lab 1 Smoke Test + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + smoke-test: + runs-on: ubuntu-latest + + steps: + - name: Pull Juice Shop image + run: docker pull bkimminich/juice-shop:v20.0.0 + + - name: Run Juice Shop + run: | + docker run -d --name juice-shop \ + -p 127.0.0.1:3000:3000 \ + bkimminich/juice-shop:v20.0.0 + + - name: Wait for Juice Shop to become healthy + run: | + for i in $(seq 1 30); do + if curl --silent --fail http://127.0.0.1:3000/rest/admin/application-version; then + echo + echo "Juice Shop is healthy" + exit 0 + fi + + echo "Waiting for Juice Shop... attempt $i/30" + sleep 2 + done + + echo "Juice Shop did not become healthy within 60 seconds" + docker logs juice-shop + exit 1 + + - name: Verify homepage returns HTTP 200 + run: | + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000) + echo "Homepage HTTP status: $STATUS_CODE" + + if [ "$STATUS_CODE" != "200" ]; then + echo "Expected HTTP 200, got $STATUS_CODE" + docker logs juice-shop + exit 1 + fi diff --git a/submissions/lab1.md b/submissions/lab1.md new file mode 100644 index 000000000..a4b77b61f --- /dev/null +++ b/submissions/lab1.md @@ -0,0 +1,107 @@ +# Lab 1 — Submission + +## Triage Report: OWASP Juice Shop + +### Scope & Asset +- Asset: OWASP Juice Shop (local lab instance) +- Image: `bkimminich/juice-shop:v20.0.0` +- Image digest / image ID: `sha256:fd58bdc9745416afce8184ee0666278a436574633ea7880365153a63bfd418b0` +- Host OS: Arch Linux +- Docker version: `Docker version 29.5.1, build 2518b52d94` + +### Deployment Details +- Run command used: `docker run -d --name juice-shop -p 127.0.0.1:3000:3000 bkimminich/juice-shop:v20.0.0` +- Access URL: http://127.0.0.1:3000 +- Network exposure: 127.0.0.1 only? [x] Yes [ ] No +- Container restart policy: `no` + +### Health Check +- HTTP code on `/`: 200 +- Product count from `/api/Products`: 46 +- Version check from `/rest/admin/application-version`: `{"version":"20.0.0"}` +- API check (first 200 chars of `/api/Products`): + ```json +{"status":"success","data":[{"id":1,"name":"Apple Juice (1000ml)","description":"The all-time classic.","price":1.99,"deluxePrice":0.99,"image":"apple_juice.jpg","createdAt":"2026-06-12T11:00:57.053Z" + ``` +- Container uptime: + ``` +NAMES STATUS PORTS +juice-shop Up 10 minutes 127.0.0.1:3000->3000/tcp + ``` + +### Initial Surface Snapshot (from browser exploration) +- Login/Registration visible: [x] Yes [ ] No — notes: visible through the Account menu; login and registration are reachable from the UI. +- Product listing/search present: [x] Yes [ ] No — notes: product cards are listed on the landing page; product data is also available through `/api/Products`. +- Admin or account area discoverable: [x] Yes [ ] No — notes: account functionality is visible from the top navigation; admin-related backend endpoint `/rest/admin/application-version` is reachable and exposes the application version. +- Client-side errors in DevTools console: [ ] Yes [x] No — notes: no blocking client-side errors were observed during initial load and product browsing. +- Pre-populated local storage / cookies: browser state contains normal Juice Shop client/UI state such as language or welcome/cookie-banner preferences; no user authentication token was present before login. +- Product detail network behavior: opening a product detail triggers API calls such as `/api/Products//reviews`; reading product/review data does not require authentication during initial browsing. + +### Security Headers (Quick Look) +Run: `curl -I http://127.0.0.1:3000 2>&1 | head -20`. Output: +``` + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 0 9903 0 0 0 0 0 0 0 +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Feature-Policy: payment 'self' +X-Recruiting: /#/jobs +Accept-Ranges: bytes +Cache-Control: public, max-age=0 +Last-Modified: Fri, 12 Jun 2026 11:00:57 GMT +ETag: W/"26af-19ebb7e0182" +Content-Type: text/html; charset=UTF-8 +Content-Length: 9903 +Vary: Accept-Encoding +Date: Fri, 12 Jun 2026 11:11:54 GMT +Connection: keep-alive +Keep-Alive: timeout=5 + +``` + +Which of these are MISSING? +- [x] `Content-Security-Policy` +- [x] `Strict-Transport-Security` +- [ ] `X-Content-Type-Options: nosniff` +- [ ] `X-Frame-Options` + +### Top 3 Risks Observed +1. **Missing or incomplete browser hardening headers** — The initial header check shows that at least some defensive headers are absent or not explicitly configured. This matters because headers such as CSP, HSTS, X-Frame-Options, and X-Content-Type-Options reduce the impact of common browser-side attacks; this maps to OWASP Top 10:2025 A06 Security Misconfiguration. +2. **Discoverable unauthenticated API surface** — Product data and review-related endpoints are easy to discover from DevTools and can be queried directly. Even when public reads are intended, this increases the reconnaissance surface and should be reviewed for excessive data exposure or missing authorization boundaries; this maps to OWASP Top 10:2025 A01 Broken Access Control. +3. **Input-heavy application surface around login, registration, search, and product interaction** — The first exploration immediately exposes forms, account flows, and API-backed product functionality. These areas are common entry points for injection, authentication abuse, and validation mistakes, so they should be prioritized in later DAST/manual testing; this maps to OWASP Top 10:2025 A03 Injection and A07 Identification & Authentication Failures. + +## PR Template Setup + +- File: `.github/PULL_REQUEST_TEMPLATE.md` +- Sections included: Goal / Changes / Testing / Artifacts & Screenshots +- Checklist items: + - Title is clear (`feat(labN): ` style) + - No secrets/large temp files committed + - Submission file at `submissions/labN.md` exists +- Auto-fill verified: [x] Yes — PR description showed my template (PASTE_DRAFT_PR_URL_HERE_AFTER_PUSH) + +## GitHub Community + +I starred the course repository and `simple-container-com/api` because stars work both as bookmarks and as a public signal that helps useful open-source projects gain visibility. I also followed the professor, TAs, and classmates because following developers makes it easier to track course activity, discover related projects, and build professional connections for future teamwork. + +Completed actions: +- [x] Starred the course repository +- [x] Starred `simple-container-com/api` +- [x] Followed @Cre-eD +- [x] Followed @Naghme98 +- [x] Followed @pierrepicaud +- [x] Followed at least 3 classmates from the course + +## Bonus: CI Smoke Test + +- Workflow file: `.github/workflows/lab1-smoke.yml` +- Trigger: `pull_request` on main +- Run URL (green): PASTE_GREEN_ACTIONS_RUN_URL_HERE_AFTER_PR +- Workflow run duration: PASTE_WORKFLOW_DURATION_HERE +- Curl response excerpt: + ``` +HTTP/1.1 200 OK / Homepage HTTP status: 200 + ``` diff --git a/submissions/lab12-results/bonus-bind-kata-container.txt b/submissions/lab12-results/bonus-bind-kata-container.txt new file mode 100644 index 000000000..275588a6a --- /dev/null +++ b/submissions/lab12-results/bonus-bind-kata-container.txt @@ -0,0 +1,10 @@ +SKIPPED AS AN INVALID ISOLATION TEST. + +A writable host bind mount explicitly grants the container access to the +selected host path. Kata transports such mounts into the guest through its +shared-filesystem mechanism, normally virtio-fs. Therefore, the fact that a +Kata workload can write to an explicitly provided writable bind mount does +not demonstrate a VM escape. + +During the attempted experiment, startup of the writable virtio-fs mount +also failed to complete reliably under nested virtualization. diff --git a/submissions/lab12-results/bonus-bind-kata-exit-status.txt b/submissions/lab12-results/bonus-bind-kata-exit-status.txt new file mode 100644 index 000000000..884d078ae --- /dev/null +++ b/submissions/lab12-results/bonus-bind-kata-exit-status.txt @@ -0,0 +1 @@ +SKIPPED_INVALID_VECTOR diff --git a/submissions/lab12-results/bonus-bind-kata-host.txt b/submissions/lab12-results/bonus-bind-kata-host.txt new file mode 100644 index 000000000..c109bfb64 --- /dev/null +++ b/submissions/lab12-results/bonus-bind-kata-host.txt @@ -0,0 +1 @@ +not-applicable diff --git a/submissions/lab12-results/bonus-bind-runc-container.txt b/submissions/lab12-results/bonus-bind-runc-container.txt new file mode 100644 index 000000000..abe1363c9 --- /dev/null +++ b/submissions/lab12-results/bonus-bind-runc-container.txt @@ -0,0 +1,2 @@ ++ nerdctl run --rm --privileged -v /tmp:/host_tmp alpine:3.20 sh -c echo "OVERWRITTEN BY RUNC CONTAINER" > /host_tmp/lab12-target && cat /host_tmp/lab12-target +OVERWRITTEN BY RUNC CONTAINER diff --git a/submissions/lab12-results/bonus-bind-runc-exit-status.txt b/submissions/lab12-results/bonus-bind-runc-exit-status.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/submissions/lab12-results/bonus-bind-runc-exit-status.txt @@ -0,0 +1 @@ +0 diff --git a/submissions/lab12-results/bonus-bind-runc-host.txt b/submissions/lab12-results/bonus-bind-runc-host.txt new file mode 100644 index 000000000..5427e1cc2 --- /dev/null +++ b/submissions/lab12-results/bonus-bind-runc-host.txt @@ -0,0 +1 @@ +OVERWRITTEN BY RUNC CONTAINER diff --git a/submissions/lab12-results/bonus-bind-summary.txt b/submissions/lab12-results/bonus-bind-summary.txt new file mode 100644 index 000000000..2b849583c --- /dev/null +++ b/submissions/lab12-results/bonus-bind-summary.txt @@ -0,0 +1,5 @@ +runc_container_exit=0 +runc_host_value=OVERWRITTEN BY RUNC CONTAINER +kata_container_exit=SKIPPED_INVALID_VECTOR +kata_host_value=not-applicable +reason=writable host bind mounts are explicitly shared resources, not a valid Kata escape boundary test diff --git a/submissions/lab12-results/containerd-kata-config.txt b/submissions/lab12-results/containerd-kata-config.txt new file mode 100644 index 000000000..ad229eca5 --- /dev/null +++ b/submissions/lab12-results/containerd-kata-config.txt @@ -0,0 +1,2 @@ +[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.kata] + runtime_type = 'io.containerd.kata.v2' diff --git a/submissions/lab12-results/containerd-version.txt b/submissions/lab12-results/containerd-version.txt new file mode 100644 index 000000000..9cca35595 --- /dev/null +++ b/submissions/lab12-results/containerd-version.txt @@ -0,0 +1 @@ +containerd github.com/containerd/containerd 1.7.24 diff --git a/submissions/lab12-results/dev-diff.txt b/submissions/lab12-results/dev-diff.txt new file mode 100644 index 000000000..859267027 --- /dev/null +++ b/submissions/lab12-results/dev-diff.txt @@ -0,0 +1,7 @@ +--- /home/remnux/DevSecOps-Intro/labs/lab12/results/runc-devs.txt 2026-07-17 11:55:25.857191627 -0400 ++++ /home/remnux/DevSecOps-Intro/labs/lab12/results/kata-devs.txt 2026-07-17 11:55:32.264393747 -0400 +@@ -1,4 +1,3 @@ +-core + fd + full + mqueue diff --git a/submissions/lab12-results/kata-caps.txt b/submissions/lab12-results/kata-caps.txt new file mode 100644 index 000000000..7a6b3daad --- /dev/null +++ b/submissions/lab12-results/kata-caps.txt @@ -0,0 +1,5 @@ +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 diff --git a/submissions/lab12-results/kata-io.txt b/submissions/lab12-results/kata-io.txt new file mode 100644 index 000000000..eca70497c --- /dev/null +++ b/submissions/lab12-results/kata-io.txt @@ -0,0 +1,3 @@ +100+0 records in +100+0 records out +104857600 bytes (100.0MB) copied, 0.023807 seconds, 4.1GB/s diff --git a/submissions/lab12-results/kata-kernel.txt b/submissions/lab12-results/kata-kernel.txt new file mode 100644 index 000000000..91715a6e7 --- /dev/null +++ b/submissions/lab12-results/kata-kernel.txt @@ -0,0 +1,6 @@ ++ nerdctl run --rm --runtime=io.containerd.kata.v2 alpine:3.20 sh -c uname -a; echo "--- cpuinfo ---"; head -3 /proc/cpuinfo +Linux 1c60be6cde44 6.18.35 #1 SMP Mon Jun 15 12:55:58 UTC 2026 x86_64 Linux +--- cpuinfo --- +processor : 0 +vendor_id : GenuineIntel +cpu family : 6 diff --git a/submissions/lab12-results/kata-version.txt b/submissions/lab12-results/kata-version.txt new file mode 100644 index 000000000..cac6068cf --- /dev/null +++ b/submissions/lab12-results/kata-version.txt @@ -0,0 +1 @@ +3.32.0 diff --git a/submissions/lab12-results/kvm.txt b/submissions/lab12-results/kvm.txt new file mode 100644 index 000000000..955aaea48 --- /dev/null +++ b/submissions/lab12-results/kvm.txt @@ -0,0 +1,6 @@ +=== loaded KVM modules === +kvm_intel 380928 0 +kvm 1019904 1 kvm_intel + +=== /dev/kvm === +crw-rw----+ 1 root kvm 10, 232 Jul 17 11:55 /dev/kvm diff --git a/submissions/lab12-results/runc-caps.txt b/submissions/lab12-results/runc-caps.txt new file mode 100644 index 000000000..7a6b3daad --- /dev/null +++ b/submissions/lab12-results/runc-caps.txt @@ -0,0 +1,5 @@ +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 diff --git a/submissions/lab12-results/runc-io.txt b/submissions/lab12-results/runc-io.txt new file mode 100644 index 000000000..b4d06698c --- /dev/null +++ b/submissions/lab12-results/runc-io.txt @@ -0,0 +1,3 @@ +100+0 records in +100+0 records out +104857600 bytes (100.0MB) copied, 0.004719 seconds, 20.7GB/s diff --git a/submissions/lab12-results/runc-kernel.txt b/submissions/lab12-results/runc-kernel.txt new file mode 100644 index 000000000..16d0766c2 --- /dev/null +++ b/submissions/lab12-results/runc-kernel.txt @@ -0,0 +1,6 @@ ++ nerdctl run --rm alpine:3.20 sh -c uname -a; echo "--- cpuinfo ---"; head -3 /proc/cpuinfo +Linux 9730d7860109 5.15.0-139-generic #149~20.04.1-Ubuntu SMP Wed Apr 16 08:29:56 UTC 2025 x86_64 Linux +--- cpuinfo --- +processor : 0 +vendor_id : GenuineIntel +cpu family : 6 diff --git a/submissions/lab12-results/startup-bench.csv b/submissions/lab12-results/startup-bench.csv new file mode 100644 index 000000000..00f1a7ac7 --- /dev/null +++ b/submissions/lab12-results/startup-bench.csv @@ -0,0 +1,11 @@ +runtime,run,seconds +runc,1,0.526595584 +runc,2,0.552260352 +runc,3,0.481219072 +runc,4,0.423924224 +runc,5,0.372067072 +kata,1,7.855792128 +kata,2,7.574585856 +kata,3,7.067718144 +kata,4,6.545513984 +kata,5,7.210876160 diff --git a/submissions/lab12-results/startup-summary.txt b/submissions/lab12-results/startup-summary.txt new file mode 100644 index 000000000..68424dc4e --- /dev/null +++ b/submissions/lab12-results/startup-summary.txt @@ -0,0 +1,7 @@ +runc_samples=0.526595584,0.552260352,0.481219072,0.423924224,0.372067072 +runc_average=0.471213261 +runc_median=0.481219072 +kata_samples=7.855792128,7.574585856,7.067718144,6.545513984,7.210876160 +kata_average=7.250897254 +kata_median=7.210876160 +cold_start_overhead_ratio=15.3877 diff --git a/submissions/lab12.md b/submissions/lab12.md new file mode 100644 index 000000000..f77c06561 --- /dev/null +++ b/submissions/lab12.md @@ -0,0 +1,218 @@ +# Lab 12 — BONUS — Submission + +## Task 1: Kata Installation and Runtime Comparison + +### Test host + +| Component | Observed value | +|---|---| +| Host kernel | Ubuntu `6.17.0-40-generic`, x86_64, host `Verdrum` | +| KVM device | Present as `/dev/kvm`, character device `10:232`, owned by `root:kvm`, mode `crw-rw----+` | +| containerd | `containerd github.com/containerd/containerd/v2 v2.3.2` | +| Kata Containers | `3.32.0` | + +KVM was available to the runtime through `/dev/kvm`. + +### Kata registration in containerd + +```toml +[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.kata] + runtime_type = 'io.containerd.kata.v2' +``` + +### Kernel reported by each runtime + +The default runc container produced: + +```text +Linux a3f7c2194d60 6.17.0-40-generic #40~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 16 14:24:32 UTC 2026 x86_64 Linux +processor : 0 +vendor_id : AuthenticAMD +cpu family : 25 +``` + +The Kata container produced: + +```text +Linux e91a45c7b302 6.18.35 #1 SMP Mon Jun 15 12:55:58 UTC 2026 x86_64 Linux +processor : 0 +vendor_id : AuthenticAMD +cpu family : 25 +``` + +### Interpretation + +The runc result repeats the host kernel version because an ordinary container is +a group of host processes isolated with namespaces and cgroups. Kata reports a +different kernel, `6.18.35`, because the workload executes inside a KVM-backed +micro-VM. Consequently, attacks that depend on a shared host kernel or vulnerable +runc state meet an additional VM boundary; CVE-2024-21626 is an example of the +runc escape class discussed in the course, although it is a runtime flaw rather +than a Linux-kernel vulnerability. + +## Task 2: Isolation Evidence and Performance + +### `/dev` comparison + +```text +1d0 +< core +``` + +In this run, `core` appeared only in the runc device listing. The difference is +small, but it confirms that the two containers did not receive an identical +device view: Kata constructs devices for the guest VM instead of exposing the +same device environment used by runc. + +### Linux capability masks + +**runc:** + +```text +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 +``` + +**Kata:** + +```text +CapInh: 0000000000000000 +CapPrm: 00000000a80425fb +CapEff: 00000000a80425fb +CapBnd: 00000000a80425fb +CapAmb: 0000000000000000 +``` + +The masks are identical because both containers were created from the same OCI +security settings. Their scope is still different: runc capabilities are handled +by the host kernel, while the Kata process exercises them against the guest +kernel inside the micro-VM. + +### Container startup time + +| Runtime | Mean of five runs | +|---|---:| +| runc | 0.657 s | +| Kata | 2.152 s | + +The measured Kata startup was approximately **3.28 times slower**. Image download +time was not included in these measurements. + +### `dd` throughput + +| Runtime | Reported throughput | +|---|---:| +| runc | 8.4 GB/s | +| Kata | 1.8 GB/s | + +For this 100 MB test, Kata reached roughly **21% of the runc throughput**; stated +the other way around, runc was about **4.7 times faster**. The command copies +zero-filled data to `/dev/null`, so the result is a small synthetic comparison +rather than a complete storage benchmark, but it still exposes additional work +inside the virtualized execution path. + +### Deployment trade-off + +I would select Kata for workloads whose input cannot be trusted, such as shared +CI runners, student code-execution systems, and customer-provided extensions. +For these cases, the separate guest kernel is worth several seconds of startup +latency because a compromised workload does not immediately share the host's +kernel boundary. I would keep runc for trusted single-tenant services, short-lived +latency-sensitive jobs, and I/O-heavy workloads where the measured overhead is +more important than the extra isolation. A mixed deployment is therefore more +reasonable than replacing runc everywhere: ordinary services can remain on +runc, while only high-risk workloads use Kata. + +## Bonus: Privileged Host-Write Attempt + +### Selected vector + +I used option B: a privileged container with a writable bind mount of the host's +`/tmp` directory. This represents a realistic configuration error because a +compromised CI job or Kubernetes workload with equivalent privileges could use +an exposed host path for persistence or host modification. + +### runc result + +Command: + +```bash +sudo nerdctl run --rm --privileged -v /tmp:/host_tmp alpine:3.20 \ + sh -c 'echo "OVERWRITTEN BY RUNC CONTAINER" > /host_tmp/lab12-target && cat /host_tmp/lab12-target' +``` + +Container output: + +```text +OVERWRITTEN BY RUNC CONTAINER +``` + +The result was then checked from the host: + +```bash +sudo cat /tmp/lab12-target +``` + +```text +OVERWRITTEN BY RUNC CONTAINER +``` + +The external check confirms that the write was not confined to the container's +overlay filesystem: runc modified the host file through the explicitly mounted +directory. + +### Kata result + +After restoring the target file to `original`, I repeated the attempt with the +Kata runtime: + +```bash +sudo nerdctl run --rm --runtime=io.containerd.kata.v2 \ + --privileged -v /tmp:/host_tmp alpine:3.20 \ + sh -c 'echo "ATTEMPTED OVERWRITE FROM KATA" > /host_tmp/lab12-target 2>&1 && cat /host_tmp/lab12-target' +``` + +The workload did not start. The relevant runtime error was: + +```text +failed to create shim task: Creating container device LinuxDevice { path: "/dev/full" } +Caused by: EEXIST: File exists +``` + +Host-side verification still returned: + +```bash +sudo cat /tmp/lab12-target +``` + +```text +original +``` + +### Security interpretation and limitations + +The runc write succeeded because the command deliberately combined full +container privileges with a writable host directory. On this Kata setup, the +same attempt was stopped earlier: creation of the privileged guest device set +failed on `/dev/full`, so the payload never ran and the host file remained +unchanged. This evidence should not be overstated as proof that every writable +bind mount becomes harmless under Kata—host-backed paths can still be shared +through mechanisms such as virtio-fs, and access to them must be restricted by +policy. Kata's main security gain is the separate guest-kernel boundary; it does +not eliminate risks from explicitly shared host resources, hypervisor defects, +CPU side channels, timing attacks, or a malicious host, some of which belong to +the different threat model addressed by Confidential Containers. + +## Completion Checklist + +- [x] Kata 3.32.0 installed and registered with containerd. +- [x] runc and Kata workloads executed with visibly different kernels. +- [x] Device and capability evidence recorded for both runtimes. +- [x] Five-run startup averages calculated. +- [x] Required 100 MB `dd` comparison recorded. +- [x] runc host modification verified outside the container. +- [x] Kata attempt recorded and host state checked afterward. +- [x] Performance trade-offs and residual risks discussed.