Короткий ответ
Снимите ss -tulnp, сверьте с матрицей «кто с каких сетей». Всё, что слушает 0.0.0.0/:: и не должно быть на WAN, закрывайте на двух уровнях: bind приложения на 127.0.0.1 или LAN 10.0.20.10, плюс UFW/nft allow только с нужного CIDR. Firewall без смены bind оставляет сервис уязвимым при ошибке правила. Не публикуйте Docker -p 80:80 если нужен только localhost. Не отключайте firewall, чтобы «проверить порт».
Хост host.example (10.0.20.10), админ admin.
Симптомы и как отличить
- Внешний nmap:
22, 80, 443, 3306, 6379; curl http://WAN_IP:9200отвечает Elasticsearch без пароля;- с localhost «всё и так работает», с офиса база открыта «удобно».
| Слушает | Смысл |
|---|---|
127.0.0.1:3306 | не WAN, если нет туннеля |
10.0.20.10:3306 | LAN-интерфейс |
0.0.0.0:3306 | все IPv4, включая WAN NIC |
[::]:3306 | все IPv6 |
Отличие от «firewall не включён»: UFW может быть active с 3306 Anywhere. Тогда фильтр пропускает лишнее — чините allow и bind, не радуйтесь Status: active. Методика съёмки: открытые порты. Базовый deny: firewall.
Возможные причины
- Дефолт Redis/Postgres
bind */listen_addresses = '*'. - Docker
ports: - "6379:6379"без127.0.0.1:. - Node exporter / cadvisor на
:9100для «удобного Grafana из дома». - Старый
ufw allow 3306. - Kubelet/профилировщики забыли.
ssh -Lне при чём: это локальный форвард на вашей станции.
Диагностика
ss -tulnp
sudo ss -tulnp | awk 'NR==1 || $1 ~ /LISTEN/'
ip -br addrРазбор Docker/podman:
docker ps -a --format '{{.ID}} {{.Names}} {{.Ports}}' 2>/dev/null
sudo lsof -iTCP -sTCP:LISTEN -P -nКонфиги (примеры, читайте свой пакет):
sudo grep -E '^bind |^protected-mode' /etc/redis/redis.conf 2>/dev/null
sudo grep -E '^listen_addresses|^port' /etc/postgresql/*/main/postgresql.conf 2>/dev/null
sudo grep -R listen /etc/mysql/ 2>/dev/null | grep -v '#'С hop:
nmap -Pn -p 22,80,443,3306,5432,6379,2375,9200,9100 10.0.20.10Не сканируйте чужие сети. Для WAN подставьте публичный IP, не RFC1918, если речь про интернет.
UFW:
sudo ufw status numberedИщите Anywhere на портах данных.
Решение
Сценарий A. Приложение bind на все интерфейсы
Redis: bind 127.0.0.1 и protected-mode yes. Если нужен LAN:
bind 10.0.20.10плюс пароль/ACL. Перезапуск unit в окно. Postgres: listen_addresses = '10.0.20.10' и pg_hba.conf только с 10.0.20.0/24, не 0.0.0.0/0.
Проверка:
ss -tlnp | grep -E '5432|6379|3306'Не должно быть 0.0.0.0 если политика — LAN/local.
Сценарий B. Docker publish
В Compose:
ports:
- "127.0.0.1:6379:6379"не "6379:6379". Пересоздание контейнера. Для доступа с другого хоста LAN — publish на 10.0.20.10:6379 плюс UFW CIDR, или overlay без хостового порта.
Не открывайте TCP 2375 без TLS. Сокет: docker.sock.
Сценарий C. Firewall режет WAN, bind пока не трогали
Временный слой, не финал:
sudo ufw delete allow 3306/tcp
sudo ufw allow from 10.0.20.0/24 to any port 3306 proto tcpОставьте консоль. Затем всё равно сузьте bind: ошибка ufw delete не должна снова выставить базу в мир.
Сценарий D. systemd unit слушает *
Environment= или ExecStart= с --port 0.0.0.0. Override:
sudo systemctl edit myserviceExecStart= сброс и новый bind. Не запускайте этот сервис от root без нужды: сервис от root.
Сценарий E. Экспортёры и «внутренние» админки
Node exporter, cAdvisor, phpMyAdmin, RabbitMQ management, Kubernetes metrics — их часто оставляют на :9100/:15672 «пока настроим VPN». С точки зрения ss это тот же WAN-риск, что и Redis. Пока VPN нет — bind 10.0.20.10 плюс UFW только с admin-CIDR, либо SSH-туннель. Не ufw allow 9100 в Anywhere «для Grafana из дома»: Grafana должна ходить в LAN или через jump.
Проверьте IPv6 отдельно: закрытый 0.0.0.0:6379 при живом [::]:6379 означает, что скан с AAAA всё ещё находит сервис. В redis/postgres слушайте явно оба семейства или отключите ненужный стек на сервисе, не net.ipv6.conf.all.disable_ipv6=1 «вместо firewall».
Сценарий F. Откат, если закрыли лишнее и упало приложение
Если после смены listen_addresses клиенты в LAN не коннектятся — верните предыдущий конфиг из снимок/git и разберите pg_hba / redis ACL, не bind *. Типичная ошибка: слушаете 10.0.20.10, а клиенты ходят на hostname, который резолвится в другой NIC (VIP, docker0, 172.17.0.1). Сверьте ss и ip addr на тот адрес, который в connection string.
Для Docker после смены publish контейнер мог остаться со старым маппингом: docker ps важнее ss на секунду гонки. docker compose up -d без recreate не всегда пересоздаёт ports. Явно up -d --force-recreate в окно.
Зафиксируйте в заявке: кто имеет право слушать WAN (обычно 22/443), кто только LAN, кто только localhost. Этот документ — вход для скана портов через квартал: diff ss без тикета = регрессия, не «так вышло».
Как проверить, что проблема устранена
ss -tulnp— нет лишних0.0.0.0/::.- nmap с WAN-hop — только согласованные 22/443/…
- nmap с LAN — порты, нужные приложению.
- UFW numbered без Anywhere на БД.
- Приложение с клиента LAN живо.
Сохраните два файла ss до/после в заявку.
Если не помогло
- Порт снова
0.0.0.0после reboot: конфиг пакета перетёрт, docker-compose без127.0.0.1, kubernetes NodePort. - Виден порт,
ssпустой: это DNAT на роутере, не этот хост. Смотрите периметр. - IPv6
:::3306при закрытом IPv4. Restart=alwaysконтейнера со старым publish.- SELinux на Ubuntu не штатно; не отключайте MAC. На RHEL
semanage port— неsetenforce 0навсегда.
Профилактика
- В шаблоне compose запрещён publish без bind-адреса.
- CI-проверка:
ss/nmapв пайплайне выкладки. - Экспортёры только на admin-сети.
- Квартальный проход baseline.
FAQ
Достаточно ли закрыть UFW и не трогать bind?
Нет. Следующий ufw allow 3306 или сбой стека откроет сервис. Bind — источник правды.
localhost и docker-proxy
127.0.0.1:6379 на хосте недоступен с LAN — это цель. Для LAN явно публикуйте LAN IP.
Зачем тогда firewall?
Defense in depth, чужие процессы, ошибка bind после обновления.
nmap показывает filtered vs closed
filtered — drop (UFW/nft). closed — RST, порт не слушает, фильтр пропускает. Для WAN лучше не светить лишние closed на данных: и bind, и drop.
Можно ли оставить 22 Anywhere?
Лучше CIDR/VPN. См. SSH. Не взамен закрытия 3306.