Skip to main content

Docker and UFW: Why Your Firewall Rules Do Not Protect Published Containers

There is a small Docker habit that causes a surprisingly large security problem on home servers:

ports:
  - "8080:80"

It looks harmless. It looks like a normal Docker Compose example. It is in half the tutorials on the internet.

Then you check UFW and it says the firewall is active. Default incoming traffic is denied. Port 8080 is not allowed. Everything looks fine.

Except the container may still be reachable.

This is one of those Linux home server problems that feels like a bug the first time you see it. It is not really a bug. It is Docker doing exactly what Docker is designed to do: create its own firewall and NAT rules so published container ports work.

That is the part many people miss.

Last updated: June 2026

This guide is for the usual homelab setup: Ubuntu, Debian or a similar Linux server, rootful Docker Engine, Docker Compose, UFW enabled, and a few self-hosted services running on an old workstation, mini PC, laptop server or small VPS.

If you are building the whole setup from scratch, read this together with the Linux Home Server Security Guide, the UFW Firewall Rules for Home Servers post, and the Docker Security for Homelab Beginners guide.


1. The short version

UFW is still useful on a Docker host, but it does not mean what many people think it means once Docker publishes ports.

What you do What many people expect What can actually happen
sudo ufw default deny incoming All unsolicited inbound traffic is blocked. Normal host services are blocked, but Docker-published ports may still be reachable through Docker's own rules.
ports: "8080:80" Container is available only if UFW allows port 8080. Docker publishes the port and creates firewall/NAT rules for it.
expose: "80" Service is available from outside. No. It is not published to the host. Other containers on the right network can use it.
127.0.0.1:8080:80 Service is private to the host. Usually yes. This is a good pattern for same-host reverse proxies and local testing.
DOCKER-USER chain Advanced firewall detail. This is where Docker expects user-defined filtering before Docker's own forwarding rules.

The practical rule is simple:

Do not publish a Docker port unless you actually want something outside Docker to reach it.

For most home servers, publish only the reverse proxy on ports 80 and 443. Keep backend containers on Docker networks. Bind local-only services to 127.0.0.1. Use the DOCKER-USER chain only when you really need host-level filtering for Docker-published ports.


2. Why this happens

UFW is a friendly frontend for Linux firewall rules. On Ubuntu, it is the normal way to manage a host firewall without writing raw iptables or nftables rules all day. The Ubuntu Server firewall documentation describes UFW as the default firewall configuration tool for Ubuntu and a user-friendly way to create an IPv4 or IPv6 host-based firewall.

For a normal service running directly on the host, the mental model works well:

Internet or LAN
  -> Linux host
      -> UFW rules
          -> SSH, Nginx, Samba, etc.

If UFW denies incoming traffic and you have not allowed a port, a normal host service should not be reachable from outside.

Docker changes the path.

When you publish a container port, Docker creates rules on the host so traffic can be redirected to the container. Docker's own packet filtering and firewalls documentation says that on Linux, Docker creates firewall rules for network isolation, port publishing and filtering. It also says that Docker and UFW use firewall rules in ways that make them incompatible with each other, because published container traffic is diverted before it goes through the UFW chains normally used for host input and output.

That means this Compose line is not just a bit of documentation:

ports:
  - "8080:80"

It is a request to publish container port 80 on host port 8080.

Docker then makes that work by adding its own rules. If the host has a public IP, that can mean public access. If the host is behind a home router, that can mean LAN access and possibly internet access if the router forwards the port.

So the real path often looks closer to this:

Internet or LAN
  -> Linux host network stack
      -> Docker NAT / forwarding rules
          -> container port

UFW still protects normal host services. It still matters for SSH. It still matters for services not handled by Docker publishing. But it is not the whole story for containers.


3. A quick test that proves the problem

Do not run this on an important public server without understanding it. Use a spare VM, a local test box, or a temporary container you can remove afterwards.

First, set a normal UFW baseline:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status verbose

You should see something like:

Status: active
Default: deny (incoming), allow (outgoing), deny (routed)

Now start a simple test container:

docker run -d --name ufw-docker-test -p 8080:80 nginx:alpine

Check what Docker thinks it published:

docker ps --format "table {{.Names}}\t{{.Ports}}"

You may see something like:

NAMES             PORTS
ufw-docker-test   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Now test from another machine, not from the Docker host itself:

curl -I http://SERVER_IP:8080

Or scan from another trusted machine:

nmap -Pn -p 8080 SERVER_IP

If the port is reachable, UFW did not save you from that published Docker port.

Clean up:

docker rm -f ufw-docker-test

Important detail: test from the right place. Testing from the Docker host itself does not prove what the LAN or internet can reach. For a home server, test from another LAN device and, if you have router port forwarding, from outside the home network as well.


4. The difference between ports, expose and Dockerfile EXPOSE

This is where many Compose files go wrong.

ports publishes a port to the host

services:
  app:
    image: example/app
    ports:
      - "8080:80"

This means:

host port 8080 -> container port 80

If you do not specify a host IP address, Docker normally binds the published port to all host addresses. On a dual-stack host, that may include IPv4 and IPv6. Docker's port publishing documentation is blunt about this: published container ports are insecure by default because they become available outside the Docker host as well.

This is the dangerous one.

127.0.0.1:8080:80 publishes only on loopback

services:
  app:
    image: example/app
    ports:
      - "127.0.0.1:8080:80"

This means:

localhost on Docker host port 8080 -> container port 80

That is useful when a reverse proxy on the same host needs to connect to the service through localhost, or when you want a temporary local test.

I still prefer Docker networks for reverse proxy setups, but loopback binding is much better than publishing to 0.0.0.0 by habit.

expose does not publish to the host

services:
  app:
    image: example/app
    expose:
      - "80"

This does not make the service reachable from the LAN or internet. It is mainly documentation for the container port and a hint for container-to-container setups.

Also, do not treat expose as a hard security boundary inside a Docker network. Containers on the same Docker network can usually reach each other by service name and container port. The real security improvement is that nothing has been published to the host.

Dockerfile EXPOSE is not publication either

EXPOSE 80

This is metadata in the image. It tells humans and tools what port the application expects to use. It does not open the port on the host by itself.

The line that changes your host exposure is usually ports in Compose or -p / --publish on the Docker command line.


5. The safest homelab pattern: publish less

The best fix is not clever firewall work. The best fix is to publish fewer things.

A good Docker home server layout looks like this:

Home router
  -> forwards 80/tcp and 443/tcp only
      -> Docker reverse proxy
          -> private Docker network
              -> backend containers with no host ports

Create a shared network for services that should sit behind the proxy:

docker network create proxy

Then put a backend service on that network without publishing it:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    expose:
      - "3001"
    networks:
      - proxy

networks:
  proxy:
    external: true

Only the reverse proxy should publish public web ports:

services:
  reverse-proxy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - proxy

volumes:
  caddy_data:
  caddy_config:

networks:
  proxy:
    external: true

That design is boring. Boring is the point.

You do not need UFW to rescue ten accidentally published admin dashboards if those dashboards are never published in the first place.

After changing Compose files, check the published ports:

docker compose up -d

docker ps --format "table {{.Names}}\t{{.Ports}}"

Good output looks like this:

NAMES           PORTS
reverse-proxy   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
uptime-kuma     3001/tcp
postgres        5432/tcp
redis           6379/tcp

The backend ports are visible as container ports, but not published as host ports. That is the habit you want.

If you are choosing the proxy itself, the companion guide is Reverse Proxy for Home Servers: Caddy vs Nginx Proxy Manager vs Traefik. If you are running monitoring, the Uptime Kuma for Home Servers guide fits well here. Monitor the public URL through the reverse proxy and, when useful, monitor the backend from inside the LAN.


6. A practical UFW baseline for a Docker host

Even with Docker in the picture, I still use UFW on home servers. I just do not pretend it controls every Docker-published port.

A basic home server baseline might be:

# SSH from LAN only. Adjust the subnet.
sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp

# Public web traffic to the reverse proxy, if this server hosts public web services.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
sudo ufw status verbose

If you do not expose public web services, do not allow 80 and 443. If SSH is only used through Tailscale, WireGuard or another private network, restrict it to that interface or source range instead of the whole LAN.

This baseline protects normal host services and documents your intended exposure. It also keeps you honest when you run:

sudo ufw status verbose

But for Docker, you still need to check:

docker ps --format "table {{.Names}}\t{{.Ports}}"
sudo iptables -S DOCKER-USER
sudo iptables -t nat -S DOCKER

On some systems, especially when Docker is not using the userland proxy, ss -tulpn may not tell the whole story for published containers. Use Docker's port output and test from another host.


7. When loopback binding is the right fix

Sometimes a service needs to be reachable from the Docker host, but not from the LAN or internet.

Use loopback binding:

services:
  adminer:
    image: adminer:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8081:8080"

Now the service should be reachable from the Docker host at:

http://127.0.0.1:8081

But it should not be reachable from another device using:

http://SERVER_LAN_IP:8081

Verify that from another machine. Do not just trust the Compose file.

This is a good pattern for:

  • temporary admin tools;
  • database web UIs used through an SSH tunnel;
  • local development services;
  • apps that a same-host reverse proxy reaches through localhost.

Example SSH tunnel from your laptop:

ssh -L 8081:127.0.0.1:8081 user@server-ip

Then open:

http://127.0.0.1:8081

For SSH itself, harden it properly first. The SSH Hardening for Home Servers post covers keys, password login, root login and safe lockout prevention.


8. When to use the DOCKER-USER chain

The DOCKER-USER chain is the place Docker leaves for your own filtering rules before Docker's forwarding rules accept container traffic. Docker's Docker with iptables documentation describes DOCKER-USER as the user-defined chain that is processed before Docker's own forwarding chains.

Use it when you really need host-level filtering for Docker-published ports.

Examples:

  • a VPS with Docker where published ports would otherwise be public;
  • a home server where you want only LAN clients to reach Docker-published ports;
  • a server where only 80/443 should be public, but some other published ports should be LAN-only;
  • a temporary migration where you cannot clean every Compose file immediately.

Before adding rules, identify your external interface:

ip route get 1.1.1.1

You might see something like:

1.1.1.1 via 192.168.1.1 dev enp3s0 src 192.168.1.50

In that example, the interface is enp3s0.

You can also extract it like this:

EXT_IF=$(ip route get 1.1.1.1 | awk '{for (i=1; i<=NF; i++) if ($i=="dev") print $(i+1)}')
echo "$EXT_IF"

Here is a conservative example policy:

# Change this to match your real LAN.
LAN_CIDR="192.168.1.0/24"

# Change this if auto-detection picks the wrong interface.
EXT_IF=$(ip route get 1.1.1.1 | awk '{for (i=1; i<=NF; i++) if ($i=="dev") print $(i+1)}')

# Keep established connections working.
sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# Allow LAN clients to reach Docker-published ports.
sudo iptables -I DOCKER-USER 2 -i "$EXT_IF" -s "$LAN_CIDR" -j RETURN

# Allow public HTTP/HTTPS to a reverse proxy if you intentionally publish it.
# This assumes the proxy container receives traffic on container ports 80 and 443.
sudo iptables -I DOCKER-USER 3 -i "$EXT_IF" -p tcp -m multiport --dports 80,443 -j RETURN

# Drop other new external connections to Docker containers.
sudo iptables -I DOCKER-USER 4 -i "$EXT_IF" -j DROP

Read that twice before using it.

It says:

  • existing connections are allowed to continue;
  • LAN clients can reach published container ports;
  • public HTTP and HTTPS can reach the reverse proxy;
  • other new external connections to Docker containers are dropped.

If you do not run a public reverse proxy, remove the 80/443 allow rule. If your LAN is not 192.168.1.0/24, change it. If your public proxy maps unusual host ports to different container ports, do not blindly reuse the 80/443 rule.

Check the result:

sudo iptables -S DOCKER-USER

Then test from another LAN device and from outside the network.

There is an important technical detail here: by the time traffic reaches DOCKER-USER, Docker may already have performed DNAT. That means simple --dport matching may see the container port, not the original host port. Docker's documentation notes that matching the original destination IP and port requires conntrack original-destination matching. For a homelab, the cleaner answer is usually to avoid odd port mappings and publish only the proxy on normal ports.

Making DOCKER-USER rules persistent

Do not rely on commands typed once into a shell. They will disappear after a reboot unless something restores them.

One practical approach is a small systemd unit that runs after Docker starts. This keeps the rules visible and easy to remove.

Create a script:

sudo nano /usr/local/sbin/docker-user-firewall.sh

Example script:

#!/bin/sh
set -eu

LAN_CIDR="192.168.1.0/24"
EXT_IF="$(ip route get 1.1.1.1 | awk '{for (i=1; i<=NF; i++) if ($i=="dev") print $(i+1)}')"

# This script owns DOCKER-USER on this host. Do not use it if you already keep
# other custom rules there unless you merge them carefully.
iptables -N DOCKER-USER 2>/dev/null || true
iptables -F DOCKER-USER

iptables -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A DOCKER-USER -i "$EXT_IF" -s "$LAN_CIDR" -j RETURN
iptables -A DOCKER-USER -i "$EXT_IF" -p tcp -m multiport --dports 80,443 -j RETURN
iptables -A DOCKER-USER -i "$EXT_IF" -j DROP
iptables -A DOCKER-USER -j RETURN

Make it executable:

sudo chmod 700 /usr/local/sbin/docker-user-firewall.sh

Create the service:

sudo nano /etc/systemd/system/docker-user-firewall.service

Unit file:

[Unit]
Description=Apply DOCKER-USER firewall rules
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-user-firewall.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Enable and run it:

sudo systemctl daemon-reload
sudo systemctl enable --now docker-user-firewall.service
sudo systemctl status docker-user-firewall.service
sudo iptables -S DOCKER-USER

This is intentionally boring. A small script you understand is better than a mysterious firewall state you cannot rebuild.

Again: this is an example policy, not a universal one. Firewalls are one of those places where copy-paste confidence can become a very long evening.


9. What I would not do first

I would not disable Docker's iptables integration as the first fix

You will see advice telling you to set Docker's iptables option to false.

I do not recommend that as a default homelab fix.

Docker's own documentation warns that preventing Docker from creating most of its firewall rules is not appropriate for most users and is likely to break container networking. You can make it work if you know exactly which rules you need to replace, but that is not where I would start on a home server.

I would not publish databases to the LAN because “it is only at home”

This is bad:

services:
  postgres:
    image: postgres:16
    ports:
      - "5432:5432"

Most applications that need Postgres can reach it over a Docker network:

services:
  app:
    image: example/app
    environment:
      DATABASE_URL: postgres://app:password@postgres:5432/app
    networks:
      - internal

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: change-me
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - internal

networks:
  internal:
    driver: bridge

No host port is needed there.

I would not use network_mode: host casually

network_mode: host removes a large part of Docker's network isolation. The container shares the host network stack. For some monitoring or low-level network tools, that can be the right answer. For a random dashboard copied from a Compose example, it is usually not.

If you use host networking, treat the service like a host service. Check what it listens on. Make sure UFW allows only what you intend. Do not assume Docker port mapping rules apply, because there is no normal Docker port mapping in host network mode.


10. IPv6: the part people forget

IPv6 makes this easier to get wrong.

Many home networks now have IPv6. Some servers have public IPv6 even when IPv4 is behind NAT. Docker's documentation notes that when a container port is mapped without a specific host address, Docker publishes to all host addresses by default, including 0.0.0.0 and [::].

So check both address families:

docker ps --format "table {{.Names}}\t{{.Ports}}"
ss -tulpn -4
ss -tulpn -6
sudo ufw status verbose

If you test from outside, test IPv4 and IPv6 where possible.

If your firewall policy only handles IPv4 but your service is reachable over IPv6, you have not blocked the service. You have only blocked one route to it.


11. A better Compose habit for every new service

Before adding a new container, ask one question:

Who needs to connect to this service?

Who needs access? Preferred Compose pattern Example
Only another container No ports. Same Docker network. App to database, app to Redis.
Only the Docker host Bind to 127.0.0.1. Temporary admin UI through SSH tunnel.
LAN only Prefer VPN/private access, or publish carefully and restrict with DOCKER-USER. Local dashboard, internal wiki.
Public web users Publish reverse proxy only on 80/443. Website, public status page.
Nobody anymore Stop and remove it. Old test container from six months ago.

This one habit catches most problems before firewall rules are needed.

I like to add a small comment in Compose files:

# No ports here on purpose.
# This service is only reached by the reverse proxy over the proxy network.

That comment is not for Docker. It is for future me, tired and troubleshooting something at midnight.


12. Monthly Docker firewall audit

This belongs in the same routine as updates, backups and log checks.

Run:

docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
sudo ufw status verbose
sudo iptables -S DOCKER-USER
sudo iptables -t nat -S DOCKER
docker network ls

Then ask:

  • Which containers publish ports?
  • Are any databases published?
  • Are any admin dashboards published?
  • Are services bound to 0.0.0.0 when they could be bound to 127.0.0.1?
  • Does the router forward anything besides what I expect?
  • Do IPv6 rules match the IPv4 rules?
  • Can I rebuild the firewall policy after a reboot?

If the answer is “I have no idea what that port is”, stop and investigate.

Also back up the Compose files before changing things. The Backing Up Docker Containers post covers the practical side: Compose files, bind mounts, named volumes, databases and restore notes.


13. Troubleshooting notes

UFW says deny, but the container is reachable

That is the exact issue this post is about. Check whether Docker published the port:

docker ps --format "table {{.Names}}\t{{.Ports}}"

If you see 0.0.0.0:8080->80/tcp, Docker published it on all IPv4 interfaces. If you see [::]:8080->80/tcp, IPv6 is involved too.

I changed UFW rules and nothing changed

You may be changing host input rules while the traffic is being forwarded to Docker containers. Inspect DOCKER-USER and Docker's NAT rules.

sudo iptables -S DOCKER-USER
sudo iptables -t nat -S DOCKER

The service is reachable from the host but not from another machine

Check whether it is bound to 127.0.0.1. That may be correct. It means only the Docker host can reach that host port.

The service is reachable on the LAN but not the internet

Check the router. On a home network, the router normally controls what the internet can reach on IPv4. If there is no port forward, the public internet should not reach random LAN ports. IPv6 can be different, so do not skip IPv6 testing.

I added DOCKER-USER rules and broke something

List the rules:

sudo iptables -S DOCKER-USER

If you need to temporarily remove a test policy:

sudo iptables -F DOCKER-USER
sudo iptables -A DOCKER-USER -j RETURN

If you created the systemd service from this post, disable it before rebooting into the same broken policy:

sudo systemctl disable --now docker-user-firewall.service

Then fix the script and test again.


14. My recommended policy for a normal home server

For a normal Linux home server running Docker, I would use this policy:

  1. UFW enabled.
  2. SSH allowed only from LAN, VPN or a trusted admin network.
  3. No database ports published to the host.
  4. No admin dashboards published directly to the internet.
  5. Reverse proxy publishes 80 and 443 if public web services are needed.
  6. Backend containers sit on user-defined Docker networks with no host ports.
  7. Temporary admin tools bind to 127.0.0.1 and are reached through SSH tunnel or private access.
  8. DOCKER-USER rules added only if the host needs a real Docker-aware firewall policy.
  9. External testing after every major network change.
  10. Compose files and firewall scripts backed up.

That gives you a setup that is easy to understand later.

Not perfect. Not enterprise. But a lot better than a folder full of Compose files publishing random ports to the world while UFW politely says everything is fine.


Frequently asked questions

Does Docker bypass UFW?

For published container ports, yes, it can effectively bypass the UFW rules people expect to apply. Docker creates its own firewall and NAT rules for port publishing, and Docker's documentation specifically warns that Docker and UFW are incompatible in this area.

Is UFW useless on a Docker server?

No. UFW is still useful for normal host services such as SSH and for documenting your intended baseline. It just should not be treated as complete protection for Docker-published ports.

Is ports the same as expose in Docker Compose?

No. ports publishes a container port to the host. expose does not publish it to the host. For most backend services behind a reverse proxy, avoid ports and use Docker networks instead.

Should I bind Docker ports to 127.0.0.1?

Use 127.0.0.1 binding when only the Docker host should reach the service. It is useful for SSH tunnels, local admin tools and some same-host proxy patterns. For container-to-container traffic, a Docker network is usually cleaner.

Should I use the DOCKER-USER chain?

Use it when you need Docker-aware firewall filtering for published ports. Do not use it as a substitute for good Compose design. The best first fix is usually to remove unnecessary ports entries.

Should I set Docker's iptables option to false?

Usually no. Docker warns that preventing Docker from creating its firewall rules is not appropriate for most users and is likely to break container networking unless you replace the needed rules yourself.

What is the safest Docker port setup for a homelab?

Publish only what must be reached from outside Docker. For public web apps, publish a reverse proxy on 80 and 443. Keep backend apps, databases and admin tools on private Docker networks or bind them to localhost when needed.


Read next


Written by MS

MS is a Linux homelab and cybersecurity enthusiast who documents practical experiments with home servers, Docker, firewalls, backups, Lynis, Fail2ban, honeypots and old hardware. The guides on IT Random Stuff are based on hands-on testing, real configurations and lessons learned from running Linux systems at home.

Comments

Popular posts from this blog

OpenCanary Honeypot on Ubuntu: 2026 Home Lab Setup Guide

I first wrote about OpenCanary years ago, and that old version badly needed an update. The tool is still useful, but the way I would deploy it in a home lab today is different: newer Ubuntu versions, Python 3, Docker as an option, cleaner logging, and no copy-pasted example credentials that look like something someone might actually reuse. This guide is the updated version: how I would set up an OpenCanary honeypot on Ubuntu in 2026 for a small home network or homelab. The goal is not to trap random people on the internet. The goal is to create a quiet internal warning system: a fake service that should never be touched, so any connection to it deserves attention. Important: this is a defensive security guide. Run OpenCanary only on systems and networks you own or have permission to monitor. Do not expose a honeypot to the public internet unless you understand the logging, legal, abuse and maintenance implications. If you are building a Linux home server security setup fro...

Lenovo ThinkPad X250 on Linux: Tweaks, Undervolting, Battery Life and 2026 Update

I wanted a cheap, small, serviceable Linux laptop. Something light enough to carry, easy enough to repair, and inexpensive enough that upgrades would still make sense. The Lenovo ThinkPad X250 was a good candidate because it has a 12.5-inch form factor, a proper ThinkPad keyboard, SSD upgrade options, replaceable parts, Ethernet, docking support and generally good Linux compatibility. I found one on eBay for around 130€ : an Intel Core i5-5300U model with 8GB RAM , a 128GB SSD , two batteries and an HD screen with a small bruise. The plan was simple: clean it, repaste it, upgrade the SSD, install Linux Mint, undervolt it and see how useful it could still be. This post started as my original 2019 notes about tweaking the Lenovo X250 in Linux. I have now updated it with a 2026 perspective, cleaner instructions, better internal links and a more realistic look at whether this old ThinkPad is still worth using. Related posts: Linux Home Server Security Checklist Docker Secu...

Strong, Unique Passwords Without Losing Your Mind

Last updated: May 27, 2026 Password Security in 2026: Password Managers, Passkeys & 2FA for Real People Password Security in 2026: Password Managers, Passkeys & 2FA That Actually Work Most people do not have a weak-password problem. They have a reused-password problem. You can invent the cleverest password in the world, but if you use it on twenty websites and one of them gets breached, you suddenly have twenty compromised accounts. That is how most real-world account takeovers happen in 2026. Not elite hackers brute-forcing your login from a dark room somewhere. Just automated credential stuffing using databases leaked years ago from services you forgot existed. One old forum breach becomes access to your email, cloud storage, streaming services, VPN account, and eventually your homelab dashboard because the same password got reused everywhere. This guide explains how to handle passwords properly today: without paranoia, without enterprise co...