Forty minutes of list-making found a VPS still running after two years, a dangling CNAME ripe for subdomain takeover, and a valid API key on a machine I'd mentally buried. The audit scripts, the decommission runbook, and the sleep-instead-of-rot policy.
Last Saturday I listed every project I'd ever deployed — side projects, hackathon demos, "quick tests." Forty minutes to write; reading it back took my breath away. Because dead projects don't die. They wait.
What the graveyard was hiding
- Three projects I'd euthanized were still running — a VPS humming for two years with dependencies from the floats era. Forgotten app = dependency museum = vulnerability list.
- A cancelled domain still CNAME'd to a service I no longer owned. The record dangled like an unplugged phone line; anyone claiming the orphaned resource receives traffic meant for me. Subdomain takeover, living exactly where nobody looks.
- A .env on the old box with a key that was still valid. Two years of "I'll clean that up someday," one working key to my current life.
None of these were hacks. All of them were mine. The scariest attack surface I own isn't production — it's the graveyard.
The audit, automated where possible
Schools teach shipping. Nobody teaches burying. So here's the part of my audit that runs as a script:
#!/bin/bash
# graveyard-audit.sh — dangling DNS + estate inventory
set -euo pipefail
# 1. dangling CNAMEs: target no longer resolves = claimable by strangers
while read -r d; do
t=$(dig +short "$d" CNAME | head -1)
[ -n "$t" ] && [ -z "$(dig +short "$t" A)" ] \
&& echo "DANGLING: $d -> $t"
done < domains.txt
# 2. what's actually running, by state
krova list --json | jq -r '.[] | "\(.name)\t\(.state)"'
# 3. secret archaeology in old project dirs — then revoke every hit
grep -rEho '(kro|ghp|aws|sk)_[A-Za-z0-9_-]{8,}' old-projects/ | sort -u
The human part stays human: every CI variable, every service account, every "temporary" panel. The script catches the dangling and the running; you catch the remembered.
The decommission runbook (order matters)
A project isn't dead when the app is down. It's dead when nothing anywhere points at it or opens it:
# memory first, then the box
krova snapshots create oldproj --name burial
krova cubes delete oldproj # delete dialog preserves a backup by default
# THEN, in the same sitting — this is the part everyone skips:
# - delete the DNS records (A, CNAME, _acme-challenge, TXT)
# - revoke the keys it held
# - remove CI/CD variables and webhooks
# - cancel service accounts / OAuth apps
Delete the box first and the DNS later, and "later" is where the graveyard grows. One sitting, full kill.
Sleep instead of rot
For the "someday maybe" tier, the respectful middle state: hibernation. On Krova Cloud a powered-off Cube bills only its disk — no running process, no open port, no public IP to begin with:
krova cubes power-off maybe-someday # pennies/month, zero exposure
krova cubes wake maybe-someday # revive in seconds when nostalgia strikes
Sleep is what keeps the graveyard from refilling: a project is either alive, asleep, or buried. Rotting is no longer a state I allow.
The honest part
- A list isn't a fix. The audit produced a document; the boring weekend of deleting records and revoking keys produced the safety.
- You can't audit what you never wrote down. New projects get a row the day they're born — domain, keys, and their planned death: who revokes what.
- The graveyard isn't the castle. The living stack still needs real walls: no public IP by default, own kernel per Cube, scoped short-lived secrets. The audit shrinks the estate attackers can explore; it doesn't replace defending what's alive.
Dead projects don't die. They wait.
Every reader has a graveyard. Some of yours are glowing right now — a box you forgot, a record you orphaned, a key that outlived its project. The audit is forty minutes; the fixes are one boring weekend.
Top comments (0)