<?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: David McHale</title>
    <description>The latest articles on DEV Community by David McHale (@david_dev_sec).</description>
    <link>https://dev.to/david_dev_sec</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3713414%2F51539356-4221-49f2-919b-8af5d175b255.png</url>
      <title>DEV Community: David McHale</title>
      <link>https://dev.to/david_dev_sec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_dev_sec"/>
    <language>en</language>
    <item>
      <title>Why Attack Surface Management Breaks in OT (and What Actually Works)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 27 May 2026 23:22:17 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/why-attack-surface-management-breaks-in-ot-and-what-actually-works-1jf9</link>
      <guid>https://dev.to/david_dev_sec/why-attack-surface-management-breaks-in-ot-and-what-actually-works-1jf9</guid>
      <description>&lt;p&gt;Attack surface management has a comfortable story.&lt;/p&gt;

&lt;p&gt;You enumerate your domains, discover the hosts behind them, fingerprint the services, find the holes, and feed the whole thing into a dashboard that refreshes on a schedule. For IT assets, that story mostly works.&lt;/p&gt;

&lt;p&gt;Then someone hands you an industrial network, and every assumption breaks at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocols were never designed to be questioned
&lt;/h2&gt;

&lt;p&gt;Most of the industrial protocols still running plants today predate the threat model we now apply to them. &lt;strong&gt;Modbus was published by Modicon in 1979.&lt;/strong&gt; It has no authentication, no encryption, and no concept of a session that could be hijacked, because none of that was relevant when the device on the other end sat three meters away on a serial loop (Modbus Organization, 2012). DNP3, S7comm, and BACnet carry the same inheritance. The ISA and IEC codified the modern expectations later, in the IEC 62443 series, but the installed base does not get rewritten because a standard arrives.&lt;/p&gt;

&lt;p&gt;This matters for attack surface management in a specific way. On an IT asset, an unauthenticated service is a finding. On an OT asset, &lt;strong&gt;unauthenticated is the protocol working as designed&lt;/strong&gt;. The exposure is real, but you cannot treat the device as misconfigured and move on. You are looking at a structural property of a system that may have a fifteen year service life and a vendor contract that voids the moment you touch the firmware.&lt;/p&gt;

&lt;h2&gt;
  
  
  You often cannot run the scan at all
&lt;/h2&gt;

&lt;p&gt;The second wall is harder. The core move of ASM is active interrogation: send a probe, read the response, infer the service. In OT, that move can take the asset down.&lt;/p&gt;

&lt;p&gt;This is not folklore. NIST's &lt;em&gt;Guide to Operational Technology Security&lt;/em&gt; treats availability and safety as first-order constraints and is explicit that security testing on live OT must be approached with caution, favoring passive monitoring over active interrogation wherever possible (NIST, 2023). The reason active scanning is risky is mundane:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A PLC's network stack is sized for deterministic control traffic, not for a scanner opening a thousand connections a second. The device does not misbehave because it is insecure. It misbehaves because it is busy keeping a physical process inside its safe envelope, and the scan is competing for the same scarce CPU.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the practitioner faces a constraint with no equivalent on the IT side. The most informative tool in the kit—the active scan—is frequently the one tool you are contractually and operationally forbidden to point at the target. Many industrial environments will only grant you a span port and a packet capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inventory problem compounds everything
&lt;/h2&gt;

&lt;p&gt;You cannot install an agent on a relay. You cannot expect a thirty year old RTU to answer an SNMP query. There is no EDR for a turbine governor. The endpoint-centric inventory methods that IT security leans on have no purchase here, which is why so many operators genuinely do not know what is on their own networks. Dragos has reported that the large majority of OT networks still lack meaningful network monitoring, which keeps poor asset visibility among the most persistent gaps it finds across the environments it assesses (Dragos, 2026).&lt;/p&gt;

&lt;p&gt;And the surface is not as air gapped as the org chart implies. IT and OT convergence, remote vendor access, and historian servers that bridge both worlds mean industrial assets routinely surface on the public internet. Internet-wide exposure of ICS protocols has been documented for over a decade, going back to the Project SHINE research (Radvanovsky &amp;amp; Brodsky, 2014), and you can still watch it live on any internet-wide scan engine today.&lt;/p&gt;

&lt;p&gt;The incidents that followed are not hypothetical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stuxnet&lt;/strong&gt; reprogrammed centrifuge controllers (Falliere et al., 2011).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;2015 and 2016 attacks on the Ukrainian grid&lt;/strong&gt; manipulated protection and switching equipment directly (Cherepanov, 2017).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triton&lt;/strong&gt; targeted a safety instrumented system, the layer of last resort designed to prevent physical harm (Dragos, 2017).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;2021 intrusion at the Oldsmar, Florida water treatment facility&lt;/strong&gt;, where an actor raised the sodium hydroxide dose through the SCADA interface, showed how low the bar for reaching a control system can be (CISA, 2021).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these required exotic protocol exploits. They required reaching devices that assumed no one hostile would ever speak to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works, and the tools that do it
&lt;/h2&gt;

&lt;p&gt;The discipline OT demands is the inverse of the ASM instinct in IT.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lead with passive. Earn the right to be active. Default to safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Start passive
&lt;/h3&gt;

&lt;p&gt;Start with passive discovery that never touches the plant. Internet-wide scan data from &lt;strong&gt;Shodan&lt;/strong&gt; and &lt;strong&gt;Censys&lt;/strong&gt; already indexes exposed ICS services by protocol, banner, and vendor, so you can map a client's externally reachable industrial footprint without sending a single packet to their network. A tool like &lt;strong&gt;&lt;code&gt;uncover&lt;/code&gt;&lt;/strong&gt; lets you query several of these engines from one interface and pull the candidate host list before deciding whether any active step is warranted at all. For the many industrial assessments that pass procurement on a passive-only attestation, this is the entire engagement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Earn the right to go active
&lt;/h3&gt;

&lt;p&gt;When active enumeration is authorized, reach for the gentlest tool that answers the question. &lt;strong&gt;Nmap&lt;/strong&gt; is the right starting point, not because it is exotic but because its ICS coverage is mature and well understood. The standard NSE library already ships protocol-aware scripts such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modbus-discover
s7-info
bacnet-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Digital Bond's &lt;strong&gt;Redpoint&lt;/strong&gt; NSE bundle extends that to a broader device set. These do identity and version enumeration rather than register manipulation, which is exactly the line you want to stay behind. The scripts are old and the repos are dormant, but the NSE invocations are stable against current Nmap and add no new toolchain to vet.&lt;/p&gt;

&lt;p&gt;For protocol-level detail, use a purpose-built scanner with a real safety posture. &lt;strong&gt;&lt;code&gt;scada-scanner&lt;/code&gt;&lt;/strong&gt; (MIT licensed) detects Modbus TCP, S7, DNP3, BACnet, EtherNet/IP, IEC 60870-5-104, OPC UA, and CODESYS, and ships a safe-mode flag that should be your default rather than your fallback. Protocol-specific tools such as &lt;strong&gt;&lt;code&gt;s7scan&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;plcscan&lt;/code&gt;&lt;/strong&gt; fill gaps when a vendor or device family needs closer attention.&lt;/p&gt;

&lt;p&gt;The selection criterion is not coverage breadth. It is whether the tool offers a passive or read-only mode, and whether its license actually lets you use it. Several of the broadest ICS scanners carry noncommercial licenses that unfortunately disqualify them from any paid engagement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the guardrails in
&lt;/h3&gt;

&lt;p&gt;Build the guardrails into the workflow, not into the operator's memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Safe mode on by default&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active OT scanning gated behind explicit per-target opt-in&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conservative concurrency and rate limits&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An audit-log line on every active probe&lt;/strong&gt; that records what ran, against what, and with which safety settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where orchestration earns its keep: when the safe configuration is the shipped default, a tired analyst at the end of a long assessment cannot accidentally fast-scan a live PLC. The point of building it into the platform, rather than leaving it to discipline, is that the safe choice becomes the path of least resistance.&lt;/p&gt;

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

&lt;p&gt;ICS and OT attack surface management is hard not because the tools are missing but because the field's central technique is the one move you frequently cannot make. The protocols answer honestly to anyone who asks. The devices misbehave when asked too loudly. The inventory resists every agent-based shortcut.&lt;/p&gt;

&lt;p&gt;The teams that do this well are not the ones with the most aggressive scanners. They are the ones who treat passive discovery as the default, active probing as a privilege, and safe mode as a starting position rather than a setting they remember to enable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In OT, the most professional thing your tooling can do is know when not to send the packet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cherepanov, A. (2017). &lt;em&gt;Win32/Industroyer: A new threat for industrial control systems.&lt;/em&gt; ESET.&lt;/li&gt;
&lt;li&gt;Cybersecurity and Infrastructure Security Agency. (2021). &lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa21-042a" rel="noopener noreferrer"&gt;&lt;em&gt;Compromise of a U.S. water treatment facility&lt;/em&gt; (Alert No. AA21-042A)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Dragos. (2017). &lt;a href="https://www.dragos.com/resources/whitepaper/trisis-analyzing-safety-system-targeting-malware/" rel="noopener noreferrer"&gt;&lt;em&gt;TRISIS: Analyzing safety system targeting malware.&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dragos. (2026). &lt;a href="https://www.dragos.com/ot-cybersecurity-year-in-review" rel="noopener noreferrer"&gt;&lt;em&gt;OT/ICS cybersecurity year in review.&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Falliere, N., Murchu, L. O., &amp;amp; Chien, E. (2011). &lt;em&gt;W32.Stuxnet dossier&lt;/em&gt; (Version 1.4). Symantec.&lt;/li&gt;
&lt;li&gt;Modbus Organization. (2012). &lt;a href="https://www.modbus.org/specs.php" rel="noopener noreferrer"&gt;&lt;em&gt;Modbus application protocol specification V1.1b3.&lt;/em&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;National Institute of Standards and Technology. (2023). &lt;a href="https://doi.org/10.6028/NIST.SP.800-82r3" rel="noopener noreferrer"&gt;&lt;em&gt;Guide to operational technology (OT) security&lt;/em&gt; (NIST SP 800-82 Rev. 3)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Radvanovsky, B., &amp;amp; Brodsky, J. (2014). &lt;em&gt;Project SHINE (SHodan INtelligence Extraction) findings report.&lt;/em&gt; Infracritical.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools referenced
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nmap&lt;/td&gt;
&lt;td&gt;Baseline scanner with mature ICS NSE coverage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/nmap/nmap" rel="noopener noreferrer"&gt;github.com/nmap/nmap&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redpoint&lt;/td&gt;
&lt;td&gt;Digital Bond ICS NSE script bundle&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/digitalbond/Redpoint" rel="noopener noreferrer"&gt;github.com/digitalbond/Redpoint&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scada-scanner&lt;/td&gt;
&lt;td&gt;Multi-protocol ICS scanner with safe mode (MIT)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/geeknik/scada-scanner" rel="noopener noreferrer"&gt;github.com/geeknik/scada-scanner&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uncover&lt;/td&gt;
&lt;td&gt;Unified frontend for Shodan/Censys/etc.&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/projectdiscovery/uncover" rel="noopener noreferrer"&gt;github.com/projectdiscovery/uncover&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s7scan&lt;/td&gt;
&lt;td&gt;Siemens S7 protocol scanner&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/klsecservices/s7scan" rel="noopener noreferrer"&gt;github.com/klsecservices/s7scan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plcscan&lt;/td&gt;
&lt;td&gt;PLC enumeration&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/meeas/plcscan" rel="noopener noreferrer"&gt;github.com/meeas/plcscan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shodan&lt;/td&gt;
&lt;td&gt;Internet-wide exposure search&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.shodan.io" rel="noopener noreferrer"&gt;shodan.io&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Censys&lt;/td&gt;
&lt;td&gt;Internet-wide exposure search&lt;/td&gt;
&lt;td&gt;&lt;a href="https://search.censys.io" rel="noopener noreferrer"&gt;search.censys.io&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HailBytes ASM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Open source ASM with OT-aware safe defaults&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/HailBytes/hailbytes-asm" rel="noopener noreferrer"&gt;github.com/HailBytes/hailbytes-asm&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>ics</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Production-Ready MCP Servers in 60 Seconds (Auth, Rate Limits, Audit Logs Included)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Sun, 24 May 2026 16:52:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/production-ready-mcp-servers-in-60-seconds-auth-rate-limits-audit-logs-included-25el</link>
      <guid>https://dev.to/david_dev_sec/production-ready-mcp-servers-in-60-seconds-auth-rate-limits-audit-logs-included-25el</guid>
      <description>&lt;p&gt;&lt;strong&gt;A TypeScript scaffold for production MCP servers that ships with pluggable auth, per-tool rate limiting, structured audit logs, and OpenTelemetry — so you can build the actual tools and not reinvent the boring parts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every MCP server tutorial I've read shows you how to register a single tool that echoes a string. Then they wave at "production concerns" and end the post.&lt;/p&gt;

&lt;p&gt;Production concerns &lt;em&gt;are&lt;/em&gt; the post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@hailbytes/mcp-server-template" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/mcp-server-template&lt;/code&gt;&lt;/a&gt; is the opinionated TypeScript scaffold I use when I need to ship an MCP server that an enterprise will actually run. It comes with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; — pluggable middleware for API keys, OAuth, and JWT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — per-client and per-tool, so one runaway agent can't take the whole server down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logging&lt;/strong&gt; — structured logs for every tool call and session event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry&lt;/strong&gt; — traces and metrics, so you can actually debug what your model did&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-transport&lt;/strong&gt; — SSE, stdio, and HTTP, picked at scaffold time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scaffold a new server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/create-mcp-server my-server &lt;span class="nt"&gt;--transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a directory you can &lt;code&gt;cd&lt;/code&gt; into and &lt;code&gt;npm run dev&lt;/code&gt; immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or embed it programmatically
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineTools&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hailbytes/mcp-server-template&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineTools&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;echo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Echoes the input back.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&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="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createMcpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Api-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestsPerMinute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire "production MCP server" diff vs. the tutorial echo example.&lt;/p&gt;

&lt;p&gt;Pair it with &lt;a href="https://www.npmjs.com/package/@hailbytes/mcp-security-scanner" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/mcp-security-scanner&lt;/code&gt;&lt;/a&gt; and you'll have a server that comes up secure by default and stays that way as you add tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/create-mcp-server my-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/hailbytes/mcp-server-template" rel="noopener noreferrer"&gt;github.com/hailbytes/mcp-server-template&lt;/a&gt; — MIT licensed.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>typescript</category>
      <category>node</category>
    </item>
    <item>
      <title>Your MCP Server Is Probably Overprivileged - Here's a Scanner For It</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Fri, 22 May 2026 20:51:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/your-mcp-server-is-probably-overprivileged-heres-a-scanner-for-it-3cmb</link>
      <guid>https://dev.to/david_dev_sec/your-mcp-server-is-probably-overprivileged-heres-a-scanner-for-it-3cmb</guid>
      <description>&lt;p&gt;&lt;strong&gt;MCP servers expose tools to LLMs, but most configs grant tools broader permissions than they need, ship without auth, and leak prompt-injection surface in tool descriptions. This scanner finds it before your model does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most MCP servers I've audited in the last few months had the same three issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;shell&lt;/code&gt; or &lt;code&gt;fs&lt;/code&gt; tool was scoped to the entire filesystem when the use case needed exactly one directory.&lt;/li&gt;
&lt;li&gt;The transport ran without auth because the local-dev SSE config got promoted to prod.&lt;/li&gt;
&lt;li&gt;Tool descriptions echoed verbatim into prompts with no sanitization — a perfect injection surface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@hailbytes/mcp-security-scanner" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/mcp-security-scanner&lt;/code&gt;&lt;/a&gt; is what I wish I'd had on day one of building MCP servers. It's a static + dynamic scanner for MCP configs and live endpoints that flags these patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan a local config&lt;/span&gt;
npx @hailbytes/mcp-security-scanner ./mcp-config.json

&lt;span class="c"&gt;# Scan a live endpoint&lt;/span&gt;
npx @hailbytes/mcp-security-scanner https://my-mcp-server.example.com

&lt;span class="c"&gt;# SARIF output + fail the build&lt;/span&gt;
npx @hailbytes/mcp-security-scanner ./config.json &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sarif &lt;span class="nt"&gt;--exit-code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Programmatic
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hailbytes/mcp-security-scanner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;configPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./mcp-config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;
  
  
  What it checks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overprivileged tools&lt;/strong&gt; — broader permissions than the declared function needs (filesystem scope, shell access, network egress)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing or weak authentication&lt;/strong&gt; — unauthenticated transports, missing token validation, plaintext secrets in config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection surface&lt;/strong&gt; — tool descriptions and output paths that pass through to model context without sanitization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsafe defaults&lt;/strong&gt; — insecure transport defaults, verbose error exposure, CORS wildcards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SARIF output drops straight into GitHub Code Scanning, so findings show up as alerts on PRs — same place your SAST results live.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @hailbytes/mcp-security-scanner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/hailbytes/mcp-security-scanner" rel="noopener noreferrer"&gt;github.com/hailbytes/mcp-security-scanner&lt;/a&gt; — MIT licensed. Pairs nicely with &lt;a href="https://github.com/hailbytes/mcp-server-template" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/mcp-server-template&lt;/code&gt;&lt;/a&gt; if you want a scaffold that comes up secure by default.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Lint Your Phishing Templates Like You Lint Your Code</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Thu, 21 May 2026 15:50:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/lint-your-phishing-templates-like-you-lint-your-code-3o1h</link>
      <guid>https://dev.to/david_dev_sec/lint-your-phishing-templates-like-you-lint-your-code-3o1h</guid>
      <description>&lt;p&gt;You spent two hours crafting a convincing IT helpdesk pretext. You ship the campaign. The click rate is 2%.&lt;/p&gt;

&lt;p&gt;It's not because employees got more savvy. It's because half your emails landed in spam, your tracking pixel was broken so you never saw the opens, and your &lt;code&gt;{{.FirstName}}&lt;/code&gt; was actually &lt;code&gt;{{.first_name}}&lt;/code&gt; and rendered as a literal string in every recipient's inbox.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.npmjs.com/package/@hailbytes/phishing-template-linter" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/phishing-template-linter&lt;/code&gt;&lt;/a&gt; after the third campaign in a row where this happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lint a directory of templates
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/phishing-template-linter ./templates/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a per-template report of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broken or unknown merge tags&lt;/li&gt;
&lt;li&gt;Missing tracking pixel / link rewrite hooks&lt;/li&gt;
&lt;li&gt;Spam-trigger phrases (the obvious ones, but also Gmail's newer heuristics)&lt;/li&gt;
&lt;li&gt;Deliverability red flags (mismatched display names, suspicious from-domain handling, bare URLs in plaintext)&lt;/li&gt;
&lt;li&gt;Missing or malformed HTML/text alternatives&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use it programmatically
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hailbytes/phishing-template-linter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateHtml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Errors fail the campaign launch; warnings get reviewed by a human&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wire it into CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/phishing-template-linter ./templates/ &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop that into your campaign-management pipeline and your phishing sims get the same pre-flight guardrails your production code already has.&lt;/p&gt;

&lt;p&gt;It's GoPhish-format aware, so the merge-tag rules know about &lt;code&gt;{{.FirstName}}&lt;/code&gt;, &lt;code&gt;{{.URL}}&lt;/code&gt;, &lt;code&gt;{{.TrackingURL}}&lt;/code&gt;, and the rest of the GoPhish template grammar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hailbytes/phishing-template-linter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/hailbytes/phishing-template-linter" rel="noopener noreferrer"&gt;github.com/hailbytes/phishing-template-linter&lt;/a&gt; — MIT licensed. Built as a companion to the HailBytes SAT platform but works standalone with any GoPhish-format templates.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Pasting URLs into Security Header Sites - Use This CLI</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 20 May 2026 15:47:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/stop-pasting-urls-into-security-header-sites-use-this-cli-4jg0</link>
      <guid>https://dev.to/david_dev_sec/stop-pasting-urls-into-security-header-sites-use-this-cli-4jg0</guid>
      <description>&lt;h2&gt;
  
  
  Get an A–F grade for your site's HTTP security headers without leaving the terminal. Use it as a library, a CLI, or a CI gate that fails deploys on regression.
&lt;/h2&gt;

&lt;p&gt;The flow goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ship a deploy.&lt;/li&gt;
&lt;li&gt;Alt-tab to securityheaders.com.&lt;/li&gt;
&lt;li&gt;Paste in the URL.&lt;/li&gt;
&lt;li&gt;Squint at the report.&lt;/li&gt;
&lt;li&gt;Realize someone removed the CSP three weeks ago and nobody noticed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted step 2 to be &lt;code&gt;npx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/security-headers https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prints a color report to the terminal. Add &lt;code&gt;--json&lt;/code&gt; to feed it into other tools, or just rely on the non-zero exit code on grade D or F to use it as a CI gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @hailbytes/security-headers https://staging.example.com &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Library
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;analyze&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hailbytes/security-headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { grade: 'A+', score: 95, percentage: 95, headers: [...] }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pass raw headers (for unit tests, or middleware that wants to grade its own response before sending):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;analyzeHeaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hailbytes/security-headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyzeHeaders&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strict-transport-security&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=31536000; includeSubDomains&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-security-policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default-src 'self'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-frame-options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DENY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;p&gt;Seven categories — HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and the Cross-Origin family (COEP/COOP/CORP). Each header gets a numeric score, a status (&lt;code&gt;good&lt;/code&gt; / &lt;code&gt;warning&lt;/code&gt; / &lt;code&gt;missing&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt;), and specific remediation strings you can drop straight into a ticket.&lt;/p&gt;

&lt;p&gt;The grading scale is the obvious one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A+&lt;/td&gt;
&lt;td&gt;≥ 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;≥ 75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;≥ 60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;≥ 40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;≥ 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;&amp;lt; 20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hailbytes/security-headers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/hailbytes/security-headers" rel="noopener noreferrer"&gt;github.com/hailbytes/security-headers&lt;/a&gt; — MIT licensed.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Score Any CVSS Vector Offline - v3.1 and v4.0, Zero Dependencies</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 20 May 2026 02:45:10 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/score-any-cvss-vector-offline-v31-and-v40-zero-dependencies-2501</link>
      <guid>https://dev.to/david_dev_sec/score-any-cvss-vector-offline-v31-and-v40-zero-dependencies-2501</guid>
      <description>&lt;p&gt;A 4 KB JavaScript library that parses and scores CVSS vectors with no network calls, no build step, and no third-party API. Use it in CI or drop a web component into any page.&lt;/p&gt;

&lt;p&gt;Every vuln management tool eventually needs to score a CVSS vector. Most of them either call out to NVD's API (slow, rate-limited, requires network egress from your scanner) or pull in a fat dependency that drags an old crypto library along for the ride.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.npmjs.com/package/@hailbytes/cvss-calc" rel="noopener noreferrer"&gt;&lt;code&gt;@hailbytes/cvss-calc&lt;/code&gt;&lt;/a&gt; because I wanted to score vectors inside a CI runner that didn't have internet access. It's a single, zero-dependency package that handles both CVSS v3.1 and v4.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score a vector in two lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;calculate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hailbytes/cvss-calc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { score: 9.8, severity: 'Critical', version: '3.1', vector: '...' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v4.0 works the same way — the library parses the version from the vector string and dispatches to the right scorer. No flag, no branching at the call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { score: 10.0, severity: 'Critical', version: '4.0', vector: '...' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Or drop it into any page as a web component
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@hailbytes/cvss-calc/dist/element.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;hailbytes-cvss-calc&lt;/span&gt; &lt;span class="na"&gt;vector=&lt;/span&gt;&lt;span class="s"&gt;"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/hailbytes-cvss-calc&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component renders a full interactive calculator. Listen for &lt;code&gt;cvss-calculated&lt;/code&gt; events to read the score from JS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'm using it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A pre-deploy CI gate that fails the build if any new CVE in the SBOM scores ≥ 7.0&lt;/li&gt;
&lt;li&gt;A ticketing integration that auto-prioritizes Jira issues by severity&lt;/li&gt;
&lt;li&gt;A static status page where each disclosed CVE renders a live, interactive calculator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scoring follows the official &lt;a href="https://www.first.org/cvss/v3.1/specification-document" rel="noopener noreferrer"&gt;FIRST CVSS v3.1&lt;/a&gt; and &lt;a href="https://www.first.org/cvss/v4.0/specification-document" rel="noopener noreferrer"&gt;v4.0&lt;/a&gt; specs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hailbytes/cvss-calc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source and docs: &lt;a href="https://github.com/hailbytes/cvss-calc" rel="noopener noreferrer"&gt;github.com/hailbytes/cvss-calc&lt;/a&gt; — MIT licensed.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why your phishing simulations land in spam (and the SPF / DKIM / DMARC fix that actually works)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Mon, 04 May 2026 02:56:41 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/why-your-phishing-simulations-land-in-spam-and-the-spf-dkim-dmarc-fix-that-actually-works-746</link>
      <guid>https://dev.to/david_dev_sec/why-your-phishing-simulations-land-in-spam-and-the-spf-dkim-dmarc-fix-that-actually-works-746</guid>
      <description>&lt;p&gt;Every security awareness program eventually has the same conversation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We sent the campaign yesterday. The dashboard says it went out. But&lt;br&gt;
nobody clicked, and three people on Slack are asking why they didn't get&lt;br&gt;
the test email."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then somebody opens their spam folder and finds the simulated phish&lt;br&gt;
sitting next to a Nigerian prince. The campaign isn't broken. The&lt;br&gt;
deliverability is.&lt;/p&gt;

&lt;p&gt;I've spent enough time debugging this for HailBytes SAT customers that I&lt;br&gt;
can write the post-mortem from memory. Here it is.&lt;/p&gt;
&lt;h3&gt;
  
  
  The core problem
&lt;/h3&gt;

&lt;p&gt;A phishing simulation is, by construction, an email designed to look&lt;br&gt;
suspicious. Modern mail providers (Microsoft 365, Google Workspace,&lt;br&gt;
Mimecast, Proofpoint) are trained on suspicious email and will quarantine&lt;br&gt;
it aggressively unless you give them strong signals to trust the sender.&lt;/p&gt;

&lt;p&gt;There are exactly four signals that matter at scale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; — does the sending IP have permission to send for this domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; — is the message body cryptographically signed by the domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; — what should receivers do if SPF/DKIM disagree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sender reputation&lt;/strong&gt; — has this IP / domain pair sent good mail
before&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get all four right and your campaigns hit the inbox. Miss any one and&lt;br&gt;
you're fighting probabilities.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: do not send from your real corporate domain
&lt;/h3&gt;

&lt;p&gt;This is the most common mistake. Teams deploy a phishing platform, point&lt;br&gt;
it at &lt;code&gt;noreply@theirrealcompany.com&lt;/code&gt;, and immediately damage their own&lt;br&gt;
domain reputation when receivers flag the test as suspicious.&lt;/p&gt;

&lt;p&gt;Use a &lt;strong&gt;separate purpose-built domain&lt;/strong&gt; for simulated phishing.&lt;br&gt;
&lt;code&gt;security-training.example-corp.com&lt;/code&gt; or a fresh look-alike domain owned&lt;br&gt;
by you. Publish DMARC on the real domain in &lt;code&gt;p=reject&lt;/code&gt;. Run the sim&lt;br&gt;
domain in &lt;code&gt;p=none&lt;/code&gt; (or &lt;code&gt;p=quarantine&lt;/code&gt; once warm) so receivers can&lt;br&gt;
classify it as deliverable-but-suspicious — exactly the behaviour you&lt;br&gt;
want for training.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: SPF that actually authorizes the sender
&lt;/h3&gt;

&lt;p&gt;If you're running HailBytes SAT (or any platform) on your own EC2&lt;br&gt;
instance and sending direct to MX, the SPF record on your sim domain&lt;br&gt;
needs to authorize that IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 ip4:203.0.113.42 -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're relaying through SES, SendGrid, or Postmark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:amazonses.com -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two &lt;code&gt;-all&lt;/code&gt; (hardfail) records will get you the strongest signal.&lt;br&gt;
&lt;code&gt;~all&lt;/code&gt; (softfail) is acceptable while warming up.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: DKIM, properly
&lt;/h3&gt;

&lt;p&gt;DKIM is the part that breaks silently. Generate a 2048-bit key&lt;br&gt;
(&lt;code&gt;openssl genrsa -out dkim.key 2048&lt;/code&gt;) and publish the public half as a&lt;br&gt;
TXT record at &lt;code&gt;selector._domainkey.sim-domain.example.com&lt;/code&gt;. In SAT we&lt;br&gt;
default to selector &lt;code&gt;s1&lt;/code&gt; — change it if you rotate keys.&lt;/p&gt;

&lt;p&gt;The signature &lt;strong&gt;must&lt;/strong&gt; cover at least &lt;code&gt;From&lt;/code&gt;, &lt;code&gt;Subject&lt;/code&gt;, &lt;code&gt;Date&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;To&lt;/code&gt;. If your SMTP relay is rewriting headers, your signature will&lt;br&gt;
break and DKIM will fail without an obvious error in the dashboard.&lt;/p&gt;

&lt;p&gt;Verify with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT s1._domainkey.sim-domain.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send yourself a test message and check &lt;code&gt;Authentication-Results&lt;/code&gt; in&lt;br&gt;
the raw headers. You want &lt;code&gt;dkim=pass&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: DMARC alignment
&lt;/h3&gt;

&lt;p&gt;DMARC requires that either SPF or DKIM passes &lt;strong&gt;and&lt;/strong&gt; the authenticated&lt;br&gt;
domain matches the From: domain. This trips up almost everyone who uses&lt;br&gt;
a relay service.&lt;/p&gt;

&lt;p&gt;If your From: is &lt;code&gt;security@sim.example.com&lt;/code&gt; but DKIM is signed by&lt;br&gt;
&lt;code&gt;amazonses.com&lt;/code&gt;, DMARC alignment fails even though DKIM itself passes.&lt;/p&gt;

&lt;p&gt;Fix: either sign with your own domain (best) or set the relay to use a&lt;br&gt;
custom MAIL FROM that aligns with your domain (acceptable).&lt;/p&gt;

&lt;p&gt;Publish DMARC at &lt;code&gt;_dmarc.sim-domain.example.com&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; pct=100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;p=none&lt;/code&gt; is intentional during warming. Move to &lt;code&gt;p=quarantine&lt;/code&gt; once&lt;br&gt;
your reports show consistent SPF and DKIM passes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: warm the IP
&lt;/h3&gt;

&lt;p&gt;A brand-new IP sending 5,000 phishing simulations on day one will go&lt;br&gt;
straight to spam, no matter how clean your DMARC is. Reputation is&lt;br&gt;
volume-and-pattern based.&lt;/p&gt;

&lt;p&gt;Realistic warming schedule for a new sim IP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Volume&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1-3&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Internal-only test accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-7&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Volunteer pilot group, mark "not spam" actively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-14&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;First small department&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15+&lt;/td&gt;
&lt;td&gt;2k+&lt;/td&gt;
&lt;td&gt;Full org, monitor postmaster tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Microsoft and Google both expose postmaster dashboards. Use them. If&lt;br&gt;
you see reputation dropping to "low" or "bad" you're sending faster&lt;br&gt;
than the receiver trusts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: per-recipient header rules
&lt;/h3&gt;

&lt;p&gt;Even with all of the above, individual mailboxes can have transport&lt;br&gt;
rules that quarantine specific patterns. The single most useful thing&lt;br&gt;
you can do as a security team is &lt;strong&gt;publish an internal allowlist&lt;br&gt;
document&lt;/strong&gt; that mail admins can apply to bypass quarantine for the&lt;br&gt;
sim domain only. Most receivers support per-domain or per-IP overrides.&lt;/p&gt;

&lt;p&gt;A reasonable Microsoft 365 example: add the sim IP to the IP Allow&lt;br&gt;
List in the connection filter, and create a transport rule that sets&lt;br&gt;
SCL to -1 for messages from &lt;code&gt;*@sim-domain.example.com&lt;/code&gt;. This bypasses&lt;br&gt;
spam filtering but, importantly, &lt;em&gt;not&lt;/em&gt; the user's perception — the&lt;br&gt;
email still arrives looking suspicious, which is the whole point.&lt;/p&gt;

&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;When customers walk through this checklist with HailBytes SAT, inbox&lt;br&gt;
placement on Microsoft 365 typically goes from 30-40% (campaign&lt;br&gt;
launched cold) to 90%+ (campaign launched after a 2-week warm). The&lt;br&gt;
single biggest jump comes from getting DKIM alignment right, not from&lt;br&gt;
any IP-warming voodoo.&lt;/p&gt;

&lt;p&gt;If you want to skip the homework, the platform handles SPF / DKIM /&lt;br&gt;
DMARC scaffolding and IP warming as part of deployment — full guide at&lt;br&gt;
&lt;a href="https://hailbytes.com/sat/" rel="noopener noreferrer"&gt;hailbytes.com/sat&lt;/a&gt;. But honestly, the&lt;br&gt;
above checklist is product-agnostic. Whatever sim platform you're&lt;br&gt;
running, work through these six steps before you blame the tool.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Giving an AI agent a recon toolbox: wiring 30+ security tools into an MCP server</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Mon, 04 May 2026 02:56:19 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/giving-an-ai-agent-a-recon-toolbox-wiring-30-security-tools-into-an-mcp-server-3nbm</link>
      <guid>https://dev.to/david_dev_sec/giving-an-ai-agent-a-recon-toolbox-wiring-30-security-tools-into-an-mcp-server-3nbm</guid>
      <description>&lt;p&gt;If you've watched a junior pen-tester spend a Monday morning typing the same&lt;br&gt;
six commands into a fresh EC2 box, you've seen the recon setup tax up close.&lt;br&gt;
&lt;code&gt;amass enum -passive -d $TARGET&lt;/code&gt;, &lt;code&gt;subfinder -d $TARGET -silent&lt;/code&gt;, pipe to&lt;br&gt;
&lt;code&gt;httpx&lt;/code&gt;, pipe to &lt;code&gt;naabu&lt;/code&gt;, feed surviving hosts into &lt;code&gt;nuclei&lt;/code&gt;, dump JSON&lt;br&gt;
somewhere, repeat next quarter when the scope changes.&lt;/p&gt;

&lt;p&gt;The work isn't hard. The glue is. Every team I've talked to has rebuilt this&lt;br&gt;
glue at least twice, usually in a different language each time.&lt;/p&gt;

&lt;p&gt;This post is about a different shape of the problem: what happens when you&lt;br&gt;
stop writing the glue yourself and instead expose the recon toolbox as&lt;br&gt;
&lt;strong&gt;MCP tools&lt;/strong&gt; that an AI agent can call?&lt;/p&gt;
&lt;h3&gt;
  
  
  Why MCP, specifically
&lt;/h3&gt;

&lt;p&gt;Agents have been doing "tool use" for a couple of years now via bespoke&lt;br&gt;
function-calling adapters. The problem with those adapters is that every&lt;br&gt;
agent framework wants its own JSON shape, every tool needs its own auth, and&lt;br&gt;
every team writes its own retry/timeout/rate-limit middleware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP (Model Context Protocol)&lt;/a&gt; collapses&lt;br&gt;
all of that into one server-side contract. Once your tools are MCP tools,&lt;br&gt;
any compliant client — Claude Desktop, Cursor, your own LangGraph agent —&lt;br&gt;
can drive them.&lt;/p&gt;

&lt;p&gt;For recon, the value is asymmetric. Recon is one of the rare security&lt;br&gt;
workflows that's &lt;strong&gt;iterative&lt;/strong&gt; and &lt;strong&gt;branching&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;enumerate subdomains → resolve → port-scan live hosts →
fingerprint services → run targeted vuln checks → pivot to new assets →
loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loop is exactly the shape an LLM is good at orchestrating, provided&lt;br&gt;
the tools return structured data and the agent can hold the inventory in&lt;br&gt;
state. You don't want the LLM running &lt;code&gt;nmap&lt;/code&gt;. You want it deciding &lt;em&gt;when&lt;/em&gt;&lt;br&gt;
to run &lt;code&gt;nmap&lt;/code&gt; and &lt;em&gt;on what&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  What we wrapped
&lt;/h3&gt;

&lt;p&gt;In HailBytes ASM (full disclosure, this is our product — built specifically&lt;br&gt;
for pen-test firms and MSSPs), the MCP server exposes the same surface as&lt;br&gt;
the REST API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt;: &lt;code&gt;start_subdomain_scan&lt;/code&gt;, &lt;code&gt;start_port_scan&lt;/code&gt;, &lt;code&gt;start_dns_scan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability&lt;/strong&gt;: &lt;code&gt;start_nuclei_scan&lt;/code&gt;, &lt;code&gt;start_template_scan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt;: &lt;code&gt;list_assets&lt;/code&gt;, &lt;code&gt;get_asset_history&lt;/code&gt;, &lt;code&gt;diff_scans&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting&lt;/strong&gt;: &lt;code&gt;export_findings&lt;/code&gt;, &lt;code&gt;get_scan_summary&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool returns JSON with stable schemas — not log scrapes — so the agent&lt;br&gt;
can plan multi-step workflows without the model having to parse stderr.&lt;/p&gt;
&lt;h3&gt;
  
  
  A working loop
&lt;/h3&gt;

&lt;p&gt;A real session looks like this (paraphrased from one of our internal eval&lt;br&gt;
runs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Map the external attack surface for example.com and flag anything
       that looks like an exposed staging environment."

Agent → start_subdomain_scan(domain="example.com")
Agent → list_assets(scan_id=...)  // 312 hosts
Agent → start_port_scan(targets=[...], top_ports=1000)
Agent → start_nuclei_scan(targets=live_hosts, severity=["medium","high"])
Agent → list_assets(filter="hostname matches /staging|stg|dev|qa/")
Agent → get_asset_history(asset_id=...)  // appeared 6 days ago
Agent → "Found 4 hosts matching staging-like patterns; one
        (stg-admin.example.com) appeared 6 days ago and exposes a Jenkins
        instance with a known CVE..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is what the agent &lt;em&gt;doesn't&lt;/em&gt; do: it doesn't shell out,&lt;br&gt;
doesn't manage AWS credentials, doesn't worry about rate limits, doesn't&lt;br&gt;
re-implement scan diffing. The MCP tools take care of all of that. The&lt;br&gt;
agent's job is the part that's actually hard — choosing the next action.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke (and what we changed)
&lt;/h3&gt;

&lt;p&gt;A few honest notes from running this at customer sites:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pagination kills agents.&lt;/strong&gt; Our first cut returned all assets in a
single response. With 30k+ subdomains in a real engagement, the agent's
context filled up before it got to the analysis step. We added cursor
pagination and a &lt;code&gt;summarize_assets&lt;/code&gt; tool that returns aggregates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit state is hostile.&lt;/strong&gt; Agents are bad at remembering "the most
recent scan." Every tool that takes a &lt;code&gt;scan_id&lt;/code&gt; now requires it
explicitly, even if there's only ever one running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-running scans need a status protocol.&lt;/strong&gt; Recon scans take minutes
to hours. We added &lt;code&gt;wait_for_scan(scan_id, timeout)&lt;/code&gt; so the agent can
block politely instead of polling in a tight loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where this fits
&lt;/h3&gt;

&lt;p&gt;If you're already running recon in-house, you don't need to buy anything to&lt;br&gt;
try this pattern — wrap your own scripts in an MCP server and you'll get&lt;br&gt;
70% of the value. The harder parts are the things that show up at&lt;br&gt;
production scale: scan diffing, asset deduplication across runs, multi-&lt;br&gt;
tenant isolation, scheduled cadence, audit trails for compliance. That's&lt;br&gt;
the part we've spent the last year on.&lt;/p&gt;

&lt;p&gt;If you want to see it end-to-end, the platform is at&lt;br&gt;
&lt;a href="https://hailbytes.com/asm/" rel="noopener noreferrer"&gt;hailbytes.com/asm&lt;/a&gt; — deploys from the AWS or&lt;br&gt;
Azure Marketplace, runs in your account, and exposes the MCP endpoint out&lt;br&gt;
of the box.&lt;/p&gt;

&lt;p&gt;Either way, I think MCP-native security tooling is going to be the&lt;br&gt;
default within 18 months. The gap between "agent can read a Splunk&lt;br&gt;
dashboard" and "agent can drive a recon engagement" is closing fast, and&lt;br&gt;
the teams that wire their own toolbox up early are going to have a real&lt;br&gt;
edge.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>mcp</category>
    </item>
    <item>
      <title>That Time SQLite File Existence Lied to Us</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 11 Feb 2026 16:57:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/that-time-sqlite-file-existence-lied-to-us-4em3</link>
      <guid>https://dev.to/david_dev_sec/that-time-sqlite-file-existence-lied-to-us-4em3</guid>
      <description>&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Our bootstrap script was killing GoPhish before database migrations could finish. Took us way too long to figure out why.&lt;/p&gt;

&lt;p&gt;Here's what we had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nv"&gt;$GOPHISH_PID&lt;/span&gt;  &lt;span class="c"&gt;# DB exists, we're good right?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;SQLite creates the database file the moment you open a connection. Migrations run &lt;em&gt;after&lt;/em&gt; that. We were killing the process as soon as &lt;code&gt;gophish.db&lt;/code&gt; appeared, before the schema was even built.&lt;/p&gt;

&lt;p&gt;Result: weird SQL errors about missing columns. Only in production. Fun times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Wait for the schema, not just the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wait for file&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Wait for migrations (check for a column we know should exist)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; sqlite3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;".schema users"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"password_change_required"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pre-flight checks
&lt;/h2&gt;

&lt;p&gt;While we were in there, we added checks before starting the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;preflight_check&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;

    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MIGRATIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"migrations directory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"config.json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATIC_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; missing+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"static directory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Missing required files: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catches broken deployments immediately instead of failing mysteriously later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging output that actually helps
&lt;/h2&gt;

&lt;p&gt;When stuff breaks, we now dump:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directory contents&lt;/li&gt;
&lt;li&gt;Database schema (if it exists)&lt;/li&gt;
&lt;li&gt;Common causes checklist&lt;/li&gt;
&lt;li&gt;All output unbuffered with &lt;code&gt;stdbuf -oL&lt;/code&gt; so you can actually see it in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters a lot when you're debugging through a cloud serial console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud VM patterns we use now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Network waiting.&lt;/strong&gt; Cloud VMs don't always have network at boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wait_for_network&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; i&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt;max_attempts&lt;span class="p"&gt;;&lt;/span&gt; i++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        if &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 https://example.com &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            return &lt;/span&gt;0
        &lt;span class="k"&gt;fi
        &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;2
    &lt;span class="k"&gt;done
    return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker readiness.&lt;/strong&gt; Service "started" doesn't mean Docker is actually ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wait_for_docker&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; docker info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
    &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;State files.&lt;/strong&gt; Know if this is first boot or a restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STATE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/myapp/state"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;do_routine_restart
&lt;span class="k"&gt;else
    &lt;/span&gt;do_initial_setup
    &lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Systemd stuff
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;network-online.target&lt;/code&gt;, not &lt;code&gt;network.target&lt;/code&gt;. They're different and it matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target docker.service&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SSH key cleanup for marketplace images
&lt;/h2&gt;

&lt;p&gt;If you're publishing VM images, clean up your dev keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_&lt;span class="k"&gt;*&lt;/span&gt; ~/.ssh/&lt;span class="k"&gt;*&lt;/span&gt;.pub ~/.ssh/known_hosts ~/.ssh/config
find ~/.ssh &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;

&lt;span class="c"&gt;# Verify it worked&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;find ~/.ssh &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; .&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: SSH files still present!"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't trust file existence as a completion signal&lt;/li&gt;
&lt;li&gt;Pre-flight checks save debugging time&lt;/li&gt;
&lt;li&gt;Cloud VMs need patience (network, Docker, everything)&lt;/li&gt;
&lt;li&gt;Unbuffer your output (&lt;code&gt;stdbuf -oL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Verify your cleanup actually worked&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These changes took us from "why does this randomly break" to "oh, it failed because X." That's the goal.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sqlite</category>
      <category>debugging</category>
      <category>linux</category>
    </item>
    <item>
      <title>We Shipped 79 PRs in a Few Weeks. Claude Code Did Most of the Work.</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 28 Jan 2026 15:01:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/we-shipped-79-prs-in-a-few-weeks-claude-code-did-most-of-the-work-35j0</link>
      <guid>https://dev.to/david_dev_sec/we-shipped-79-prs-in-a-few-weeks-claude-code-did-most-of-the-work-35j0</guid>
      <description>&lt;p&gt;We maintain &lt;a href="https://github.com/HailBytes/gophish" rel="noopener noreferrer"&gt;HailBytes GoPhish&lt;/a&gt;, a fork of the open-source phishing simulation toolkit. We wanted to add a bunch of enterprise features (MFA, SSO, encryption at rest, audit logging, white-labeling) and honestly didn't have the bandwidth to do it the traditional way.&lt;/p&gt;

&lt;p&gt;So we tried something different. Claude Code is now our most prolific contributor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;GoPhish is kind of a pain to work on. Go backend, JavaScript frontend, systemd services, bash deployment scripts, SQL migrations, webpack. When you touch one thing, you often need to touch five others.&lt;/p&gt;

&lt;p&gt;We had a feature list that would take a small team months. We have... not that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's a real morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;claude
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Fix the bootstrap script killing gophish before migrations &lt;span class="nb"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude reads the bash scripts, traces the systemd dependency chain, finds the race condition, fixes it, commits. Done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit cd29bc7
Author: Claude &amp;lt;noreply@anthropic.com&amp;gt;

    Fix bootstrap killing gophish before migrations complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Real Examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Service debugging:&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;&amp;gt; Fix Linux services not starting after VM image restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude traced through the systemd units, found the missing dependencies, fixed the boot sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend bugs:&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;&amp;gt; Fix privacy settings not saving due to deprecated jQuery methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our jQuery upgrade broke &lt;code&gt;.attr()&lt;/code&gt; on checkboxes (should be &lt;code&gt;.prop()&lt;/code&gt;). Claude found all the occurrences and fixed them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Big refactors:&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;&amp;gt; Update bootstrap script with patterns from cloud_tools and scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;666-line diff. Network waiting, readiness checks, state file tracking, proper error handling. One commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe the problem, not the solution.&lt;/strong&gt; "Fix the bug where X happens" beats "change line 47 to Y"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let it explore.&lt;/strong&gt; Claude reads related files and often finds issues you didn't know about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the diff.&lt;/strong&gt; It commits with clear messages. You review, test, merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You still own the final call.&lt;/strong&gt; We run tests and do manual QA. Fast doesn't mean careless&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Surprised Us
&lt;/h2&gt;

&lt;p&gt;The bash scripts. We expected Claude to be good at Go and JavaScript. We didn't expect it to nail systemd services and deployment scripts. It gets the whole stack.&lt;/p&gt;

&lt;p&gt;Cross-cutting changes too. Adding SSO touched Go handlers, SQL migrations, JavaScript, CSS, docs. Claude handled the full vertical.&lt;/p&gt;

&lt;p&gt;And bug hunting. "The sidebar is pushing down the main content" and Claude reads the CSS, finds the &lt;code&gt;position: fixed&lt;/code&gt; conflict, fixes it, explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;From our recent git log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 Claude-authored commits in the last two weeks&lt;/li&gt;
&lt;li&gt;Changes across Go, JavaScript, Bash, SQL, CSS, systemd&lt;/li&gt;
&lt;li&gt;Features enterprise customers are paying for&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you maintain an open-source project and your issue backlog haunts you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @anthropic-ai/claude-code
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with a real bug. Something annoying that's been sitting there forever. See what happens.&lt;/p&gt;




&lt;p&gt;We're &lt;a href="https://hailbytes.com" rel="noopener noreferrer"&gt;HailBytes&lt;/a&gt;. We build security tools for pentesters and security awareness teams. GoPhish 0.14.2 ships this month.&lt;/p&gt;




&lt;p&gt;What's your experience using AI on real projects? I'm curious what's working for people.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>We Hardened Ubuntu 24.04 for Security Tools (And Broke Everything First)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Wed, 21 Jan 2026 16:50:00 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/we-hardened-ubuntu-2404-for-security-tools-and-broke-everything-first-kf2</link>
      <guid>https://dev.to/david_dev_sec/we-hardened-ubuntu-2404-for-security-tools-and-broke-everything-first-kf2</guid>
      <description>&lt;h2&gt;
  
  
  We Hardened Ubuntu 24.04 for Security Tools (And Broke Everything First)
&lt;/h2&gt;

&lt;p&gt;We maintain &lt;a href="https://github.com/HailBytes/rengine-ng-rc" rel="noopener noreferrer"&gt;HailBytes reNgine&lt;/a&gt;, a web recon platform. Wanted to deploy hardened golden images to AWS and Azure following industry benchmarks.&lt;/p&gt;

&lt;p&gt;Should be simple. Run hardening script, create AMI, done.&lt;/p&gt;

&lt;p&gt;Except our scanning tools immediately stopped working. 🔥&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Our "secure" kernel settings:&lt;/span&gt;
kernel.unprivileged_bpf_disabled &lt;span class="o"&gt;=&lt;/span&gt; 1  &lt;span class="c"&gt;# Block BPF&lt;/span&gt;
net.core.bpf_jit_harden &lt;span class="o"&gt;=&lt;/span&gt; 2           &lt;span class="c"&gt;# Maximum BPF hardening&lt;/span&gt;

&lt;span class="c"&gt;# What our tools actually needed:&lt;/span&gt;
&lt;span class="c"&gt;# BPF access. For packet capture. For scanning. For everything.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the thing: &lt;strong&gt;security tools often need the exact privileges that security hardening removes.&lt;/strong&gt; Fun paradox.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Claude Code Helped
&lt;/h2&gt;

&lt;p&gt;We used &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; to iterate on the hardening scripts. Three problems it helped us solve:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Azure VMs Were Detected as AWS
&lt;/h3&gt;

&lt;p&gt;Both clouds use the same metadata IP. Our detection was just checking if the endpoint responded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This is wrong&lt;/span&gt;
&lt;span class="nv"&gt;METADATA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://169.254.169.254"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METADATA_URL&lt;/span&gt;&lt;span class="s2"&gt;/latest/meta-data/"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"AWS!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: actually validate the response format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;detect_cloud_provider&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Check Azure FIRST (it has a specific field)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;azure_check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Metadata:true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"http://169.254.169.254/metadata/instance?api-version=2021-02-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 2 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$azure_check&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"azEnvironment"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"azure"&lt;/span&gt;
        &lt;span class="k"&gt;return
    fi&lt;/span&gt;

    &lt;span class="c"&gt;# Then check AWS (validate instance ID format)&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;aws_check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"http://169.254.169.254/latest/meta-data/instance-id"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--connect-timeout&lt;/span&gt; 2 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$aws_check&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^i-[a-f0-9]+ &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt;
        &lt;span class="k"&gt;return
    fi

    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"generic"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Azure returns JSON with &lt;code&gt;azEnvironment&lt;/code&gt;. AWS returns plain text. Check the actual content, not just connectivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ubuntu 24.04 Renamed the SSH Service
&lt;/h3&gt;

&lt;p&gt;Worked fine on 22.04. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart sshd  &lt;span class="c"&gt;# "Unit sshd.service not found" 💀&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ubuntu 24.04 changed it from &lt;code&gt;sshd&lt;/code&gt; to &lt;code&gt;ssh&lt;/code&gt;. Cool. Cool cool cool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;systemctl restart ssh 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; systemctl restart sshd 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log &lt;span class="s2"&gt;"SSH hardened"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;error &lt;span class="s2"&gt;"Failed to restart SSH"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Finding the Right Security Tradeoffs
&lt;/h3&gt;

&lt;p&gt;This was the real work. Hardening that doesn't break the app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;CIS Says&lt;/th&gt;
&lt;th&gt;What We Used&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unprivileged_bpf_disabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 (blocked)&lt;/td&gt;
&lt;td&gt;0 (allowed)&lt;/td&gt;
&lt;td&gt;Scanning needs packet capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bpf_jit_harden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 (max)&lt;/td&gt;
&lt;td&gt;1 (moderate)&lt;/td&gt;
&lt;td&gt;Performance matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppArmor&lt;/td&gt;
&lt;td&gt;enforce all&lt;/td&gt;
&lt;td&gt;complain for Docker&lt;/td&gt;
&lt;td&gt;Containers need flexibility&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each deviation is documented. Auditors will ask. Have your answers ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Auto-detect cloud provider&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh

&lt;span class="c"&gt;# Or force it&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--cloud&lt;/span&gt; azure
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--cloud&lt;/span&gt; aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH hardening (Ed25519, modern ciphers only)&lt;/li&gt;
&lt;li&gt;UFW firewall (ports 22, 443, 8082)&lt;/li&gt;
&lt;li&gt;Kernel hardening (ASLR, SYN cookies, anti-spoofing)&lt;/li&gt;
&lt;li&gt;Fail2Ban&lt;/li&gt;
&lt;li&gt;Auditd logging&lt;/li&gt;
&lt;li&gt;Auto security updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Still runs Docker and security tools&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test on the actual target platform.&lt;/strong&gt; Ubuntu version differences will get you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud metadata APIs are tricky.&lt;/strong&gt; Validate the response format, not just reachability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your security tradeoffs.&lt;/strong&gt; Future you and your auditors will need this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistants catch edge cases.&lt;/strong&gt; Claude flagged the SSH rename before we hit production.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Grab It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/HailBytes/rengine-ng-rc
&lt;span class="nb"&gt;cd &lt;/span&gt;rengine-ng-rc/scripts
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./harden_ubuntu_2404.sh &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works for any Ubuntu 24.04 server, not just reNgine.&lt;/p&gt;

&lt;p&gt;How do you handle the security vs. functionality tradeoff? Would love to hear what's worked for you. Update incoming this week.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;#security #devops #ubuntu #cloudcomputing #opensource&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ubuntu</category>
      <category>ai</category>
    </item>
    <item>
      <title>Our Godot Game Only Crashed on Expensive PCs (Here's Why)</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Fri, 16 Jan 2026 21:02:35 +0000</pubDate>
      <link>https://dev.to/david_dev_sec/our-godot-game-only-crashed-on-expensive-pcs-heres-why-40jl</link>
      <guid>https://dev.to/david_dev_sec/our-godot-game-only-crashed-on-expensive-pcs-heres-why-40jl</guid>
      <description>&lt;p&gt;Players started reporting that our game was freezing during boss fights. No crash logs. No Sentry reports. Just "Not Responding" and then... nothing.&lt;/p&gt;

&lt;p&gt;The weird part? It was happening on RTX 4090s while working fine on older hardware. Yeah.&lt;/p&gt;

&lt;p&gt;My brother and I run Lost Rabbit Digital together, and we've been building Starbrew Station (a space coffee shop idle game, it's a whole thing) in Godot 4.5. Here's what we found and how we fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Problems
&lt;/h2&gt;

&lt;p&gt;We tracked it down to four separate issues that were all making things worse together.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Loading Resources During Gameplay
&lt;/h3&gt;

&lt;p&gt;This looks harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start_boss_fight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss_scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://scenes/InspectionBossFight.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boss_scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not. That &lt;code&gt;load()&lt;/code&gt; call freezes everything for 100-500ms while it reads from disk. Combine that with shader compilation and you hit Windows TDR (Timeout Detection and Recovery). Windows kills any process that doesn't respond to the GPU driver within ~2 seconds. No crash report, just dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Preload at startup instead.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InspectionBossFightScene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"res://scenes/InspectionBossFight.tscn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start_boss_fight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InspectionBossFightScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# instant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing for shader materials. Don't create them at runtime, create templates at startup and duplicate them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Awaits That Wait Forever
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_achievement_unlocked&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;game_saved&lt;/span&gt;
    &lt;span class="n"&gt;show_notification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the save fails and never emits that signal? This waits forever. The game just hangs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Fire and forget.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_achievement_unlocked&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_save&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;show_notification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# don't wait for confirmation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Tweens Piling Up
&lt;/h3&gt;

&lt;p&gt;Every UI animation created a new tween. We never killed the old ones. After a few hours of gameplay, thousands of dead tween references just sitting in memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix: Track them and kill the old one before making a new one.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Dictionary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fade_ambient_light&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_tween&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_active_tweens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ambient_fade"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tween&lt;/span&gt;
    &lt;span class="n"&gt;tween&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tween_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Loops With No Safety Limits
&lt;/h3&gt;

&lt;p&gt;Our fighter cleanup loop could churn through thousands of invalid entries in one frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cleanup_fighters&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_instance_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix: Add iteration limits.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MAX_ITERATIONS_PER_FRAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cleanup_fighters&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_ITERATIONS_PER_FRAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_instance_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="n"&gt;fighters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For bigger operations (like applying buffs to 100+ units at once), chunk it across frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;apply_cascade_inspiration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# 10 per frame&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply_inspiration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process_frame&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preload resources at startup.&lt;/strong&gt; Runtime &lt;code&gt;load()&lt;/code&gt; on Windows can trigger GPU driver timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't await signals that might never fire.&lt;/strong&gt; Fire and forget instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track your tweens.&lt;/strong&gt; Kill old ones before creating new ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put limits on loops.&lt;/strong&gt; Especially ones processing dynamic arrays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk big operations across frames.&lt;/strong&gt; Your players will thank you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The kicker: high-end PCs hit these bugs &lt;em&gt;faster&lt;/em&gt; because they ran more game loops per second. Sometimes the best hardware finds the worst problems.&lt;/p&gt;




&lt;p&gt;We're Lost Rabbit Digital on &lt;a href="https://github.com/Lost-Rabbit-Digital" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Starbrew Station is built with Godot 4.5.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>performance</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
