<?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 (@dhruv_malaviya_cdcc71e595).</description>
    <link>https://dev.to/dhruv_malaviya_cdcc71e595</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/dhruv_malaviya_cdcc71e595</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhruv_malaviya_cdcc71e595"/>
    <language>en</language>
    <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/dhruv_malaviya_cdcc71e595/its-october-strangers-are-sending-you-code-heres-how-i-say-yes-2lm9</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/my-smart-bulb-shares-a-network-with-my-production-keys-15ca</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/i-did-recon-on-my-own-company-using-only-what-we-published-6cd</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/staging-went-down-last-month-our-customers-tweeted-about-it-4hfj</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/i-audited-my-dead-side-projects-some-werent-dead-1e7b</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/i-deleted-my-entire-security-stack-my-apps-got-safer-3png</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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/dhruv_malaviya_cdcc71e595/my-monitoring-missed-the-breach-my-credit-card-caught-it-3ae4</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/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>
    <item>
      <title>I Gave My AI Assistant the Keys to My Cloud. Here's Why I'm Not Worried.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Fri, 11 Sep 2026 11:44:52 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/i-gave-my-ai-assistant-the-keys-to-my-cloud-heres-why-im-not-worried-2b46</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/i-gave-my-ai-assistant-the-keys-to-my-cloud-heres-why-im-not-worried-2b46</guid>
      <description>&lt;p&gt;My AI client can provision real servers now — over Krova Cloud's MCP server. The reason I sleep: scoped keys, a sandbox space, unreachable boxes, and a legible bill. Setup, guardrails, and the threat model inside.&lt;/p&gt;

&lt;p&gt;I typed a sentence into my AI client: "stand up a 2-CPU box with Docker for the staging API, call it staging-api." It provisioned a real server, verified the boot, and reported back — SSH ready, no public ports exposed. Ten seconds of magic, then the feeling you're supposed to have: what did I just hand an LLM the ability to do?&lt;/p&gt;

&lt;p&gt;The answer is the whole point of this post. Spoiler: it's not "trust the model." It's architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://krova.cloud/" rel="noopener noreferrer"&gt;Krova Cloud&lt;/a&gt; ships an MCP server, so Claude/Cursor/any MCP client can drive the API in plain English. Config is the standard shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "mcpServers": {
    "krova": {
      "command": "npx",
      "args": ["-y", "@krovacloud/mcp"],
      "env": {
        "KROVA_API_KEY": "kro_sandbox_...",
        "KROVA_SPACE_ID": "space_..."
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas the docs call out, both worth knowing before you debug ghosts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The space ID is the step everyone misses. The key authenticates you but doesn't say which space; without KROVA_SPACE_ID the server starts happily and then every call fails with "No spaceId provided." It's in your dashboard URL.&lt;/li&gt;
&lt;li&gt;Verify it's actually connected. Ask it to list your Cubes. Real Cubes with status and hourly cost = connected. A vague essay about what Krova is = the model answering from memory; check the MCP status panel, don't rephrase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The guardrails (the reason I sleep)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sandbox space, its own key. Keys are scoped to a single space and carry your permissions there. The assistant lives in a space that holds nothing precious — per the docs' own advice, "use a separate space for anything experimental, so the key cannot reach production Cubes at all." Prod is managed by me, slowly, with my own hands.&lt;/li&gt;
&lt;li&gt;Approval prompts stay on. The tool surface includes delete_cube, restore_cube, delete_domain — an agent that can create can also remove. I read what a call will do before allowing it. The prompt is a feature, not friction.&lt;/li&gt;
&lt;li&gt;Machines with no address. Every Cube the assistant can create is a Firecracker microVM with no public IP by default — private NAT'd network, default-deny inbound. A rogue box is isolated and unreachable, not a fresh attack surface.&lt;/li&gt;
&lt;li&gt;An enumerable, cheap blast radius. One nightly audit answers "what is it running right now?":
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;krova context use sandbox        # contexts = kubectl-style profiles per space
krova list --json \
  | jq -r '.[] | select(.state=="running") | "\(.name)\t\(.createdAt)"'
# forgotten box? one word:
krova cubes delete staging-api-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Billing is by the minute, so worst case is a forgotten box costing the price of a coffee — visible in one list, deleted in one word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wrong question&lt;/strong&gt;&lt;br&gt;
We keep asking "can we trust the model?" No. Not fully, not ever. Models misread; prompts inject; docs lie.&lt;/p&gt;

&lt;p&gt;The right question: what's the worst thing this key can do? Mine can spin up isolated microVMs with no public address, in a sandbox space, billed by the minute, visible in one sentence. I haven't made the AI careful. I've made carelessness survivable.&lt;/p&gt;

&lt;p&gt;The AI doesn't make the infrastructure safe. The infrastructure makes AI access safe.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Prompt injection is real. A malicious page my assistant reads could instruct it to provision nonsense or delete things in its space. The layers absorb it: scoped key, sandbox space, approvals, unreachable boxes. Four imperfect layers multiplying into something sleepable.&lt;/li&gt;
&lt;li&gt;The key is a credential. It acts as you in that space. Config file, never a repo; one key per machine; revoke the ones you're not using — deletion takes effect immediately.&lt;/li&gt;
&lt;li&gt;It won't do ops for you. The assistant provisions; backups, patching and restore drills are still mine. Isolation is the platform's job, operations remain the owner's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The tool didn't change. The blast radius did.&lt;/strong&gt;&lt;br&gt;
The same assistant that's terrifying with AdministratorAccess and a production VPS is harmless with a scoped key and disposable, unreachable boxes. Give it a library card, not a master key.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudsecurity</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Docker Is Fine. The Host Was the Problem.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Mon, 07 Sep 2026 04:07:53 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/docker-is-fine-the-host-was-the-problem-4hg8</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/docker-is-fine-the-host-was-the-problem-4hg8</guid>
      <description>&lt;p&gt;My containers were renting rooms in a stranger's house — shared kernel, shared VPS. The fix wasn't abandoning Docker: it was running it inside a Firecracker microVM on Krova Cloud with its own kernel. Setup, compose, and honest trade-offs inside.&lt;/p&gt;

&lt;p&gt;The moment came during a routine docker ps on the shared "container hosting" VPS where my side projects lived. The list was longer than I expected — other tenants' containers humming next to mine. Then the second thought: they're all running on the same kernel as me.&lt;/p&gt;

&lt;p&gt;Containers were never the border. As packaging, Docker is gorgeous — same compose file from laptop to server. The problem was the landlord: my reproducible containers renting rooms in a house full of strangers, sharing the foundation. Namespaces partition the view, not the physics; a kernel bug in the box next to yours is, in the strictest sense, your bug now.&lt;/p&gt;

&lt;p&gt;The industry's answer is real but heavy (gVisor, Kata, runtime classes, profile reviews). I didn't need a fleet. I needed a smaller house.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker inside a microVM&lt;/strong&gt;&lt;br&gt;
On Krova Cloud a Cube is a Firecracker microVM — own kernel, no public IP — and images ship with Docker Engine preinstalled, so the stack just moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova images    &lt;span class="c"&gt;# shows the slugs; pick the Docker-preinstalled variant&lt;/span&gt;

krova cubes create home-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04-docker &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/id_ed25519.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compose file is untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# compose.yaml — identical to my laptop&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./web&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pgdata:/var/lib/postgresql/data"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship it and attach the domain (traffic enters via managed TLS ingress — the Cube itself has no public address):&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 home-1 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'git clone git@github.com:me/stack.git &amp;amp;&amp;amp; cd stack &amp;amp;&amp;amp; docker compose up -d'&lt;/span&gt;
krova domains add home-1 &lt;span class="nt"&gt;--domain&lt;/span&gt; app.example.com &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify what actually changed&lt;/strong&gt;&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;krova ssh home-1 &lt;span class="nt"&gt;--&lt;/span&gt; docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}'&lt;/span&gt;
&lt;span class="go"&gt;stack-web-1
&lt;/span&gt;&lt;span class="gp"&gt;stack-db-1          #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the list I expect. nothing &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="nb"&gt;.&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 home-1 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;span class="gp"&gt;6.8.0               #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;a kernel nobody &lt;span class="k"&gt;else &lt;/span&gt;is renting
&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 home-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 NAT&lt;span class="s1"&gt;'d network; no public IP to scan
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties did the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The sharing didn't disappear — the strangers did. My containers still share a kernel; now it's mine. A neighbor's kernel bug can't reach me because there is no neighbor.&lt;/li&gt;
&lt;li&gt;No public IP. Default-deny inbound; only ports I explicitly open are reachable (and those can be IP-allowlisted). I open none.&lt;/li&gt;
&lt;li&gt;Boringly cheap. Sized exactly to my stack — 2 vCPU / 4 GB / 40 GB runs about $10/mo, billed by the minute.
I kept all of Docker's convenience and moved the one thing that was never Docker's job: the boundary.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Inside the Cube, my containers share my kernel with each other. If one of my containers gets popped, the others are in reach. Acceptable, because they're all mine — the blast radius is my estate, not a stranger's. The boundary moved to the right place, not a perfect place.&lt;/li&gt;
&lt;li&gt;Running other people's code? User uploads, plugins, AI agents — that's a different threat and the wrong shape; you want a microVM per workload there, not per host. Different post, different tool.&lt;/li&gt;
&lt;li&gt;Operational stuff stays yours. Backups of the named volume, image updates, compose hygiene — a private kernel doesn't do ops for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Whose kernel?&lt;/strong&gt;&lt;br&gt;
"Containers vs VMs" was never the question. It's whose kernel. Packaging and isolation got mashed together for a decade, and we defended the mash-up with config reviews instead of asking that out loud.&lt;/p&gt;

&lt;p&gt;Keep the packaging. Change the landlord.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>cloudsecurity</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>A Customer Asked Who Else Was on Their Server. I Didn't Have a Good Answer.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:55:27 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/a-customer-asked-who-else-was-on-their-server-i-didnt-have-a-good-answer-3aic</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/a-customer-asked-who-else-was-on-their-server-i-didnt-have-a-good-answer-3aic</guid>
      <description>&lt;p&gt;A security questionnaire exposed that my tenants shared a kernel. Now each tenant runs in a dedicated Firecracker microVM on Krova Cloud — here's the signup-time provisioning, the sleep policy, and the honest cost math.&lt;/p&gt;

&lt;p&gt;The question came from a customer, buried in a security questionnaire: "Describe the tenant isolation of your production environment."&lt;/p&gt;

&lt;p&gt;My honest answer was "we use containers" — which, the more I thought about it, was a description of packaging, not isolation. Every customer's workload shared one kernel. Namespaces drew polite lines; a kernel bug doesn't respect politeness. The threat model that kept me up wasn't strangers on the internet. It was my own tenants, one kernel apart.&lt;/p&gt;

&lt;p&gt;Now each tenant's workload runs in its own Cube on Krova Cloud — a Firecracker microVM with its own kernel. Here's the actual setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning at signup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The signup flow shells out to a script. Idempotency key included, so a retried webhook can't create two Cubes for one tenant:&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;# provision-tenant.sh &amp;lt;tenant-slug&amp;gt;&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="nv"&gt;TENANT&lt;/span&gt;&lt;span class="o"&gt;=&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;

krova cubes create &lt;span class="s2"&gt;"tenant-&lt;/span&gt;&lt;span class="nv"&gt;$TENANT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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/tenant_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; 1 &lt;span class="nt"&gt;--ram&lt;/span&gt; 2 &lt;span class="nt"&gt;--disk&lt;/span&gt; 20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--idempotency-key&lt;/span&gt; &lt;span class="s2"&gt;"signup-&lt;/span&gt;&lt;span class="nv"&gt;$TENANT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# tenant gets their own subdomain over managed TLS ingress&lt;/span&gt;
krova domains add &lt;span class="s2"&gt;"tenant-&lt;/span&gt;&lt;span class="nv"&gt;$TENANT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--domain&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TENANT&lt;/span&gt;&lt;span class="s2"&gt;.app.example.com"&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing from the API when the orchestrator does it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://krova.cloud/api/v1/spaces/&lt;span class="nv"&gt;$SPACE&lt;/span&gt;/cubes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-KEY: &lt;/span&gt;&lt;span class="nv"&gt;$KROVA_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: signup-&lt;/span&gt;&lt;span class="nv"&gt;$TENANT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "tenant-acme",
    "image": "ubuntu-24.04",
    "resources": { "vcpu": 1, "ramGb": 2, "diskGb": 20 },
    "sshPublicKey": "ssh-ed25519 AAAA..."
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify what the tenant actually got — this is the part I print into the security questionnaire&lt;br&gt;
answer:&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;krova ssh tenant-acme &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 NAT&lt;span class="s1"&gt;'d network. no public address.
&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 tenant-acme &lt;span class="nt"&gt;--&lt;/span&gt; ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;only the tenant&lt;span class="s1"&gt;'s own app listens; nothing reachable from the internet
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API agrees: publicIpv4 stays null until you explicitly map a port. Default-deny is the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three properties that matter&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Own kernel per tenant. One customer's kernel bug can't reach another customer. Hardware-enforced, not configured-correctly-on-a-good-day.&lt;/li&gt;
&lt;li&gt;No overselling. Per the docs, RAM and disk are reserved 1:1 — "your resources" finally means your resources. The noisy neighbor didn't get quieter; he moved out.&lt;/li&gt;
&lt;li&gt;No public IP. Tenants' boxes aren't scannable addresses. Traffic enters via managed TLS ingress on their subdomain; only ports I explicitly open are reachable, and I open none.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Keeping the bill survivable&lt;/strong&gt;&lt;br&gt;
One Cube per tenant is one bill per tenant, so the economics get engineering:&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;# nightly: tenants inactive 7+ days go to sleep — stopped Cubes bill disk only&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'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^tenant-'&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; cube&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;is_active &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="o"&gt;||&lt;/span&gt; krova cubes power-off &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="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# tenant logs in next morning → auth layer wakes them:&lt;/span&gt;
krova cubes wake tenant-acme   &lt;span class="c"&gt;# back in seconds, disk intact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Micro sizes for light tenants, sleep for idle ones, right-size when usage says so. Rough math: 30 tenants on $5–10 Cubes is $150–300/mo — priced into plans as a trust line-item, because that's exactly what it is. Insurance you can quote in a sales call.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Isolation is a boundary, not authorization. A wrong WHERE tenant_id = ... in my own app still leaks data between tenants. The microVM stops machines from touching; it doesn't stop my bugs. App-level tenant checks stay and get tested.&lt;/li&gt;
&lt;li&gt;You write some orchestration. Provision, sleep, wake, deprovision-on-churn, snapshot before migrations — maybe a hundred lines plus cron. Real cost; still smaller than the cluster I didn't adopt.&lt;/li&gt;
&lt;li&gt;Draw the line where the question gets asked. Ten thousand free-tier users don't each get a VM. I started with tenants handling serious data — the ones filling out questionnaires. That's where the risk and the trust live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tenancy is billing. Isolation is physics.&lt;/strong&gt;&lt;br&gt;
"Multi-tenant" drifted into meaning "shares everything." It doesn't have to: bill tenants together, run them apart. When a customer asks who else is on their machine, there's only one answer that ends the conversation happily: nobody.&lt;/p&gt;

</description>
      <category>cloudsecurity</category>
      <category>saas</category>
      <category>devops</category>
      <category>multitenancy</category>
    </item>
    <item>
      <title>I Deleted Production at 6 PM on a Friday. Here's Why It Was Fine.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:16:41 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/i-deleted-production-at-6-pm-on-a-friday-heres-why-it-was-fine-akj</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/i-deleted-production-at-6-pm-on-a-friday-heres-why-it-was-fine-akj</guid>
      <description>&lt;p&gt;A cleanup script, an empty variable, and my side project's files gone at 6:04 PM. The fix wasn't skill — it was blast radius, snapshots, and restore drills on Krova Cloud.&lt;/p&gt;

&lt;p&gt;The command was a cleanup script. The variable was empty. The path resolved somewhere it absolutely should not have, and by 6:04 PM my side project's app config and two weeks of uploads were gone.&lt;/p&gt;

&lt;p&gt;Friday. Six PM. You know the feeling.&lt;/p&gt;

&lt;p&gt;Nobody tells you this when you're getting into infrastructure security: the most dangerous attacker in your threat model has your SSH key, good intentions, and a deadline. It's you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The old shape made me fragile&lt;/strong&gt;&lt;br&gt;
Everything lived on one VPS: app, database, cron, the nginx config only God understood. "One machine" means one blast radius — my typo didn't take out a service, it took out the estate. Recovery was a weekend of reconstructing configs from memory like an archaeologist.&lt;/p&gt;

&lt;p&gt;I'd spent years defending that box from strangers and got taken out by myself in four seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The new shape: small boxes, snapshot-first&lt;/strong&gt;&lt;br&gt;
I can't uninstall my own hands, so I moved to small single-purpose Cubes on Krova Cloud — Firecracker microVMs, own kernel each, no public IP — and made every risky change start the same way:&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 scary thing&lt;/span&gt;
krova snapshots create web-1 &lt;span class="nt"&gt;--name&lt;/span&gt; pre-migration
krova snapshots list web-1

&lt;span class="c"&gt;# the scary thing goes scary&lt;/span&gt;
krova snapshots restore web-1 snap_abc123   &lt;span class="c"&gt;# disk rolls back; minutes, not weekends&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restore replaces the Cube's disk with the snapshot. A mistake now eats one small machine with one job; worst case I delete the Cube and redeploy. Note even the delete dialog defaults to "preserve a backup before deleting" — the platform assumes you'll hurt yourself eventually and pads the floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backups that outlive the Cube&lt;/strong&gt;&lt;br&gt;
Snapshots are a seatbelt, not a vault. For the database Cube I keep Backups — they store the disk and the config (vCPU/RAM/disk/image/region/mappings) and survive deleting the Cube they came from. The workflow that saved my weekend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redeploy the Backup into a fresh Cube. The original stays untouched — which makes this perfect for drills and staging copies.&lt;/li&gt;
&lt;li&gt;Verify what actually came back, because a green status is not your data:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;port&amp;gt; ubuntu@&amp;lt;drill-cube-host&amp;gt;
systemctl is-active myapp          &lt;span class="c"&gt;# enabled units come back on their own&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8080/     &lt;span class="c"&gt;# the app answers&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /srv/uploads                &lt;span class="c"&gt;# the files are actually there&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Tear the drill Cube down when done.
One gotcha worth knowing: redeploying alongside a live original gives you a Cube with all the data and none of the domains (mapping conflicts are skipped, not stolen). Exactly right for a drill, exactly wrong if you expected a failover.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The monthly drill&lt;/strong&gt;&lt;br&gt;
Untested recovery is folklore. Mine runs while everything is calm:&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;# restore-drill.sh — first Tuesday of the month&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. redeploy latest Backup of db-1 into a fresh Cube (dashboard/API)&lt;/span&gt;
&lt;span class="c"&gt;# 2. verify from outside my normal tooling&lt;/span&gt;
ssh &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRILL_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ubuntu@&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRILL_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
  systemctl is-active postgresql
  pg_dump app_db | gzip | wc -c    # sane, non-zero size
'&lt;/span&gt;
&lt;span class="c"&gt;# 3. drill over&lt;/span&gt;
krova cubes delete &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRILL_CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the drill ever fails, it fails on a Tuesday, on purpose, with coffee — not at 6 PM on a Friday with adrenaline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest part&lt;/strong&gt;&lt;br&gt;
Krova's own docs say it plainly, and I love them for it: "Backups are not a backup strategy on their own." Everything above lives on the same platform as the Cube. It covers bad deploys, deleted files, eager deletes — not the platform-shaped hole. So the database also dumps nightly to object storage I control, and the .cube download link (presigned, 15 minutes) gets treated like a credential, because it is one — anyone holding it holds your whole disk.&lt;/p&gt;

&lt;p&gt;And small machines don't make you careful. I still typo. The point was never to become perfect. The point was to stop needing to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for the person with the key&lt;/strong&gt;&lt;br&gt;
Audit your worst incidents and you'll see the pattern: the expensive ones usually start with a person who had legitimate access and a bad moment. Small blast radii, snapshot-first habits, backups that outlive the machine, drills on a schedule. That's the whole architecture.&lt;/p&gt;

&lt;p&gt;You'll deal with that person someday. I know because I've met them. It was me, on a Friday, at 6 PM.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloudsecurity</category>
      <category>microvm</category>
      <category>programming</category>
    </item>
    <item>
      <title>My Server Sleeps at Night. That Turned Out to Be a Security Feature.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:03:41 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/my-server-sleeps-at-night-that-turned-out-to-be-a-security-feature-469j</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/my-server-sleeps-at-night-that-turned-out-to-be-a-security-feature-469j</guid>
      <description>&lt;p&gt;I gave my side project a sleep schedule on Krova Cloud to save money — power off at 1 AM, wake at 7. The surprise side effect: zero attack surface while asleep. Here's the setup, the cron, and the trade-offs.&lt;/p&gt;

&lt;p&gt;At 3 a.m., my production server is off. Not down — off. On purpose.&lt;/p&gt;

&lt;p&gt;It started as a money thing. The project has human hours: traffic ~8:00 to midnight, dead until morning. The old VPS ran 24/7 and I paid for eight idle hours every night. Also, 3 a.m. on a VPS is when the botnet zoo shows up — brute-force loops, scanner sweeps, auth-log horror anthologies.&lt;/p&gt;

&lt;p&gt;Then it clicked: this project has a sleep schedule. Servers are allowed to have sleep schedules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;br&gt;
The project now lives on a Cube on Krova Cloud — a Firecracker microVM with its own kernel and no public IP (private NAT'd network, managed TLS ingress, only explicitly opened ports reachable). The sleep schedule is the dumbest automation I own:&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;# crontab -e — on a host OTHER than web-1 (CI cron / small always-on box)&lt;/span&gt;
0 1 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;   krova cubes power-off web-1   &lt;span class="c"&gt;# lights out: compute billing stops, disk preserved&lt;/span&gt;
0 7 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;   krova cubes wake web-1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;15 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-sf&lt;/span&gt; https://app.example.com/healthz &lt;span class="o"&gt;||&lt;/span&gt; curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALERT_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'text=web-1 failed to wake'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are real CLI verbs (&lt;a class="mentioned-user" href="https://dev.to/krovacloud"&gt;@krovacloud&lt;/a&gt;/cli), not pseudocode. Semantics per the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;running → billed per minute for compute + disk&lt;/li&gt;
&lt;li&gt;stopped → billed for disk storage only&lt;/li&gt;
&lt;li&gt;data survives both directions; wake starts the Cube from its saved disk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So sleeping 8 hours a day removes ~a third of compute time from the bill, and the stopped hours cost pocket change. On my 2 vCPU / 4 GB / 40 GB Cube that's the difference between "hosting" and "rounding error." (krova pricing prints the per-resource hourly rates if you want to do your own math.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The surprise: the 3 a.m. noise died&lt;/strong&gt;&lt;br&gt;
First week I checked the logs out of habit. Nothing. No brute force, no scanners. Of course:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no SSH daemon listening&lt;/li&gt;
&lt;li&gt;no web server process&lt;/li&gt;
&lt;li&gt;nothing in memory, no kernel of mine executing&lt;/li&gt;
&lt;li&gt;nothing to scan, because there's no public address anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot exploit a process that isn't running. The nighttime attack surface isn't small; it's zero. Awake, the Cube still isn't a scannable box — no public IP, default-deny inbound — so the daytime surface stays tiny too.&lt;/p&gt;

&lt;p&gt;Verify the states yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova get web-1 &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .state   &lt;span class="c"&gt;# "stopped" at 1:05 AM&lt;/span&gt;
krova cubes wake web-1
krova ssh web-1 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;uptime&lt;/span&gt;               &lt;span class="c"&gt;# back in seconds, disk intact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What sleep forced on my architecture (all good things)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All state on disk. In-memory state dies at 1 a.m., so sessions in Redis-on-disk or DB rows, not globals. Sleep cured my lazy state management.&lt;/li&gt;
&lt;li&gt;Boot must be boring. Everything starts via systemd units; if wake + boot doesn't produce a working app with zero human input, that's a bug I fix in daylight.&lt;/li&gt;
&lt;li&gt;Health check = first request. I curl the domain after wake in the same cron pipeline; if boot ever fails, I know before users do.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 7 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;   krova cubes wake web-1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;15 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-sf&lt;/span&gt; https://app.example.com/healthz &lt;span class="o"&gt;||&lt;/span&gt; curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALERT_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'text=web-1 failed to wake'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where the schedule lives, and the two ways it dies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A commenter caught that v1 of this post never said where the crontab runs. Outside the Cube, as above — a powered-off host can't wake itself, and the 7 AM alert branch can't fire from inside a stopped Cube. But that creates the mirror failure: if &lt;em&gt;that&lt;/em&gt; host dies, the Cube never sleeps, everything looks healthy, and the bill silently goes back to 24/7. So each failure gets a checker that is alive when it fires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asleep when it should be awake&lt;/strong&gt; → the Cube is off, it can't report itself. Only an external check notices (the 7 AM healthz + alert above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awake when it should be asleep&lt;/strong&gt; → the Cube is on, so something inside &lt;em&gt;can&lt;/em&gt; talk:
&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;# inside web-1 — this one MAY live on the Cube, it only needs to fire while awake&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;   awake-during-sleep-window.sh   &lt;span class="c"&gt;# alerts if the Cube finds itself running at 2 AM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any time — the interval watcher.&lt;/strong&gt; Both bullets above are point samples; the bill is an interval. A stray &lt;code&gt;wake&lt;/code&gt; at 3 AM slips between them and looks healthy by 7. The right observable turned out to be the state log Krova already emits: register a webhook for &lt;code&gt;cube.running&lt;/code&gt; / &lt;code&gt;cube.stopped&lt;/code&gt; (HMAC-signed, timestamped) and you get every transition:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/spaces/$SPACE/webhooks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://watcher.example.com/krova"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cube.running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cube.stopped"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alert on any &lt;code&gt;cube.running&lt;/code&gt; outside the 7 AM window the second it happens; sum the running→stopped intervals nightly — ~960 compute-minutes expected, more is the alarm. The bill is the ground truth; the samples were approximations.&lt;/p&gt;

&lt;p&gt;Three checkers now: two point samples for the cheap obvious failures, one event stream for the interval. And the schedule itself lives outside anything it controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objections, fairly handled&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wake isn't instant. First morning request waits a few seconds for boot. Correct choice for side projects / internal tools / staging with quiet hours. Wrong choice for 24/7 global SaaS — don't sleep those.&lt;/li&gt;
&lt;li&gt;Sleep ≠ security while awake. It shrinks the window; it doesn't replace the walls. The daytime Cube still needs its real properties: no public IP, own kernel, explicit ports only. Krova gives you those; sleep is just schedule math on top.&lt;/li&gt;
&lt;li&gt;Timezones. If your users are global, your "quiet hours" may not exist. Look at actual traffic before copying my cron.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The reframe&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We treat "always on" like a moral quality. For a lot of workloads it's just an expensive, attackable assumption. I added sleep to save money; the quietest security upgrade I've ever shipped was the side effect.&lt;/p&gt;

&lt;p&gt;Give it a bed with no address. Let it sleep.&lt;/p&gt;

&lt;p&gt;Does anyone else run real workloads on a sleep schedule? Or does powering off "production" at night terrify you? Which camp are you in, and what do your quiet hours look like? Comments open.&lt;/p&gt;

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