<?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>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>
    <item>
      <title>A Stranger's Pull Request Almost Stole My Cloud Credentials. So Every CI Job Gets a Fresh microVM Now.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Sat, 29 Aug 2026 08:48:58 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/a-strangers-pull-request-almost-stole-my-cloud-credentials-so-every-ci-job-gets-a-fresh-microvm-3a2b</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/a-strangers-pull-request-almost-stole-my-cloud-credentials-so-every-ci-job-gets-a-fresh-microvm-3a2b</guid>
      <description>&lt;p&gt;My self-hosted CI runner was the least trusted machine I owned. Now every job runs in a disposable Firecracker microVM on Krova Cloud — here's the setup, the scripts, and the trade-offs.&lt;/p&gt;

&lt;p&gt;Earlier this year a boring dependency-bump PR came in to one of my open-source projects. One hunk touched the CI config, just slightly. I almost merged it with one eye on the diff.&lt;/p&gt;

&lt;p&gt;Then I actually read it: one extra step on every build. And my self-hosted runner — a cheap VPS I'd set up to save CI minutes — ran jobs from forks. It also had cloud credentials sitting in a config file some past version of me had left there "for convenience."&lt;/p&gt;

&lt;p&gt;A malicious PR wouldn't have needed to escape anything. It could have curled my environment to a pastebin and the runner would have obliged.&lt;/p&gt;

&lt;p&gt;The real lesson wasn't about that PR. It's that a self-hosted runner is a machine that executes strangers' code, and then keeps living. My cheapest machine was my most dangerous one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I didn't just containerize the runner&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;containers are the standard answer, and they're not nothing. But on a runner they're a screen door where you need a wall:&lt;/p&gt;

&lt;p&gt;shared kernel with the host&lt;br&gt;
mounted caches that outlive the job (a poisoned cache hands itself to the next build)&lt;br&gt;
often a Docker socket&lt;br&gt;
a filesystem that persists between builds&lt;br&gt;
I didn't want politeness. I wanted a boundary. So now every CI job gets a fresh Firecracker microVM — own kernel, own rootfs — and the machine dies when the job ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The per-job lifecycle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://krova.cloud/" rel="noopener noreferrer"&gt;Krova Cloud&lt;/a&gt; each microVM is a Cube. Provisioning is one command and boots in under a second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @krovacloud/cli

krova cubes create ci-job-812 &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;# ✓ Cube provisioned  ·  booted in 0.9s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper my pipeline calls per job:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&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;$BUILD_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# fresh box per job — nothing carries over, ever&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;# secrets injected at runtime as short-lived, scoped tokens.&lt;/span&gt;
&lt;span class="c"&gt;# never in the image, never on disk after teardown&lt;/span&gt;
krova ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"DEPLOY_TOKEN=&lt;/span&gt;&lt;span class="nv"&gt;$DEPLOY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt; ./run-build.sh"&lt;/span&gt;

&lt;span class="c"&gt;# teardown is the security feature&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the orchestrator does it directly (retries included — note the idempotency key so a retried request doesn't double-provision):&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: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "ci-job-812",
    "image": "ubuntu-24.04",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What the runner looks like from inside&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that changed my threat model. The job's machine has no public IP — it lives on a private NAT'd network:&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;root@ci-job-812:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ip &lt;span class="nt"&gt;-brief&lt;/span&gt; addr
&lt;span class="go"&gt;lo               UNKNOWN        127.0.0.1/8
&lt;/span&gt;&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 it.
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;root@ci-job-812:~#&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;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nothing listening that you didn&lt;span class="s1"&gt;'t start.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;no SSH on the internet, no scannable surface.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inbound is default-deny; only ports you explicitly open are reachable, and those can be IP-allowlisted. A CI box opens none.&lt;/p&gt;

&lt;p&gt;So the three properties that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Nothing persists. No poisoned cache, no leftover cron, no creds on disk for the next build.&lt;/li&gt;
&lt;li&gt;The boundary is real. Own kernel per Cube (Firecracker — same tech behind AWS Lambda). A kernel bug popped in a build lands in a box that's seconds from deletion.&lt;/li&gt;
&lt;li&gt;No address to attack. The runner isn't a scannable VPS with port 22 on the internet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cost: billed by the minute, so a 12-minute job on a 2 vCPU / 4 GB Cube is a few cents. Ephemeral only works when teardown is cheap — that's the whole reason this architecture is affordable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this doesn't fix (read this part)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Isolation ≠ safety. If you hand a job a long-lived production credential, a fresh VM just means the credential gets stolen from a brand-new machine. So on top of the VM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;short-lived, scoped tokens per job — injected at runtime, gone at teardown&lt;/li&gt;
&lt;li&gt;no shared volumes between jobs&lt;/li&gt;
&lt;li&gt;egress restricted where it matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Krova gives you the blast radius. Secret hygiene is still your job, on any platform. Anyone selling "secure CI" as a checkbox is selling something.&lt;/p&gt;

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

&lt;p&gt;That PR moved CI from "infrastructure" to "supply chain" in my head. The code your pipeline runs is code you didn't write; the machine that runs it should be disposable, bounded, and unreachable.&lt;/p&gt;

&lt;p&gt;Give every job its own box. Kill the box when it's done.&lt;/p&gt;

&lt;p&gt;Has a PR ever targeted your CI instead of your code? And if you run ephemeral runners — microVMs, containers, or bare-metal chaos — what does your teardown look like? Genuinely comparing notes, comments open.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>microvm</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>I Gave an AI Agent Root Access. Nothing Broke.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Tue, 25 Aug 2026 12:35:43 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/i-gave-an-ai-agent-root-access-nothing-broke-3hmc</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/i-gave-an-ai-agent-root-access-nothing-broke-3hmc</guid>
      <description>&lt;p&gt;How I sandbox LLM agents in disposable Firecracker microVMs on Krova Cloud — no public IP, one-command lifecycle, and why containers weren't a real boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why containers weren't enough&lt;/strong&gt;&lt;br&gt;
The standard advice — "run the agent in a Docker container, drop privileges" — fails in one specific way: containers share a kernel. Every container on that host is one kernel bug away from its neighbors. That's exactly why Firecracker exists. AWS built it to run other people's code at scale because namespaces weren't a real boundary.&lt;/p&gt;

&lt;p&gt;So the agent gets a microVM. Own kernel, own rootfs, own everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;br&gt;
On Krova Cloud each microVM is called a Cube. Whole provisioning is one command:&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="go"&gt;npm i -g @krovacloud/cli

krova cubes create agent-1 --cpu 2 --ram 4 --disk 40 --image ubuntu-24.04
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;✓ Cube provisioned  ·  booted &lt;span class="k"&gt;in &lt;/span&gt;0.9s
&lt;span class="go"&gt;
krova ssh agent-1
&lt;/span&gt;&lt;span class="gp"&gt;root@agent-1:~#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No public IP. The Cube sits on a private NAT'd network. There's no address to scan, no port 22 on the internet, no inbound surface. Check for yourself from inside:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@agent-1:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ip &lt;span class="nt"&gt;-brief&lt;/span&gt; addr
&lt;span class="go"&gt;lo               UNKNOWN        127.0.0.1/8
&lt;/span&gt;&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 it.
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;root@agent-1:~#&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;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;only listeners &lt;span class="k"&gt;*&lt;/span&gt;you&lt;span class="k"&gt;*&lt;/span&gt; started — nothing is reachable from outside
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic only enters through ports you explicitly open, and those can be IP-allowlisted. For an agent box, I open nothing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's disposable. Boots in under a second, billed by the minute. So I stopped treating it as a pet: each agent session gets a fresh Cube, and when the session ends it's deleted. No leftover cron jobs, no stale credentials sitting in a long-lived environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The session lifecycle I actually run&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;CUBE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"agent-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# fresh box per session&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;# credentials injected at runtime as short-lived env vars,&lt;/span&gt;
&lt;span class="c"&gt;# never baked into the image&lt;/span&gt;
krova ssh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CUBE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# session over? nothing to clean up&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when I automate it from CI instead of my laptop, it's the v1 API — idempotency key included so retries don't double-provision:&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: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "agent-session-42",
    "image": "ubuntu-24.04",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A session that lives 20 minutes on a 2 vCPU / 4 GB Cube costs a few cents, minute-billed. Cheaper than the coffee I drink while it runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The parts I still don't trust (read this)&lt;/strong&gt;&lt;br&gt;
Isolation is not the same as safety. Let me be blunt, because this is where people get hurt:&lt;/p&gt;

&lt;p&gt;A microVM stops the agent from escaping to your other workloads. It does not stop it from wrecking its own box.&lt;br&gt;
It does not stop it from using credentials you handed it. AdministratorAccess AWS keys + a bulletproof sandbox = a very secure machine making a very expensive mistake.&lt;br&gt;
So the boring controls still apply, on top of the VM:&lt;/p&gt;

&lt;p&gt;Scoped, short-lived tokens — injected at runtime, never in the image&lt;br&gt;
No shared volumes with anything you care about&lt;br&gt;
Egress restricted where it matters (the agent doesn't need your whole network)&lt;br&gt;
The VM is the blast radius. Credential scoping is the actual control. Krova gives you the boundary — the hygiene inside it is still your job, same as any self-hosted box.&lt;/p&gt;

&lt;p&gt;Anyone telling you "just sandbox it and you're done" is selling something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;&lt;br&gt;
I used to think agent safety was a model problem. A big chunk of it is an infrastructure problem you can solve today with 2018-era technology:&lt;/p&gt;

&lt;p&gt;Give it a real boundary (own kernel, not namespaces)&lt;br&gt;
Give it no address (no public IP)&lt;br&gt;
Throw it away when you're done&lt;br&gt;
That's the whole trick.&lt;/p&gt;

&lt;p&gt;How are you sandboxing agents right now — containers, microVMs, or "vibes and hope"? And has anyone actually measured escape risk on their current setup? Would love to compare notes in the comments.&lt;/p&gt;

</description>
      <category>microvm</category>
      <category>devops</category>
      <category>cloudsecurity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Stopped Sharing Servers With Strangers</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:32:27 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/i-stopped-sharing-servers-with-strangers-3dcf</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/i-stopped-sharing-servers-with-strangers-3dcf</guid>
      <description>&lt;p&gt;I used to not think about who else was running on my host.&lt;/p&gt;

&lt;p&gt;Then one day it hit me: my VPS, my containers, my database — all of it was sharing a Linux kernel with workloads from people I'd never met. Their bugs, their breaches, their noisy neighbors were all one kernel vulnerability away from being my problem.&lt;/p&gt;

&lt;p&gt;It's not paranoia. It's just architecture.&lt;/p&gt;

&lt;p&gt;The shared kernel reality&lt;br&gt;
Most cloud hosting looks like this under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

┌─────────────────────────────┐
│        Host Kernel          │  ← shared by everyone
├─────────┬─────────┬─────────┤
│ Tenant  │ Tenant  │   You   │
│    A    │    B    │         │
└─────────┴─────────┴─────────┘


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Containers give you process isolation. They don't give you kernel isolation. Dirty Pipe, runc escapes, privilege escalation chains — they all exploit the same truth: break the kernel, and the walls between tenants get flexible.&lt;/p&gt;

&lt;p&gt;Most of the time, it's fine. Cloud providers are good at what they do. But "mostly fine" isn't the same as "isolated."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I switched to&lt;/strong&gt;&lt;br&gt;
I started using Krova Cloud, which gives every workload its own Firecracker microVM called a Cube.&lt;/p&gt;

&lt;p&gt;Each Cube boots its own kernel. Its own userspace. Its own private network. No public IP by default.&lt;/p&gt;

&lt;p&gt;Spin one up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash

npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @krovacloud/cli

krova cubes create myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disk&lt;/span&gt; 40 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04

krova ssh myapp
root@myapp:~#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No shared kernel. No security groups. No public IP.&lt;/p&gt;

&lt;p&gt;If you prefer the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash

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: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "myapp",
    "image": "ubuntu-24.04",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 },
    "sshPublicKey": "ssh-ed25519 AAAA...",
    "region": "eu-central"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What changed&lt;/strong&gt;&lt;br&gt;
The differences are subtle but real:&lt;/p&gt;

&lt;p&gt;No more shared-kernel CVEs keeping me up at night&lt;br&gt;
No more wondering if a noisy neighbor is causing latency spikes&lt;br&gt;
No public IP for bots to scan and brute-force&lt;br&gt;
My workload, my kernel, my rules&lt;br&gt;
I also stopped thinking about firewall rules as the main line of defense. When there's no public IP, the attack surface drops dramatically.&lt;/p&gt;

&lt;p&gt;For admin access, I open a port mapping locked to my IP:&lt;/p&gt;

&lt;p&gt;Bash&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova ports add myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowed-ips&lt;/span&gt; 203.0.113.10/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else stays unreachable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I run on Cubes now&lt;/strong&gt;&lt;br&gt;
Side project APIs&lt;br&gt;
A Postgres database&lt;br&gt;
CI runners&lt;br&gt;
Anything that processes user-submitted files&lt;br&gt;
Experiments I don't fully trust yet&lt;br&gt;
For each of these, I ask one question: does this need to share a kernel with anyone? If no, a Cube feels right.&lt;/p&gt;

&lt;p&gt;For trusted internal microservices, I still use containers. The density and speed are worth the trade. But for anything where isolation matters, I reach for a microVM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost reality&lt;/strong&gt;&lt;br&gt;
A 2 vCPU / 4 GB RAM / 40 GB disk Cube on Krova runs about $10/month. Comparable to — sometimes cheaper than — a VPS with a public IP.&lt;/p&gt;

&lt;p&gt;The hardware isn't the expensive part. The peace of mind is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest catch&lt;/strong&gt;&lt;br&gt;
Krova doesn't replace AWS, GCP, or Azure for complex multi-service platforms. It doesn't have a hundred managed services. It doesn't pretend to.&lt;/p&gt;

&lt;p&gt;What it gives you is a clean, isolated place to run a server. For some workloads, that's exactly enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;br&gt;
If you want to see what no-public-IP isolation feels like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash

npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @krovacloud/cli
krova signup
krova cubes create &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="nt"&gt;--ram&lt;/span&gt; 2 &lt;span class="nt"&gt;--disk&lt;/span&gt; 20 &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04
krova ssh &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash

ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing public. No shared kernel. Just your own small server in the cloud.&lt;/p&gt;

&lt;p&gt;Do you think about who else is running on your host? Or is it one of those things you stopped worrying about? Let me know.&lt;/p&gt;

</description>
      <category>cloudsecurity</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>microvm</category>
    </item>
    <item>
      <title>Why I Run My Postgres on a microVM Now</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:21:55 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/why-i-run-my-postgres-on-a-microvm-now-19f4</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/why-i-run-my-postgres-on-a-microvm-now-19f4</guid>
      <description>&lt;p&gt;&lt;strong&gt;A quick note on scope:&lt;/strong&gt; This post walks through a simple self-hosted Postgres setup on a &lt;a href="https://krova.cloud" rel="noopener noreferrer"&gt;Krova Cloud&lt;/a&gt; Cube — perfect for side projects and small workloads. If you're running production with strict RPO/RTO requirements, skip to the production section. Backups are not optional.&lt;/p&gt;

&lt;p&gt;I used to be the person who told everyone to use a managed database.&lt;/p&gt;

&lt;p&gt;"Don't self-host Postgres," I'd say. "You're not a DBA. Let AWS handle backups and replication."&lt;/p&gt;

&lt;p&gt;Then my managed bill started climbing. And I realized I wasn't using 90% of the features I was paying for. And I started wondering if there was a middle ground between "managed service" and "container on a shared host."&lt;/p&gt;

&lt;p&gt;There is. It's called a microVM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with how we run databases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Databases hold the data you actually care about. Yet we routinely:&lt;/p&gt;

&lt;p&gt;Run them on the same server as the app&lt;br&gt;
Leave them in containers on shared kernels&lt;br&gt;
Expose port 5432 to the internet because it's "convenient"&lt;br&gt;
Pay managed service prices for single-node setups that don't need half the features&lt;br&gt;
I've done all of these. Most of us have.&lt;/p&gt;

&lt;p&gt;A database doesn't need to be reachable from the internet. It doesn't need to share a kernel with strangers. It needs isolation, stable resources, and a sane backup strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a microVM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A microVM is a tiny virtual machine. It boots its own kernel. It has its own userspace. It's isolated by hardware virtualization, like any real VM.&lt;/p&gt;

&lt;p&gt;The difference is speed. Firecracker — the open-source microVM tech from AWS — can boot a VM in under a second. Fast enough that microVMs feel as lightweight as containers, but with real isolation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;container:  shared kernel, fast, soft boundary&lt;br&gt;
microVM:    own kernel, fast, real VM boundary&lt;br&gt;
managed DB: someone else's problem, expensive, shared infrastructure&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Krova Cloud Cubes&lt;/strong&gt;&lt;br&gt;
Krova Cloud gives you Firecracker microVMs called Cubes. Each one has its own kernel and no public IP by default.&lt;/p&gt;

&lt;p&gt;Creating a database Cube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @krovacloud/cli

krova cubes create db-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ram&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disk&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04

krova ssh db-1
root@db-1:~#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No VPC. No security groups. No public IP.&lt;/p&gt;

&lt;p&gt;If you prefer the API:&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: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "db-1",
    "image": "ubuntu-24.04",
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 80 },
    "sshPublicKey": "ssh-ed25519 AAAA...",
    "region": "eu-central"
  }'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Installing Postgres&lt;/strong&gt;&lt;br&gt;
Once you're SSH'd in, it's just a normal Ubuntu server:&lt;/p&gt;

&lt;p&gt;Bash&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql postgresql-contrib
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;postgresql
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the user and database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE USER app WITH PASSWORD 'your-strong-password';"&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"CREATE DATABASE app_db OWNER app;"&lt;/span&gt;
Edit /etc/postgresql/16/main/postgresql.conf to listen on the private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;interface:&lt;/p&gt;

&lt;p&gt;conf&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;listen_addresses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'localhost,10.0.0.4'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And /etc/postgresql/16/main/pg_hba.conf to allow the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;host&lt;/span&gt;    &lt;span class="err"&gt;app_db&lt;/span&gt;    &lt;span class="err"&gt;app&lt;/span&gt;    &lt;span class="err"&gt;10.0.0.0/24&lt;/span&gt;    &lt;span class="err"&gt;scram-sha-256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Your database is running on a real VM with its own kernel, on a private network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connecting from your app&lt;/strong&gt;&lt;br&gt;
Your app runs in another Cube. You connect using the database Cube's private IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL = "postgresql://app:your-strong-password@10.0.0.4:5432/app_db"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No public IP. No exposed port. The database only talks to your app.&lt;/p&gt;

&lt;p&gt;For admin access, open an IP-allowlisted SSH mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;krova ports add db-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowed-ips&lt;/span&gt; 203.0.113.10/32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backups&lt;/strong&gt;&lt;br&gt;
This is where self-hosting gets serious. A managed service gives you backups. A microVM gives you the tools to build your own.&lt;/p&gt;

&lt;p&gt;For a small project, a daily &lt;code&gt;pg_dump&lt;/code&gt; to encrypted object storage is a starting point:&lt;/p&gt;

&lt;p&gt;bash&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; postgres app_db | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/backup/app_db-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz"&lt;/span&gt;
&lt;span class="c"&gt;# encrypt and upload to your preferred storage&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cost comparison&lt;br&gt;
For 2 vCPU, 4 GB RAM, 80 GB disk:&lt;/p&gt;

&lt;p&gt;Managed Postgres-&amp;gt;&lt;br&gt;
Approximate monthly cost:$50–200+&lt;br&gt;
Notes:Backups, replication, monitoring included&lt;/p&gt;

&lt;p&gt;VPS with public IP-&amp;gt;&lt;br&gt;
Approximate monthly cost:$20–50&lt;br&gt;
Notes:Shared kernel, public attack surface&lt;/p&gt;

&lt;p&gt;Krova Cloud Cube-&amp;gt;&lt;br&gt;
Approximate monthly cost:~$10&lt;br&gt;
Notes:Own kernel, no public IP, per-minute billing&lt;/p&gt;

&lt;p&gt;markdown&lt;br&gt;
Prices vary, but the gap is real. Just remember: the Cube is the cheap part. The operational work — backups, patching, monitoring, recovery — is the real cost of self-hosting, regardless of platform.&lt;/p&gt;

&lt;p&gt;For a side project or small SaaS, the savings add up. For mission-critical workloads, budget for the operational layer too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side project vs. production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The simple setup in this post is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Side projects&lt;/li&gt;
&lt;li&gt;Small SaaS apps&lt;/li&gt;
&lt;li&gt;Learning and experimentation&lt;/li&gt;
&lt;li&gt;Workloads where you want full control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production, you don't need to leave Krova — you need to add operations on top of it. WAL archiving, PITR, automated failover, and mature monitoring can all run on Cubes. Krova handles the isolation and networking. You handle the database operations.&lt;/p&gt;

&lt;p&gt;If your team doesn't have the bandwidth for that operational work, a managed service is the right call. That's a staffing decision, not a platform limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real reason I switched&lt;/strong&gt;&lt;br&gt;
It wasn't just cost. It was isolation.&lt;/p&gt;

&lt;p&gt;I don't want my database sharing a kernel with random workloads. I don't want port 5432 discoverable on the internet. I don't want to rely on a firewall rule I might misconfigure at 2 AM.&lt;/p&gt;

&lt;p&gt;A microVM gives me a real boundary. Krova Cloud makes it easy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;br&gt;
If you want to experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @krovacloud/cli
krova signup
krova cubes create db-test &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="nt"&gt;--ram&lt;/span&gt; 2 &lt;span class="nt"&gt;--disk&lt;/span&gt; 20 &lt;span class="nt"&gt;--image&lt;/span&gt; ubuntu-24.04
krova ssh db-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Postgres, create a database, and notice that nothing is publicly reachable.&lt;/p&gt;

&lt;p&gt;Do you self-host Postgres? Use a managed service? I'd love to hear where you draw the line — and why.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>microvm</category>
    </item>
    <item>
      <title>Stop Hardening Your Server. Start Hiding It.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:39:49 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/stop-hardening-your-server-start-hiding-it-442o</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/stop-hardening-your-server-start-hiding-it-442o</guid>
      <description>&lt;p&gt;I used to be proud of my server hardening skills.&lt;/p&gt;

&lt;p&gt;SSH keys, fail2ban, UFW, SELinux, read-only root, the whole thing. I could lock down a VPS in twenty minutes and sleep soundly.&lt;/p&gt;

&lt;p&gt;Then I checked the auth logs on a fresh server and saw thousands of brute-force attempts in 24 hours. The server had been online for maybe six hours.&lt;/p&gt;

&lt;p&gt;I wasn't being targeted. I was just visible.&lt;/p&gt;

&lt;p&gt;That's when I started wondering: what if the best security isn't better hardening — it's not being findable in the first place?&lt;/p&gt;

&lt;p&gt;The public IP problem&lt;br&gt;
Every VPS gives you a public IPv4 address by default. It's convenient. It's also an invitation.&lt;/p&gt;

&lt;p&gt;Bots scan the entire internet constantly. If you have SSH on port 22 and a public IP, you're getting hit. It's not personal. It's automatic.&lt;/p&gt;

&lt;p&gt;I had done everything "right":&lt;/p&gt;

&lt;p&gt;Key-based SSH only&lt;br&gt;
fail2ban&lt;br&gt;
UFW with minimal ports&lt;br&gt;
Automatic updates&lt;br&gt;
Non-root user&lt;br&gt;
But the attempts never stopped. Because my server had an address.&lt;/p&gt;

&lt;p&gt;My app didn't need one. My users hit a domain. My database shouldn't be reachable from outside. Admin access was just for me. The public IP was solving almost nothing while creating a permanent target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hiding the server instead&lt;/strong&gt;&lt;br&gt;
I moved the project to Krova Cloud, which gives you Firecracker microVMs called Cubes. The default: no public IP.&lt;/p&gt;

&lt;p&gt;Your Cube sits on a private network. Web traffic comes through managed ingress with automatic HTTPS. SSH is only available through an explicit port mapping locked to your IP.&lt;/p&gt;

&lt;p&gt;Creating one:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;`npm i -g &lt;a class="mentioned-user" href="https://dev.to/krovacloud"&gt;@krovacloud&lt;/a&gt;/cli&lt;/p&gt;

&lt;p&gt;krova cubes create myapp \&lt;br&gt;
  --cpu 2 \&lt;br&gt;
  --ram 4 \&lt;br&gt;
  --disk 40 \&lt;br&gt;
  --image ubuntu-24.04&lt;/p&gt;

&lt;p&gt;krova ssh myapp&lt;br&gt;
root@myapp:~#`&lt;/p&gt;

&lt;p&gt;No VPC. No security groups. No public IP.&lt;/p&gt;

&lt;p&gt;If you prefer the API:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;curl -X POST &lt;a href="https://krova.cloud/api/v1/spaces/$SPACE/cubes" rel="noopener noreferrer"&gt;https://krova.cloud/api/v1/spaces/$SPACE/cubes&lt;/a&gt; \&lt;br&gt;
  -H "X-API-KEY: $KROVA_KEY" \&lt;br&gt;
  -H "Idempotency-Key: $(uuidgen)" \&lt;br&gt;
  -d '{&lt;br&gt;
    "name": "myapp",&lt;br&gt;
    "image": "ubuntu-24.04",&lt;br&gt;
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 },&lt;br&gt;
    "sshPublicKey": "ssh-ed25519 AAAA...",&lt;br&gt;
    "region": "eu-central"&lt;br&gt;
  }'&lt;br&gt;
&lt;strong&gt;What changed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After 48 hours, I checked the auth logs.&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;`# Before (public VPS)&lt;br&gt;
Failed password for root from 185.x.x.x&lt;br&gt;
Failed password for root from 103.x.x.x&lt;br&gt;
Failed password for root from 45.x.x.x&lt;br&gt;
...thousands more&lt;/p&gt;

&lt;h1&gt;
  
  
  After (Krova Cube)
&lt;/h1&gt;

&lt;h1&gt;
  
  
  nothing`
&lt;/h1&gt;

&lt;p&gt;No brute-forces. No port scans. No 2 AM anxiety about whether I left something exposed.&lt;/p&gt;

&lt;p&gt;The server still works. Users still reach the API through the domain. HTTPS still terminates at Krova's ingress. But the machine itself doesn't exist on the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin access without a public IP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you need SSH, you open a port mapping and lock it to your IP:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;krova ports add myapp \&lt;br&gt;
  --protocol tcp \&lt;br&gt;
  --port 22 \&lt;br&gt;
  --allowed-ips 203.0.113.10/32&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Only your IP can reach it. Everyone else sees nothing.&lt;/p&gt;

&lt;p&gt;For web traffic, point your domain at Krova's ingress. They handle TLS and hide the origin. Your app doesn't need to know it's not directly exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus: real isolation&lt;/strong&gt;&lt;br&gt;
Krova Cubes are Firecracker microVMs, so each one boots its own kernel. Not shared with other tenants. Not shared with the host.&lt;/p&gt;

&lt;p&gt;For a small side project, that's probably more isolation than necessary. But after reading one too many container escape write-ups, I prefer a real VM boundary for anything I care about.&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VPS:     public IP + shared kernel&lt;br&gt;
Container: maybe public IP + definitely shared kernel&lt;br&gt;
Cube:    no public IP + own kernel&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;&lt;br&gt;
Hardening is good. Hiding is better.&lt;/p&gt;

&lt;p&gt;A server without a public address can't be brute-forced. Can't be port-scanned. Can't be accidentally exposed by a bad firewall rule.&lt;/p&gt;

&lt;p&gt;We've accepted public IPs as the default because they're familiar. But most apps don't need their machine to be reachable from the whole internet. They need a domain, TLS, and a way for the owner to admin things.&lt;/p&gt;

&lt;p&gt;Everything else is attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;br&gt;
If you're curious, Krova signup doesn't require a card and the first $5 top-up doubles to $10:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm i -g @krovacloud/cli&lt;br&gt;
krova signup&lt;br&gt;
krova cubes create test --cpu 1 --ram 2 --disk 20 --image ubuntu-24.04&lt;br&gt;
krova ssh test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside the Cube, run:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ss -tlnp&lt;/code&gt;&lt;br&gt;
Nothing public. That's the whole point.&lt;/p&gt;

&lt;p&gt;Am I wrong about public IPs? Is hardening still the better default? Tell me why , I genuinely want to hear the counterarguments.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Side Project Doesn't Need a Public IP. Here's the Proof.</title>
      <dc:creator>Dhruv Malaviya</dc:creator>
      <pubDate>Mon, 10 Aug 2026 11:08:53 +0000</pubDate>
      <link>https://dev.to/dhruv_malaviya_cdcc71e595/your-side-project-doesnt-need-a-public-ip-heres-the-proof-2mem</link>
      <guid>https://dev.to/dhruv_malaviya_cdcc71e595/your-side-project-doesnt-need-a-public-ip-heres-the-proof-2mem</guid>
      <description>&lt;p&gt;I used to think every server needed a public IP. It's just... how servers work, right?&lt;/p&gt;

&lt;p&gt;Then I moved my side project , a small Node.js API with a Postgres database , to Krova Cloud. Cubes don't get public IPs by default. Within a day, I realized I'd been accepting a bunch of nonsense I didn't have to.&lt;/p&gt;

&lt;p&gt;No more SSH brute-forces. No open ports accidentally exposed. No anxiety about whether my firewall rules were perfect.&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;

&lt;p&gt;The problem with public IPs&lt;br&gt;
I ran my project on a cheap VPS for two years. It worked. But one bored afternoon I checked the auth logs:&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Failed password for root from 185.x.x.x&lt;br&gt;
Failed password for root from 103.x.x.x&lt;br&gt;
Failed password for root from 45.x.x.x&lt;/code&gt;&lt;br&gt;
Thousands of attempts per day. From everywhere.&lt;/p&gt;

&lt;p&gt;I had key-based SSH. I had fail2ban. I had UFW. But the attempts never stopped because my server's address was public. That's the whole job of a public IP — be findable.&lt;/p&gt;

&lt;p&gt;The thing is, my app didn't need to be findable. My domain was. My users hit the API through the domain. The server itself just needed to exist.&lt;/p&gt;

&lt;p&gt;Krova Cloud: a server without a public address&lt;br&gt;
Krova Cloud gives you Firecracker microVMs called Cubes. Each Cube has:&lt;/p&gt;

&lt;p&gt;Its own kernel&lt;br&gt;
No public IP&lt;br&gt;
A private network&lt;br&gt;
Managed ingress for web traffic&lt;br&gt;
IP-allowlisted port mappings for admin access&lt;br&gt;
Creating one looks like this:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;`npm i -g &lt;a class="mentioned-user" href="https://dev.to/krovacloud"&gt;@krovacloud&lt;/a&gt;/cli&lt;/p&gt;

&lt;p&gt;krova cubes create fileapi \&lt;br&gt;
  --cpu 2 \&lt;br&gt;
  --ram 4 \&lt;br&gt;
  --disk 40 \&lt;br&gt;
  --image ubuntu-24.04&lt;/p&gt;

&lt;p&gt;krova ssh fileapi&lt;br&gt;
root@fileapi:~#`&lt;br&gt;
That's the whole setup. No VPC. No security groups. No public IP.&lt;/p&gt;

&lt;p&gt;If you prefer APIs:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -X POST https://krova.cloud/api/v1/spaces/$SPACE/cubes \&lt;br&gt;
  -H "X-API-KEY: $KROVA_KEY" \&lt;br&gt;
  -H "Idempotency-Key: $(uuidgen)" \&lt;br&gt;
  -d '{&lt;br&gt;
    "name": "fileapi",&lt;br&gt;
    "image": "ubuntu-24.04",&lt;br&gt;
    "resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 },&lt;br&gt;
    "sshPublicKey": "ssh-ed25519 AAAA...",&lt;br&gt;
    "region": "eu-central"&lt;br&gt;
  }'&lt;/code&gt;&lt;br&gt;
Migrating the app&lt;br&gt;
My stack was simple:&lt;/p&gt;

&lt;p&gt;Node.js API running on port 3000&lt;br&gt;
Postgres on the same machine&lt;br&gt;
Nginx as a reverse proxy with HTTPS&lt;br&gt;
On Krova, I kept the same setup but pointed my domain at Krova's managed ingress instead of a public IP. HTTPS was handled automatically. The origin stayed hidden.&lt;/p&gt;

&lt;p&gt;For SSH, I opened a port mapping locked to my home IP:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;krova ports add fileapi \&lt;br&gt;
  --protocol tcp \&lt;br&gt;
  --port 22 \&lt;br&gt;
  --allowed-ips 203.0.113.10/32&lt;/code&gt;&lt;br&gt;
Only I can reach SSH. Everyone else sees the domain and the API. The server itself is invisible.&lt;/p&gt;

&lt;p&gt;The result&lt;br&gt;
After 48 hours, I checked the auth logs.&lt;/p&gt;

&lt;p&gt;Zero SSH attempts.&lt;/p&gt;

&lt;p&gt;Not "reduced." Not "better." Zero. Because there's no address to attack.&lt;/p&gt;

&lt;p&gt;My API response times were the same. My users didn't notice anything. My deploy process barely changed. But the background noise of the internet trying to break in just... stopped.&lt;/p&gt;

&lt;p&gt;What about isolation?&lt;br&gt;
I mentioned Cubes are Firecracker microVMs. That means each one boots its own kernel.&lt;/p&gt;

&lt;p&gt;For a tiny side project, that's probably overkill. But honestly? I like overkill when it comes to isolation. I've read enough container escape write-ups to prefer a real VM boundary for anything I care about.&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;&lt;code&gt;VPS:        public IP + shared kernel with other tenants&lt;br&gt;
Container:  maybe public IP + definitely shared kernel&lt;br&gt;
Krova Cube: no public IP + own kernel&lt;/code&gt;&lt;br&gt;
For me, that's an easy trade.&lt;/p&gt;

&lt;p&gt;Is this for every project?&lt;br&gt;
No.&lt;/p&gt;

&lt;p&gt;If you're building a complex multi-region microservices platform, use AWS or GCP. Krova is simpler, which means it does less.&lt;/p&gt;

&lt;p&gt;But for side projects, small SaaS apps, CI runners, databases, or anything where "give me an isolated server" is enough, this is the cleanest setup I've found.&lt;/p&gt;

&lt;p&gt;Try it yourself&lt;br&gt;
If you're curious, signup doesn't need a card and the first $5 top-up doubles to $10:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm i -g @krovacloud/cli&lt;br&gt;
krova signup&lt;br&gt;
krova cubes create test --cpu 1 --ram 2 --disk 20 --image ubuntu-24.04&lt;br&gt;
krova ssh test&lt;/code&gt;&lt;br&gt;
Once inside, run:&lt;/p&gt;

&lt;p&gt;Bash&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ss -tlnp&lt;/code&gt;&lt;br&gt;
Nothing is listening publicly. That's the whole point.&lt;/p&gt;

&lt;p&gt;The lesson&lt;br&gt;
We accept public IPs as the default because they're familiar. But most apps don't need their server to be addressable from the entire internet. They need a domain, TLS, and a way for the owner to admin things.&lt;/p&gt;

&lt;p&gt;Deleting the public IP didn't just reduce my attack surface. It removed the background anxiety that came with it.&lt;/p&gt;

&lt;p&gt;Have you run anything without a public IP? Did it feel wrong at first, or immediately better? Drop a comment — I'd love to hear.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>sideprojects</category>
      <category>microvm</category>
    </item>
  </channel>
</rss>
