<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aiden Bolin</title>
    <description>The latest articles on DEV Community by Aiden Bolin (@accuoa).</description>
    <link>https://dev.to/accuoa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3906607%2Fe6bdb864-8f09-41ec-a9d4-401cc0ef18af.png</url>
      <title>DEV Community: Aiden Bolin</title>
      <link>https://dev.to/accuoa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/accuoa"/>
    <language>en</language>
    <item>
      <title>How to check which Linux CVEs actually affect your servers in 30 seconds (and which are being exploited right now)</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 17 Jun 2026 16:43:51 +0000</pubDate>
      <link>https://dev.to/accuoa/how-to-check-which-linux-cves-actually-affect-your-servers-in-30-seconds-and-which-are-being-3p2i</link>
      <guid>https://dev.to/accuoa/how-to-check-which-linux-cves-actually-affect-your-servers-in-30-seconds-and-which-are-being-3p2i</guid>
      <description>&lt;p&gt;If you run a couple of Linux boxes for a side project or a small SaaS, you've probably had this moment: a CVE hits the news, your monitoring tool spits out 400 "high severity" findings, and you have no idea which ones actually matter. CVSS says everything is a 7.8. You patch nothing, or you patch everything and pray nothing breaks.&lt;/p&gt;

&lt;p&gt;The honest truth is that &lt;strong&gt;most CVEs on your box will never be exploited.&lt;/strong&gt; The number you actually care about is small. Here's how to find it fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command, no signup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's read-only and anonymous. Nothing is stored server-side, and you can read the script before you run it (you should always read anything you pipe to bash). It reads your distro, kernel version, and &lt;code&gt;dpkg&lt;/code&gt;/&lt;code&gt;apk&lt;/code&gt;/&lt;code&gt;rpm&lt;/code&gt; package list, matches them against the vendor advisories — Ubuntu USN, Debian DSA, Alpine, RHEL — cross-referenced with NVD, and prints back the few CVEs that actually apply to &lt;em&gt;your&lt;/em&gt; installed versions. It supports Ubuntu, Debian, Alpine, AlmaLinux, and Rocky.&lt;/p&gt;

&lt;p&gt;But matching CVEs to packages is the easy half. The useful half is &lt;strong&gt;ranking.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  KEV vs EPSS: the two numbers that matter more than CVSS
&lt;/h2&gt;

&lt;p&gt;CVSS scores severity &lt;em&gt;if&lt;/em&gt; a vulnerability is exploited. It says nothing about whether anyone is &lt;em&gt;actually&lt;/em&gt; exploiting it. Two free, authoritative data sources fix that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CISA KEV (Known Exploited Vulnerabilities).&lt;/strong&gt; This is a catalog the U.S. government maintains of CVEs that are being exploited &lt;em&gt;in the wild, right now&lt;/em&gt;. If a CVE is on the KEV list, it's not theoretical — someone has a working exploit and is using it. This is your drop-everything signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FIRST EPSS (Exploit Prediction Scoring System).&lt;/strong&gt; A daily-updated probability (0 to 1) that a given CVE will be exploited in the next 30 days. A CVE with EPSS 0.94 is far more urgent than one at 0.002, even if both are "CVSS 7.8."&lt;/p&gt;

&lt;p&gt;So the priority order isn't "highest CVSS first." It's:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On CISA KEV&lt;/strong&gt; → actively exploited → patch today&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High EPSS&lt;/strong&gt; → likely to be exploited soon → patch this week&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else&lt;/strong&gt; → real, but not on fire → schedule it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scanner sorts findings exploited-first and flags the KEV ones as &lt;em&gt;actively exploited&lt;/em&gt;, so the thing at the top of the list is genuinely the thing you should do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;Instead of a wall of CVSS scores, you get a ranked, decision-first list with the exact remediation command for each finding:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Illustrative output below — the CVE IDs, package versions, and EPSS scores are placeholders to show the shape of the result, not a real scan. Run the command above to see your own box's actual findings.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ACTIVELY EXPLOITED]  CVE-XXXX-XXXXX  openssl 3.0.2  (CISA KEV, EPSS 0.91)
  → sudo apt install --only-upgrade openssl

[HIGH RISK]           CVE-XXXX-XXXXX  curl 7.81.0    (EPSS 0.62)
  → sudo apt install --only-upgrade curl

[PATCH WHEN ABLE]     CVE-XXXX-XXXXX  nginx 1.18.0   (EPSS 0.01)
  → sudo apt install --only-upgrade nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point: you don't get a research project. You get a short list and the exact one-liner to fix each item. Triage is already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually work? We dogfood it.
&lt;/h2&gt;

&lt;p&gt;When CVE-2026-31431 dropped — "Copy Fail," a local privilege-escalation bug in the Linux kernel's &lt;code&gt;algif_aead&lt;/code&gt; module — we ran this same flow on our own production VPS and had it mitigated in about 30 minutes. The scan also surfaced several other real outstanding CVEs on that box that we then worked through.&lt;/p&gt;

&lt;p&gt;We keep the audit log for that machine public: &lt;strong&gt;mindsparkstack.com/patch/audit/mss-vps&lt;/strong&gt;. Timestamped actions, what was found, what was fixed. It's there partly to prove the tool isn't vaporware and partly because an audit trail you can hand someone is genuinely useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in the ecosystem
&lt;/h2&gt;

&lt;p&gt;To be fair about the alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vuls / Trivy / Grype&lt;/strong&gt; are excellent open-source scanners. If you enjoy self-hosting the scanner, building the triage layer, and writing your own remediation playbooks, use them — they're powerful and free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snyk / Tenable / Wiz&lt;/strong&gt; are the enterprise tier. They're great and they will not sell to a 1-to-3-person shop at a price that makes sense.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap in the middle is "I have 3 servers, I don't want a wall of scores, just tell me which few CVEs are on fire and the command to run." That's the niche. The free scan above fills most of it with zero commitment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line aside
&lt;/h2&gt;

&lt;p&gt;If you want that scan running hourly with real-time alerts and your own audit-log URL instead of running it by hand, there's a managed tier (free for 3 servers; paid plans from $9/mo, 14-day trial, no card) at &lt;a href="https://mindsparkstack.com/patch" rel="noopener noreferrer"&gt;mindsparkstack.com/patch&lt;/a&gt; — but honestly, run the free command first and see if the ranked output is useful to you at all. That's the part that matters.&lt;/p&gt;

</description>
      <category>security</category>
      <category>linux</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Vuls vs Trivy vs Grype: when to pick which CVE scanner (from the team that built one more)</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:47:18 +0000</pubDate>
      <link>https://dev.to/accuoa/vuls-vs-trivy-vs-grype-when-to-pick-which-cve-scanner-from-the-team-that-built-one-more-2bjd</link>
      <guid>https://dev.to/accuoa/vuls-vs-trivy-vs-grype-when-to-pick-which-cve-scanner-from-the-team-that-built-one-more-2bjd</guid>
      <description>&lt;h1&gt;
  
  
  Vuls vs Trivy vs Grype: when to pick which CVE scanner
&lt;/h1&gt;

&lt;p&gt;I shipped a CVE patch-ops tool last month. The most common feedback from engineers, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Why not just use Vuls?"&lt;/li&gt;
&lt;li&gt;"Doesn't Trivy already do this?"&lt;/li&gt;
&lt;li&gt;"Isn't Grype better?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are fair. They are all good. Here is the honest comparison I wish someone had handed me before I built mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vuls.io — the original self-hosted host scanner
&lt;/h2&gt;

&lt;p&gt;Vuls is the closest open-source equivalent to a managed patch-ops product. It's mature (started in 2016), in Go, and it does the same fundamental work: pull advisories from the upstream feeds, snapshot your box's package state, match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Vuls if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want everything on-prem / air-gapped — no third party sees your inventory.&lt;/li&gt;
&lt;li&gt;You have at least an afternoon of ops time to wire it up (config server, cron, report exporter, your own alerting).&lt;/li&gt;
&lt;li&gt;You're comfortable writing your own remediation playbooks. Vuls tells you the package + fixed version; what you do with it is up to you.&lt;/li&gt;
&lt;li&gt;You already have a Prometheus/Grafana stack you can plug the JSON output into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Vuls if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a 1-3 person dev shop and ops time is the bottleneck. You'll set it up, it'll run for two weeks, then a cron will silently fail and you'll forget about it for two months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trivy — containers and IaC, host CVEs as a bonus
&lt;/h2&gt;

&lt;p&gt;Trivy from Aqua is the most popular scanner now, but it's container-and-IaC-shaped. It scans images, Dockerfiles, Terraform/CloudFormation, Kubernetes manifests, SBOM files, and yes — also host filesystems via &lt;code&gt;trivy rootfs /&lt;/code&gt;. The latter is a real feature, but it's a sidecar to the container story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Trivy if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your security risk is concentrated in container images and you ship a lot of them.&lt;/li&gt;
&lt;li&gt;You want SBOM generation + license scanning + secret detection + misconfig in one binary.&lt;/li&gt;
&lt;li&gt;You're running Kubernetes and want admission-controller integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Trivy for host CVE management if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your fleet is bare-metal VPSes (no containers) — you're paying for a container model that doesn't fit your shape.&lt;/li&gt;
&lt;li&gt;You want per-host audit URLs that your customers can read, not just a CLI report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Grype — SBOM-first, simple
&lt;/h2&gt;

&lt;p&gt;Grype from Anchore is the cleanest of the three. Generate an SBOM with &lt;code&gt;syft&lt;/code&gt;, pipe it to &lt;code&gt;grype&lt;/code&gt;, get findings. It does exactly that and not much else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Grype if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already producing SBOMs as part of your build pipeline (you should be).&lt;/li&gt;
&lt;li&gt;You want a tool that does one thing well — match SBOM packages against the vulnerability DB.&lt;/li&gt;
&lt;li&gt;You're scripting CI gates and need predictable exit codes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip Grype if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the fix-action layer ("here is the apt command to run") — Grype is a finder, not a fixer.&lt;/li&gt;
&lt;li&gt;You need continuous monitoring of a running host, not a snapshot at build time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  StackPatch — the one I built
&lt;/h2&gt;

&lt;p&gt;Built specifically for indie SaaS shops running 1-3 Linux boxes. The bet is that the gap between "free OSS scanners you have to babysit" and "$25-50K/yr Snyk-class products" leaves out tens of thousands of indie founders. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosted by us.&lt;/strong&gt; No &lt;code&gt;apt install vuls&lt;/code&gt;. SSH read-only or a small read-only agent, both of which you can revoke instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action-first.&lt;/strong&gt; Every finding ships with the exact &lt;code&gt;apt install --only-upgrade pkg=fixed-version&lt;/code&gt; one-liner, or the modprobe blacklist syntax for kernel-module CVEs, or the &lt;code&gt;pro attach + apt upgrade&lt;/code&gt; flow for Ubuntu Pro / ESM fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public audit URL per server.&lt;/strong&gt; A read-only URL you can hand to your enterprise prospects: "here is our security posture, timestamped." Replaces the emailed PDF that's stale a week later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie-priced.&lt;/strong&gt; $99 lifetime for the first 50 founders, then $19-49/mo. Snyk's smallest sales-team-required tier is roughly 10x that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick StackPatch if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a solo founder or 1-3 person team on Ubuntu / Debian / Alpine / AlmaLinux / Rocky.&lt;/li&gt;
&lt;li&gt;You want patch ops to be a 5-minute habit, not a 5-hour setup.&lt;/li&gt;
&lt;li&gt;You want a public security URL more than you want CLI integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip StackPatch if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need air-gap / on-prem — use Vuls.&lt;/li&gt;
&lt;li&gt;Your scope is container images, not running hosts — use Trivy.&lt;/li&gt;
&lt;li&gt;You're already producing SBOMs in CI — use Grype.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bare-metal VPS fleet, no security team&lt;/td&gt;
&lt;td&gt;StackPatch (hosted) or Vuls (self-hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container-heavy CI/CD&lt;/td&gt;
&lt;td&gt;Trivy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBOM-driven build pipeline&lt;/td&gt;
&lt;td&gt;Grype&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Air-gapped / no third party allowed&lt;/td&gt;
&lt;td&gt;Vuls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise + budget + compliance team&lt;/td&gt;
&lt;td&gt;Snyk / Tenable / Wiz&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The competitive landscape is honest. I'm not pretending StackPatch beats Vuls on universality or Trivy on container coverage. I'm betting on a different problem shape: indie founders who'd pay $19-99 for a hosted patch-ops tool that hands them a per-server URL and an exact remediation command.&lt;/p&gt;

&lt;p&gt;If you're in that shape, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; takes 30 seconds. If you're in a different shape, one of the other three is the right answer — and I'd rather lose the sale than be the wrong tool for your workload.&lt;/p&gt;

&lt;p&gt;Full comparison breakdowns are on &lt;a href="https://mindsparkstack.com/patch/vs-vuls?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-vuls&lt;/a&gt;, &lt;a href="https://mindsparkstack.com/patch/vs-trivy?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-trivy&lt;/a&gt;, and &lt;a href="https://mindsparkstack.com/patch/vs-grype?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=scanner-compare" rel="noopener noreferrer"&gt;/patch/vs-grype&lt;/a&gt;. Comments open — I'll defend or concede whichever way the argument goes.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>linux</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Matching live CVEs to your actual apt packages in ~800 lines of Python</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:46:42 +0000</pubDate>
      <link>https://dev.to/accuoa/matching-live-cves-to-your-actual-apt-packages-in-800-lines-of-python-119h</link>
      <guid>https://dev.to/accuoa/matching-live-cves-to-your-actual-apt-packages-in-800-lines-of-python-119h</guid>
      <description>&lt;h1&gt;
  
  
  Matching live CVEs to your actual apt packages in ~800 lines of Python
&lt;/h1&gt;

&lt;p&gt;41,000+ CVEs are indexed in the NVD right now. The vast majority do not affect you. The interesting engineering question is: which ones do, and what is the exact one-liner to fix them?&lt;/p&gt;

&lt;p&gt;I spent a weekend building the matcher. Here is the shape of the problem and the shape of the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The matching problem
&lt;/h2&gt;

&lt;p&gt;A CVE record from one of the upstream feeds (Ubuntu USN, Debian Security Tracker, Alpine secdb, OSV.dev for the RHEL family, NVD as a fallback) looks roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USN-7100-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenSSH client vulnerability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cves"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2026-35414"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-2026-35387"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"releases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noble"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"binaries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"openssh-client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1:9.6p1-3ubuntu13.5"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-09T00:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server, meanwhile, has a snapshot like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dpkg-query &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'${Package}=${Version}\n'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
openssh-client&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
openssh-server&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
openssh-sftp-server&lt;span class="o"&gt;=&lt;/span&gt;1:9.6p1-3ubuntu13.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match logic: for each advisory, intersect on package name. For every hit, compare the installed version against the fixed version using &lt;code&gt;dpkg --compare-versions&lt;/code&gt;. If &lt;code&gt;installed lt fixed&lt;/code&gt;, you are affected; emit the exact &lt;code&gt;apt install &amp;lt;pkg&amp;gt;=&amp;lt;fixed&amp;gt;&lt;/code&gt; one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual matcher loop
&lt;/h2&gt;

&lt;p&gt;The core is brutally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_affected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wrap dpkg --compare-versions. Returns True if installed &amp;lt; fixed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpkg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--compare-versions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match_advisory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;codename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;releases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binaries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_affected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advisory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;advisory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cves&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;package&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;installed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remediation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install --only-upgrade &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fixed_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that over the full advisory cache (USN + DSA + Alpine secdb + OSV-RPM) against &lt;code&gt;dpkg-query&lt;/code&gt; output, and you get a per-server findings list in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets interesting
&lt;/h2&gt;

&lt;p&gt;The version compare is the easy part. The hard parts are the edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Kernel-module CVEs.&lt;/strong&gt; Some CVEs aren't a package version bump at all. CVE-2026-31431 (the "Copy Fail" algif_aead local-priv-esc, disclosed late April) was patched at the upstream kernel level — but Ubuntu had not shipped a fixed kernel for noble at advisory time. So &lt;code&gt;apt install --only-upgrade linux-image-generic&lt;/code&gt; was a no-op. The correct mitigation was a modprobe blacklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/modprobe.d/cve-2026-31431-copyfail.conf
&lt;/span&gt;&lt;span class="n"&gt;blacklist&lt;/span&gt; &lt;span class="n"&gt;algif_aead&lt;/span&gt;
&lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;algif_aead&lt;/span&gt; /&lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the matcher needs a &lt;code&gt;playbook_class&lt;/code&gt; field on each advisory: &lt;code&gt;apt_upgrade&lt;/code&gt;, &lt;code&gt;kernel_reboot&lt;/code&gt;, &lt;code&gt;modprobe_block&lt;/code&gt;, &lt;code&gt;apt_upgrade_esm&lt;/code&gt; (Ubuntu Pro), &lt;code&gt;oss_only_fix&lt;/code&gt;. The default is &lt;code&gt;apt_upgrade&lt;/code&gt;; the rest are hand-curated for kernel/firmware CVEs because the upstream feeds don't tag them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ubuntu Pro / ESM.&lt;/strong&gt; Many "fixed" Ubuntu CVEs are fixed only in Ubuntu Pro (the ESM channel). Free for personal + small-team use, but the fix won't apply without attaching a Pro token. The matcher emits a different playbook for those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pro attach &amp;lt;token&amp;gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--only-upgrade&lt;/span&gt; &amp;lt;pkg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Multi-arch and source vs binary package confusion.&lt;/strong&gt; &lt;code&gt;libssl3&lt;/code&gt; (binary) and &lt;code&gt;openssl&lt;/code&gt; (source) are both real package names. The advisory binaries map is the source of truth — match on that, not the source name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Apt pinning and snapshot repos.&lt;/strong&gt; If a customer is pinned to an older &lt;code&gt;noble-updates&lt;/code&gt; snapshot, the &lt;code&gt;fixed&lt;/code&gt; version may not be installable on their box. The matcher flags this as a &lt;code&gt;remediation_blocked: pin_conflict&lt;/code&gt; state instead of pretending the fix is one apt command away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed plumbing
&lt;/h2&gt;

&lt;p&gt;The actual fetcher is 9 cron jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-usn-poller.py&lt;/code&gt; — every 30 minutes, polls &lt;code&gt;https://ubuntu.com/security/notices.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-dsa-poller.py&lt;/code&gt; — every 30 minutes, parses &lt;code&gt;https://security-tracker.debian.org/tracker/data/json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-alpine-secdb-poller.py&lt;/code&gt; — every hour, fetches alpine-secdb branch refs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-osv-rpm-poller.py&lt;/code&gt; — every hour, OSV.dev RHEL/AlmaLinux/Rocky ecosystems&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-nvd-poller.py&lt;/code&gt; — every 6 hours, NVD modified feed as a backstop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-matcher.py&lt;/code&gt; — every hour, runs the match loop against the inventory cache&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-index-builder.py&lt;/code&gt; — twice daily, regenerates the per-CVE static pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-alert-dispatcher.py&lt;/code&gt; — every 15 minutes, fans out new findings to email/Telegram/Discord webhooks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stackpatch-customer-backup.py&lt;/code&gt; — daily, snapshots inventory + findings per server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;State lives as JSONL at &lt;code&gt;/var/lib/stackpatch/{advisories,findings,inventory}/&lt;/code&gt;. Not a database — for fewer than 10,000 servers, JSONL + atomic rename is faster and easier to back up than Postgres. A V2 with FTS will probably move it, but premature SQL is a tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "use it on yourself" rule
&lt;/h2&gt;

&lt;p&gt;The first server we pointed the matcher at was our own production VPS — the one this blog is served from. The matcher found 4 outstanding CVEs we hadn't seen. We patched 3 (the OpenSSH set) within an hour; the 4th is gated on attaching a Pro token, which is correctly flagged. That ratio — "matcher finds real CVEs on its own author's box" — is the only credibility test I trust for a security tool.&lt;/p&gt;

&lt;p&gt;If you run Linux on a VPS and you don't have a continuous CVE-to-action pipeline, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=matcher-deepdive" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; takes about 30 seconds. Or read the &lt;a href="https://mindsparkstack.com/patch?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=matcher-deepdive" rel="noopener noreferrer"&gt;open methodology page&lt;/a&gt;. I'm @aiden_bolin in the comments if you have edge-case advisories the matcher misses — that is exactly the feedback I'm hunting for right now.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>The indie SaaS security stack I run on a $7/mo VPS</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Wed, 13 May 2026 19:46:06 +0000</pubDate>
      <link>https://dev.to/accuoa/the-indie-saas-security-stack-i-run-on-a-7mo-vps-1457</link>
      <guid>https://dev.to/accuoa/the-indie-saas-security-stack-i-run-on-a-7mo-vps-1457</guid>
      <description>&lt;h1&gt;
  
  
  The indie SaaS security stack I run on a $7/mo VPS
&lt;/h1&gt;

&lt;p&gt;When you're a 1-3 person dev shop, you can't afford Snyk and you don't have a security team. You also can't afford to get breached. Here's the full stack I actually run on the $7/mo Hostinger VPS that serves my production SaaS.&lt;/p&gt;

&lt;p&gt;This is not a "best practices" article. Every line below corresponds to a config file in production right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. UFW — block everything by default
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow ssh
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five commands. The single largest reduction in attack surface you can make in 30 seconds. Verify with &lt;code&gt;sudo ufw status verbose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SSH keys only, no passwords
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/etc/ssh/sshd_config.d/00-hardening.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;PasswordAuthentication&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PermitRootLogin&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PubkeyAuthentication&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveInterval&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;span class="k"&gt;ClientAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;MaxAuthTries&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo systemctl restart ssh&lt;/code&gt;. Now password brute-force attempts hit a wall on the first packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. fail2ban — auto-ban brute-forcers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;600&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three failed SSH attempts in 10 minutes → IP banned for an hour. Check &lt;code&gt;sudo fail2ban-client status sshd&lt;/code&gt; after a week — you'll see hundreds of banned IPs. Most of the noise just stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. unattended-upgrades — apt patches without you
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unattended-upgrades
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;--priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;low unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Allowed&lt;/span&gt;-&lt;span class="n"&gt;Origins&lt;/span&gt; {
    &lt;span class="s2"&gt;"${distro_id}:${distro_codename}-security"&lt;/span&gt;;
    &lt;span class="s2"&gt;"${distro_id}ESM:${distro_codename}"&lt;/span&gt;;
};
&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Automatic&lt;/span&gt;-&lt;span class="n"&gt;Reboot&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;;
&lt;span class="n"&gt;Unattended&lt;/span&gt;-&lt;span class="n"&gt;Upgrade&lt;/span&gt;::&lt;span class="n"&gt;Mail&lt;/span&gt; &lt;span class="s2"&gt;"ops@yourdomain.com"&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately leave &lt;code&gt;Automatic-Reboot "false"&lt;/code&gt; because a 3am reboot during traffic is its own incident. Instead, see step 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Stripe webhook signature validation
&lt;/h2&gt;

&lt;p&gt;If you take payments, every webhook handler must verify the signature header. Without this, anyone can POST &lt;code&gt;customer.subscription.created&lt;/code&gt; events at your endpoint and grant themselves access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... safe to process&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail closed. If &lt;code&gt;STRIPE_WEBHOOK_SECRET&lt;/code&gt; is unset, return 503 — never silently skip the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Kernel patching with an actual reboot strategy
&lt;/h2&gt;

&lt;p&gt;This is the part most indie shops skip. &lt;code&gt;unattended-upgrades&lt;/code&gt; installs the new kernel but won't reboot. The running kernel stays vulnerable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/usr/local/bin/kernel-patch-check.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;installed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'linux-image-*'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^ii/ {print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/linux-image-//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$running&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$installed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Reboot pending: running=&lt;/span&gt;&lt;span class="nv"&gt;$running&lt;/span&gt;&lt;span class="s2"&gt; installed=&lt;/span&gt;&lt;span class="nv"&gt;$installed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Kernel patch ready on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ops@yourdomain.com
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cron it hourly. Get an email the moment a new kernel ships and is installed. Then you reboot during your next low-traffic window — manually, with eyes on the systemd units coming back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The CVE-to-action layer (the missing piece)
&lt;/h2&gt;

&lt;p&gt;Steps 1-6 cover known-class vulnerabilities being delivered via apt. They don't cover the case where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CVE drops for a kernel module (CVE-2026-31431 / "Copy Fail", late April) and no Ubuntu kernel fix is shipped yet — you need a &lt;code&gt;modprobe blacklist&lt;/code&gt; for the affected module, not an apt update.&lt;/li&gt;
&lt;li&gt;A CVE is fixed only in Ubuntu Pro (ESM) and your free Ubuntu install can't see the fix — you need to attach a free Pro token first.&lt;/li&gt;
&lt;li&gt;You want to know within 5 minutes that an advisory landed for your stack, not when the cron mail eventually arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried Snyk's free tier. Hit the 200-vulns/mo limit in under a week. Their smallest sales-team-required tier was ~$25K/yr. I tried Vuls.io — works, but I spent more time tuning it than I would have spent reading the advisories myself.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://mindsparkstack.com/patch?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=indie-saas-stack" rel="noopener noreferrer"&gt;StackPatch&lt;/a&gt; for exactly this shape. SSH read-only or small read-only agent. Continuous match against the upstream feeds (USN, DSA, Alpine secdb, OSV.dev for RHEL family). Per-server public audit URL. Exact remediation command per finding — including the modprobe-blacklist case, not just &lt;code&gt;apt upgrade&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;$99 lifetime for the first 50 founders, then $19-49/mo. The free quickscan is a one-liner you can curl right now without signing up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That'll spit out your kernel + apt package state matched against the live USN feed. No data sent anywhere unless you opt into the waitlist + monitoring at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest pitch
&lt;/h2&gt;

&lt;p&gt;This stack — UFW, SSH hardening, fail2ban, unattended-upgrades, kernel patch checker, webhook signature validation, and a CVE-to-action layer — covers most of what a security team would set up for a small Linux fleet. The first six pieces are free and take about an hour. The seventh piece is the gap most indie shops just live with, because the existing tools are either ops-heavy or enterprise-priced.&lt;/p&gt;

&lt;p&gt;If you have a more complete stack, the comments are open and I will steal from you. If you're missing pieces, the &lt;a href="https://mindsparkstack.com/patch/scan?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=indie-saas-stack" rel="noopener noreferrer"&gt;free quickscan&lt;/a&gt; will tell you which ones in 30 seconds.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>security</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>I built a CVE patch-ops tool for indie SaaS shops in a weekend (open scan, honest comparison vs vuls.io)</title>
      <dc:creator>Aiden Bolin</dc:creator>
      <pubDate>Fri, 01 May 2026 14:02:25 +0000</pubDate>
      <link>https://dev.to/accuoa/i-built-a-cve-patch-ops-tool-for-indie-saas-shops-in-a-weekend-open-scan-honest-comparison-vs-3llk</link>
      <guid>https://dev.to/accuoa/i-built-a-cve-patch-ops-tool-for-indie-saas-shops-in-a-weekend-open-scan-honest-comparison-vs-3llk</guid>
      <description>&lt;h1&gt;
  
  
  I built a CVE patch-ops tool for indie SaaS shops in a weekend (open scan, honest comparison vs vuls.io)
&lt;/h1&gt;

&lt;p&gt;Last week Hostinger emailed every customer about CVE-2026-31431 — the "Copy Fail" Linux kernel local-privilege-escalation. A 732-byte Python script gets root via &lt;code&gt;algif_aead&lt;/code&gt; AF_ALG + &lt;code&gt;splice()&lt;/code&gt;. Every kernel between 2017 and the upstream fix in early 2026.&lt;/p&gt;

&lt;p&gt;I run a one-person SaaS on a Hostinger VPS. I read the advisory, dropped a &lt;code&gt;modprobe&lt;/code&gt; blacklist, and was patched in 30 minutes. Then I realized: most indie SaaS founders running their own boxes wouldn't read that email until tomorrow. Some would never read it at all.&lt;/p&gt;

&lt;p&gt;Patch ops for the indie-SaaS tier doesn't exist. vuls.io is great if you have a security engineer with half a day. Enterprise tools are unaffordable. The gap is "I have 1–10 Linux servers, I know I should patch, the workflow is too annoying to do consistently."&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;StackPatch&lt;/strong&gt; — &lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash&lt;/code&gt; for a free anonymous CVE check, $99 lifetime founder seat for hourly monitoring with a public audit URL.&lt;/p&gt;

&lt;p&gt;This post is the build log: architecture, the honest vs-vuls comparison, and the bash one-liner you can run on your VPS in five seconds to see what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free quickscan: 5 seconds, 0 signup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script reads &lt;code&gt;/etc/os-release&lt;/code&gt;, &lt;code&gt;uname -r&lt;/code&gt;, the top 200 packages from &lt;code&gt;dpkg-query&lt;/code&gt;, and POSTs them to a public API. The API runs the live USN feed (Ubuntu) and the Debian Security Tracker (~110K fix-records across bookworm/trixie/bullseye) against your inventory using &lt;code&gt;dpkg --compare-versions&lt;/code&gt;, returns matching CVEs with the exact remediation command.&lt;/p&gt;

&lt;p&gt;The source is served as &lt;code&gt;text/plain&lt;/code&gt; so you can &lt;code&gt;curl https://mindsparkstack.com/scan.sh&lt;/code&gt; and read it before piping to bash. No persistent state server-side beyond a 5-min cache. No identifying info collected (no hostname, no IP, no env vars).&lt;/p&gt;

&lt;p&gt;On a real noble box with &lt;code&gt;openssh-client 1:9.6p1-3ubuntu13.10&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== StackPatch quickscan ===
  distro:   ubuntu
  codename: noble
  kernel:   6.8.0-100-generic
  packages: 187

⚠️  2 active CVE matches on your stack right now (worst: high).
   Run the recommended commands above. To monitor every server hourly...

  [HIGH] CVE-2026-31431  Linux kernel "Copy Fail" — local-priv-esc via algif_aead
        why: Linux kernel local-priv-esc; 732-byte Python script gets root...
        match: Running kernel: 6.8.0-100-generic
        recommend: Apply persistent modprobe blacklist for algif_aead now...

  [HIGH] USN-8222-1  OpenSSH 9.6p1 vulnerabilities
        match: openssh-client: installed 1:9.6p1-3ubuntu13.10 &amp;lt; fixed 1:9.6p1-3ubuntu13.16
        recommend: sudo apt-get install --only-upgrade -y openssh-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the demo. Five seconds, real CVEs, exact commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: file-based, Python stdlib, no DB
&lt;/h2&gt;

&lt;p&gt;The whole backend is JSONL files on disk + cron + a small Next.js layer. There's no Postgres. There's no Kubernetes. The matcher is one Python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode of the matcher join
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;usn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cached_usns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;usn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_packages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_codename&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;dpkg_lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;dpkg --compare-versions&lt;/code&gt; for Debian-policy-correct version comparison.&lt;/strong&gt; Lexicographic compare is wrong for &lt;code&gt;1:9.6p1-3ubuntu13.10&lt;/code&gt; vs &lt;code&gt;1:9.6p1-3ubuntu13.16&lt;/code&gt; (lex says "10" &amp;gt; "16"). Spawning &lt;code&gt;dpkg&lt;/code&gt; once per pair is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-filter by codename.&lt;/strong&gt; USN &lt;code&gt;release_packages&lt;/code&gt; is keyed by &lt;code&gt;noble | jammy | focal | bionic&lt;/code&gt;. Reading the user's &lt;code&gt;/etc/os-release&lt;/code&gt; &lt;code&gt;VERSION_CODENAME&lt;/code&gt; upfront lets the matcher skip 90% of records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the USN window.&lt;/strong&gt; I scan the last 200 USNs (sorted by ID). Older ones are fine to stale; if a 2017 USN matters to your 2024 box, you have bigger problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debian Security Tracker is huge.&lt;/strong&gt; The &lt;code&gt;tracker.json&lt;/code&gt; is 70 MB with 36K fix-records per release. I pre-build per-codename indexes (&lt;code&gt;{package: [{cve, fixed_version, urgency}]}&lt;/code&gt;) once daily so the matcher loads ~12 MB instead of 70 MB per request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inventory + matcher run on three crons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="m"&gt;3&lt;/span&gt; * * * *           &lt;span class="n"&gt;inventory&lt;/span&gt;   (&lt;span class="n"&gt;reads&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;os&lt;/span&gt;-&lt;span class="n"&gt;release&lt;/span&gt;, &lt;span class="n"&gt;uname&lt;/span&gt;, &lt;span class="n"&gt;dpkg&lt;/span&gt;, &lt;span class="n"&gt;docker&lt;/span&gt;, &lt;span class="n"&gt;ports&lt;/span&gt;, &lt;span class="n"&gt;modprobe&lt;/span&gt;)
&lt;span class="m"&gt;23&lt;/span&gt;,&lt;span class="m"&gt;53&lt;/span&gt; * * * *       &lt;span class="n"&gt;USN&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;    (&lt;span class="n"&gt;Ubuntu&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="n"&gt;Notices&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;, &lt;span class="n"&gt;twice&lt;/span&gt; &lt;span class="n"&gt;hourly&lt;/span&gt;)
&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; * * *           &lt;span class="n"&gt;DSA&lt;/span&gt; &lt;span class="n"&gt;poll&lt;/span&gt;    (&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="n"&gt;Tracker&lt;/span&gt;, &lt;span class="n"&gt;daily&lt;/span&gt; — &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;huge&lt;/span&gt;)
&lt;span class="m"&gt;33&lt;/span&gt; * * * *          &lt;span class="n"&gt;matcher&lt;/span&gt;     (&lt;span class="n"&gt;joins&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; × &lt;span class="n"&gt;USN&lt;/span&gt; × &lt;span class="n"&gt;DSA&lt;/span&gt;, &lt;span class="n"&gt;writes&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="n"&gt;JSONL&lt;/span&gt;)
&lt;span class="m"&gt;40&lt;/span&gt; * * * *          &lt;span class="n"&gt;alerts&lt;/span&gt;      (&lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. The Next.js layer is a thin wrapper that exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/quickscan&lt;/code&gt; — anonymous POST, returns matches in &amp;lt;1s&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/enroll&lt;/code&gt; — paid customer enrolls a server with a token, returns audit URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/stackpatch/inventory&lt;/code&gt; — authenticated inventory POST from the agent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/patch/audit/&amp;lt;slug&amp;gt;&lt;/code&gt; — public posture page per server&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The killer workflow: the public audit URL
&lt;/h2&gt;

&lt;p&gt;This is what I didn't see in any other tool. Every monitored server gets a URL like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://mindsparkstack.com/patch/audit/mss-vps&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It shows current findings, applied mitigations, recent resolutions, package counts, kernel state. Updates hourly. Customers (or your customers' enterprise prospects) can verify your security posture without an NDA, without a stale PDF.&lt;/p&gt;

&lt;p&gt;When a $3K/year customer asks "how do you handle server security updates?" — you send the link. Done.&lt;/p&gt;

&lt;p&gt;This isn't a security scanner. It's a security-response receipt. That distinction is the whole reason the product exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest vs vuls.io
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vuls.io" rel="noopener noreferrer"&gt;vuls.io&lt;/a&gt; is the obvious comparison. It's free, OSS, mature since 2016, 10K+ GitHub stars, supports Ubuntu/Debian/RHEL/CentOS/Amazon Linux/openSUSE/Alpine/FreeBSD/Windows.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://mindsparkstack.com/patch/vs-vuls" rel="noopener noreferrer"&gt;/patch/vs-vuls&lt;/a&gt; as an honest 12-row green/red/grey side-by-side. Honest enough that the page actively recommends vuls.io if you fit those constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a security engineer with half a day to set up &lt;code&gt;go-cve-dictionary&lt;/code&gt; + &lt;code&gt;goval-dictionary&lt;/code&gt; + &lt;code&gt;gost&lt;/code&gt; + &lt;code&gt;cve-search&lt;/code&gt; and rebuild them on cron&lt;/li&gt;
&lt;li&gt;You need FreeBSD / Windows / openSUSE support&lt;/li&gt;
&lt;li&gt;Compliance forbids any package data leaving your network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick StackPatch instead if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a one-person SaaS shop with 1–10 boxes on Ubuntu, Debian, Alpine, AlmaLinux, or Rocky Linux&lt;/li&gt;
&lt;li&gt;You want the answer in 5 minutes, not half a day&lt;/li&gt;
&lt;li&gt;You want the exact &lt;code&gt;apt&lt;/code&gt; / &lt;code&gt;apk&lt;/code&gt; / &lt;code&gt;dnf&lt;/code&gt; / kernel-reboot / modprobe-blacklist one-liner, not just a CVE link&lt;/li&gt;
&lt;li&gt;You want a public audit URL for sales due diligence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a Sunday-afternoon scanner project sounds fun: vuls.io. If you want to be patched and provably so by tomorrow morning: StackPatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What V1 doesn't do
&lt;/h2&gt;

&lt;p&gt;I'm not going to lie about the gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No FreeBSD / Windows / openSUSE yet&lt;/strong&gt; (V1+ covers Ubuntu, Debian, Alpine, AlmaLinux, Rocky Linux — 18 release versions across those 5 distros, 41K unique CVEs cross-indexed from USN + DSA + Alpine secdb + OSV-rpm + NVD)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-apply&lt;/strong&gt; (deliberate — you should review the command, security-product trust is fragile)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi-user RBAC, no SSO, no compliance attestations&lt;/strong&gt; (this is for solo founders, not security teams)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Kubernetes&lt;/strong&gt; (out of scope for V1; you're not running k8s on a $5/mo VPS anyway)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SLA on remediation&lt;/strong&gt; (we tell you the command; you run it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those are dealbreakers, vuls.io or an enterprise tool is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: $99 lifetime for the first 50
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free quickscan:&lt;/strong&gt; anonymous, no signup, no limits. Run it on as many boxes as you like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$99 lifetime founder seat:&lt;/strong&gt; 3 servers, hourly monitoring, real-time email alerts, public audit URL, every V2+ feature included. &lt;strong&gt;50 only&lt;/strong&gt;, then it's a monthly subscription tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no growth-hack reason for the lifetime price. It's a forcing function — a recurring-revenue product is cheaper for me long-term but it lets me delay shipping. Lifetime sales mean I have to keep adding founders, which means I have to keep shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mindsparkstack.com/scan.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://mindsparkstack.com/scan.sh&lt;/code&gt; (read first, then pipe).&lt;br&gt;
Comparison: &lt;code&gt;https://mindsparkstack.com/patch/vs-vuls&lt;/code&gt;&lt;br&gt;
Live demo audit: &lt;code&gt;https://mindsparkstack.com/patch/audit/mss-vps&lt;/code&gt; (our own VPS, public)&lt;br&gt;
Buy ($99 lifetime, 50 only): &lt;code&gt;https://mindsparkstack.com/patch&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Comments welcome — what's missing, what's wrong, what would make you switch from vuls.io. I read everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Aiden runs MindSparkStack. StackPatch is the patch-ops layer. Source: this is shipped on Hostinger, with Next.js 16 + Python stdlib + cron, running on the same VPS whose audit URL is public above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
