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
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
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.
Five questions on the mechanisms behind the seven mistakes, not just the list of them. Answers and reasoning at the end.











