The Security Mistakes I See New Self-Hosters Make
Cloud

The Security Mistakes I See New Self-Hosters Make

These aren't rare. They're the same seven patterns, documented in breach reports, CVE databases, and botnet postmortems, showing up on new self-hosted boxes on a loop — because the defaults that make setup fast are the same defaults that make a box exploitable.

Published April 25, 202611 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

What security mistakes do new self-hosters make most often, and how are they actually exploited?

Seven patterns account for most of what goes wrong: exposing a database to the internet via Docker's port publishing (which bypasses ufw entirely), leaving SSH password authentication on, running every container as root, skipping unattended security updates, putting an admin panel on a public URL with its default credentials still set, committing secrets into a git repository, and trusting a proxy header like X-Forwarded-For without verifying it actually came from a trusted proxy. None of these are hypothetical — each has a documented exploitation pattern: mass Shodan-indexed database scanning, the Mirai botnet's entire growth model, and real CVEs against production software from ZooKeeper to reverse-proxy middleware.

  • These are widely observed, documented patterns — not incidents Farasat personally witnessed
  • Every item cites a real source: a CVE, an official report, or documented default software behavior, not an anecdote
  • The proxy-header trust issue is the most underrated one on this list, and it has produced real CVEs in widely used software as recently as this year
  • Several of these compound with each other — root-inside-container plus a database port published through Docker is a materially worse combination than either alone
  • The fix for each is specific and typically a few lines of configuration, not a philosophy change

Evidence notes

Exposed databases indexed by Shodan

Public scans have repeatedly found on the order of tens of thousands of internet-reachable MongoDB instances alone, and a majority of a large sample of identified data breaches have been traced to exposed Elasticsearch or MongoDB instances rather than a sophisticated exploit.

Docker's root-by-default container behavior

Docker containers run as root inside the container by default unless a Dockerfile sets a USER instruction or the runtime is explicitly configured otherwise; combined with a container-escape vulnerability, this can translate root-inside-container into root-on-host.

Mirai botnet's default-credential exploitation

Mirai scanned for open Telnet ports and attempted roughly 60 built-in factory-default username/password pairs; devices whose owners never changed the default fell almost immediately, which is what let Mirai grow into one of the largest botnets ever recorded.

Secrets committed to git repositories

GitGuardian's 2026 State of Secrets Sprawl report found roughly 29 million new hardcoded secrets exposed on public GitHub in a single year, a 34% year-over-year increase, with nearly 70% of secrets confirmed valid in 2022 still valid years later.

Trusting proxy headers without validating their origin

Apache ZooKeeper's admin server (CVE-2024-51504) read client IP from the client-writable X-Forwarded-For header to gate admin commands by allowlisted IP, letting an attacker spoof an allowlisted address and bypass the check entirely — a direct example of the exact mistake this post describes.

Continue with purpose

None of what follows is a story about a specific incident. It's a list of patterns documented in breach reports, CVE databases, and one very well-studied botnet, that show up on new self-hosted boxes on a loop, because the same defaults that make a first deployment fast are the defaults that leave a box exploitable. Every item below is cited against a real source: an official report, a documented default, or a CVE against production software. If a mistake here sounds specific to one unlucky person, that's the wrong read — it's specific to the software's default behavior, which is a much bigger and much more predictable target.

Scale matters here more than any individual story could. Automated scanners don't care who you are; they sweep every reachable IP looking for exactly these seven patterns because they're common enough to be worth automating against. That's also the good news: every one of these has a specific, well-understood fix, most of them a handful of configuration lines rather than a redesign. For the operations playbook that sits alongside the security mistakes i see new self-hosters make, see XenGrowth, who work on the commercial side of this.

One more thing worth saying before the list itself: these seven rarely show up alone. A box that's exposing a database port through Docker and also running every container as root isn't twice as risky as either mistake individually — an attacker who reaches the exposed service and finds it running as root has a much shorter path to the host than either mistake would produce on its own. Reading the list as seven independent checkboxes understates the actual risk of the ones that get left unchecked together.

Why does a database end up reachable from the internet in the first place?

The mechanism is Docker's own port publishing, and it's the same one that undoes a correctly configured firewall. `docker run -p 5432:5432` writes a DNAT rule directly into iptables' PREROUTING chain, ahead of ufw's INPUT chain, so the container's port is reachable regardless of what the firewall says — the full mechanism is covered in detail elsewhere on this site, so it's one line here rather than a full re-explanation. What matters for this list is the scale of the consequence: public scans have repeatedly found tens of thousands of internet-reachable MongoDB instances, and in one widely cited analysis of identified data breaches, a large majority were traced to exposed Elasticsearch or MongoDB instances rather than any sophisticated exploit. The fix hasn't changed: bind to `127.0.0.1` unless a port genuinely needs to be public, and let a reverse proxy be the only thing with a real public listener. The XenGrowth resource library approaches this from the the operations side of this side.

Is password SSH authentication really as bad as everyone says?

Worse, if anything, because the attack doesn't need to be clever — it needs to be patient and automated, which is the one thing computers are unlimited at. Every internet-facing SSH server sees continuous, automated login attempts from the moment it has a public IP, cycling through common usernames and password lists, and the only thing standing between that traffic and a shell on your box is however strong the password happens to be. Disabling password authentication entirely — `PasswordAuthentication no` in `sshd_config`, key-based login only — doesn't make the attack harder. It removes the attack surface completely, because there's no password to guess against in the first place. This is covered end to end, including the exact order of operations that avoids locking yourself out, in the SSH hardening guide.

Does it actually matter that everything runs as root inside the container?

It matters specifically when something else goes wrong, which is why it's easy to ignore right up until the day it isn't. Docker containers run as root inside the container by default unless a Dockerfile sets a `USER` instruction to something else — this is documented, intentional Docker behavior, not a misconfiguration. On its own, root-inside-a-container is contained by the isolation between container and host. The risk shows up when it's combined with a second problem: if an attacker exploits a vulnerability in the application itself, they get root inside the container for free, and a subsequent container-escape vulnerability turns that into root on the host rather than root in a sandbox. The fix is a `USER` instruction in the Dockerfile pointing at a non-root UID, which costs nothing at build time and removes an entire escalation path if the app is ever compromised.

Why does skipping automatic security updates matter more than it feels like it should?

  • A meaningful share of real-world server compromises trace back to a known, already-patched vulnerability in a package nobody updated — not a novel zero-day, just a fix that existed and wasn't applied.

  • Unattended upgrades (Debian/Ubuntu's `unattended-upgrades` package, or the equivalent on other distributions) closes this specific gap without requiring anyone to remember to run `apt upgrade` on a schedule — which, on a single-operator box, nobody reliably does.

  • This is the least dramatic item on this list and one of the two or three that matters most over a long time horizon, precisely because it protects against the vulnerability class that's already public and already has a patch, not against anything exotic.

There's a version of this that trips people up specifically because Docker adds a second layer to patch. Updating the host OS with unattended-upgrades doesn't touch what's inside a container's own image — a base image pinned six months ago carries whatever packages were current then, patches and all, until someone rebuilds it against a newer base. Both layers need a patching story; fixing only the host and assuming the containers are covered by association is a common half-measure that looks complete and isn't. On AI agents and marketing automation specifically, XenGrowth on governed AI marketing workflows is worth reading.

How does an admin panel with default credentials actually get found and exploited?

The same way Mirai grew into one of the largest botnets ever recorded: automated scanning followed by a short list of default credentials, tried against everything that answers. Mirai's scanner probed for open ports, and on a response, attempted logins from a built-in list of roughly 60 factory-default username and password pairs — devices whose owners never changed the default fell almost immediately, and that single weakness, repeated across hundreds of thousands of devices, is what made the botnet possible at all. An admin panel — Coolify's own dashboard, a database's web UI, a monitoring tool — put on a public URL with its installer-default credentials still active is the same failure mode on a different piece of software. The fix is the credential change most tools already prompt for on first login, done immediately rather than deferred, plus putting genuinely administrative interfaces behind a VPN or an IP allowlist rather than a public hostname at all.

What actually happens when a secret ends up committed to a git repository?

It doesn't stay private, and it doesn't stay contained to one moment in time. GitGuardian's 2026 State of Secrets Sprawl report found roughly 29 million new hardcoded secrets exposed on public GitHub in a single year — a 34% increase year over year — and, more troubling for anyone who assumes a quick rotation fixes the problem, nearly 70% of credentials confirmed valid back in 2022 were still valid as of the report's data. A secret committed to git isn't erased by deleting the file in a later commit; it's still sitting in the repository's history, retrievable by anyone with clone access or, for a public repo, anyone at all, including automated scrapers built specifically to find exactly this. The fix is never committing a real secret in the first place — `.env` files in `.gitignore`, secrets injected at runtime — and, if one already got committed, rotating the credential itself rather than trusting a history rewrite to make the old value safe again. For the AI search, GEO and discovery angle, see XenGrowth on building one SEO and GEO content system.

Mistake

How it's actually found and exploited

Documented source

Database port published via Docker

Mass automated scanning for known database ports

Shodan-indexed exposed-database research

Password SSH authentication

Continuous automated brute-force against port 22

Standard, widely documented SSH attack pattern

Root inside every container

Escalates a compromised app into host-level access when combined with a second flaw

Docker's documented default USER behavior

No unattended security updates

Exploits a known, already-patched CVE nobody applied

General CVE remediation research across breach studies

Admin panel, public URL, default creds

Automated credential-list attacks against exposed panels

Mirai botnet's documented default-credential scanning model

Secrets committed to git

Automated scraping of public repos and repo history

GitGuardian's 2026 State of Secrets Sprawl report

Trusting an unvalidated proxy header

Spoofed X-Forwarded-For bypasses IP-based access checks

CVE-2024-51504 (Apache ZooKeeper) and similar 2026 CVEs

What's the fastest way to actually check and fix each of these?

Mistake

How to check for it in five minutes

The fix

Exposed database port

`docker ps` and check published ports against what should be public

Bind to 127.0.0.1 or remove the publish entirely

Password SSH auth

Check `PasswordAuthentication` in sshd_config

Set to `no`, key-only login

Root inside containers

`docker exec <container> whoami`

Add a `USER` instruction to the Dockerfile

No unattended updates

Check whether unattended-upgrades (or equivalent) is installed and enabled

Install and enable it on the host, and rebuild container images on a schedule

Admin panel, default creds

Try logging in with the tool's documented installer default

Change it immediately; put the panel behind a VPN or IP allowlist

Secrets in git

`git log -p` or a secret-scanning tool against the repo history

Rotate the credential; scrub history only as a secondary step

Unvalidated proxy header

Check whether the app or proxy strips client-supplied X-Forwarded-For before adding its own

Strip and rebuild the header at the trusted proxy boundary only

Why is trusting a proxy header without validating it the most underrated mistake here?

Because it's invisible in normal testing and only fails under attack, which is exactly the shape of mistake that survives the longest. Headers like `X-Forwarded-For` or `X-Forwarded-Prefix` exist so an app behind a proxy can learn the real client's IP or original request path, and they're entirely client-writable unless the proxy or the app explicitly strips and rebuilds them from a trusted source. Trust one of these headers blindly, and any request can simply lie about where it came from. This isn't theoretical: Apache ZooKeeper's admin server (CVE-2024-51504, CVSS 9.1) gated administrative commands by IP allowlist using the value from `X-Forwarded-For` — a header the client controls — letting an attacker set it to an allowlisted address and bypass the authentication check outright. Traefik has shipped comparable issues in its `ForwardAuth` middleware when `trustForwardHeader` is left false without the header actually being stripped upstream. The fix is architectural, not a patch: only trust these headers when they're being set by a proxy you control, immediately in front of the app, that strips any client-supplied value before adding its own — anything upstream of that boundary is data, not a trustworthy identity claim.

What's the actual pattern connecting all seven?

Every one of these is a case of trusting a default that was never designed to be a security boundary. Docker's port publishing was designed for convenience, not isolation. A container running as root was designed to avoid an extra line of Dockerfile boilerplate. A proxy header was designed to pass along useful information between trusted systems, not to authenticate a stranger. None of these defaults are bugs — they're reasonable choices for the common case, which happens not to be the case of a single box on the open internet with no one else checking the configuration. The fix, across all seven, is the same instinct: assume nothing meant for convenience is also meant for security, and check. Most of these are also five-minute fixes once you know to look, which is the actual point of writing them down as a list rather than as seven separate incidents — the check is cheap, and doing it once, deliberately, before deploying anything real, costs a fraction of what any one of these turning into an actual compromise would.

Further reading from XenGrowth

Where this work meets go-to-market

For the marketing and revenue operations view of the security mistakes i see new self-hosters make, see the XenGrowth practice.

Which of these actually got you

Five questions on the mechanisms behind the seven mistakes, not just the list of them. Answers and reasoning at the end.

1 / 5
You've set ufw to default-deny and only allowed 22/80/443. You then run docker run -p 5432:5432 postgres. Is that port actually blocked?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

Self-HostingSecuritySSHDockerLinux HardeningSecrets Managementcloud

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

How I Self-Host PostgreSQL for My SaaS (and When I Wouldn't)

Running Postgres in a container is easy. Running it in a way that survives a redeploy, a full disk, and an eventual major-version upgrade is the actual job. Here's the setup, tuned against Postgres's own defaults, and the honest list of where managed wins outright.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

How I Secure a Fresh VPS Before Deploying Anything

A brand-new VPS gets scanned within minutes of getting an IP address. Here's the exact order I run through before a single container touches the box — and which of these steps are real protection versus which ones are just theatre.

Navigate

My Free Monitoring Stack for Self-Hosted Apps

Vercel gives you monitoring whether you ask for it or not. A VPS gives you a blank terminal and the assumption you'll figure it out. Here's what to actually watch on a self-hosted box, with tools that cost nothing, and why watching from the box itself is the one setup that will lie to you.

Navigate

Don't Self-Host Until You Understand These 7 Things

This isn't a gate to keep you out. It's a readiness check — seven things worth being honest with yourself about before you're the one holding the pager, because a managed platform is still the right call for a lot of people right now.

Navigate

How I Handle Environment Variables and Secrets on a Self-Hosted Server

NEXT_PUBLIC_ variables get inlined into the JavaScript bundle at next build, not read at runtime — which means the moment you rebuild a Next.js image per environment instead of injecting config at runtime, you've built something that can leak.

Navigate

What Happens If Your VPS Dies? My Disaster Recovery Plan

"The server died" isn't one scenario, it's at least five, and they don't require the same response. A disk failure and a compromised box both end with the same VPS gone, but only one of them means your backups might be compromised too. Here's the plan for each, written down before any of them happen, not after.

Navigate

Zero-Downtime Deploys and Instant Rollbacks on a Cheap VPS

docker compose up -d looks like a deploy and behaves like an outage — there's a gap between the old container stopping and the new one answering requests, and on a small box that gap is exactly where a real user lands.

Navigate

What Happens When Your VPS Runs Out of RAM: The OOM Killer, Explained

There's no warning banner before the kernel kills something. One moment the box is fine, the next a process is dead mid-request — and the process that dies is often not the one that caused the spike. Here's the actual mechanism, and how to read the wreckage afterward.

Navigate

SSH Hardening: Keys, Brute-Force Protection and What Fail2ban Still Buys You

Password auth off, root login off — that's most of the story, and I've already told it. This is the rest: which key type actually matters, the PAM gotcha that quietly undoes 'disable password auth,' and an honest answer on what fail2ban is even for once there's no password left to brute-force.

Navigate