Порты Docker: что вы на самом деле открываете?

Запуск всего приложения, его окружения и всех его зависимостей по одной команде  —  разве это не мечта? Для тех, кто знает, как работает Docker, это невероятный инструмент для безопасного развертывания приложений.

Однако для начинающих в нем таится ужасающий риск  —  обойти по неведению все настройки брандмауэра и выложить контейнеры в открытый доступ в интернет. Недавно я читал документацию Docker по работе в сетях, и огромное оранжевое предупреждение напомнило мне о том, что застало меня врасплох, когда я только начинал работать с Docker.

Хотя данная статья посвящена проблеме безопасной работы в Docker, моя основная претензия не связана только с Docker. Есть веские причины, по которым Docker работает именно так, как работает. Он очень неплохо документирован. На странице, посвященной сетевым технологиям, вы увидите строгое предупреждение, а, опустившись немного ниже по странице, найдете следующий абзац, в котором прямо говорится о взаимодействии Docker и брандмауэров:

Это означает, что при публикации порта через Docker этот порт будет открыт независимо от того, какие правила указаны в настройках брандмауэра. Если вы хотите, чтобы правила применялись даже при публикации порта через Docker, необходимо добавить эти правила в цепочку DOCKER-USER.


Эта статья  —  скорее размышление по поводу превалирующих установок и реакций разработчиков на подобные проблемы. Складывается впечатление, что индустрия разработки ПО специально ориентирована на формирование специалистов, которые используют инструменты, не понимая, как они работают, а затем подвергаются нападкам, когда неизбежно попадают впросак.

Для создания и развертывания приложения нет особых препятствий. В то же время, чтобы построить здание, вам придется пройти многочисленные проверки и процедуры. Планы должны быть утверждены до начала строительства согласно требованиям безопасности, экологии и зонирования. Вы должны соблюсти всевозможные правила, например противопожарные нормы. Прежде чем заселять здание, необходимо провести его проверку на соответствие требованиям, особенно в отношении таких важных систем, как электро-, водо- и газоснабжение. Зато развертывать программное обеспечение не помешает никто. Такая свобода действий является преимуществом нашей профессии, но это палка о двух концах.

В идеале в профессиональной среде должны осуществляться проверки кода опытными разработчиками, аудиты безопасности, тесты на проникновение и другие системы контроля и противовесов, обеспечивающие безопасность приложений. Но далеко не каждый разработчик-одиночка, написавший приложение ради своего удовольствия, обладает достаточным опытом для проведения таких проверок или хотя бы знает об их необходимости. Именно к таким разработчикам я и обращаюсь в этой статье.

Проблема

Расскажу о том, когда впервые столкнулся с этой проблемой. Я работал с небольшой базой данных в MySQL на своем компьютере, сосредоточившись на общедоступном наборе данных для одного из проектов. База данных работала на порту 3306, но брандмауэр был настроен на блокировку всех входящих соединений. У меня был небольшой Python-скрипт, который я мог запускать для загрузки данных из первоисточника и обновлять свою небольшую локальную базу данных, когда мне нужны были свежие данные для работы. Со временем я перешел от установленной версии MySQL к Docker-контейнеру. Чтобы Python-скрипты выполнялись без необходимости вносить в них какие-либо изменения, я просто добавил аргумент `-p 3306:3306` в команду `docker run`.

Точно помню, что читал ранее упомянутое большое оранжевое предупреждение в документации Docker по работе в сетях:

Открытие портов контейнера по умолчанию небезопасна. Это означает, что при открытии портов контейнера они становятся доступны не только Docker-хосту, но и внешнему миру.

“Хорошо, что есть большое оранжевое предупреждение. Стоит еще раз удостовериться в том, что настройки моего брандмауэра блокируют это”.

Однажды я отлаживал совершенно не связанную с сетью проблему  —  кажется, я работал в другой комнате на своем ноутбуке, пытаясь отладить общий доступ Samba или что-то в этом роде. Запустил nmap, чтобы проверить, какие порты открыты на моем компьютере, и испытал шок, увидев открытый порт 3306, хотя в брандмауэре было установлено правило, запрещающее соединения по этому порту.

К счастью, мое внешнее воздействие ограничивалось домашней сетью. База данных была защищена паролем (хотя и довольно слабым) и в любом случае содержала только открытую информацию. Но я с ужасом подумал о том, сколько разработчиков могут запускать базы данных приложений в контейнерах с открытыми портами, невольно открывая доступ к своим данным всему интернету.

Похоже, это волнует не одного меня

Очевидно, что я не одинок в своей обеспокоенности этой проблемой. Подтверждением тому являются сообщения об ошибках, такие как это и это, обсуждения и статьи на различных платформах и даже проект по устранению данной проблемы. Если почитать обсуждения по поводу сообщений об ошибках, то можно обнаружить бесконечные споры о том, является ли это проблемой вообще.

Как я уже говорил, меня раздражает не то, что Docker работает таким образом, а преобладающее пренебрежительное отношение к неопытным разработчикам, демонстрируемое большинством участников подобных обсуждений. Приведенные ниже комментарии на Reddit были написаны 9 лет назад, но прекрасно иллюстрируют мою точку зрения:

“Я считаю, что если вы используете docker, то должны понимать: UFW  —  это всего лишь фронтенд для iptables и любой процесс с соответствующими привилегиями может изменить цепочку iptables”.

“Я почти уверен в том, что у пользователей Docker должно хватить ума, чтобы разобраться в нем самостоятельно. Надеюсь, что не ошибаюсь”.

Такая позиция неконструктивна. Конечно, было бы здорово, если бы каждый любитель-самоучка, развертывающий приложение, хорошо разбирался в продвинутых практиках безопасности. Но реальность такова, что в нашей среде нет никаких барьерных условий и инструкций для обеспечения этого.

Попутное замечание об обучении

Вы не знаете того, чего не знаете.

Мы лучше всего учимся через обратную связь. Если вы пишете код, который дает сбой, то сразу понимаете, что где-то ошиблись. Исправление кода и его работа дают положительную обратную связь, укрепляя уверенность в подходе к решению конкретной проблемы. Благодаря такой незамедлительной обратной связи можно легко научиться писать код, просто начав работать над проектом и решая возникающие проблемы. Но если код не поддерживается или небезопасен, вы получите обратную связь только через несколько месяцев или лет, если вообще получите. А это означает, что такие понятия, как безопасность, нелегко освоить самостоятельно, методом проб и ошибок.

Решение специфической проблемы с Docker

Если вы еще здесь, то, возможно, ищете способ защитить свои контейнеры. Если это так, благодарю вас за то, что не остались равнодушным к проблеме с Docker. Вот несколько подходов к ее решению.

  1. Оптимальным выходом из положения является отказ от публикации портов, если только вы не хотите публично открыть какой-то порт. Для межконтейнерного взаимодействия лучше всего создать выделенную сеть Docker специально для этого трафика и убедиться, что контейнеры подключены к этой сети. Основное ограничение заключается в том, что эта технология не работает для процессов, не запущенных внутри контейнера. Не все приложения подходят для работы с контейнерами, и иногда требуется запустить на хосте процесс, который должен получить доступ к контейнеру.
  2. Распространенным решением (которое не рекомендуется в официальной документации) является установка значения iptables в false в /etc/docker/daemon.json. Это не позволит Docker добавить свои сетевые правила, а значит, сетевая работа в контейнерах вообще исключается. Если вы все же хотите пойти по этому пути, то придется добавить правила вручную. При выполнении этой нетривиальной задачи легко ошибиться  —  без понимания того, что вы делаете, можно получить небезопасную конфигурацию или, наоборот, заблокировать доступ к серверу.
  3. Более эффективным подходом, не нарушающим сетевую работу контейнеров, будет установление привязки адреса по умолчанию только к локальному. Это можно сделать, задав в файле /etc/docker/daemon.json значение ip равным 127.0.0.1. На мой взгляд, это нужно было сделать по умолчанию с самого начала, и контейнеры были бы доступны извне хоста только в случае, если они явно сконфигурированы для этого. Но сейчас уже слишком поздно что-то менять. Я использовал именно такой подход, но у него есть один недостаток. Он не предполагает переносимости контейнеров. Если вы захотите запустить контейнер на другом хосте, вам нужно не забыть изменить конфигурацию. В идеале контейнер должен работать одинаково на любом хосте. Контейнер, который по своей сути небезопасен (вы можете просто забыть поменять настройки на хосте)  —  не лучшая идея.
  4. Лучшей практикой должно стать привыкание к явной привязке IP-адресов в командах Docker и в файле docker-compose.yml. Вы должны взять за правило никогда не писать -p 3306:3306, вместо этого писать -p 127.0.0.1:3306:3306, а в тех случаях, когда нужно открыть порт для внешнего мира, явно указывать его: -p 0.0.0.0:3306:3306. Именно такой подход я применяю тогда, когда сети Docker не подходят.

Теперь возникает вопрос, на который я не знаю ответа: как исправить множество руководств, непроизвольно рекомендующих пользователю открыть свою базу данных всему миру?


Решение проблемы в целом

Несколько лет мне хотелось разразиться тирадой по этому поводу. Позиция разработчиков, как правило, вызывает у меня раздражение. Слишком распространенным стало мышление, основанное на принципе: “Если ты не знаешь всего, что знаю я, тебе не стоит этим заниматься”.

Однако наша профессия тем и отличается от остальных, что не все приходят в нее, чтобы зарабатывать деньги, многие  —  просто ради удовольствия. И меня беспокоит то, что разработчики-любители, несмотря на благонамеренное стремление соблюсти меры безопасности, рискуют упустить что-нибудь при размещении приложения на VPS или резервном ноутбуке, используемом в качестве сервера.

Решение проблемы в целом требует двусторонних усилий.

  1. Неопытные разработчики, желающие просто что-то создавать, продолжайте в том же духе. Читайте как можно больше, не пропускайте официальную документацию по используемым инструментам и впитывайте как можно больше информации. И помните: вы не знаете того, чего не знаете, и лучший способ научиться  —  продолжать пробовать новое.
  2. Опытные разработчики (особенно те, кто участвует в разработке широко распространенных инструментов с открытым исходным кодом, принимая ключевые проектные решения о поведении по умолчанию), вы обязаны защищать тех, кто никогда не будет читать документацию, полагаясь на краткие руководства, написанные такими же энтузиастами. Ваш долг  —  выбирать безопасные настройки по умолчанию (возможно, для Docker это уже поздно, но не для нового инструмента, над которым вы работаете). Когда же разработчики-любители применяют ваши инструменты не по назначению, старайтесь не относиться к ним пренебрежительно и неуважительно. Напротив, используйте эти ситуации для обучения и воспитания, а возможно, и для самообразования.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Brendan Gray: Docker Ports: What Are You Really Publishing?

Предыдущая статьяОбработка событий в JavaScript: всплытие, перехват, делегирование и распространение событий
Следующая статьяМеханизм повторных попыток в Spring Boot: @Retryable и @Recover