I got hacked, my server started mining Monero this morning. | Unfinished Side Projects
A Next.js RCE vulnerability turned an analytics container into a Monero miner for 10 days—but non-root user config, zero volume mounts, and no privileged access meant the malware never escaped, proving container isolation actually works when you don't fuck it up.
Read Original Summary used for search
TLDR
• "I don't use Next.js" doesn't protect you when your dependencies (like Umami analytics) are built with Next.js—CVE-2025-66478 exploited RSC deserialization for RCE
• Container forensics: malware at 819% CPU looked like it was on the host filesystem (/tmp/.XIN-unix/javae), but Docker just shows container processes in ps aux—the path didn't actually exist on the host
• The difference between "annoying" and "catastrophic": running as non-root user with no volume mounts and no privileged access meant docker rm fixed everything—no persistence, no host access, no rootkits
• Sophisticated malware (disguised in /app/node_modules/next/dist/server/lib/, process names like javae, persistence attempts) still can't escape proper container boundaries
• Defense in depth saved him from his own laziness: should have had UFW enabled, fail2ban running, and actually updated Umami when the CVE dropped—but container config was the last line that held
In Detail
A developer woke up to a Hetzner abuse report: his server was scanning IPs in Thailand and had a load average of 15+ despite serving maybe 20 daily users. Investigation revealed a cryptocurrency miner running at 819% CPU—but the twist is that proper container configuration turned what could have been a complete infrastructure rebuild into deleting one container. The exploit chain: CVE-2025-66478 in Next.js's React Server Components had an unsafe deserialization flaw in the "Flight" protocol. An attacker sent a crafted HTTP request to his Umami analytics container (which he didn't realize was built with Next.js), achieved RCE, and installed XMRig miners that had been running since December 7th.
The critical forensic moment came when checking if /tmp/.XIN-unix/javae existed on the host—it didn't. Docker shows container processes in the host's ps aux output because they share the same kernel, but they're in separate mount namespaces. His Umami container ran as user nextjs (UID 1001), wasn't privileged, and had zero volume mounts. This meant the malware could mine crypto and scan networks but couldn't access the host filesystem, install cron jobs, create systemd services, or persist across restarts. A Reddit user with the same exploit got completely owned because his container ran as root—the malware installed persistence mechanisms and survived reboots.
The lessons are concrete: audit your entire dependency stack (third-party tools are full applications with complex stacks), write your own Dockerfiles to understand what user processes run as, avoid USER root and --privileged unless necessary, and don't mount volumes you don't need. The author also hardened his setup with UFW (blocking all inbound except SSH/HTTP/HTTPS), plans to move to key-based SSH auth with fail2ban, and set up proper monitoring so he doesn't learn about compromises from his hosting provider. The sophistication gap is real—this malware disguised itself in legitimate paths, used process names that blend in, and attempted persistence—but it was still bounded by basic container isolation. Good security practices beat sophisticated malware.