В первой части мы нашли на общем сервере PostgreSQL с портом наружу и векторную базу без ключа. Когда попросили их закрыть, услышали три возражения: «мне база нужна открытая», «там всё равно пароль» и «в этой базе нет ничего важного». Каждое звучит разумно, и каждое смешивает две разные вещи. Разберём по очереди — с тем, как дать доступ, ничего не открывая.
Это вторая часть цикла «Общий сервер: безопасность и восстановление».
Главная путаница: ports — не для связи контейнеров
Частый вопрос: «если убрать ports, как приложение попадёт в базу?» Никак не изменится. Секция ports публикует порт на хосте, то есть открывает его для входа извне машины. Контейнеры в одной пользовательской сети Docker видят друг друга по имени сервиса через встроенный DNS и могут достучаться до любого порта соседа — опубликован он или нет.
services:
app:
image: registry.example.com/app:1.4
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app # «db» — имя сервиса, не IP
networks: [proxy, internal]
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.example.com`)
- traefik.http.services.app.loadbalancer.server.port=8813 # внутренний порт, наружу не публикуется
db:
image: postgres:16
networks: [internal] # ни ports, ни сети прокси — снаружи базы не видно
volumes: ["./var/postgres:/var/lib/postgresql/data"]
networks:
proxy: { external: true }
internal: {}
Прокси тоже ходит в приложение по сети Docker: метка loadbalancer.server.port говорит, на какой внутренний порт стучаться. Поэтому у приложения, которое уже стоит за прокси, строка ports: "8813:8813" ничего не добавляет, кроме дыры.
«Мне нужна открытая база»
На деле это значит «мне нужен доступ», а не «всему интернету». Доступ одному человеку даётся без публикации порта:
- SSH-туннель — с компьютера разработчика:
ssh -N -L 5432:127.0.0.1:5432 user@server, дальше IDE подключается кlocalhost:5432. Для этого на сервере порт публикуется только на петлю:ports: ["127.0.0.1:5432:5432"]; - веб-клиент базы — за прокси, на своём поддомене, с авторизацией на прокси;
- совсем крайний случай — доступ с одного адреса правилом в цепочке
DOCKER-USER, а не отовсюду.
У нас compose был параметризован переменной адреса публикации, и закрыть базу оказалось одной строкой в .env: POSTGRES_BIND_IP=127.0.0.1. listen_addresses = * внутри контейнера при этом оставили — без него не подключатся соседи по сети Docker.
«Там всё равно пароль»
Пароль — это авторизация. Закрытый порт — изоляция. Они защищают от разного:
- открытый порт базы получает подбор паролей круглые сутки — посмотрите журнал любой такой базы через неделю;
- уязвимость, срабатывающая до проверки пароля, бьёт напрямую. Пароль от неё не спасает, закрытый порт — спасает;
- база отдаёт баннер с версией — сканеры собирают такие списки, чтобы прийти, когда выйдет уязвимость.
И даже если доступ снаружи по какой-то причине оставлен, правило host all all all scram-sha-256 в pg_hba.conf (любая база, любой пользователь, любой адрес) сужается в любом случае: конкретная база, конкретный пользователь, конкретная сеть.
«В этой базе нет ничего важного»
Это довод о конфиденциальности: «украсть нечего». Но у открытой базы без ключа есть ещё целостность и доступность:
- без ключа можно не только читать, но и удалять и записывать. Кто угодно снесёт коллекцию, и приложение, которое на неё опирается, ляжет;
- можно подмешать свои данные — поиск начнёт отдавать чужое;
- чужое бесплатное хранилище на вашем диске и плацдарм внутри вашей сети Docker;
- «неважное» часто только кажется таким: в данных коллекции лежат идентификаторы, адреса файлов и внутренние пути.
Довод «защищать дорого» здесь тоже не работает: ключ у векторной базы задаётся переменной окружения, публикация на 127.0.0.1 — двумя символами в compose. Когда исправление бесплатное, причин оставлять дыру нет.
Порядок перевода живого сервиса
- подключить сервис к сети прокси, поставить метки маршрута — проверить, что он отвечает по доменному имени;
- перевести клиентов (фронт, другие сервисы) на доменное имя или имя контейнера вместо
адрес:порт; - убрать
portsили перевесить на127.0.0.1, пересоздать контейнер; - внешним сканом убедиться, что порт закрыт, а сервис по-прежнему отвечает через прокси;
- базе без ключа — ключ, и клиентам — тот же ключ в переменной окружения.
Мы переводили так фронт и бэкенд нового приложения: сначала сеть, сборку на сервере оставили на потом. Сеть закрывается за вечер, а миграция сборки на CI — отдельный проект, и ждать её, оставив базу открытой, незачем.
Нужно дать доступ к базе подрядчику, не открывая её?
Опишите, кто и откуда подключается, — подскажу туннель или маршрут через прокси под вашу схему.
// загружаю комментарии…