← Todas las noticias
security5 min de lectura

0.0.0.0 is not localhost: what you're actually exposing

Por Seaworthy · 12 de junio de 2026

Close-up of a blue UTP ethernet network cable, representing network connectivity and server infrastructure

Photo by Jordan Harrison on Unsplash

The difference that actually matters

When a server process starts, it has to pick a network interface to listen on. Your machine has several: the loopback interface (lo, 127.0.0.1), the Ethernet or Wi-Fi interface with your LAN or public IP, maybe a VPN adapter, maybe a Docker bridge.

127.0.0.1 (localhost) means: accept connections only from this machine. Nothing external can reach it.

0.0.0.0 means: accept connections on every interface. All of them. Including the one with your cloud instance's public IP address.

That's the whole distinction. It sounds simple, and it is. The reason it causes real damage is that 0.0.0.0 is often the default, and developers either don't notice or assume a firewall is doing the work their binding should be doing.

How this plays out in practice

You spin up Redis to cache some data. The default Redis config prior to version 3.2 bound to 0.0.0.0. Or you start a development Postgres instance with a permissive pg_hba.conf. Or your admin panel, your metrics endpoint, your internal API: all running happily on 0.0.0.0.

If you're on a laptop on a home network, you're probably fine. If you're on a cloud VM with a public IP and no firewall, you are directly accessible to anyone who port-scans your IP. Shodan, a search engine that continuously scans the public internet, has indexed millions of open Redis instances, exposed Mongo databases, and admin panels with no authentication. Most of those were not deliberately exposed. Someone just accepted a default.

The Redis mass-compromise of 2016-2018 is the textbook case. Attackers scanned for port 6379, connected with no credentials (because Redis has no auth by default), and used the CONFIG SET dir and CONFIG SET dbfilename commands to write SSH public keys into the server's authorized_keys file. Full shell access from an open port that the developer probably thought was "just local."

How to check what you're actually listening on

On Linux or macOS:

# Show all listening TCP sockets with the process name
ss -tlnp
# or if ss isn't available
netstat -tlnp

Look at the Local Address column. Anything showing 0.0.0.0:<port> or *:<port> is listening on all interfaces. Anything showing 127.0.0.1:<port> is loopback-only.

State    Recv-Q   Send-Q   Local Address:Port   ...
LISTEN   0        128      0.0.0.0:5432         # postgres, reachable externally
LISTEN   0        128      127.0.0.1:6379       # redis, loopback only
LISTEN   0        511      0.0.0.0:3000         # your app, probably fine if intentional

On a cloud VM, also check what your security group or firewall rules look like. A process bound to 0.0.0.0 behind a firewall that blocks the port is contained, but that's a layered defence, not a substitute for getting the binding right in the first place. Firewall rules change. Ports get opened for debugging. Binding correctly means the process is safe regardless.

Fixing it

For most databases and caches, the fix is one line in the config file.

Redis (redis.conf):

bind 127.0.0.1

PostgreSQL (postgresql.conf):

listen_addresses = 'localhost'

MySQL / MariaDB (my.cnf):

bind-address = 127.0.0.1

For application servers, the pattern depends on the framework. In Node with Express, you pass the bind address as the second argument to listen:

// This listens on all interfaces
app.listen(3000);

// This listens only on loopback
app.listen(3000, '127.0.0.1');

Many frameworks default to 0.0.0.0 when no address is specified, because that's convenient for containers and production use. In development that default is often wrong.

The Docker complication

Docker makes this messier. When you run a container with -p 5432:5432, Docker publishes that port by manipulating iptables rules directly. It can bypass your ufw or firewalld rules entirely. A Postgres container published this way is accessible on the host's public IP even if your firewall says port 5432 is closed, because Docker's iptables rules take precedence.

The fix here is to publish only to loopback:

docker run -p 127.0.0.1:5432:5432 postgres

Or in docker-compose.yml:

ports:
  - "127.0.0.1:5432:5432"

That single addition means the container's port is mapped only to the host's loopback interface, not to all interfaces. It's one of those things that should be in the default templates but isn't.

The broader point

The reason this keeps happening is that 0.0.0.0 is the convenient default for tools that are designed to run in environments where network topology is handled elsewhere: container orchestrators, VPCs with tight security groups, servers behind load balancers. Those defaults make sense in those contexts. They make much less sense when you're running a local dev database or an admin panel on a VM that has a public IP.

The assumption that "the firewall handles it" is fragile. Firewalls get misconfigured, rules get added by teammates, cloud provider security groups have complex inheritance. The safest approach is to be explicit about your bind address so your process does the right thing regardless of what's happening at the network layer.

Seaworthy flags services and configs with insecure bind addresses as part of its configuration checks.

Este artículo fue generado por IA y resume fuentes públicamente disponibles.

De los creadores de Seaworthy

Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.

Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.

$ npx seaworthycode