blog
Postmortem: The Disguised Crypto Miner on My VPS - Blog - Naufaldi Rafif Satriya
A postmortem of a disguised XMRig miner running as systemguard on my VPS: what happened, how it was detected, how it was removed, and what I changed after...
# Postmortem: The Disguised Crypto Miner on My VPS
On June 23, 2026, I found a disguised crypto miner running on my VPS.
It was not obvious at first. The symptom looked like a normal resource problem: RAM usage was higher than expected, CPU was noisy, and disk usage had crossed the point where I wanted to clean Docker images and logs.
After checking the top processes, the real problem became clear:
systemguard
The name sounded harmless. The process was not.
It was an XMRig Monero miner disguised as a service called systemguard, installed as a root-owned systemd service and configured to keep running in the background.
This post documents what happened, what I found, what I removed, and what I changed afterward.
---
Executive Summary
The VPS had a root-level service named systemguard consuming around 2.3 GiB of RAM and a large amount of CPU. The binary identified itself as XMRig 6.24.0 and connected to a mining pool at 46.249.100.110:443.
The miner was removed from the active system, evidence was quarantined, public Docker and Caddy admin exposure was closed, and safe disk cleanup reduced root disk usage from 72% to 45%.
The exact initial entry point could not be proven because the service was created on January 28, 2026, while the available auth logs only covered a much later window. The strongest contributing risks found during remediation were public exposure of Docker-related metadata and Caddy admin ports, plus the fact that Docker access on the machine effectively granted root-equivalent host access.
---
Impact
The immediate impact was resource hijacking:
RAM before removal:
5.0 GiB used / 3.3 GiB available
RAM after removal:
2.7 GiB used / 5.6 GiB available
The miner used roughly 2.3 GiB of memory by itself and frequently consumed multiple CPU cores. It also contributed to noisy logs and made normal application performance harder to reason about.
Disk usage was not caused only by the miner, but the incident triggered a storage review:
Disk before cleanup:
/ = 35G used / 14G free / 72%
Disk after safe cleanup:
/ = 22G used / 27G free / 45%
No application database volumes were pruned. No aggressive Docker image cleanup was performed.
---
Detection
The investigation started with a basic RAM and disk check.
The first suspicious process was:
PID USER COMMAND RSS
2396865 root systemguard ~2.3 GiB
The service looked legitimate at a glance. That is part of why this kind of persistence works: the name was generic enough to blend into a Linux host.
Checking the systemd unit showed:
[Unit]
Description=systemguard runner
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/usr/libexec/systemguard
ExecStart=/usr/libexec/systemguard/run_systemguard.sh
Restart=always
RestartSec=5s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
The binary was not owned by any Ubuntu package:
/usr/libexec/systemguard/systemguard
/usr/libexec/systemguard/run_systemguard.sh
/etc/systemd/system/systemguard.service
Running strings against the binary exposed the real identity:
XMRIG_VERSION
Monero
cryptonight-monerov7
cryptonight-monerov8
miner
pool_wallet
systemguard 6.24.0
46.249.100.110:443
That was enough to classify it as a disguised XMRig miner.
---
Timeline
All times below are UTC.
2026-01-28 00:24:57
systemguard service, runner, and binary created on disk
2026-06-23 05:18
RAM and disk investigation started
2026-06-23 05:25
systemguard identified as disguised XMRig/Monero miner
2026-06-23 06:13
miner quarantined and removed from active system
2026-06-23 06:40
edge proxy recreated to close public 2375 and 2019 exposure
2026-06-23 06:46
safe cleanup completed; disk reduced from 72% to 45%
The most important forensic timestamp is January 28, 2026. That is when the malicious service and binary were created.
Unfortunately, the available auth logs did not go back that far. That means I can document what existed and when it appeared, but I cannot honestly claim a proven initial exploit path from the retained logs alone.
---
What the Miner Did
The service was designed for persistence:
- Start after network is online
- Run from
/usr/libexec/systemguard - Restart automatically every 5 seconds if it exits
- Run as root through systemd
- Hide behind a generic service name
The runner script also had an idle-awareness behavior: it tried to run when no interactive user activity was observed. That is a common stealth pattern because it makes the miner less obvious during active shell sessions.
The active miner connected to:
46.249.100.110:443
The logs showed mining work being assigned from that endpoint.
---
Remediation
The normal user did not have passwordless sudo, but it did have Docker access. On Linux, Docker access is effectively root-equivalent if a user can start containers with host mounts or host namespaces.
That was used to remove the miner safely and keep evidence:
Quarantine:
/root/quarantine-systemguard-20260623-061357
Removed from active paths:
/etc/systemd/system/systemguard.service
/etc/systemd/system/multi-user.target.wants/systemguard.service
/usr/libexec/systemguard
Verified afterward:
systemguard.service: Unit could not be found
No systemguard process running
No active connection to 46.249.100.110:443
This stopped the immediate resource abuse.
---
Hardening Changes
During investigation, two public exposures stood out:
0.0.0.0:2375 Docker socket proxy
0.0.0.0:2019 Caddy admin API
Both were wrong for this server.
The edge proxy stack was recreated from the intended Compose file so Caddy only publishes:
80/tcp
443/tcp
After hardening, public listeners were limited to:
22/tcp
80/tcp
443/tcp
Local checks against the previously exposed ports returned connection refused:
127.0.0.1:2375 connection refused
127.0.0.1:2019 connection refused
SSH was not changed during this pass to avoid lockout. Root login is still an item to close once access paths are confirmed.
---
Safe Cleanup
The cleanup intentionally avoided risky Docker operations.
Done:
npm cache clean
yarn cache clean
pnpm store prune
Go build cache cleanup
Homebrew cache cleanup
old temp/build cache cleanup
systemd journal vacuum and cap
Not done:
No docker system prune -a
No docker volume prune
No database volume cleanup
No removal of active Node versions
That kept the running apps safe while still reclaiming around 13.7 GB.
---
What Went Well
- The abnormal RAM usage made the miner visible.
- The process list quickly showed
systemguardas the largest suspicious consumer. - The fake service name did not survive basic package ownership and binary string checks.
- Evidence was quarantined instead of deleted immediately.
- Public Docker and Caddy admin ports were closed without changing SSH.
- Cleanup recovered a large amount of disk space without touching app data.
---
What Could Have Gone Better
- The original install date was months before discovery.
- Retained auth logs were too recent to prove the initial entry point.
- Docker access was root-equivalent, which made remediation possible but also highlighted a risk.
- Public admin-like ports existed longer than they should have.
- The server had no swap configured, leaving less cushion during memory pressure.
- One application,
chat-websocket, was still restart-looping separately and wasting CPU/logs.
---
Action Items
Immediate:
- Keep
2375and2019closed publicly. - Keep the quarantine copy for forensic reference.
- Monitor for any reappearance of
systemguard, XMRig strings, or new unknown systemd units. - Fix the
chat-websocketrestart loop.
Next:
- Confirm safe SSH access path.
- Set
PermitRootLogin no. - Add firewall rules and fail2ban.
- Add Docker log rotation.
- Add a small swap file.
- Review root and user
authorized_keys. - Review running containers and exposed labels periodically.
Longer term:
- Add a scheduled security audit.
- Keep a lightweight incident runbook.
- Track service creation changes.
- Treat Docker group membership as root-level access.
---
Lessons Learned
The most important lesson is simple:
If a process name sounds boring, that does not mean it is safe.
Attackers do not need a dramatic process name. systemguard looked like something that could belong on a server. It did not.
The second lesson is that Docker is not a soft permission boundary on a VPS. If a user can control Docker, that user can usually become root on the host. That helped during cleanup, but it is also exactly why Docker exposure deserves the same caution as SSH exposure.
Finally, cleanup should be staged. It would have been easy to run an aggressive Docker prune and hope for the best. Instead, the safer path was:
- Stop the active compromise
- Close obvious exposure
- Clean logs and caches
- Avoid touching app data
- Verify services after each step
That approach kept the apps running while removing the immediate threat.
---
Closing Note
This was not a glamorous incident. It was a practical reminder that a small VPS still needs real operational hygiene.
Monitor the boring things. Check process names. Check ports. Keep logs long enough to answer hard questions. And never expose admin surfaces just because they are "only for internal tools."