I've been shipping fast for the past year — solo, no ops team, no security engineer. Last month I decided to spend one Saturday pretending to be an attacker targeting my own infrastructure. Not a formal pentest. Just the lazy-but-methodical recon any bored script kiddie does in their first ten minutes: certificate transparency logs, a port scan, some curl requests, and a lot of guessing subdomain names.
I expected to find one or two embarrassing things. I found six. Three of them were doors I didn't even remember leaving open.
This post is the full list, the exact commands I used, and what I changed afterward. None of this requires paid tooling. All of it took under four hours.
Why bother? Because attackers don't find your bugs, they find your leftovers
The things that got me were never the parts of the system I thought about carefully. My payment flow is hardened — I spent real time on it. What got me were the artifacts of past shipping: a staging environment from a launch six months ago, a debug endpoint added during one bad Tuesday, a subdomain pointing at a service I'd deleted.
Attack surface isn't what you build. It's what you forgot you built.
The recon loop I ran (steal it)
1. Ask Certificate Transparency what you own
Every TLS cert you've ever issued is publicly logged. This is the single highest-signal, zero-effort step:
curl -s "https://crt.sh/?q=%25.yourdomain.com&output=json" \
| jq -r '.[].name_value' | sort -u
This gave me 23 subdomains. I honestly believed I had about 8.
2. Find out which ones still resolve
for sub in $(cat subs.txt); do
ip=$(dig +short "$sub" | head -n1)
[ -n "$ip" ] && echo "$sub -> $ip"
done
Dead subdomains pointing at deprovisioned services are their own risk class (subdomain takeover), but the live ones are where the surprises hide.
3. Port-scan the IPs you just discovered
nmap -sV --top-ports 1000 <your-ip>
4. curl everything with and without trailing paths
curl -sI https://each-subdomain.yourdomain.com/
curl -s https://each-subdomain.yourdomain.com/robots.txt
robots.txt is a map of what you asked crawlers to avoid — which is a decent proxy for what you didn't want seen.
That's the whole loop. crt.sh → dig → nmap → curl. Free, thirty minutes, and it found everything below.
What I found
1. A staging subdomain, live, six months stale. staging.mydomain.com still pointed at an old VPS, still had HTTP basic auth — with the same password as one of my dev accounts I'd reused in a hurry. The code on it predated three security fixes. Gone within the hour. The lesson isn't "staging is bad," it's: staging environments need an expiry date written down the day you create them.
2. A .git directory served over HTTP. On that same stale box, /.git/config returned 200. That means anyone could have reconstructed the full repo history — including a .env committed in month one and later removed. Removed from git, but git history is forever unless you rewrite it. I'd done the "delete the file and commit" dance and told myself the key was gone. It wasn't. (It had been rotated by then, by luck not by process — more on that in the failure section.)
3. A debug endpoint on production. /api/debug/state dumped internal queue lengths, worker hostnames, and the app version. Harmless-looking, but it told an attacker exactly which CVEs to look up and exactly how my workers were named. It was added during one bad incident, gated behind nothing, and forgotten.
4. A Raspberry Pi running a dashboard, port-forwarded, no auth. This one hurt because it was my homelab Pi — I'd port-forwarded 8080 during setup to test remote access, then "temporarily" left it for eleven months. The dashboard exposed job names, last-run outputs, and (indirectly) which internal services exist. My own logs told a stranger more about my architecture than my public site did.
5. Verbose error pages leaking stack traces. One route threw a 500 with a full Python traceback, including file paths and a partial connection string. Fixed by setting a generic error handler I should have had on day one.
6. An SSH port open on the VPS with password auth enabled. Key-based auth was set up and in use — but PasswordAuthentication yes was still the default. Brute-force attempts in auth.log were constant; I'd just never looked. One-line fix, eleven months overdue.
The honest failure section
Here's the part that stings.
Finding #2 — the exposed .git — should have been caught by me in month one. I knew I'd committed a .env early on. I knew I'd "cleaned it up" with a follow-up commit. I even remember thinking, briefly, I should check whether that's actually safe. And then a customer email came in and I moved on.
The reason it didn't become a catastrophe is not that I was careful. The key in that .env had been rotated four months earlier because a different incident forced me to. If that rotation hadn't happened, my recon Saturday would have been a breach-postmortem Saturday instead.
That's the uncomfortable lesson: my security posture was being maintained by accidents and adrenaline, not by process. Things got fixed when they broke loudly. Nothing got fixed quietly — which is exactly the category all six findings fell into. They were all silent. None of them would ever have thrown an error, sent an alert, or shown up in my analytics.
So the real change I made wasn't any individual fix. It was putting the recon loop on a calendar: the first Saturday of every month, the crt.sh → dig → nmap → curl sequence, thirty minutes, written into the same cron-like schedule as my backups. Findings go in a list. The list gets reviewed before any new launch, because launches are when new leftovers get created.
Two smaller habits came out of it:
-
Every "temporary" thing gets a deletion date in its commit message or DNS record description.
TEMP-until-2026-04 staging boxshows up in crt.sh output like a confession. - Anything bound to a port gets asked one question: what does an unauthenticated stranger see? If the answer isn't "nothing" or "a login page," it doesn't ship.
What I'd tell you if you're solo and shipping fast
You are not going to build a perfect perimeter. You don't have time, and honestly you don't need one — you need to not be the easiest target in the attacker's tab list. Almost every finding above was a leftover, not a design flaw. Leftovers are cheap to find (crt.sh is free) and cheap to kill (most of mine were rm -rf or a config one-liner).
The gap between "I think my stuff is fine" and "I know what my stuff looks like from outside" is about thirty minutes of commands you can run today. Close that gap before someone else closes it for you.
The full checklist + scripts are in Ship Safe — The Launch-Day Security Kit — code LAUNCH90 at checkout makes it $1.50.
If you run the loop above and find something scary, that's the system working. The scary part is the month where you find nothing and you're not sure whether it's clean or whether you scanned the wrong domain.
Top comments (0)