On a Friday afternoon my VPS ground to a halt. OpenClaw — the AI agent that runs my infrastructure — stopped responding. When I finally got a terminal, top showed a process chewing through 381% CPU and 2.4 GB of RAM. The process was /tmp/mysql, running as user ollama.
My first instinct was right, and also completely wrong.
The wrong fix that looked right
I assumed the obvious: a brute-forced SSH login. The signs were all there — PermitRootLogin yes, password authentication enabled, and 30+ failed SSH attempts every hour. I killed the miner, rotated the root password, disabled password auth, and installed fail2ban with 24-hour bans plus a recidive jail for repeat offenders.
It worked. For 18 hours. Then the miner came back.
I killed it again. It came back again — four times total. Each time it respawned under the ollama user, so each time I assumed the Ollama service itself was compromised. I checked the systemd unit, /usr/share/ollama/, the ollama user’s crontab, its shell init files — everything clean. I restarted Ollama. The miner kept coming back.
The breakthrough: the process tree doesn’t lie
What finally cracked it was process-tree analysis. Next to the miner I found two init processes that didn’t belong to any systemd unit. When I checked their root filesystem:
ls /proc/680266/root/tmp/
I found the stash: mysql, kinsing, init, libsystem.so.
Those weren’t host processes. They were running inside a Docker container — specifically simstudio-db-1, the PostgreSQL container for a self-hosted app called SimStudio.
The reason the miner kept showing up as the ollama user is the part that fooled me for three days: the container’s process UID collides with the host’s ollama account. Docker runs container processes as whatever UID the image declares, and when that UID matches a host user, ps reports the host name. Process ownership is meaningless when containers share a UID namespace with the host — the process said “ollama”, but it was a database container the whole time.
The actual root cause
The miner was the Kinsing malware — a cryptojacking worm that scans the internet for exposed databases and Docker APIs. It got in through a port I’d forgotten about: PostgreSQL 5432, exposed through Docker’s proxy with no password on the postgres user.
The image was pgvector/pgvector:pg17. Kinsing found the open port, dropped four files into the container’s /tmp:
/tmp/mysql— the XMRig miner (named to look like a database binary)/tmp/kinsing— the loader/backdoor/tmp/init— a fake init process for persistence/tmp/libsystem.so— a malicious shared library
It even left a ransom database named readme_to_recover.
The cleanup
docker exec simstudio-db-1 rm -f /tmp/mysql /tmp/kinsing /tmp/init /tmp/libsystem.so
docker restart simstudio-db-1
ps aux | grep -E "mysql|kinsing" | grep -v grep # empty = clean
Resolution took under three minutes once I found the real source. Finding it took three days.
What I changed so it can’t happen again
ufwnow denies 5432 outright. Postgres is only ever reached over the internal Docker network.- A strong password on the
postgresuser. - A daily container security scan that greps every running container for the Kinsing file names and removes them (cron, 2 AM).
- fail2ban + rkhunter + unattended upgrades, and SSH locked to key-only.
The three lessons I’ll actually remember
- Process ownership is meaningless with containers. A process running as “ollama” was a Postgres container. UID collisions make
pslie. - Docker isolation is not security. A container on the default bridge with an exposed port is a server with an exposed port.
- A monitoring alert is worth more than a clean
top. Slack + email alerts caught the respawn twice — the miner never ran longer than six hours before I knew.
Guides & explainers
Filed under: Cybersecurity