ZELFROSTER / REPORTS
← Back to all reports
Security incident / Investigation notes

A Monero miner in my PostgreSQL container

I thought Caddy was using all my CPU cores and around 2.3 GB of RAM. On checking, we found a Monero miner inside my PostgreSQL container. Let’s see what was going on.

By Zelfroster••Compromise confirmed•Sensitive details removed
Observed CPU~389%
Container RAM2.51 GiB
Mining softwareXMRig
Currency / algorithmXMR / RandomX
In this report

What happened?

I noticed what looked like Caddy using all four CPU cores and around 2.3 GB of RAM. That seemed wrong for a reverse proxy, so we took a closer look.

Caddy itself was fine, using just 32 MiB of RAM. The postgres user inside Docker and caddy on the host shared UID 999, so the host’s process list showed the container processes under the wrong-looking name.

The real problem was inside the PostgreSQL container: a Monero miner and scripts that kept restarting it. The container was using around 380 to 389% CPU and 2.51 GiB of RAM.

What did we find?

  • The database container used postgis/postgis:15-3.4.
  • Docker published port 5432 on all IPv4 and IPv6 host interfaces. We did not independently test whether it was reachable from the internet through any upstream firewall.
  • The container configuration had a very weak three-character password for postgres. We did not query the database to check the active password or the account's privileges.
  • A process named postgres: background writer was using around 379% CPU and 2.3 GiB of RAM. Its parent was the container's main database process. Permission restrictions stopped us from checking that specific process's executable, so the name alone does not tell us what it really was.
  • Inside the container were /tmp/.x/xmrig, /tmp/.x/config.json, /tmp/.x/watch.pl, and mining logs.
  • The logs showed XMRig 6.26.0, RandomX, four CPU threads, and a 2,336 MB allocation.
  • The configuration listed Monero pools and a payout wallet. We did not establish that the wallet belonged to me.
  • Several processes were running perl /tmp/.x/watch.pl.
  • Reading that script showed a loop that ran every 30 seconds, restarted the miner, handled stopped processes, downloaded a replacement binary, and sent reports to a remote endpoint.
  • There were other suspicious files and processes too: /tmp/ccrl, /tmp/fssu, /tmp/.perf.c/, /tmp/.xdiag/, a remote-access CLI, and ptardiff. We did not fully analyze what each one did.
  • PostgreSQL logs repeatedly showed server processes killed with signal 9, followed by recovery. We did not establish what was killing them.

When did this happen? (UTC)

TimeObservation
11 October, 17:17:57Docker metadata says the container started.
13 October, around 04:01Birth timestamps of suspicious files and directories.
13 October, around 04:24 to 04:25/tmp/ccrl was created and modified.
13 October, 04:30:08Birth timestamp of /tmp/.x.
13 October, 04:30:19XMRig logged its RandomX dataset allocation.
13 October, around 08:47 to 08:53We observed high resource use, mining files, watchdogs, and repeated database recovery.

These timestamps help put things in order, but they do not prove exactly when someone first got in. Filesystem timestamps and logs can be incomplete or changed.

What was the miner doing?

Now, you might be wondering what this miner was actually doing. XMRig was doing proof-of-work calculations for a cryptocurrency mining pool. The configuration named Monero, and the logs showed RandomX, the algorithm used here. The configured rewards went to a wallet we had not established as mine.

RandomX uses quite a bit of CPU and memory. Here, the miner allocated a 2,336 MB dataset and used four CPU threads. So this was not just a brief spike from processing requests. It needed that memory to keep mining.

Also, simply killing the miner would not clean things up. The watchdog script was designed to restart it and could download the binary again if it was missing.

Files and addresses worth checking

IndicatorObserved use
130.94.16.173:8443Configured mining endpoint.
130.94.16.173:80Watchdog payload download and reporting endpoint.
/look/xmrig.xPayload download path embedded in watchdog.
/tj/xmr.phpReporting path embedded in watchdog.
/tmp/.x/watch.plMiner watchdog.
/tmp/.x/xmrigMining binary.
/tmp/.x/config.jsonMining configuration.
/tmp/.perf.c/, /tmp/.xdiag/Suspicious payload directories.

The configuration also listed pool.supportxmr.com:3333 and gulf.moneroocean.stream:10001. These are mining services. Seeing them here does not mean their operators were involved in the break-in, and these addresses do not tell us who the attacker was.

How could it have got in?

The database port was published on all interfaces, and the configured password was very weak. That is a possible way in. With PostgreSQL superuser access, features like COPY ... PROGRAM can allow someone to run commands in the operating system.

But we did not find evidence of the original source IP, login attempt, SQL command, or the exact way the software was installed. Scanning, guessing the password, and using COPY ... PROGRAM are possible explanations, not things we proved. We do not know who started it.

One more thing: publishing a port on every interface does not by itself prove it was open to the internet. We did not inspect the upstream network controls during this investigation.

What was affected?

  • The container was taking almost all the CPU capacity and around 2.5 GiB of RAM.
  • Database processes kept dying and recovering. We did not check kernel or cgroup evidence to prove that this was caused by running out of memory.
  • The unauthorized software showed that the container was compromised. Its database contents and accessible credentials should be treated as possibly read or changed.
  • We did not establish whether the host was also compromised, whether data was copied out, or whether the attacker reached other systems.
  • Docker reported around 1 GB of images, writable-layer files, and volume storage combined. Its 20+ GB block-I/O counter was cumulative writes, not 20 GB of files sitting on disk. We did not establish the cause of those writes.

What should you do next?

  1. Contain the affected container and block external database access. Keep relevant logs and configuration somewhere secure, and collect more evidence before removing it where that is practical and safe.
  2. Preserve a snapshot of the writable layer and database volume. Do not delete the volume or assume that it is clean just because the container was replaced.
  3. Rebuild using a trusted, updated image. Restore a checked backup from before the incident, or carefully validate the data you keep. Review roles, extensions, triggers, and executable files before restoring them.
  4. Change the active database password and any secrets that were accessible. Changing POSTGRES_PASSWORD in the container configuration alone does not change the password in an already initialized database volume.
  5. If only local access is needed, bind port 5432 to 127.0.0.1 or use a private Docker network. Check whether it is actually reachable from outside. Docker-published ports do not always behave as expected with host firewall rules.
  6. Review database roles, extensions, scheduled jobs, persistence on the host, Docker access, mounts, and privileges. Check kernel and cgroup logs to understand why processes were killed with signal 9.
  7. Use database roles with only the permissions they need, set resource limits, keep software updated, and monitor it. Limits can reduce the damage, but they do not remove the compromise.

What is still unknown?

This report comes from process listings, Docker stats and metadata, files inside the container, and selected logs. We did not take a full forensic image, collect payload hashes, audit the database, capture network traffic, or investigate the whole host.

Some early explanations were too confident about who did it, the exact high-CPU executable, memory-related kills, and how the attacker got in. Those points were not proved, so I have kept that clear here. We also did not establish a live hashrate or accepted-share count, so there is no earnings estimate.

Not finding evidence of host compromise or data theft does not mean neither happened. More investigation is needed to know how far this went and whether everything has been cleaned up.

That’s all for now. Thanks for reading :)