Chris Enoch · · Field notes

Agentic security updates

From an X post to a tested production rollout.

I saw a post on X warning about a new Linux kernel vulnerability and asked my coding agent to check it. That started a much larger piece of work: verify the advisory, inspect our actual exposure, look for related issues, and carry the fixes through our normal test and publish cycle.

The post that started this review — Derek Ross (@derekmross), October 30, 2026, on X.
PSA for anyone running a Linux server, node, or relay CVE-2026-98432 is a critical Linux kernel bug (CVSS 9.8). Someone on the internet can send crafted TCP packets and take over your machine. No login needed. Ubuntu and Red Hat haven't shipped a fixed kernel yet. Debian, SUSE, Oracle and Amazon have. What to do today: 1. Check your kernel: uname -r Fixed versions: 6.1.190+, 6.6.159+, 6.12.113+, 6.18.56+, 7.2.10+ or 7.3+. Below that, you're exposed. 2. If your distro has a fix, update and reboot. 3. If not, block the attack. It needs TCP packets with "IP options," which normal traffic never uses. One nftables rule drops them. Log what gets dropped so you can see if someone's probing you. 4. Turn off MPTCP unless you need it: sysctl -w net.mptcp.enabled=0 Programs built with Go 1.24+ use MPTCP automatically, and that's how the known exploit gets in. 5. Make it survive a reboot, and check it still works after one. If you run a Bitcoin node, a Lightning node or a Nostr relay, your machine is on the internet by design. Do this today. Self-hosting is freedom. Patching is part of the job. 🛡️

This is the alert as shared at the time. We checked its claims against vendor advisories and our actual configuration; the actions and verification limits below describe what we did.

The system at the center of this was DocLifts, my live workout app. It runs on an Akamai Cloud VPS with Docker, PostgreSQL, a Python image-screening service, a scheduled privacy worker and Cloudflare Tunnel. Enoch.ai and runthe.ai are separate static sites; checking their availability did not mean they shared every dependency or vulnerability.

I supplied the initial lead and authorized the work. Codex coordinated specialist agents across image scanning, application dependencies, the photo service, and tunnel/database review. The useful result was a set of tested changes and a record of the remaining uncertainty.

First, verify the warning

The post named CVE-2026-98432. We checked the vendor advisory and the host's installed kernel instead of treating the post's version list as the final authority. At the time of our October 30 review, Canonical listed the Ubuntu 26.04 kernel fix as work in progress. Our host needed a mitigation while we waited for a supported patch.

We disabled Multipath TCP and added an nftables rule to drop IPv4 TCP packets carrying IP options. Before applying the host rule, we tested it in an isolated network namespace: ordinary TCP traffic connected, and the options-bearing test traffic was dropped. We then checked a fresh SSH connection and the rule's counter after applying it.

This test was an exception to our usual local-first workflow. Docker's Linux environment on the laptop was initially unresponsive, so the isolated network test ran on the VPS. The later application builds and full regression gate ran locally after Docker recovered.

We configured the mitigation to load at startup through sysctl and systemd. We did not reboot to prove persistence, and we did not call the kernel patched. Those were explicit follow-ups.

Scan what actually ships

Next I asked for a broader dependency review. We scanned the four running container images and the scheduled worker image with Trivy 0.76.1, supplemented that with OSV package checks, and inspected where flagged components lived and how they were used.

That last step mattered. A package-version match tells you where to investigate. Establishing exploitability also requires the relevant code path, input and permissions. One critical tar finding was inside a root-owned Corepack cache; we found no evidence that the non-root application could reach it through a request. Other findings involved build tools, installer dependencies or startup helpers rather than a persistent network service.

We still removed unnecessary components where practical. Keeping unused tooling in a runtime image creates more software to maintain and more findings to investigate.

Patch, rebuild, then scan again

The application and privacy-worker work moved Node from 24.22.1 to 24.22.2, brought Undici to 7.30.1 and Zod to 4.6.7, and consolidated old esbuild copies on 0.28.2. We removed npm and the root Corepack cache from the deployed runtime and gave the scheduled worker a dedicated runtime build stage.

The Python photo-safety image provided the clearest example of why the finished artifact needs another scan. We updated FileLock to 4.2.5 and refreshed the packaging tools. The rescan then found advisories in libraries bundled inside pip itself. Once dependencies and the model were installed, the running classifier did not need pip, so we removed it from the final image.

The exact locally built image then showed zero reported vulnerabilities across its 48 inventoried Python packages. That result had a boundary: the Debian base layer still had 164 reported findings, including 44 rated high. We kept those visible. A clean Python package result was not a clean bill of health for the entire container.

We also ran real classifier inference and rejection tests for malformed, empty, oversized and incorrectly typed inputs. An image that scans better still has to do its job.

The database needed a different answer

PostgreSQL 16.15 already matched the current upstream image digest at the time of the check, and its container did not publish a database port on the host. There was no PostgreSQL version upgrade to apply in this pass.

Its image still contained flagged dependencies. For example, Debian classified the libxml2 finding CVE-2026-6653 as a minor issue with no dedicated security update for Trixie. We had not demonstrated an affected XML path in our application. Go-version findings in the gosu startup helper also needed context; gosu was not a persistent TLS service.

We recorded those limits instead of marking every database finding “not applicable.” Before deploying the application changes, we also took a restricted-permission database backup and verified that PostgreSQL's restore tooling could read its archive listing. That check was not a full restore drill.

Watch the tunnel and the application separately

We updated Cloudflare's tunnel connector to 2026.10.7 and pinned its image digest. The release notes described an HTTP/2 concurrent-stream regression fix; we did not present that release as a fix for every scanner finding. An x/net version match remained, with exploitability unestablished.

I had noticed a possible interruption during an earlier tunnel upgrade, so this time we watched. We brought up a temporary connector, waited for its connections and readiness, replaced the primary connector, checked it, and then removed the temporary replica.

The HTTP monitor recorded successful responses throughout that tunnel handover. During the separate web-container restart, it recorded three 502 responses over approximately three seconds, followed by recovery. That is the observed result. We did not have an entirely interruption-free deployment.

Keep the ordinary release discipline

The full local gate passed with 997 server tests, three skips, 42 client tests and 174 browser end-to-end tests, alongside formatting, type, schema and production-build checks. Branch tests, container checks and CodeQL passed before merge. The follow-up removal of pip went through the gate again.

One container check initially failed because its read-only filesystem omitted the writable temporary directory the production worker uses. We corrected the test configuration to match production and reran it. The test failure was part of the work, not something to hide behind the eventual green result.

We retained previous images for rollback, deployed through the established Compose wrapper, and changed the worker's image while its timer was paused. After resuming it, we checked a successful scheduled run. Production checks covered service health, sign-in, MCP discovery and the public sites. Discovery returning HTTP 200 did not prove every authenticated MCP tool worked.

The application and container changes are recorded in DocLifts PR #58 and PR #59.

What made the agents useful

The agents could work across the boundaries that usually turn maintenance into a backlog: vendor research, host configuration, package inventories, Dockerfiles, tests, deployment and verification. Parallel review helped cover those areas, while the release process gave the work a shared set of checks.

The limits remained important. The kernel still needed its vendor patch and a reboot check. Some base-image and bundled-library findings awaited upstream fixes or further investigation. A separate local privilege-escalation lead had only a limited namespace-restriction check; that was not proof that all exploit paths were blocked. This was maintenance and exposure reduction, not an incident-response investigation or an independent security certification.

For me, this is a practical use of agentic software: take an alert I might otherwise bookmark, investigate it against the system I actually run, and carry the resulting work through tests and deployment. The evidence includes the fixes, the failed checks we corrected, the short disruption, and the work still left to do.

Written with Codex from the recorded October 30, 2026 maintenance session, at my request. Versions, advisory status and scan counts describe that session; they are not a live security-status feed.

More writing · The DocLifts case study