В первой части мы нашли на общем сервере 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. Когда исправление бесплатное, причин оставлять дыру нет.

Порядок перевода живого сервиса

  1. подключить сервис к сети прокси, поставить метки маршрута — проверить, что он отвечает по доменному имени;
  2. перевести клиентов (фронт, другие сервисы) на доменное имя или имя контейнера вместо адрес:порт;
  3. убрать ports или перевесить на 127.0.0.1, пересоздать контейнер;
  4. внешним сканом убедиться, что порт закрыт, а сервис по-прежнему отвечает через прокси;
  5. базе без ключа — ключ, и клиентам — тот же ключ в переменной окружения.

Мы переводили так фронт и бэкенд нового приложения: сначала сеть, сборку на сервере оставили на потом. Сеть закрывается за вечер, а миграция сборки на CI — отдельный проект, и ждать её, оставив базу открытой, незачем.

Нужно дать доступ к базе подрядчику, не открывая её?

Опишите, кто и откуда подключается, — подскажу туннель или маршрут через прокси под вашу схему.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.