<?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>Forem: David McHale</title>
    <description>The latest articles on Forem by David McHale (@david_dev_sec).</description>
    <link>https://forem.com/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>Forem: David McHale</title>
      <link>https://forem.com/david_dev_sec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/david_dev_sec"/>
    <language>en</language>
    <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://forem.com/david_dev_sec/why-your-phishing-simulations-land-in-spam-and-the-spf-dkim-dmarc-fix-that-actually-works-746</link>
      <guid>https://forem.com/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://forem.com/david_dev_sec/giving-an-ai-agent-a-recon-toolbox-wiring-30-security-tools-into-an-mcp-server-3nbm</link>
      <guid>https://forem.com/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://forem.com/david_dev_sec/that-time-sqlite-file-existence-lied-to-us-4em3</link>
      <guid>https://forem.com/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://forem.com/david_dev_sec/we-shipped-79-prs-in-a-few-weeks-claude-code-did-most-of-the-work-35j0</link>
      <guid>https://forem.com/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://forem.com/david_dev_sec/we-hardened-ubuntu-2404-for-security-tools-and-broke-everything-first-kf2</link>
      <guid>https://forem.com/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://forem.com/david_dev_sec/our-godot-game-only-crashed-on-expensive-pcs-heres-why-40jl</link>
      <guid>https://forem.com/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>
    <item>
      <title>Run Phishing Simulations for $37/Month Instead of $30,000/Year</title>
      <dc:creator>David McHale</dc:creator>
      <pubDate>Thu, 15 Jan 2026 23:39:34 +0000</pubDate>
      <link>https://forem.com/david_dev_sec/run-phishing-simulations-for-37month-instead-of-30000year-nig</link>
      <guid>https://forem.com/david_dev_sec/run-phishing-simulations-for-37month-instead-of-30000year-nig</guid>
      <description>&lt;p&gt;Most enterprise phishing simulation tools charge $3-5 per user per year. For a 10,000 person company, that's $30,000-50,000 annually.&lt;/p&gt;

&lt;p&gt;We run unlimited simulations on a $37/month Azure VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool: GoPhish
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gophish/gophish" rel="noopener noreferrer"&gt;GoPhish&lt;/a&gt; is an open-source phishing simulation framework. It's been around for 10+ years, has 10,000+ installations, and is MIT licensed. &lt;/p&gt;

&lt;p&gt;I've supported it for the last 8 years or so, since early 2018, maintaining the core repo and answering issues as they've popped up.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create realistic phishing campaigns&lt;/li&gt;
&lt;li&gt;Track who opens, clicks, and submits credentials&lt;/li&gt;
&lt;li&gt;Measure improvement over time&lt;/li&gt;
&lt;li&gt;Import thousands of targets via CSV&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? Vanilla GoPhish lacks enterprise basics: no MFA, no encryption at rest, no audit logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Added
&lt;/h2&gt;

&lt;p&gt;We forked GoPhish and added what production environments actually need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MFA/TOTP&lt;/td&gt;
&lt;td&gt;Your admin panel shouldn't be a security hole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSO (Google/Microsoft)&lt;/td&gt;
&lt;td&gt;One-click login for your team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-256 encryption&lt;/td&gt;
&lt;td&gt;Stored credentials aren't plaintext anymore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logging&lt;/td&gt;
&lt;td&gt;SIEM export for compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White-label branding&lt;/td&gt;
&lt;td&gt;Your logo, not ours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-click deployment&lt;/td&gt;
&lt;td&gt;Azure/AWS in ~5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick Start (Azure)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create Ubuntu 24.04 VM from GoPhish 0.14.2 public image on Azure (Standard_B2s = $37/month)&lt;/li&gt;
&lt;li&gt;Get your auto-generated admin password from Azure Serial Console&lt;/li&gt;
&lt;li&gt;Login at &lt;a href="https://your-ip:3333" rel="noopener noreferrer"&gt;https://your-ip:3333&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The setup script handles systemd services, TLS certificates, and Ubuntu hardening. &lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;10,000 Users/Year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;KnowBe4&lt;/td&gt;
&lt;td&gt;~$30,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proofpoint&lt;/td&gt;
&lt;td&gt;~$40,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud-hosted GoPhish&lt;/td&gt;
&lt;td&gt;~$3,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted GoPhish&lt;/td&gt;
&lt;td&gt;~$360&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same capabilities. Fraction of the cost. Your data stays on your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/HailBytes/gophish" rel="noopener noreferrer"&gt;github.com/HailBytes/gophish&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Marketplace:&lt;/strong&gt; Search "GoPhish" or "HailBytes"&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Questions? Cheers, drop them in the comments.&lt;/p&gt;

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