HardStacked
Share:

Attack surface · /proc

That miner is not your problem — someone ran code on your server

Published · HardStacked · 9 min

Your server is hot and one process is eating the whole CPU. top names it kdevtmpfsi, or xmrig, or kinsing. You kill it, the CPU drops, and a minute later it is back. People call it a virus; it is a cryptocurrency miner, and the two names are the same fight.

The process you can see is not the problem. Someone got code to run on this machine, and the miner is just what they chose to run. Killing it and deleting the file lowers the CPU without repairing anything: the door it came through is still open, and whoever opened it may have left more behind. This page finds the real binary, sweeps for others, explains why /tmp is always involved, removes the persistence that keeps bringing it back, and then closes the door so the same attack cannot start.

Everything shown here was reproduced in a throwaway Debian 13 (trixie) aarch64 virtual machine, with no network, on 2026-08-17 — not on an Ubuntu x86 VPS. The mechanism is the same everywhere, because it rests on mount options and /proc, which behave identically across Linux distributions; the commands below are unchanged on Ubuntu or Debian. There is no malware in these outputs. /tmp/kdevtmpfsi is a copy of /bin/sleep, renamed — created with cp /bin/sleep /tmp/kdevtmpfsi. The payload is harmless on purpose; only the mechanism is real. This page covers Linux only. It does not cover Windows.

01. The two line verdict

The process you are watching — kdevtmpfsi, xmrig, whatever it calls itself — is the fever, not the disease. Someone obtained code execution on your server and is spending your CPU to mine cryptocurrency. The cron entry you will find in section 05 runs as root, which tells you the reach they had.

The maxed-out CPU is what you can see. Arbitrary code execution as root is what actually happened. Cleaning the first without answering the second leaves you exactly as exposed as you were an hour ago.

02. Identify the binary for real

These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.

ps prints a name, and that name is chosen by whoever dropped the file. Nothing forces it to be true. kdevtmpfsi is picked to sit next to real kernel threads like kdevtmpfs and disappear into the list.

ps
861 kdevtmpfsi /tmp/kdevtmpfsi 900

Even here the args column gives it away: a genuine kernel thread does not run out of /tmp. But do not trust the display at all. Ask the kernel instead. For any suspicious PID, /proc/<pid>/exe is a symbolic link the kernel maintains to the file that is actually executing:

bash
$ ls -l /proc/861/exe

output

lrwxrwxrwx 1 root root 0 /proc/861/exe -> /tmp/kdevtmpfsi

The kernel sets that link at exec time; the attacker does not control it. If it points into /tmp, /var/tmp, or /dev/shm, you have found your binary. If the file was deleted after launch — a common trick — the link target ends in (deleted), which is itself the answer: a live process whose executable no longer exists on disk is not a normal service.

03. The full sweep

You may not know the PID, or there may be more than one. One line lists every process whose executable lives in a world-writable directory:

bash
$ ls -l /proc/*/exe 2>/dev/null | grep -E '/tmp/|/var/tmp/|/dev/shm/'

output

lrwxrwxrwx 1 root root 0 /proc/861/exe -> /tmp/kdevtmpfsi

Each line is one PID — the number inside /proc/PID/exe — running a binary out of a directory anyone can write to. That is your list of processes to examine. On a healthy server this command prints nothing at all, which makes it a fast first check even when you only suspect something is wrong.

04. Why it is always /tmp

The miner keeps landing in /tmp, /var/tmp, or /dev/shm for one reason: those are the directories any user can write to. An exploit with only limited access can still drop a file there. All that is left is to run it — and by default, nothing stops that. Here is a stock Debian 13 machine:

bash
$ mount | grep -E ' /tmp | /var/tmp | /dev/shm '

output

tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,inode64) tmpfs on /tmp type tmpfs (rw,nosuid,nodev,...)

nosuid and nodev are present, but noexec is not. So a file written to /tmp can be executed, and on this default machine the drop-and-run succeeds — the shell returns the background PID, [1] 861, and the miner is off. World-writable and executable is the whole of what it needs, and nothing more.

05. The persistence, and the kill that does not hold

You kill the process — pkill -x kdevtmpfsi — and the CPU drops. That is the trap. The miner is almost always restarted by a scheduled task. Look in /etc/cron.d/, the user crontabs, and the systemd timers:

/etc/cron.d/x
* * * * * root /tmp/kdevtmpfsi 900

Every minute, cron runs the binary again. You kill the process, cron brings it back inside the minute, and the CPU graph makes it look like the kill failed. It did not; the persistence outlived it.

Worse, the falling CPU proves nothing on its own. After the pkill above, the process is gone — and the file and the cron entry are still sitting on disk:

ls -l
-rw-r--r-- 1 root root 35 /etc/cron.d/x -rwxr-xr-x 1 root root 68048 /tmp/kdevtmpfsi

So the order matters. Remove the scheduled task first, then kill the process, then delete the binary — in that sequence, or cron re-launches it between your steps. While you are there, check the places a foothold hides: ~/.ssh/authorized_keys for keys you did not add, every user’s crontab, and systemd units you do not recognise.

One note on how it arrived, because it changes what you check. When this shows up inside or alongside Docker, it is usually a service that was reachable from the internet without authentication — an exposed Docker API, a Redis with no password, a database open to the world. The kinsing family scans for exactly those and drops kdevtmpfsi through them. If this host runs Docker, Redis, or PostgreSQL, treat their exposure as the likely entry point, not an afterthought.

06. What stops it from starting

Cleaning puts you back where you were: vulnerable. Hardening changes the outcome. Mount /tmp, /var/tmp, and /dev/shm with noexec,nosuid,nodev, so that a file dropped there cannot be run at all. In /etc/fstab:

/etc/fstab
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0 /var/tmp /var/tmp none bind,noexec,nosuid,nodev 0 0 tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0

Note the middle line. Do not mount /var/tmp as tmpfs. Unlike /tmp, /var/tmp is meant to survive a reboot, and making it a RAM filesystem silently discards whatever is in it — we tested it: a file placed there before the change was gone the moment the mount was applied, and still gone after the reboot. Binding the directory to itself keeps the real filesystem, and only adds the options.

Now the part almost every guide gets wrong. Running mount -a after editing /etc/fstab reports success and does not protect you:

mount -a
mount: (hint) your fstab has been modified, but systemd still uses the old version; use "systemctl daemon-reload" to reload. mount -a exit=0

(Our test machine runs a French locale, so it printed that hint in French; the wording above is the same message as an English system shows it.)

Exit code 0, no error — and /tmp is still mounted without noexec. The reason is that /tmp is already mounted by systemd, so mount -a leaves it alone; /var/tmp, which was not mounted, does pick up the options immediately. You end up half protected, with a success message.

So never trust the exit code. Reboot — or run systemctl daemon-reload and remount — then verify by trying the thing you are trying to prevent:

bash
$ cp /bin/true /tmp/zz && /tmp/zz && echo "NOT protected" || echo "protected"

On a correctly mounted machine the shell refuses to run it. On our test machine, whose locale is French, that refusal reads Permission non accordée; on an English system you will see Permission denied. Same file, same directory, and the attack does not start. After the reboot we also checked systemctl --failed: nothing broken.

There is a cost, and you should hear it before you apply this. noexec on /dev/shm breaks workloads that legitimately execute code from there. JVM containers — Keycloak is the common one — use /dev/shm for executable code and will refuse to start. Test on a staging host before you push this to production.

To confirm the options are actually in place, a configuration scanner reads your mount settings and tells you which ones are missing. audit-pi (free, open source) does this. Be clear about what it is: a read-only configuration scanner. It closes the door before anyone comes through — it does not detect an intrusion in progress, and it is not an antivirus. On a machine that is already compromised, what ps and cron show it can itself be manipulated by the attacker, so the honest use of such a tool is on a clean system, to keep it clean.

07. What you actually do

Here is the conclusion most pages avoid. The cron entry ran as root. If someone could execute code as root, you cannot prove what else they left — a second dormant implant, an added SSH key, a modified system binary. Deleting the files you can see does not restore trust in the machine; it only restores the CPU graph.

The answer that holds is to rebuild. Reinstall from a clean image, restore your data (not your binaries), and bring the server back up with the doors already shut:

None of this is personal. On a fresh cloud box left exposed, we counted 132 distinct accounts tried against it in about 41 hours — admin, postgres, ansible, ubnt, telecomadmin. It is automated, constant, and it sweeps the whole internet; your server was found because it was reachable, not because it was chosen.

The same gap — between the name a tool prints and the file the kernel is actually running — runs through a firewall that reports green while blocking nothing.

Share: