<?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: Dhruv Malaviya</title>
    <description>The latest articles on DEV Community by Dhruv Malaviya (@dhruonweb).</description>
    <link>https://dev.to/dhruonweb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3964043%2Fdf5eaffb-4a72-4192-97c9-e06ca85c2406.JPG</url>
      <title>DEV Community: Dhruv Malaviya</title>
      <link>https://dev.to/dhruonweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhruonweb"/>
    <language>en</language>
    <item>
      <title>Nine Keys Can SSH Into Production. Five Owners Don't Work Here Anymore.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:20:41 +0000</pubDate>
      <link>https://dev.to/dhruonweb/nine-keys-can-ssh-into-production-five-owners-dont-work-here-anymore-88g</link>
      <guid>https://dev.to/dhruonweb/nine-keys-can-ssh-into-production-five-owners-dont-work-here-anymore-88g</guid>
      <description>&lt;p&gt;I cat'd authorized_keys on our oldest box for a boring reason and kept reading: the 2023 contractor, a dead CI pipeline, two ex-engineers, an 'ops-temp' nobody can attribute. The sweep, the smoke alarm, the one-week evidence trick, and keys-that-expire culture.&lt;/p&gt;

&lt;p&gt;I opened the file for a boring reason — a connectivity weirdness — and then just kept reading. ~/.ssh/authorized_keys on our oldest production box: nine lines, nine doors.&lt;/p&gt;

&lt;p&gt;Matching comments to memory: the 2023 migration contractor (still there), the CI pipeline decommissioned last spring (still there), two engineers who left — one warmly, one not (still there), a key commented ops-temp that nobody including our ops lead can attribute to a human, and one line with no comment at all, which is its own small horror genre.&lt;/p&gt;

&lt;p&gt;Four keys belonged to current humans who still needed access. Five were sediment. The door remembered everyone, because nobody had ever asked it to forget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;authorized_keys is an attendance sheet&lt;/strong&gt;&lt;br&gt;
The oldest access-control file in our profession is a plain-text list on disk: appended to for a decade, reviewed never. Every line was added on someone's worst day — incident, deadline, "just for tonight, add my key." Keys arrive during emergencies; emergencies don't do cleanup; departures miss the file entirely, because it isn't in any system. It's wallpaper. And unlike a password, a key never expires out of embarrassment — it waits for its laptop to be sold, lost, or inherited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sweep&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# keys-audit.sh — every authorized_keys on every box, with its "who/why" comments&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;hosts.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
    [ -f "$f" ] &amp;amp;&amp;amp; {
      echo "== $HOSTNAME $f — $(grep -cvE "^[[:space:]]*$|^[[:space:]]*#" "$f") keys"
      grep -vE "^[[:space:]]*$|^[[:space:]]*#" "$f" | awk "{print \"   \", \$NF}"
    }; done'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;tee &lt;/span&gt;keys-audit.txt
&lt;span class="c"&gt;# then the human step: diff the comment column against the current roster.&lt;/span&gt;
&lt;span class="c"&gt;# every line that doesn't match a living owner is sediment.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ours diffed to five. Yours will too; that number is your real access list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four changes that stuck&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keys got names and reasons. One comment format, enforced at add-time:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;ana.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ana@laptop — incident-4821 — 2026-11-03"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /home/ubuntu/.ssh/authorized_keys
&lt;span class="c"&gt;# a key that can't state its reason in six words doesn't get added.&lt;/span&gt;
&lt;span class="c"&gt;# 'ops-temp' failed this test immediately and was deleted with a small ceremony.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Offboarding gained a server step. Badge, laptop, SSO — and this, on every box:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'ana@'&lt;/span&gt; /root/.ssh/authorized_keys /home/&lt;span class="k"&gt;*&lt;/span&gt;/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The step exists because the 2023 contractor taught us it doesn't happen by osmosis.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The file got a smoke alarm. Any change pages someone — not blocks, pages; additions are sometimes legitimate at 2 a.m., silent additions never are:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# auditd: watch writes and attributes to authorized_keys on every box&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'-w /root/.ssh/authorized_keys -p wa -k ssh_sediment'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/audit/rules.d/ssh.rules
&lt;span class="nb"&gt;sudo &lt;/span&gt;augenrules &lt;span class="nt"&gt;--load&lt;/span&gt;
&lt;span class="c"&gt;# wire the audit event to your pager; our worst file became our best-monitored one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deletion ran on evidence, not folklore. We almost kept the unattributed key "in case something depends on it." Instead: a week of fingerprints.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^#LogLevel INFO/LogLevel VERBOSE/'&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload ssh
&lt;span class="c"&gt;# after a week — which fingerprints actually authenticated?&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ssh &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"-7 days"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'SHA256:[A-Za-z0-9+/=]+'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; used.txt
&lt;span class="c"&gt;# and per line in the file — which keys never appeared in used.txt?&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1" "$2}'&lt;/span&gt; /root/.ssh/authorized_keys | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; t k&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;fp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&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;$t&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | ssh-keygen &lt;span class="nt"&gt;-lf&lt;/span&gt; - 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&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="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; used.txt &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"UNUSED ALL WEEK: &lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing used it. Measure before you mourn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New boxes start at one&lt;/strong&gt;&lt;br&gt;
The habit stuck because new machines made it easy: on the Cubes we run at Krova Cloud, a box boots with exactly one key — the one handed at creation — and root login is off by default. Additions become a decision you make, not sediment you inherit. Humans increasingly skip static keys entirely: short-lived certs through a bastion for people, per-pipeline keys rotated with the pipeline for robots. Zero sediment, not zero keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static keys aren't a sin. Plenty of honest estates run them fine because they run the reviews. The sin is the unreviewed list, not the mechanism.&lt;/li&gt;
&lt;li&gt;Ceremony has a cost. Certs and bastions add moving parts and one more thing to fail during the incident that justified the shortcut. We tiered: short-lived access on sensitive boxes, named-and-alarmed keys elsewhere. Perfection is a budget line, not a value.&lt;/li&gt;
&lt;li&gt;The alarm will page you once legitimately, at 2 a.m., during a real incident. Thank it. That page is the system proving it can still tell the difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The door remembers. You don't have to.&lt;/strong&gt;&lt;br&gt;
Go cat the file — the actual one, on the actual oldest box, right now. Count the lines; count the owners who are gone. Ours was nine and five. We deleted four, adopted one (the CI key got a name, a rotation, and an apology), and the ninth — the comment-less line — turned out to be me, from a forgotten 2022 incident.&lt;/p&gt;

&lt;p&gt;The door remembered me too.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>It's Halloween, So Here's My Field Guide to the Monsters Already Living in Your Stack</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:47:03 +0000</pubDate>
      <link>https://dev.to/dhruonweb/its-halloween-so-heres-my-field-guide-to-the-monsters-already-living-in-your-stack-5o6</link>
      <guid>https://dev.to/dhruonweb/its-halloween-so-heres-my-field-guide-to-the-monsters-already-living-in-your-stack-5o6</guid>
      <description>&lt;p&gt;Seven creatures, one per audit: the Zombie process, the Ghost cron, the Trojan paste, the Spider at the door, the Gremlin in CI, the Kraken in the schema, and the Daemon you should adopt. Each with its habitat, its detection one-liner, and its banishment.&lt;/p&gt;

&lt;p&gt;Every infrastructure has a bestiary. Nobody writes it down, which is why the monsters keep winning. Mine got catalogued one audit at a time over a year of confessions on this blog — seven distinct creatures, every one still living in somebody's production right now. It's Halloween, so: name them, detect them, banish them. Or adopt them, in one case. Field guides are honest about which animals are pets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Zombie 🧟&lt;/strong&gt;&lt;br&gt;
Died once, kept walking. Deprecated services still serving, restart loops older than the intern who "fixed" them.&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;# dead-but-walking in the process table&lt;/span&gt;
ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,ppid,stat,comm | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$3 ~ /^Z/'&lt;/span&gt;
&lt;span class="c"&gt;# and the container flavor: restart loops&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}\t{{.Status}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'restart|back-off'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: graveyard rules — kill it fully (process, DNS, keys, CI variables) or adopt it with an owner. Half-killed zombies are how sequels happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ghost 👻&lt;/strong&gt;&lt;br&gt;
The spirit of a departed colleague, still doing their job. Ours ran at 3:07 AM and reconciled invoices.&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;for &lt;/span&gt;u &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; /etc/passwd&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'^\s*#|^\s*$'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/^/&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;: /"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;   &lt;span class="c"&gt;# every line that can't name an owner, on purpose, is a ghost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: the séance — owner, purpose, loud failure, or a funeral. Plus staleness alerts: a ghost that stops working is an incident nobody feels for three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trojan 🐴&lt;/strong&gt;&lt;br&gt;
Arrives as a gift, opens the door from inside. Pasted snippets, dependency diffs, October PRs titled fix typo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'process\.env|os\.environ|getenv'&lt;/span&gt;      &lt;span class="c"&gt;# env going somewhere&lt;/span&gt;
git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'base64|eval\(|atob'&lt;/span&gt;                   &lt;span class="c"&gt;# obfuscation&lt;/span&gt;
git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s1"&gt;'curl .*(-d|--data)|wget |fetch\('&lt;/span&gt;     &lt;span class="c"&gt;# new outbound call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: review before merge even at twenty lines; provenance comments; pin what you can pin. Most Trojans aren't malicious — just someone else's bug, fossilized with full rights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Spider 🕷️&lt;/strong&gt;&lt;br&gt;
The internet's automated fauna, patiently trying every handle. Lives on your public address and every port you forgot&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;# before: the porch the spiders sit on&lt;/span&gt;
ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'0\.0\.0\.0|::'&lt;/span&gt;          &lt;span class="c"&gt;# listeners facing everyone&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ssh &lt;span class="nt"&gt;--since&lt;/span&gt; yesterday | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'Failed password'&lt;/span&gt;

&lt;span class="c"&gt;# after (no public IP, default-deny, managed ingress): both commands return silence&lt;/span&gt;
krova tcp list web-1                        &lt;span class="c"&gt;# empty. no porch, no spiders.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: don't fight it at the door; remove the door. My spider population went extinct in a weekend and I did nothing heroic — just changed the default. (The one entry where my hosting is the whole spell: Krova Cubes ship with no public address.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Gremlin 👺&lt;/strong&gt;&lt;br&gt;
Feeds on observation. The CI-only failure, the bug that vanishes under the debugger, timestamps disagreeing by exactly one hour in exactly one region.&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;# evict its conditions: unseeded randomness and un-pinned clocks in tests&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s1"&gt;'Math\.random|random\.random|Date\.now|time\.now'&lt;/span&gt; tests/ src/ &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-viE&lt;/span&gt; &lt;span class="s1"&gt;'seed|fake|clock'&lt;/span&gt;             &lt;span class="c"&gt;# every hit is a gremlin bed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: you don't kill gremlins, you bore them out: fixed clocks, seeded randomness, reproducible builds. Boredom is the only exorcism that holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Kraken 🐙&lt;/strong&gt;&lt;br&gt;
Too big to fight, too central to flee. Tentacles in every schema; migrations scheduled in quarters; a cloud labeled LEGACY.&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;# count the tentacles before you wake it&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rc&lt;/span&gt; &lt;span class="s1"&gt;'legacy_orders'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.sql'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt;: &lt;span class="nt"&gt;-k2&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: nobody slays the Kraken. Build seams, route one tentacle at a time around it, let it sleep. Anyone selling you a slaying is selling you an outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Daemon 😇&lt;/strong&gt;&lt;br&gt;
The only monster that works for you. Night-shift process keeping caches warm, certs renewed, queues drained. A daemon is not a demon — it's a guardian spirit we forgot to thank.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'^\.|systemd|ssh|cron'&lt;/span&gt;   &lt;span class="c"&gt;# your daemons&lt;/span&gt;
&lt;span class="c"&gt;# adoption papers per daemon: owner, runbook path, silence alert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Banish: absolutely not. Adopt: name it, log it, alert on its silence. A daemon with an owner is infrastructure; a daemon without one is next year's Ghost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;br&gt;
None of these are moral failures. Every stack I've audited hosted at least four of the seven simultaneously. The monsters aren't the shame; the unnamed monsters are. An inventory turns fear into a checklist, and a checklist turns 3 AM into Tuesday.&lt;/p&gt;

&lt;p&gt;And the seventh entry is the lesson: the difference between a guardian and a ghost is rarely the creature. It's whether somebody wrote its name down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your monsters&lt;/strong&gt;&lt;br&gt;
Tonight of all nights: run the seven detections. Seven chairs at the table; see who sits down. Then write the field guide — one page per monster: habitat, tells, banishment, or adoption papers if it earns them.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>security</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Meeting Bot Heard the Password. It Wrote It Down. Forever.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:10:35 +0000</pubDate>
      <link>https://dev.to/dhruonweb/the-meeting-bot-heard-the-password-it-wrote-it-down-forever-3bmg</link>
      <guid>https://dev.to/dhruonweb/the-meeting-bot-heard-the-password-it-wrote-it-down-forever-3bmg</guid>
      <description>&lt;p&gt;I searched our meeting transcripts for 'password' and got spoken connection strings, MFA digits, a verbatim angry customer, and a frame-captured dashboard. The sweep, the bot-free policy, the synthetic demo box, and retention as a setting.&lt;/p&gt;

&lt;p&gt;I typed one word into our transcript search box: password. Four minutes later I closed the laptop and went for a walk.&lt;/p&gt;

&lt;p&gt;Hit one: an API connection string read aloud during pairing ("just paste what I say"). Hit two: an MFA code, digit by digit to IT, transcribed correctly. Hit three: a customer's name and their angriest sentence of the year, verbatim, in a sales summary anyone can open. Hit four wasn't audio: a screen-shared dashboard captured frame by frame, real customer rows in 4K.&lt;/p&gt;

&lt;p&gt;The room was never bugged. It was scheduled — recurring, 30 minutes, automatic notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meetings became data. Nobody designed the data.&lt;/strong&gt;&lt;br&gt;
A transcript is a log no one engineered: nothing scrubs it, nothing scopes it, everything searches it. Retention defaults to forever because forever is what storage does when nobody decides otherwise. The vendor keeps a copy — the vendor is the one transcribing. And unlike every datastore we own, this one ingests secrets, PII, and candid opinions at conversational speed, from everyone, daily, without a schema.&lt;/p&gt;

&lt;p&gt;Screen sharing is the same leak with better resolution: the terminal corner in a demo, the .env tab during a "quick look," the autocomplete that finished a customer's email on a projector. We'd never put those in a doc. We put them in a recording, which is a doc with witnesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sweep&lt;/strong&gt;&lt;br&gt;
Export whatever your tool allows, then:&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;# spoken-secret shapes&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rniE&lt;/span&gt; &lt;span class="s1"&gt;'password|passphrase|token|api[_-]?key|secret|mfa|verification code|connection string'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  transcripts/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-viE&lt;/span&gt; &lt;span class="s1"&gt;'password (policy|manager|reset)'&lt;/span&gt;

&lt;span class="c"&gt;# real humans in the room-data&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; c&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlq&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; transcripts/ &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;"CUSTOMER IN TRANSCRIPTS: &lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; top-accounts.txt

&lt;span class="c"&gt;# recordings + screen shares, from the calendar/admin export&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.recording or .screenShare)
  | "\(.date)\t\(.organizer)\t\(.title)"'&lt;/span&gt; meetings.json | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer-name grep is the one that ends careers of assumptions. Ours returned eleven accounts, including two under NDA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets are never spoken. Pairing shares a vault link, not a voice. Awkward for a week, then seatbelts.&lt;/li&gt;
&lt;li&gt;Sensitive calls are bot-free by default — incidents, credential handling, escalations. Routine calls keep notes; the rule is classification, same as staging: real things don't live in rooms with pretend rules. The enforcement is boring:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# weekly: any flagged meeting next week still carrying a bot/recording flag?&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.title | test("incident|postmortem|migration|cred"; "i"))
  | select(.bot or .recording) | .title'&lt;/span&gt; meetings-next-7d.json
&lt;span class="c"&gt;# non-empty output = fix the invite, not the culture&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Demos run on a demo box. Screen shares that might show data show synthetic data. Disposable Cube on Krova Cloud: redeployed fresh, seeded fake, deleted after the call:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# demo-box.sh up|down&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;up&lt;span class="p"&gt;)&lt;/span&gt;
    krova cubes create demo-box &lt;span class="nt"&gt;--cpu&lt;/span&gt; 2 &lt;span class="nt"&gt;--ram&lt;/span&gt; 4 &lt;span class="nt"&gt;--disk&lt;/span&gt; 40 &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04 &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--user-data&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;seed-fake-customers.yml&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    krova domains add demo-box &lt;span class="nt"&gt;--domain&lt;/span&gt; demo.example.com &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="p"&gt;;;&lt;/span&gt;
  down&lt;span class="p"&gt;)&lt;/span&gt; krova cubes delete demo-box &lt;span class="p"&gt;;;&lt;/span&gt;   &lt;span class="c"&gt;# nothing true was ever in it&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#cloud-config&lt;/span&gt;
&lt;span class="na"&gt;runcmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sf https://storage.private/fake-customers.sql.gz | gunzip | sudo -u postgres psql app&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl restart app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo can leak all it wants; there's nothing true in it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The notes tool got treated like a datastore. Retention set to a number we chose, old exports purged on a schedule (find … -mtime +90 -delete — exports are copies too), search access reviewed, and the vendor's terms actually read for the first time since someone clicked "enable for workspace."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transcripts are genuinely good — searchable decisions, async teammates, onboarding gold. I don't want the tool gone; I want its defaults to have opinions. Burning it down moves the leak back into memory, which retains worse and searches never.&lt;/li&gt;
&lt;li&gt;Small teams can't negotiate vendor terms. So minimize what enters the room: the bot can't transcribe a secret nobody says, nor capture a dashboard showing fake rows. Input control beats contract control.&lt;/li&gt;
&lt;li&gt;"But the notes!" is a real loss, not resistance. The compromise that stuck: notes everywhere by default, bot-free where stakes say so, and the safe path made easier than the risky one — one-click demo box, vault links in every pairing doc.&lt;/li&gt;
&lt;li&gt;This isn't the castle. Walls stay walls: no public IP by default, own kernel per Cube, scoped secrets. Transcript hygiene shrinks what conversations leak; architecture shrinks what machines leak. Both, always.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Forever is a setting&lt;/strong&gt;&lt;br&gt;
Every "forever" in your stack — transcripts, recordings, screenshots, exports, free-tier syncs — is a default nobody revisited. The scariest datastore you own is probably the one that fills itself while everyone talks.&lt;/p&gt;

&lt;p&gt;Go type password into your transcript search. Then token. Then your biggest customer's name. Four minutes. I'll wait.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>workplace</category>
      <category>ai</category>
    </item>
    <item>
      <title>There's a Cron Job in Production That Nobody Remembers Writing.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:06:28 +0000</pubDate>
      <link>https://dev.to/dhruonweb/theres-a-cron-job-in-production-that-nobody-remembers-writing-1i0c</link>
      <guid>https://dev.to/dhruonweb/theres-a-cron-job-in-production-that-nobody-remembers-writing-1i0c</guid>
      <description>&lt;p&gt;It runs at 3:07 AM, reconciles our invoices, and its author left in 2022. The crontab séance: inventory every scheduled job, give each an owner + purpose + loud failure or a funeral, crontab-as-code, staleness alerts, and ephemeral job boxes.&lt;/p&gt;

&lt;p&gt;Every company has ghosts like this. October is a fine month to introduce yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled jobs are employees without HR files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A cron job is a colleague: it has a shift, a responsibility, credentials, access. Unlike every human on payroll, it never got an onboarding (what does it need and why), a performance review (does it still succeed, still matter), an offboarding (its author left; its badge stayed active), or a manager (when it fails at 3 AM, who gets paged? nobody — the silence compounds).&lt;/p&gt;

&lt;p&gt;Our invoice ghost had a valid API credential embedded in the script, a hardcoded path that survived two migrations by luck, and a failure mode that surfaces as accounting confusion in three weeks, not an alert in three minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The séance&lt;/strong&gt;&lt;br&gt;
Print every crontab on every box — including the "temporary" ones — and hold one rule over each line: every job gets an owner, a purpose, and a loud failure — or a funeral.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# cron-seance.sh — run per host; collect the output, do the arithmetic, feel the chill&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== user crontabs =="&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;u &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f1&lt;/span&gt; /etc/passwd&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&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; &lt;span class="o"&gt;{&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;$u&lt;/span&gt;&lt;span class="s2"&gt; ---"&lt;/span&gt;&lt;span class="p"&gt;;&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;$c&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;-vE&lt;/span&gt; &lt;span class="s1"&gt;'^\s*#|^\s*$'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== system dirs =="&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rHvE&lt;/span&gt; &lt;span class="s1"&gt;'^\s*#|^\s*$'&lt;/span&gt; /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/crontab 2&amp;gt;/dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== systemd timers =="&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NF&amp;gt;6'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pages of output, eleven engineers. Then per line: who owns it, what happens when it fails, who knows why it exists. Lines that can't answer are ghosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopt the load-bearing ones, bury the rest&lt;/strong&gt;&lt;br&gt;
The invoice job didn't get deleted — it got hired: named owner, repo with tests, credentials out of the script and into scoped runtime injection, and alerts on both outcomes:&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;# the job's last line — a heartbeat:&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; /var/run/heartbeat/invoice-recon

&lt;span class="c"&gt;# checker, on the scheduler host (outside the box it watches):&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/30 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;  find /var/run/heartbeat &lt;span class="nt"&gt;-name&lt;/span&gt; invoice-recon &lt;span class="nt"&gt;-mmin&lt;/span&gt; +1500 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; alert &lt;span class="s2"&gt;"invoice-recon: silent too long"&lt;/span&gt;     &lt;span class="c"&gt;# "it didn't run" is an incident too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six jobs got funerals — and funeral meant the line, the script, its credentials, and the downstream thing it fed. Half-buried cron is how ghosts get sequels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crontab-as-code, or the ghosts return&lt;/strong&gt;&lt;br&gt;
"I'll just add it on the server" is the exact sentence that spawns ghosts. Now the box's crontab is generated from a repo file, and CI is the only writer:&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;# infra/cron/invoice-recon.cron — reviewed like any colleague's PR&lt;/span&gt;
&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/bin/bash
&lt;span class="nv"&gt;MAILTO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;                          &lt;span class="c"&gt;# silence is not monitoring&lt;/span&gt;
07 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; root /opt/jobs/invoice-recon.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/jobs/invoice-recon.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the only path into /etc/cron.d&lt;/span&gt;
krova ssh jobs-1 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'sudo install -m 644 /srv/infra/cron/*.cron /etc/cron.d/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where the jobs live matters too&lt;/strong&gt;&lt;br&gt;
Some scheduled work moved to ephemeral boxes — spin up, do the thing, die (same pattern as our CI):&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;CUBE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"job-invoice-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
krova cubes create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="nt"&gt;--ram&lt;/span&gt; 2 &lt;span class="nt"&gt;--disk&lt;/span&gt; 20 &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04
krova ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'INVOICE_KEY="'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INVOICE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'" /opt/invoice-recon.sh'&lt;/span&gt;  &lt;span class="c"&gt;# creds at runtime&lt;/span&gt;
krova cubes delete &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;        &lt;span class="c"&gt;# leaves a bill line and a log, not a mystery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An ephemeral job can't accumulate drift or host a forgotten second job. Resident crons that remain live behind the usual walls: no public IP, own kernel, default-deny inbound. Ghosts are an ownership problem first, a hosting problem second — but right hosting makes ownership visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some ghosts are the business. Deleting the invoice job would have been the incident. The audit finds load-bearing ghosts before they fail; it doesn't ban ghosts.&lt;/li&gt;
&lt;li&gt;Silence is the real vulnerability. A job that fails loudly is a bug; one that fails silently is debt with interest. Staleness alerts caught two more ghosts we'd otherwise have met during an accounting argument.&lt;/li&gt;
&lt;li&gt;Culture takes a week to feel heavy. Then someone proposed a cron change with a test, an owner, and a rollback note — in a review, like a normal colleague — and the habit had a heartbeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Meet your ghosts before they meet you&lt;/strong&gt;&lt;br&gt;
Tonight, on every box you own: open the crontab, read each line out loud, ask the three questions. Every line that can't answer is a ghost. Some are harmless. One of them, somewhere, is your invoice reconciliation.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>containers</category>
      <category>cloudsecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Audited Every Tool That Can Read Our Company. The List Was 43 Long.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:56:10 +0000</pubDate>
      <link>https://dev.to/dhruonweb/i-audited-every-tool-that-can-read-our-company-the-list-was-43-long-1486</link>
      <guid>https://dev.to/dhruonweb/i-audited-every-tool-that-can-read-our-company-the-list-was-43-long-1486</guid>
      <description>&lt;p&gt;43 external services held standing access to our Slack, Drive, code, and contacts — signed in modals, forgotten in sprints. The sweep per platform, the grant inventory, export-over-connect, vendor funerals, and breach-news as a revocation trigger.&lt;/p&gt;

&lt;p&gt;After the secrets rotation and the docs recon, a vendor breach headline asked the question I couldn't un-ask: who outside this building can read us right now? Not hackers — guests. The answer was forty-three external services with standing access to our words, files, code, or customers. Most I recognized. One was a "free CRM trial" from 2024 that had been syncing our contacts, continuously, for two years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth is a supply chain we sign ourselves&lt;/strong&gt;&lt;br&gt;
Every grant is a key handed to another company — to their security posture, their employees, their future acquirers, their worst day. When a vendor gets breached, the attacker doesn't need your password; they need the vendor's, and your grant does the rest.&lt;/p&gt;

&lt;p&gt;The audit's greatest hits: a 2021 marketing integration still reading every public Slack channel; a Chrome extension with Drive access installed by someone who left in 2023; a deprecated GitHub App owned by a vendor that got acquired (our code's readers now include a company we never chose); and the two-year CRM sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sweep&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GitHub: every app installed on the org, with its permissions and age&lt;/span&gt;
gh api orgs/&lt;span class="nv"&gt;$ORG&lt;/span&gt;/installations &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.installations[] |
  "\(.app_slug)\t\(.created_at)\t\(.permissions | to_entries |
   map("\(.key)=\(.value)") | join(","))"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the human pages (no API tells the whole truth):&lt;/p&gt;

&lt;p&gt;Slack → Manage apps (workspace admin)&lt;br&gt;
Google Workspace → Security → Access and data controls → Third-party apps &amp;amp; services&lt;br&gt;
Browser: every extension logged into a company account&lt;br&gt;
Payments, DNS, registrar, monitoring dashboards — the boring crown&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The inventory (one doc, five columns that matter)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| vendor      | surface | scopes           | data reach | funeral plan          |
|-------------|---------|------------------|------------|-----------------------|
| backup tool | drive   | read             | HIGH       | revoke on migration   |
| standup bot | slack   | channels:history | MED        | downgrade → post-only |
| crm-trial   | contacts| read+sync        | HIGH       | KILL (done, RIP)      |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Owner + "since" column too. You cannot revoke what you haven't listed, and a row with no owner is a grant nobody will ever bury.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export over connect&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the analytics tool gets a monthly batch, not a standing read:&lt;/span&gt;
psql &lt;span class="nt"&gt;-U&lt;/span&gt; app app_db &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="s2"&gt;opy (select * from events) to stdout csv"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; events-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.csv.gz
&lt;span class="c"&gt;# upload one-way; the connection you never open can't leak&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downgrades are free — most vendors don't notice, which tells you how little they needed the scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Funerals and triggers&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vendors attend the same funeral as projects. Tool dies in our stack → its grant dies in the same sitting. A grant is just a project that lives in someone else's building.&lt;/li&gt;
&lt;li&gt;Breach news is a revocation trigger, not a newsletter. Vendor in headlines → grant reviewed that day.&lt;/li&gt;
&lt;li&gt;The re-audit is scheduled, not remembered:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 9 1 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"open the four connected-apps pages. count the keys."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;br&gt;
Some grants are load-bearing. SSO, backups, the payment processor. Tier vendors by data reach, review in that order, forever. Triage, not purity.&lt;br&gt;
Small teams can't audit 43 vendors' internals. So minimize standing access instead — it requires no vendor questionnaire. You can't inspect their locks; you can stop handing out keys.&lt;br&gt;
The walls still matter. None of this replaces no-public-IP defaults, own kernel per Cube, scoped secrets. Vendor hygiene shrinks the surface of trust; architecture shrinks the surface of reach. Both, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forty-three is a normal number&lt;/strong&gt;&lt;br&gt;
We weren't careless. We were modern. Every grant was reasonable on the day it was signed; the risk was the accumulation of reasonable decisions, each still active, none still reviewed.&lt;/p&gt;

&lt;p&gt;Every OAuth grant is a key you gave to another company's worst day. Count your keys.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>devops</category>
      <category>oauth</category>
    </item>
    <item>
      <title>It's October. Strangers Are Sending You Code. Here's How I Say Yes.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:02:00 +0000</pubDate>
      <link>https://dev.to/dhruonweb/its-october-strangers-are-sending-you-code-heres-how-i-say-yes-2lm9</link>
      <guid>https://dev.to/dhruonweb/its-october-strangers-are-sending-you-code-heres-how-i-say-yes-2lm9</guid>
      <description>&lt;p&gt;October routes thousands of first-timers to your repo. The review order that keeps CI safe (pipeline first, deps second), grep for the three exfil shapes, fork-PR secret hygiene, per-job disposable runners, and how to reject a PR kindly&lt;/p&gt;

&lt;p&gt;First PR of the season arrived October 2nd: account three days old, title fix typo, diff mostly a typo. Mostly.&lt;/p&gt;

&lt;p&gt;October is the month strangers send you code, and if you maintain anything with a .git directory, your queue is about to have neighbors. Beautiful — and, if you're not careful, the month your CI meets someone's cousin's script. I've been the maintainer who flinched at a malicious hunk and the first-timer whose tiny PR got reviewed with kindness. Both wrote this post. The goal isn't keeping strangers out; it's keeping the door open without leaving it unlocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review order: pipeline, deps, then everything&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. anything that EXECUTES — a typo PR that edits workflows is not a typo PR&lt;/span&gt;
git diff main...HEAD &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'.github/'&lt;/span&gt; &lt;span class="s1"&gt;'Makefile'&lt;/span&gt; &lt;span class="s1"&gt;'*.mk'&lt;/span&gt; &lt;span class="s1"&gt;'Dockerfile'&lt;/span&gt; &lt;span class="s1"&gt;'*compose*.y*ml'&lt;/span&gt;

&lt;span class="c"&gt;# 2. dependency deltas — typosquats live here dressed as helpfulness&lt;/span&gt;
git diff main...HEAD &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*requirements*.txt'&lt;/span&gt; &lt;span class="s1"&gt;'package.json'&lt;/span&gt; &lt;span class="s1"&gt;'go.mod'&lt;/span&gt; &lt;span class="s1"&gt;'Cargo.toml'&lt;/span&gt;
&lt;span class="c"&gt;#    three questions per new package: who maintains it, how old is it, why this one&lt;/span&gt;

&lt;span class="c"&gt;# 3. then everything else, at the depth its blast radius deserves&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The three exfil shapes (grep them)&lt;/strong&gt;&lt;br&gt;
Most malicious October diffs aren't clever — same three shapes, different variable names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'process\.env|os\.environ|getenv|/proc/self/environ'&lt;/span&gt;   &lt;span class="c"&gt;# environment going somewhere&lt;/span&gt;
git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'base64|eval\(|atob|xxd -r'&lt;/span&gt;                            &lt;span class="c"&gt;# obfuscation in an unobfuscated repo&lt;/span&gt;
git diff main...HEAD | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'curl .*(-d|--data|-F)|wget |requests\.post|fetch\('&lt;/span&gt;   &lt;span class="c"&gt;# new outbound calls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real tell is scope creep: the mismatch between the title's promise and the diff's appetite. A fix-typo that adds a network call, a cron, or a download gets the hard look. Paranoia is vague and exhausts you; a checklist is cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safe room: fork PRs and disposable runners&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fork PRs never see secrets. Keep the platform defaults that make this true (approval required before a fork first-timer's workflow runs; no secret exposure to forks). A stranger's first PR should execute in a room with nothing valuable in it.&lt;/li&gt;
&lt;li&gt;Every job gets a throwaway box. My CI runs each job in a fresh microVM — on &lt;strong&gt;Krova Cloud&lt;/strong&gt; a job spins a Cube with its own kernel and no public IP, then dies:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CUBE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-job-&lt;/span&gt;&lt;span class="nv"&gt;$PR_NUMBER&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$RUN_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
krova cubes create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--cpu&lt;/span&gt; 2 &lt;span class="nt"&gt;--ram&lt;/span&gt; 4 &lt;span class="nt"&gt;--disk&lt;/span&gt; 40 &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04
&lt;span class="c"&gt;# run the PR's tests inside; fork PRs mount zero secrets&lt;/span&gt;
krova cubes delete &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;     &lt;span class="c"&gt;# teardown is the security feature&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A poisoned cache from a malicious build can't greet the next build, because there is no next build on the same machine. When the room is safe, review relaxes from interrogation to conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be kind on purpose (with receipts)&lt;/strong&gt;&lt;br&gt;
The three-day-old account is someone's first week in this community. The checklist handles the threat; your words handle the human:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thanks for this! The typo fix is perfect — merging. 🎉
One note: the workflow change isn't needed for the typo, so I've
split it into its own PR where it can get a proper review.
Welcome aboard — really glad your first one landed here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say thanks in every review including rejects. Explain a "no" in two sentences; a silent close teaches nobody. Never talk to a first-timer like a threat actor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;br&gt;
You cannot deep-review a flood. Tier it: CI/deps/scripts get the full checklist; docs and typos get a fast, grateful yes. The tiering is the practice.&lt;br&gt;
Automation flags shapes; humans read intent. A perfect auto-merge tool is just a faster door.&lt;br&gt;
The safe room doesn't replace review. Isolation limits what a bad PR can do; only reading limits what a bad PR can become.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the door open&lt;/strong&gt;&lt;br&gt;
October's queue is a gift pile. Some gifts are typos, some are gems, one might be wired. Open all of them — just open the wired one with the right room behind you.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>hacktoberfest</category>
      <category>cybersecurity</category>
      <category>devops</category>
    </item>
    <item>
      <title>My Smart Bulb Shares a Network With My Production Keys.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Sat, 03 Oct 2026 08:10:56 +0000</pubDate>
      <link>https://dev.to/dhruonweb/my-smart-bulb-shares-a-network-with-my-production-keys-15ca</link>
      <guid>https://dev.to/dhruonweb/my-smart-bulb-shares-a-network-with-my-production-keys-15ca</guid>
      <description>&lt;p&gt;Nineteen devices on one flat home LAN: my work laptop with prod access, a 2021-firmware bulb, a camera from a dead brand, and an ESP_4F2A nobody admits to buying. The fix: split the network, promote the router to perimeter, and treat home like a café — tunnels, scoped contexts, no standing creds.&lt;/p&gt;

&lt;p&gt;I opened my router's device list like you open a fridge at midnight — without a plan, slightly guilty. Nineteen devices. My work laptop, which can reach production, was on the same flat home network as a $12 smart bulb with 2021 firmware, a camera from a brand whose website no longer exists, a TV phoning home to an unplaceable timezone, and something labeled ESP_4F2A nobody in my household admits to buying.&lt;/p&gt;

&lt;p&gt;A home LAN is flat: everything trusts everything. The security of the machine holding my prod keys was, that evening, partially defined by a light bulb I bought on sale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The living-room test&lt;/strong&gt;&lt;br&gt;
The café test for laptops has a home version: if the cheapest device on your network gets popped, what's the path from there to the thing you'd cry about? On a flat LAN the answer is usually "a scan, then a try." Not because the bulb is an evil mastermind — because flat networks turn one weak device into a viewing position over everything else.&lt;/p&gt;

&lt;p&gt;First, see the whole estate:&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;# who actually lives on your LAN (router DHCP list is the honest source; this cross-checks)&lt;/span&gt;
nmap &lt;span class="nt"&gt;-sn&lt;/span&gt; 192.168.1.0/24 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Nmap scan report|MAC'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The split (free, boring, an afternoon)&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;work vlan  : laptop, work phone        → reaches SSH endpoints, nothing else inbound
iot vlan   : bulb, camera, TV, ESP_*   → internet only; sandbox where blast radius = each other
guest wlan : visitors                  → internet only, timed
# exceptions are deliberate, visible, few:
#   laptop → printer (print only)
#   phone  → doorbell app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Router settings, zero dollars. The bulb keeps bulbing; it just loses its view of my laptop. Then the router itself got promoted to perimeter: admin password off the sticker, auto-update on, remote management off. It's the only device in the house actually doing security — it deserved the ceremony.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Home is a café now&lt;/strong&gt;&lt;br&gt;
The laptop rules travel home: no standing production credentials, short-lived sessions, and services reached by tunnel — nothing listens publicly just because I'm on the couch:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# home-is-a-cafe.sh&lt;/span&gt;
krova context use home          &lt;span class="c"&gt;# scoped: create/wake/ssh ok; delete prod = no&lt;/span&gt;

krova ssh db-1  &lt;span class="nt"&gt;-L&lt;/span&gt; 5432:localhost:5432 &amp;amp;   &lt;span class="c"&gt;# postgres over the encrypted channel&lt;/span&gt;
krova ssh web-1 &lt;span class="nt"&gt;-L&lt;/span&gt; 8080:localhost:8080 &amp;amp;   &lt;span class="c"&gt;# app same; internet gets nothing&lt;/span&gt;

&lt;span class="c"&gt;# local clients now hit localhost; the databases never "listened for me"&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5432 &lt;span class="nt"&gt;-U&lt;/span&gt; app app_db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A popped bulb's "scan, then try" now finds localhost ports bound to encrypted tunnels it can't join, and a context whose worst day is a forgotten dev box — visible in one list, deleted in one word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Segmentation is messy in real life. The printer needs the laptop; the doorbell wants the phone; convenience files weekly complaints. The goal isn't a perfect diagram — it's that the default path from popped bulb to prod keys is gone, and exceptions are deliberate.&lt;/li&gt;
&lt;li&gt;You can't audit a household. Kids' tablets, a partner's phone, the in-laws visiting Wi-Fi. The split is the admission that you'll never control every device — so control what they can see instead.&lt;/li&gt;
&lt;li&gt;Home hygiene doesn't fix the castle. The walls still live server-side: no public IP by default, own kernel per Cube, default-deny inbound, scoped secrets. Segmentation shrinks the path to those walls; it doesn't replace them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Go look at your device list&lt;/strong&gt;&lt;br&gt;
Not tomorrow. Now. Count the devices, find the oldest firmware, and ask the only question that matters: does that device share a network with the keys?&lt;/p&gt;

&lt;p&gt;Mine did — neighbors for two years, and the bulb had the worse posture. The fix cost an afternoon and a new SSID name. Best security review I've done all year, and the arithmetic was free.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>workplace</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>I Did Recon on My Own Company , Using Only What We Published.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:01:37 +0000</pubDate>
      <link>https://dev.to/dhruonweb/i-did-recon-on-my-own-company-using-only-what-we-published-6cd</link>
      <guid>https://dev.to/dhruonweb/i-did-recon-on-my-own-company-using-only-what-we-published-6cd</guid>
      <description>&lt;p&gt;Twenty minutes of public-only recon on my own company: the README architecture diagram, job postings as a vendor list, an error-message hostname, .env.example as a secrets schema. The fix: split the manual from the map, scrub errors at the boundary, review jobs like publications.&lt;/p&gt;

&lt;p&gt;Quarterly habit: twenty minutes attacking myself with public information only. No credentials, no insider knowledge. This quarter I pointed it at the asset I'd never audited because I never thought of it as one: our documentation.&lt;/p&gt;

&lt;p&gt;The notebook I ended up with made me slightly nauseous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the public pile gave away&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;README architecture diagram — service names, queue names, the worker fleet. A map with a legend, updated last quarter.&lt;/li&gt;
&lt;li&gt;A support-thread screenshot — an error toast containing an internal hostname and stack path. Errors are involuntary documentation.&lt;/li&gt;
&lt;li&gt;Job postings — "must know Kafka, Datadog, Auth0, Terraform" is a vendor list with a budget attached, including which parts we're unhappy enough to replace.&lt;/li&gt;
&lt;li&gt;A 2023 conference talk, still indexed, "simplified" diagram 90% accurate.&lt;/li&gt;
&lt;li&gt;.env.example — variable names are a schema of your secrets; ours enumerated every third-party integration, alphabetically, with comments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it was a breach. All of it was publishing. Recon is the cheapest phase of an attack, and we were subsidizing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run your own twenty minutes (the sweep)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. involuntary docs: hostnames, internal ranges, vendor names in the repo&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rniE&lt;/span&gt; &lt;span class="s1"&gt;'(\.internal|\.local|10\.0\.|192\.168\.|hostname|stack)'&lt;/span&gt; docs/ README.md | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;

&lt;span class="c"&gt;# 2. the secrets schema&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; .env.example          &lt;span class="c"&gt;# read it like an attacker: that's an inventory&lt;/span&gt;

&lt;span class="c"&gt;# 3. your jobs page is a publication&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'kafka|datadog|auth0|pagerduty|terraform'&lt;/span&gt; &lt;span class="nb"&gt;jobs&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.md

&lt;span class="c"&gt;# 4. then do the human part:&lt;/span&gt;
&lt;span class="c"&gt;#    - search "yourproduct architecture"&lt;/span&gt;
&lt;span class="c"&gt;#    - site:yourdomain.com diagram&lt;/span&gt;
&lt;span class="c"&gt;#    - open your last three conference/blog artifacts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The manual vs. the map&lt;/strong&gt;&lt;br&gt;
Docs have two audiences by default, and only one is friendly; the document can't tell them apart. So we split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs-public/    # the MANUAL — behavior, interfaces, examples. Generous.
docs-internal/  # the MAP — topology, internal names, runbooks, vendor wiring.
                # Access-controlled, reviewed like code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the habits that cut involuntary leaks:&lt;/p&gt;

&lt;p&gt;Errors get scrubbed at the boundary — what happened, never where:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;log&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;                    &lt;span class="c1"&gt;// hostnames + stacks stay in logs&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// no internal names in the toast&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;.env.example documents shape, not inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- # Stripe live key for payments team
- STRIPE_KEY=sk_live_...
- KAFKA_BROKERS=kafka-3.internal:9092
&lt;/span&gt;&lt;span class="gi"&gt;+ PAYMENTS_KEY=        # your payments provider key (scoped, rotated)
+ EVENT_BROKER=        # event pipeline endpoint
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Job postings reviewed like publications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Own our Kafka + Datadog + Auth0 stack.
&lt;/span&gt;&lt;span class="gi"&gt;+ Own high-throughput event pipelines, observability, and auth integrations.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candidates understand the second version perfectly. So does the person drafting your attack plan — except they learn less from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not security by obscurity. The walls stay walls: no public IP by default, own kernel per Cube, default-deny inbound, scoped secrets. Doc discipline just stops handing out the floor plan of the building the walls protect.&lt;/li&gt;
&lt;li&gt;Open source moves the line, not the principle. If your topology is the repo, the discipline shifts to what isn't: your environments, vendors, operational wiring. Publish the software's manual; your instance's manual is yours.&lt;/li&gt;
&lt;li&gt;Total erasure is impossible. Mirrored slides live forever. The goal is that today's publishing is deliberate, and the freshest intel an attacker gets is stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Run your own twenty minutes&lt;/strong&gt;&lt;br&gt;
Search your product name plus "architecture." Read your last three job postings like a stranger with a grudge. Open the .env.example. Screenshot one customer-facing error.&lt;/p&gt;

&lt;p&gt;Then ask, for each artifact: who needs this to use us — and who else just learned something?&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>cloudsecurity</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Staging Went Down Last Month. Our Customers Tweeted About It.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:27:17 +0000</pubDate>
      <link>https://dev.to/dhruonweb/staging-went-down-last-month-our-customers-tweeted-about-it-4hfj</link>
      <guid>https://dev.to/dhruonweb/staging-went-down-last-month-our-customers-tweeted-about-it-4hfj</guid>
      <description>&lt;p&gt;Our 'staging' environment had real tenants via a forgotten DNS door, a two-year-old prod dump, and debug shortcuts — because environments are labeled by intention, not contents. The fix: classify by data, and make staging ephemeral with nightly rebirth on Krova Cloud.&lt;/p&gt;

&lt;p&gt;One guard, courtesy of a commenter: &lt;strong&gt;the rebirth has a supply chain.&lt;/strong&gt; The scrub and seed scripts are built against a schema, and migrations move the schema under them. If they live in a wiki or a separate repo, a migration silently breaks the nightly rebirth and nobody notices until the next demo. So they're versioned right next to the schema migrations — same repo, same PR — and the rebirth ends with a smoke test (&lt;code&gt;healthz&lt;/code&gt; plus a sane row count) that alerts on failure. Cleanup became a property of the system; this keeps the system honest about its own breakage.&lt;/p&gt;

&lt;p&gt;The alert said staging was unreachable. My first thought was who cares. The second, four seconds later: then why are customers tweeting that they can't log in?&lt;/p&gt;

&lt;p&gt;The investigation found staging had been serving real users for months — a forgotten DNS record, a redirect from an old marketing page, customers onboarded during a demo and never moved. Staging hadn't "become production." It had become production for some people, which is the same thing with worse defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the audit found&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A prod dump, copied "just once" two years ago. Real names, real emails, real rows. A copy of production data is production-shaped, whatever the hostname says.&lt;/li&gt;
&lt;li&gt;Relaxed rules everyone knew about. Debug endpoints. An admin panel behind a path instead of a password. A test account whose password lives in an offsite whiteboard photo. In prod these are incidents; in staging they were convenience — right up until real people lived there.&lt;/li&gt;
&lt;li&gt;No backups, no alerts, no drills. Nobody backs up a costume. Then the costume had tenants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule we now enforce in every planning meeting: production isn't a name. It's wherever your users' data lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classify by data, not hostname&lt;/strong&gt;&lt;br&gt;
Anything holding real user data gets production care — backups, alerts, auth, patching — even if it's called staging, demo, or bob-box. Real data enters scrubbed or not at all; the "one quick dump" is banned.&lt;/p&gt;

&lt;p&gt;And staging itself is now ephemeral by design: it dies and is reborn nightly, scrubbed on boot, deleted every Friday. An environment that dies weekly cannot accumulate secrets, drift, tenants, or folklore.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# staging-reborn.sh — nightly&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

krova cubes delete staging        &lt;span class="c"&gt;# old staging dies completely&lt;/span&gt;

krova cubes create staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssh-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/deploy.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vcpu&lt;/span&gt; 2 &lt;span class="nt"&gt;--ram&lt;/span&gt; 4 &lt;span class="nt"&gt;--disk&lt;/span&gt; 40 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;scrub-cloud-init.yml&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#cloud-config&lt;/span&gt;
&lt;span class="c1"&gt;# scrub-cloud-init.yml — staging is reborn clean every night&lt;/span&gt;
&lt;span class="na"&gt;runcmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sf -H "Authorization&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer ${DUMP_TOKEN}"&lt;/span&gt;
      &lt;span class="s"&gt;https://storage.private/scrubbed-dump.sql.gz&lt;/span&gt;
      &lt;span class="s"&gt;| gunzip | sudo -u postgres psql app&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl restart app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;a href="https://krova.cloud/" rel="noopener noreferrer"&gt;Krova Cloud&lt;/a&gt; this lifecycle is affordable because it's billed by the minute, and a stopped Cube bills only its disk — Friday's delete is the default state, not an event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The doors that make environments lie&lt;/strong&gt;&lt;br&gt;
Orphaned DNS is how staging acquires tenants. The weekly check:&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="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; d&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; CNAME | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# any record still pointing at staging is a door&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&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; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;staging&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DOOR: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; domains.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus the classification check — ask staging what it's holding, not what it's named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova ssh staging &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql app &lt;span class="nt"&gt;-tAc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT count(*) FROM users WHERE email NOT LIKE '%@example.com';"&lt;/span&gt;
&lt;span class="c"&gt;# any number above zero means staging is lying to you&lt;/span&gt;

krova ssh staging &lt;span class="nt"&gt;--&lt;/span&gt; ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;span class="c"&gt;# debug ports listening? that's a prod-shaped wound on a staging-shaped box&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synthetic data has a realism tax. Some bugs only bite real-shaped data — the 400-character name, the emoji in the address line. We use a smaller, weirder synthetic set plus occasional scrubbed real samples, reviewed like code. Perfection isn't on the menu; classification is.&lt;/li&gt;
&lt;li&gt;Small teams can't afford full parity. Fine. The rule isn't "staging must equal production." It's "nothing real lives in a place with pretend rules." One sentence, enforceable at any size.&lt;/li&gt;
&lt;li&gt;Ephemeral staging doesn't fix prod. Prod still needs real walls: no public IP by default, own kernel per Cube, scoped secrets. This post is about misclassification, not about replacing the castle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environments lie. Data doesn't.&lt;/strong&gt;&lt;br&gt;
Go ask your staging what it's holding — not the hostname, the contents. If the answer includes real names, real emails, or one customer who wanders in through a forgotten door, you don't have a staging environment. You have an unmonitored production wearing a costume.&lt;/p&gt;

&lt;p&gt;Ours wore it for months. The customers never noticed. That's not a compliment. That's the scariest part.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloudsecurity</category>
      <category>programming</category>
      <category>staging</category>
    </item>
    <item>
      <title>I Audited My Dead Side Projects. Some Weren't Dead.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Mon, 21 Sep 2026 13:06:00 +0000</pubDate>
      <link>https://dev.to/dhruonweb/i-audited-my-dead-side-projects-some-werent-dead-1e7b</link>
      <guid>https://dev.to/dhruonweb/i-audited-my-dead-side-projects-some-werent-dead-1e7b</guid>
      <description>&lt;p&gt;Forty minutes of list-making found a VPS still running after two years, a dangling CNAME ripe for subdomain takeover, and a valid API key on a machine I'd mentally buried. The audit scripts, the decommission runbook, and the sleep-instead-of-rot policy.&lt;/p&gt;

&lt;p&gt;Last Saturday I listed every project I'd ever deployed — side projects, hackathon demos, "quick tests." Forty minutes to write; reading it back took my breath away. Because dead projects don't die. They wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the graveyard was hiding&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Three projects I'd euthanized were still running — a VPS humming for two years with dependencies from the floats era. Forgotten app = dependency museum = vulnerability list.&lt;/li&gt;
&lt;li&gt;A cancelled domain still CNAME'd to a service I no longer owned. The record dangled like an unplugged phone line; anyone claiming the orphaned resource receives traffic meant for me. Subdomain takeover, living exactly where nobody looks.&lt;/li&gt;
&lt;li&gt;A .env on the old box with a key that was still valid. Two years of "I'll clean that up someday," one working key to my current life.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these were hacks. All of them were mine. The scariest attack surface I own isn't production — it's the graveyard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit, automated where possible&lt;/strong&gt;&lt;br&gt;
Schools teach shipping. Nobody teaches burying. So here's the part of my audit that runs as a script:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# graveyard-audit.sh — dangling DNS + estate inventory&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# 1. dangling CNAMEs: target no longer resolves = claimable by strangers&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; d&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; CNAME | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&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; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dig +short &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; A&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="se"&gt;\&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;"DANGLING: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; domains.txt

&lt;span class="c"&gt;# 2. what's actually running, by state&lt;/span&gt;
krova list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "\(.name)\t\(.state)"'&lt;/span&gt;

&lt;span class="c"&gt;# 3. secret archaeology in old project dirs — then revoke every hit&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rEho&lt;/span&gt; &lt;span class="s1"&gt;'(kro|ghp|aws|sk)_[A-Za-z0-9_-]{8,}'&lt;/span&gt; old-projects/ | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human part stays human: every CI variable, every service account, every "temporary" panel. The script catches the dangling and the running; you catch the remembered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decommission runbook (order matters)&lt;/strong&gt;&lt;br&gt;
A project isn't dead when the app is down. It's dead when nothing anywhere points at it or opens it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# memory first, then the box
krova snapshots create oldproj --name burial
krova cubes delete oldproj          # delete dialog preserves a backup by default

# THEN, in the same sitting — this is the part everyone skips:
#  - delete the DNS records (A, CNAME, _acme-challenge, TXT)
#  - revoke the keys it held
#  - remove CI/CD variables and webhooks
#  - cancel service accounts / OAuth apps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the box first and the DNS later, and "later" is where the graveyard grows. One sitting, full kill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sleep instead of rot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the "someday maybe" tier, the respectful middle state: hibernation. On Krova Cloud a powered-off Cube bills only its disk — no running process, no open port, no public IP to begin with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova cubes power-off maybe-someday   &lt;span class="c"&gt;# pennies/month, zero exposure&lt;/span&gt;
krova cubes wake maybe-someday        &lt;span class="c"&gt;# revive in seconds when nostalgia strikes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sleep is what keeps the graveyard from refilling: a project is either alive, asleep, or buried. Rotting is no longer a state I allow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A list isn't a fix. The audit produced a document; the boring weekend of deleting records and revoking keys produced the safety.&lt;/li&gt;
&lt;li&gt;You can't audit what you never wrote down. New projects get a row the day they're born — domain, keys, and their planned death: who revokes what.&lt;/li&gt;
&lt;li&gt;The graveyard isn't the castle. The living stack still needs real walls: no public IP by default, own kernel per Cube, scoped short-lived secrets. The audit shrinks the estate attackers can explore; it doesn't replace defending what's alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dead projects don't die. They wait.&lt;/strong&gt;&lt;br&gt;
Every reader has a graveyard. Some of yours are glowing right now — a box you forgot, a record you orphaned, a key that outlived its project. The audit is forty minutes; the fixes are one boring weekend.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>sideprojects</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Deleted My Entire Security Stack. My Apps Got Safer.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Wed, 16 Sep 2026 19:01:13 +0000</pubDate>
      <link>https://dev.to/dhruonweb/i-deleted-my-entire-security-stack-my-apps-got-safer-3png</link>
      <guid>https://dev.to/dhruonweb/i-deleted-my-entire-security-stack-my-apps-got-safer-3png</guid>
      <description>&lt;p&gt;fail2ban, the ufw ruleset, the WAF, the log-watchers — all purged in a month, and my apps got safer, because every one of them was a compensating control for a single inherited default: a public IP. The purge checklist, the before/after, and what I kept.&lt;/p&gt;

&lt;p&gt;Uninstalling fail2ban felt like removing a smoke detector. My finger hovered for a genuine minute. Then I pressed enter, and over the next month almost everything went: the ufw ruleset I'd maintained for years, the WAF config, auditd's screaming cron, the nightly "security" scripts whose output I'd stopped reading in 2024.&lt;/p&gt;

&lt;p&gt;My apps got safer. That sentence is the opposite of what it sounds like, so let me show the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The compensating question&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before the purge I asked what each tool was actually compensating for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fail2ban → SSH reachable from the entire internet&lt;/li&gt;
&lt;li&gt;ufw ruleset → a public address with ports that must be filtered&lt;/li&gt;
&lt;li&gt;WAF → the app directly reachable by anything that finds its IP&lt;/li&gt;
&lt;li&gt;log-watchers → the noise generated by all of the above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different tools, one root cause: the machine has a public address — a decision I never made, just inherited, because "server comes with an IP" is how servers work. I wasn't running a security stack. I was running a payment plan on a bad default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changing the default&lt;/strong&gt;&lt;br&gt;
The workloads moved to Cubes on Krova Cloud — Firecracker microVMs, own kernel each, and the part that retired the stack: no public IP. Private NAT'd network, traffic through managed TLS ingress, default-deny inbound, only explicitly opened ports reachable. I open none.&lt;/p&gt;

&lt;p&gt;Before/after, same eyes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;BEFORE — old VPS, the street:
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;span class="gp"&gt;LISTEN 0.0.0.0:22      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the brute-force magnet
&lt;span class="go"&gt;LISTEN 0.0.0.0:80
LISTEN 0.0.0.0:443

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;AFTER — Cube, from inside:
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;krova ssh app-1 &lt;span class="nt"&gt;--&lt;/span&gt; ip &lt;span class="nt"&gt;-brief&lt;/span&gt; addr
&lt;span class="gp"&gt;eth0  UP  10.0.x.x/24          #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;private. that&lt;span class="s1"&gt;'s the whole list.
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;krova ssh app-1 &lt;span class="nt"&gt;--&lt;/span&gt; ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;span class="gp"&gt;LISTEN 127.0.0.1:8080          #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the app, listening to itself
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;krova tcp list app-1
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;empty. no port forwards, nothing to filter, nothing to ban.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no address there's nothing to brute-force, so fail2ban has no job. With default-deny and no surface, the ruleset has no job. One migration weekend retired four tools and ~1,000 lines of compensating config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The purge checklist (and why it's safe)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt purge fail2ban          &lt;span class="c"&gt;# no public SSH → nothing to ban&lt;/span&gt;
ufw disable                 &lt;span class="c"&gt;# retired, NOT "off and exposed" —&lt;/span&gt;
                            &lt;span class="c"&gt;# safe only because the platform default&lt;/span&gt;
                            &lt;span class="c"&gt;# is private + default-deny inbound&lt;/span&gt;
&lt;span class="c"&gt;# WAF config → archived; app-level validation stays in the app&lt;/span&gt;
crontab &lt;span class="nt"&gt;-e&lt;/span&gt;                  &lt;span class="c"&gt;# delete auditd + nightly scanner lines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that ufw disable line carefully. It is safe because the exposure left first. On a box with a public IP, the same command is a breach with extra steps. Order matters: default first, meds second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did NOT delete&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets hygiene — scoped, short-lived, injected at runtime.&lt;/li&gt;
&lt;li&gt;Backups + restore drills — off-host copies, scheduled restores into a fresh Cube.&lt;/li&gt;
&lt;li&gt;Patching &amp;amp; dependency cadence — the boring rhythm continues.&lt;/li&gt;
&lt;li&gt;Application security — authn/authz, input validation, tenant checks. The purge hit tools defending the door; the app's own locks were never the target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep your tools if your symptom is mandatory. Gameservers, UDP media, weird protocols — real public-address workloads exist. This isn't "firewalls are a scam"; it's "ask whether the symptom is optional." For a shocking number of web workloads, it is.&lt;/li&gt;
&lt;li&gt;Architecture isn't a sedative. No public IP shrinks the surface; it doesn't review your PRs. The threat model got smaller, not zero.&lt;/li&gt;
&lt;li&gt;Ops stay yours. Isolation is the platform's job; patching, backups, drills remain the owner's, same as any self-hosted box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I didn't remove the locks. I removed the street.&lt;/strong&gt;&lt;br&gt;
Most of my "security" was pain management for a street I never needed. When the street went away, the pharmacy closed.&lt;/p&gt;

&lt;p&gt;Ask your stack the compensating question tonight. You might find a default you inherited, a wound you've been medicating for years — and a weekend where the meds go away.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>cloudsecurity</category>
    </item>
    <item>
      <title>My Monitoring Missed the Breach. My Credit Card Caught It.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:06:19 +0000</pubDate>
      <link>https://dev.to/dhruonweb/my-monitoring-missed-the-breach-my-credit-card-caught-it-3ae4</link>
      <guid>https://dev.to/dhruonweb/my-monitoring-missed-the-breach-my-credit-card-caught-it-3ae4</guid>
      <description>&lt;p&gt;A leaked key spun up instances in a region my dashboards didn't watch; the invoice screamed first. Now I treat spend as a security sensor — per-minute resolution, a prepaid fuse, and a nightly audit on Krova Cloud.&lt;/p&gt;

&lt;p&gt;The leak was a CI variable I'd forgotten existed, wired to a cloud API key with far too much confidence. Over one weekend, someone's script used it to spin up instances in a region I'd never deployed to.&lt;/p&gt;

&lt;p&gt;My dashboards stayed green — they were watching the resources I knew about. Nobody's monitoring watches a region they forgot exists. The first thing that screamed was my credit card statement, Monday morning, looking like a typo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spend is the only metric that can't lie by omission&lt;/strong&gt;&lt;br&gt;
CPU, latency, error rates — they measure what you chose to point them at. Money aggregates everything, including what you never thought to watch. An attacker can stay quiet to avoid your alerts. They cannot consume resources without touching your bill. Every leaked key, runaway loop and forgotten box has one shared property: it costs money per minute it exists.&lt;/p&gt;

&lt;p&gt;The invoice isn't accounting. It's the widest-coverage security sensor you own — and the most ignored. The catch is resolution and caps: a monthly invoice is a smoke detector that mails you a letter after the fire. I needed the signal during the fire, and a ceiling on the fire's size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup on Krova&lt;/strong&gt;&lt;br&gt;
Three properties make spend usable as a control on &lt;a href="https://krova.cloud/" rel="noopener noreferrer"&gt;Krova Cloud&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Per-minute resolution. Metered by the minute, no rounding up. High-resolution billing = high-resolution signal; a loop shows up within the hour, not at month-end.&lt;/li&gt;
&lt;li&gt;A literal fuse. Billing is prepaid credit, and when a space's balance hits zero its Cubes are automatically powered off — no data loss. A leaked key physically cannot spend past the credit I chose in advance. Dollar-capped blast radius.&lt;/li&gt;
&lt;li&gt;Hourly cost on every Cube. Each Cube reports its own costPerHour, so the audit is arithmetic, not estimation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nightly audit — expected list vs. actual list, plus burn rate:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# spend-audit.sh — reality vs. budget, every night&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
krova context use prod

&lt;span class="c"&gt;# what's actually running, and what it burns&lt;/span&gt;
krova list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[]
  | select(.state=="running")
  | "\(.name)\t\(.resources.vcpu)vcpu\t$\(.costPerHour)/h"'&lt;/span&gt;

&lt;span class="c"&gt;# total burn rate vs. plan&lt;/span&gt;
krova list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'[.[]
  | select(.state=="running") | .costPerHour] | add // 0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; * * *   &lt;span class="n"&gt;spend&lt;/span&gt;-&lt;span class="n"&gt;audit&lt;/span&gt;.&lt;span class="n"&gt;sh&lt;/span&gt; | &lt;span class="n"&gt;diff&lt;/span&gt; - &lt;span class="n"&gt;expected&lt;/span&gt;.&lt;span class="n"&gt;txt&lt;/span&gt; &amp;amp;&amp;amp; &lt;span class="n"&gt;true&lt;/span&gt; || &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="s2"&gt;"reality ≠ budget"&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt; * * * *   &lt;span class="n"&gt;burn&lt;/span&gt;-&lt;span class="n"&gt;rate&lt;/span&gt;-&lt;span class="n"&gt;check&lt;/span&gt;.&lt;span class="n"&gt;sh&lt;/span&gt;   &lt;span class="c"&gt;# alert if hourly spend exceeds plan + 20%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stranger's box can't hide when the whole estate fits in one jq. And for in-guest anomalies, the complement is event-driven: Krova's webhooks include resource.alert.cpu/memory/disk — spend catches the economic anomaly, those catch the hot process. Tripwire plus camera.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sizing the fuse&lt;/strong&gt;&lt;br&gt;
Prepaid means choosing your ceiling on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sandbox space gets exactly one experiment's worth of credit. If it melts down, the fuse trips before the surprise does.&lt;/li&gt;
&lt;li&gt;Prod gets normal load plus headroom — sized like a breaker: above normal, below pain. Misjudge it and the fuse is also an availability incident, so this is a staffing decision, not a slider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bill says something is wrong, not what. Sensor first, forensics second: when spend twitches, the running-list and logs find the limb.&lt;/li&gt;
&lt;li&gt;Cheap attacks hide under the noise. A patient $0.02/hour adversary sits inside your variance. Spend-catching nails the realistic failures — leaks, loops, zombies — not a careful human. For that you still need walls: no public IP by default, own kernel per Cube, default-deny inbound. The bill is a layer, not the castle.&lt;/li&gt;
&lt;li&gt;Ops still yours. The audit tells you a box exists; patching, backups and drills remain your job, same as any self-hosted estate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The cheapest SOC tool you already own&lt;/strong&gt;&lt;br&gt;
We file the invoice under "finance" instead of "security," then get surprised by it. Move it. Read it daily, alert on its rate of change, cap what it can reach.&lt;/p&gt;

&lt;p&gt;My monitoring is good now. But the sensor I'd least want to lose is the one my accountant installed by accident.&lt;/p&gt;

</description>
      <category>cloudsecurity</category>
      <category>devops</category>
      <category>finops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
