Приветствую всех! В своей прошлой и по совместительству первой статье я рассказывал про упаковку приложения в докер контейнеры (ссылка на нее находится в конце этой статьи). В комментариях мне сделали замечание, что я не упомянул про защиту приложения и запуск от non-root. Что ж, исправлюсь и сделаю это в отдельной статье. Напомню, что я написал простое приложение для голосование за лучший ресторан и попытался по простому объяснить, как произвести его контейнеризацию. Также уточню, что упор я делаю именно на упаковку приложения в докер контейнеры, а не на бизнес-логику и UI.
Есть несколько релизов:
-
https://github.com/codyRhett/restaurantVote/tree/release/1.0.2 — версия проекта для запуска локально на компьютере. Для запуска необходимо установить postgresql и создать БД
-
https://github.com/codyRhett/restaurantVote/tree/release/1.0.1 — версия для запуска в контейнерах
Но! конкретно в этой статье речь пойдет о версии:
-
https://github.com/codyRhett/restaurantVote/tree/release/1.0.3 — версия для запуска в контейнерах от непривилегированного пользователя
Запуск контейнера от непривилегированного пользователя (non-root) — это критически важный слой защиты от эксплойтов. Разберем, как это работает, на примерах и технических деталях. DeepSeek дает вот такое лаконичное определение эксплоиту:
Эксплоит — это программный код, скрипт или метод, который использует уязвимость в системе, приложении или сети для выполнения несанкционированных действий. Это может быть кража данных, получение контроля над устройством, нарушение работы системы и т.д.
Но мы же понимаем, что лучше один раз увидеть, чем семь раз услышать. Возьмем кейс, который в идеале никогда не должен произойти в вашем приложении. Для этого вернемся к предыдущей версии проекта, а именно клонируем релиз 1.0.1 (https://github.com/codyRhett/restaurantVote/tree/release/1.0.1)
Обратите внимание на REST контроллер UserRestController, а конкретнее на его тестовый endpoint:
@GetMapping("/execute") public String executeCommand(@RequestParam("cmd") String cmd) { log.debug("executeCommand"); try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); return reader.lines().collect(Collectors.joining("\n")); } catch (IOException e) { return "Error: " + e.getMessage(); } }
В качестве RequestParam передается строка cmd, которая преобразуется в исполняемую команду. И злоумышленник может передать туда, например, команду rm -rf с указанием пути на системные файлы и спокойно удалить их, если контейнер запущен от привилегированного пользователя — root. Повторюсь:
… что ни один нормальный разработчик не сделает такого уязвимого эндпоинта.
Это я сделал чисто для примера. А способов выполнить данный скрипт много, в том числе просто зайти в контейнер через docker exec и выполнить эту команду там.
Как же это будет выглядеть?
Собираем war файл через mvn package. Далее исполняем docker-compose up и ждем пока создадутся контейнеры. После этого выполняем классический набор запросов в командной строке:
-
docker ps — получаем список запущенных контейнеров
-
docker exec -it 49d672f4e359 /bin/bash — заходим в командную строку нашего основного запущенного контейнера
-
cd ../ — переходим в основную директорию
-
делаем GET запрос в эндпоинт:
curl http://localhost:8080/api/user/execute?cmd=rm%20-rf%20/tmp/%20--no-preserve-root
Кстати, этот же запрос можно кинуть через браузер. Этот GET запрос передает команду rm -rf /tmp —no-preserve-root, которая удаляет папку tmp вместе с ее содержимым.
-
Далее исполняем команду ls и убеждаемся, что папки tmp больше нет. Тоже самое можно проделать с остальными папками. Думаю, не надо объяснять, почему это плохо. И вообще с большинством системных папок можем делать, что угодно — удалять, перезаписывать и т д. И все это потому, что контейнер запущен от root.
Как обезопасить себя от такого поведения?
Клонируем себе релиз 1.0.3 (https://github.com/codyRhett/restaurantVote/tree/release/1.0.3). Отличие от предыдущих релизов заключается в двух файлах — dockerfile и docker-compose.yml. Я их переписал для безопасного запуска контейнеров, чтобы усложнить задачу злоумышленников взломать наш сервис.
Начнем с того, что необходимо немного модифицировать dockerfile, чтобы наглядно продемонстрировать как работают привилегии. Приведенный ниже код демонстрирует конфигурацию для запуска от non-root:
FROM adoptopenjdk/openjdk11:ubi # Создаем системного юзера и группу с явным UID/GID RUN useradd -r -u 1001 appuser && \ # создаем директорию app mkdir /app && \ # Настройка прав доступа к директории /app \ # 7 (владелец): Чтение + запись + выполнение (rwx). # 5 (группа): Чтение + выполнение (r-x). # 0 (остальные): Нет прав (---). chmod 750 /app && \ # Назначение владельца директории /app chown appuser:appuser /app && \ mkdir -p /app/data/logs && \ chmod -R 750 /app/data/logs && \ chown -R appuser:appuser /app/data/logs # укзываем рабочую дирректорию WORKDIR /app # Укажите переменную окружения для пути к логам ENV LOG_DIR=/app/data/logs # Копируем файлы с правами ARG WAR_FILE=target/restaurantVote-1.0-SNAPSHOT.war # Копирование файлов с назначением владельца и группы COPY --chown=appuser:appuser ${WAR_FILE} /app/application.war COPY --chown=appuser:appuser src/main/webapp /app/webapp # Явное переключение пользователя и рабочей директории # Используем UID вместо имени. Запуск от имени нового созданного юзера USER 1001 ENTRYPOINT ["java","-jar","/app/application.war"]
Что же тут нового и необычного?
-
Мы создаем группу и пользователя appuser и присваиваем ему идентификатор 1001
-
Даем права папкам app, где лежит исполняемый файл и app/data/logs, куда будем складывать логи (chmod — дает права доступа к файлам и директориям, chown — смена владельца папок, потому что по умолчанию владельцами папок является root)
-
Копируем файлы программы в новые папки с назначением владельца и группы
-
Явно указываем от какого пользователя осуществляем запуск контейнера USER 1001
Не буду расписывать тут подробно про то, какие бывают права. Если вкратце, то наши права зашифрованы числом 750. Первая цифра — права на владельца, вторая — на группу, третья на всех остальных. В нашем случае — 1. все права, 2. чтение+выполнение и 3. нет прав соответственно. В докер файле я привел просто пример, как можно использовать права непривилегированного пользователя. Можно создавать папки, файлы и настраивать к ним такие права доступа, какие пожелаем.
Следующий этап — это уже настройка непосредственно контейнера
Создание DOCKER-COMPOSE
Приведенный ниже код демонстрирует docker-compose.yml для безопасного запуска контейнеров
version: '2' services: app: image: 'restaurant' build: . security_opt: - no-new-privileges cap_drop: - ALL volumes: - ./host_logs:/app/data/logs # папка на хосте -> контейнер - app_volume:/app/data/logs user: "1001:1001" container_name: app ports: - "8080:8080" depends_on: - db environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5433/restaurant - SPRING_LIQUIBASE_URL=jdbc:postgresql://db:5433/restaurant - SPRING_DATASOURCE_USERNAME=postgres - SPRING_DATASOURCE_PASSWORD=1234 - SPRING_JPA_HIBERNATE_DDL_AUTO=update db: image: 'postgres:latest' container_name: db build: . user: "999:999" security_opt: - no-new-privileges cap_drop: - ALL environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=1234 - POSTGRES_DB=restaurant - PGPORT=5433 volumes: app_volume:
На что тут стоит обратить внимание?
security_opt: - no-new-privileges
Эта опция запрещает процессам внутри контейнера повышать свои привилегии (например, через sudo или su). Даже если злоумышленник получит доступ к контейнеру, он не сможет стать root.
cap_drop: - ALL
Эта опция отключает все привилегии ядра Linux для контейнера.
-
Изменить владельца файлов (CAP_CHOWN).
-
Работать с сетевыми настройками (CAP_NET_ADMIN).
-
Монтировать файловые системы (CAP_SYS_ADMIN).
user: “1001:1001”
Эта опция говорит о том, что контейнер запускается от пользователя с идентификатором 1001 (которого мы создали на предыдущем шаге)
Теперь проделываем те же действия, как и для релиза 1.0.1. НО! Обязательно выполнить билд без использования кэша через команду:
docker compose build --no-cache
Это делаем для того, чтобы не наступать на те же грабли, что и я. В кэше сохранился мой предыдущий образ, и для создания контейнера использовался именно он, и я долго не мог понять, что происходит.
После этого запускаем контейнеры, заходим в restaurant и через команду ls -l можем видеть владельцев файлов и директорий в корне контейнера. Также через whoami можно посмотреть пользователя, от которого запущен контейнер.
Далее заходим в bash контейнера через docker exec и кидаем GET запрос на удаление папки tmp:
curl http://localhost:8080/api/user/execute?cmd=rm%20-rf%20/tmp/%20--no-preserve-root
Что же мы видим? Папка tmp не удалилась, потому что владельцем этой папки является root, а контейнер запущен от пользователя appuser, у которого мы сильно урезали права. Также попробуем напрямую удалить файлы из папки app.
Видим, что мы успешно удалили файл application.war, потому что владельцем папки является пользователь appuser, от которого и запущен контейнер.
Собственно, это все!
Итого: Если злоумышленник и зайдет в контейнер, то он сможет удалить только то, что не очень важно для нас (благодаря гибкой настройке прав). Например, как в данном случае, не сможет удалить системные файлы, владельцем которых является root. В зависимости от ваших потребностей вы можете ставить свои ограничения, но запуск контейнера ВСЕГДА должен осуществляться НЕ от Root!
Вы спросите, а какой смысл, если пользователь, например, зайдет в контейнер из под root и вообще возможно ли такое? А я отвечу — это проблемы инфраструктуры и такого в принципе не должно быть. А запуск не от привилегированного пользователя — просто необходимая ступень защиты, которая должна усложнить жизнь злоумышленникам. И эту защиту можно обойти, но на это нужно время.
Использование appuser подразумевает, что по умолчанию в контейнере нет пользователя root, а все процессы запущены с минимальными привилегиями.
Моя предыдущая статья, где подробно на примерах рассказываю, что такое контейнеризация и как упаковывать приложения в докер контейнеры:
Docker для начинающих: простое развертывание приложения за несколько шагов
ссылка на оригинал статьи https://habr.com/ru/articles/912326/
Добавить комментарий