All articles
#pgadmin#docker#misconfiguration#incident-response#rce#cryptominer#postgres#container-security

Three seconds from internet scan to cryptominer: pgAdmin in desktop mode

Alexander Norman

A pgAdmin container published to the internet with SERVER_MODE set to False has no authentication at all. Anyone who reaches the port is already logged in as administrator. In a recent incident an automated scanner went from first request to running cryptominer in three seconds, using a pre-saved database connection the container helpfully provided.

The short version

Four settings in one Docker Compose block combined into a full compromise. Individually each looks reasonable, and three of them appear in tutorials and sample configurations. Together they published an authenticated database administration tool to the open internet.

The whole attack took three seconds, start to finish, and was carried out by an automated scanner using python-requests. This was not a targeted attack. It was a machine finding an open door on a schedule.

The four settings

yaml
pgadmin:
  ports:
    - "5050:80"                                          # 1. published on 0.0.0.0
  environment:
    PGADMIN_CONFIG_SERVER_MODE: 'False'                  # 2. no authentication at all
    PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'     # 3. saved credentials usable freely
    PGADMIN_SERVER_JSON_FILE: '/pgadmin4/servers.json'   # 4. pre-configured DB connection
    PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin} #    (default password, moot by now)

1. "5050:80" binds to every interface. Docker's short port syntax defaults to 0.0.0.0, so the container is reachable from the internet unless a firewall says otherwise. Writing "127.0.0.1:5050:80" would have limited it to the host.

2. SERVER_MODE: 'False' is the critical one. pgAdmin has two modes. Server mode is multi-user and asks for a login. Desktop mode assumes it is running on your own laptop where the operating system is the security boundary, so it does not authenticate at all. In a container published to the internet, every visitor is automatically the administrator.

This is why the logs contained no failed logins and no brute force. There was no login step to fail. It is also why this class of incident is easy to miss when reviewing logs: the attacker's requests look exactly like an administrator's.

3. MASTER_PASSWORD_REQUIRED: 'False' removes the gate protecting saved connection credentials.

4. PGADMIN_SERVER_JSON_FILE pre-loads a database connection, and the accompanying pgpass file holds the database password in plaintext. So the automatic administrator arrived to find a working database connection already configured.

The default password on line five is almost a footnote. Once authentication is disabled entirely, the password protects nothing.

What the attacker did

mermaid
flowchart TD
    A["Mass scanner finds<br/>open port 5050"] --> B["POST /browser/master_password<br/>clears the credential gate"]
    B --> C["Creates and connects<br/>a server object"]
    C --> D["POST /import_export/job<br/>runs a binary as a 'job'"]
    D --> E["Downloads and starts<br/>cryptominer"]
    E --> F["DELETE server object<br/>removes its own trace"]

The execution step abuses pgAdmin's import/export feature, which runs a binary on the server as a background job. It is a documented capability, and the CVE-2024-3116 class of issues covers exactly this path being reachable by someone who should not have it. With authentication disabled, "someone who should not have it" is the entire internet.

The cleanup step is worth noting. The scanner deleted the server object it created, which removes the obvious trace from the pgAdmin UI. The request log still had everything, and the miner binary's creation timestamp lined up with the request timestamps to the second.

What was and was not affected

The database itself came through intact: no attacker-created roles, no superusers, no external connections, no sign of tampering. The miner wanted CPU, not data, which is typical for opportunistic scanning.

That is luck rather than design. The pgpass file containing the database password was readable by the automatic administrator, so the credential must be treated as disclosed regardless of whether it was used. Rotating it is not optional.

What to do

If you run pgAdmin in a container:

  • Never set SERVER_MODE: 'False' on anything reachable from a network. It is a laptop setting.
  • Bind admin tools to localhost (127.0.0.1:5050:80) and reach them over SSH port-forwarding or a VPN. An administration interface has no business on a public port.
  • Do not pre-load database credentials into the container. Convenience for you is convenience for whoever arrives first.
  • If you must expose it, put authentication in front of it and keep MASTER_PASSWORD_REQUIRED on.

More generally: treat every published port as an inventory item. docker ps shows what you published; compare it against what you meant to publish. In this case a single docker port review would have caught it, at any point, before the scanners did.

If you think it happened to you: stop the container rather than deleting it first, so you keep the evidence. Preserve the request logs, hash any unexpected binaries, and check both the database roles and the active connections. Then rotate every credential the container could read, and remove the tool rather than reconfiguring it if you do not actually need it running.

The uncomfortable part

Three of these four settings exist to make life easier during development, and they show up in blog posts and starter templates because they genuinely do. The failure was not exotic. It was a development-shaped configuration reaching an internet-shaped environment, which is one of the most common ways systems are lost.

The defence is boring and effective: know what is published, and assume anything on a public port will be found by an automated scanner within days.


Adminor works with attack surface monitoring and security testing. If you want to know what your organisation currently has published to the internet, get in touch.

Want to see what your external attack surface actually looks like? Free health check, no credit card, two minutes.