Безпека Docker – Безкорінний режим (Частина 3)

17.05.2024 3 хвилин Автор: D2-R2

Це третя частина статті про безпеку Docker. У цій частині ми розглянемо безкорінний режим, який дозволяє запускати Docker контейнери без прав адміністратора. Це забезпечує додатковий рівень захисту та знижує ризики, пов’язані з потенційними вразливостями. Навчіться правильно налаштовувати та використовувати безкорінний режим для підвищення безпеки ваших контейнерів.

Безкорінний режим

Зменшення ризику використання вразливостей у Docker Daemon і запущених контейнерах має вирішальне значення. Docker пропонує безкорінний режим, що забезпечує додатковий рівень безпеки. Основною відмінністю між цим режимом і методами ізоляції, описаними раніше, є відсутність привілеїв root для Docker Daemon на Docker Host у режимі «rootless». Тоді вся система Docker працює в так званому «простір користувача».

Звучить вражаюче, правда? Можливо, вам цікаво, чому ми раніше обговорювали інші методи, коли режим «без коріння» здається кращим. Так, цей режим забезпечує вищий рівень безпеки, але він не позбавлений обмежень. Поточний список обмежень можна знайти на наступній сторінці:

Ми не будемо надто глибоко вникати в це, оскільки це динамічна тема, яка розвиватиметься з розробкою Docker. Однак варто зазначити, що на даний момент використання «rootless» режиму виключає можливість використання AppArmor (я розповім про цей інструмент більш детально в наступній частині статті) і може вимагати додаткових кроків налаштування, якщо ви плануєте запускати контейнери з незвичайними налаштуваннями. Перш ніж зважитися на впровадження режиму «rootless», ознайомтеся зі списком обмежень.

Встановлення режиму «Rootless» складається з двох основних кроків. Спочатку нам потрібно вимкнути запущений на даний момент Docker Daemon, а потім перезапустити сервер (лістинг 23, малюнок 38).

sudo systemctl disable --now docker.service docker.socket
sudo reboot

Лістинг 23. Встановлення режиму Rootless.

Малюнок 38. Безкоренева конфігурація – вимкнення Docker Daemon і перезапуск сервера.

Потім ми переходимо до виконання dockerd-rootless-setuptool.shсценарію. Якщо ви встановили офіційний пакет Docker, ви повинні знайти цей сценарій у /usr/binкаталозі.

Перед початком інсталяції нам ще потрібно встановити необхідні залежності. У випадку Ubuntu це пакет uidmap(лістинг 24).

sudo apt install -y uidmap

Лістинг 24. Встановлення пакета uidmap.

Однак перш ніж приступити до установки режиму «Rootless», варто звернути увагу на один важливий аспект. Існує висока ймовірність того, що спроба розпочати процес встановлення на цьому етапі може призвести до помилки або попередження, вміст якого показано на малюнку № 39.

Малюнок 39. Перший запуск сценарію dockerd-rootless-setuptool.sh.

Це означає, що кожного разу нам доведеться вручну запускати Daemon, одночасно видаючи команду з останнього рядка на малюнку 39. Це незручне та неоптимальне рішення. Однак ми можемо вирішити цю проблему, використовуючи обхідний шлях, запропонований одним із користувачів Docker (лістинг 25).

sudo apt install systemd-container
sudo machinectl shell reynard@

Лістинг 25. Встановлення інструменту machinectl.

machinectlце інструмент для взаємодії з машинами та контейнерами у systemdсумісній системі.

Нарешті настав час запустити режим Rootless. Ми досягаємо цього, вводячи команди з лістингу 26 (Малюнок 40):

cd /usr/bin
dockerd-rootless-setuptool.sh install

Лістинг 26. Початок процесу встановлення режиму Rootless.

Малюнок 40. Запуск Docker Daemon у режимі Rootless.

Начебто все працює. Ми бачимо, що Docker Daemon запущено, і він знаходиться в режимі Rootless! Ми можемо підтвердити це, перевіривши список запущених процесів (Малюнок 41).

Малюнок 41. Список запущених процесів.

Усі процеси, пов’язані з демоном Docker, запускаються від імені reynardкористувача. Звичайно, у практичному застосуванні доцільно додатково надати цьому користувачеві дозволи групі sudoабо іншим привілейованим групам, щоб ця модифікація мала повне значення.

Швидше за все, на початку роботи з демоном Rootless ви зіткнетеся з помилкою, як показано на малюнку 42.

Малюнок 42. Проблема з підключенням до Docker Daemon.

Щоб вирішити цю проблему, нам потрібно ненадовго повернутися до результатів dockerd-rootless-setuptool.shроботи скрипта. Одне з останніх повідомлень, які він повернув, виглядало так, як на малюнку 43.

Малюнок 43. Змінні середовища, які потрібно встановити.

Як запропоновано, нам потрібно встановити відповідні змінні середовища. Швидше за все, змінна $PATHвже встановлена ​​в системі – ми можемо перевірити це за допомогою команди echo $PATH. Згодом нам потрібно встановити DOCKER_HOSTзмінну. Якщо ми просто виконаємо команду з лістингу 26 у консолі, це вирішить проблему, але лише тимчасово.

export DOCKER_HOST=unix:///run/user/1001/docker.sock

Лістинг 27. Змінні середовища, які потрібно встановити.

Рекомендовано додати цей запис, наприклад, до файлу конфігурації .bashrcабо .zshrc(файлу конфігурації системної оболонки). Після того, як ви це зробите, вам все одно потрібно перезавантажити конфігурацію ( source .bashrc).

Крім того, було б доцільно виконати наступні команди з лістингу 28.

systemctl --user enable docker
sudo loginctl enable-linger $(whoami)

Лістинг 28. Увімкнення функції «linger».

Перший відповідає за запуск демона Docker разом із запуском системи. Хоча команда loginctl enable-lingerвикористовується в системах на основі systemd. Він призначений для того, щоб дозволити користувачеві залишати свої служби та програми працювати у фоновому режимі після виходу з системи. У стандартній конфігурації, коли користувач виходить із системи, усі його процеси припиняються. Увімкнення функції «затримка» змінює цю поведінку.

Останньою дією, яку ми маємо зробити, є вибір відповідного контексту, у якому працюватиме клієнт Docker. Ми можемо зробити це, видавши команду docker context use rootless(Малюнок 44).

Малюнок 44. Зміна контексту Docker.

Тепер все повинно працювати!

Малюнок 45. Підтвердження роботи контейнера в режимі Rootless.

Якщо підсумувати розділ статті про безкорінний режим, то це безперечно рішення, яке варто розглянути та застосувати. Однак пам’ятайте, що це не вирішить усіх проблем і не усуне вразливості безпеки, наприклад, у Docker Daemon, самих контейнерах або програмах, які в них працюють. Режим Rootless допоможе вам обмежити ризик використання потенційних вразливостей.

Контейнерний зв’язок (ізоляція контейнера)

За замовчуванням контейнери, запущені на певному хості Docker, можуть спілкуватися один з одним через стандартний мережевий стек. Це відбувається тому, що запущені контейнери призначаються мережевому інтерфейсу за замовчуванням, bridge. Ми можемо легко перевірити це, запустивши другий контейнер, наприклад, під назвою ubuntu2(за допомогою команди docker run -itd --name ubuntu2 ubuntu:22.04), і перевіривши, чи можемо ми встановити зв’язок із контейнером, який був запущений раніше (тобто ubuntu1).

Ми почнемо спілкування за допомогою netcatінструменту. Netcat прослуховуватиме порт 4444/tcpу контейнері під назвою ubuntu1. Потім ми спробуємо підключитися до цього контейнера з ubuntu2контейнера, а саме на порту 4444/tcp. Щоб полегшити це, нам потрібно встановити netcatпакет в обидва контейнери, використовуючи репозиторій Ubuntu за замовчуванням. Зображення контейнерів мають дуже обмежений список пакетів порівняно зі стандартними інсталяціями. Ми повинні виконати команду apt update && apt install -y netcatна обох контейнерах, тобто на ubuntu1і ubuntu2.

Малюнок 46. Встановлення пакета netcat.

Тепер ми відкриємо два термінали, використовуючи для цього дві вкладки. На першій вкладці (верхній) ми запустимо netcat, який буде слухати порт 4444/tcp. Це буде контейнер під назвою ubuntu1. Потім з другого контейнера ми спробуємо підключитися до контейнера ubuntu1. Але перш ніж це зробити, нам потрібно перевірити IP-адреси, призначені обом машинам. Зазвичай ми робимо це за допомогою ip addrкоманди або старішої ifconfigкоманди. Однак через обмежену кількість пакетів ці команди недоступні. Натомість ми можемо використати менш поширену команду hostname -I.

Малюнок 47. Перевірка IP-адрес.

Команда command docker container exec <container name> <command>запускає <command>в контексті контейнера <container name>. Завдяки цьому ми дізналися, що контейнери мають призначені IP-адреси відповідно 172.17.0.2і 172.17.0.3(мал. 47).

Настав час запуститися netcatв режимі прослуховування на першому контейнері. Ми можемо зробити це, виконавши netcat -nlvp 4444команду. У другому вікні другого контейнера давайте підготуємо команду для виконання, яка є: echo test | netcat 172.17.0.2 4444. Команда echoчерез конвеєр передає текст «test» через конвеєр. Цей текст буде надіслано netcatна сервер 172.17.0.2ubuntu1) після встановлення з’єднання на порту 4444/tcp(Малюнок 48).

Малюнок 48. Підготовка прослуховування netcat.

Відразу після виконання команди в другій (нижній) консолі в контейнер №1 було відправлено текст «test» (мал. 49).

Малюнок 49. Підтвердження встановлення з’єднання та передачі даних.

Як продемонструвала ця вправа, ми змогли встановити мережеве з’єднання між двома контейнерами без жодних перешкод. Важко передбачити всі можливі випадки, однак особа, відповідальна за безпеку середовища Docker, має знати, що конфігурація за замовчуванням дозволяє встановлювати такі з’єднання. Така конфігурація не рекомендована, тому вам слід розглянути можливість реалізації принаймні однієї з двох рекомендацій щодо цього.

Перший, досить «радикальний» варіант — це глобальне відключення можливості зв’язку між контейнерами за допомогою стандартного мережевого стеку. Ми можемо зробити це, налаштувавши iccпараметр (абревіатура від inter-container communication ) на false. Найпростіше встановити цей параметр у daemon.jsonфайлі, який ми вже мали можливість відредагувати.

Лістинг 29 показує приклад файлу конфігурації з iccвимкненою опцією ( cat /etc/docker/daemon.json):

{
  "userns-remap": "default",
   "icc": false
}

Лістинг 29. Вимкнення опції icc у файлі daemon.json.

Після впровадження змін необхідно перезавантажити конфігурацію Docker Daemon, а потім перезапустити контейнери для застосування нової конфігурації (Малюнок 50).

Малюнок 50. Редагування конфігурації та перезапуск демона Docker.

Тоді ми можемо перевірити, чи впроваджені зміни мали бажаний ефект. Цього разу  додавши -w 5параметр до netcatкоманди, запущеної в другій консолі. Він визначає час, через який netcatслід припиняти спроби підключення, якщо вони не закінчуються успішно – в даному випадку через 5 секунд (мал. 51).

Малюнок 51. Повторна спроба підключення.

Як бачите, цього разу текст test2не потрапив до контейнера №1. Внесені нами зміни в конфігурацію дали бажаний ефект!

Повне блокування можливості встановлення з’єднань між контейнерами не завжди буде можливим. Часто, за задумом, наше середовище має забезпечувати зв’язок між контейнерами. Наприклад, контейнер, у якому виконується програма, повинен встановити з’єднання з базою даних, що працює в іншому контейнері. Отже, чи існують інші методи сегментації внутрішньої мережі Docker?

Давайте на мить відновимо попередню конфігурацію середовища, тобто видалимо з файлу daemon.jsonзапис, пов’язаний з iccпараметром, або змінимо значення цього поля на true. Щоб застосувати зміни, нам все одно потрібно перезавантажити демон Docker (лістинг 30, малюнок 52).

sudo systemctl restart docker

Лістинг 30. Перезапуск демона Docker.

Малюнок 52. Повернення до попередньої конфігурації середовища.

Давайте тепер оживимо два контейнери, які ми мали можливість використовувати раніше, а саме ubuntu1і ubuntu2. Потім ми перевіримо, як виглядає мережева конфігурація цих контейнерів. Ми зробимо це за допомогою команд docker inspectі docker network(лістинг 31).

docker start ubuntu1 ubuntu2
docker inspect ubuntu1 --format '{{ .Name }}: {{ .NetworkSettings.Networks }}'
docker inspect ubuntu1 --format '{{ .Name }}: {{ .NetworkSettings.Networks.bridge.NetworkID }}'
docker inspect ubuntu2 --format '{{ .Name }}: {{ .NetworkSettings.Networks }}'
docker inspect ubuntu2 --format '{{ .Name }}: {{ .NetworkSettings.Networks.bridge.NetworkID }}'
docker network ls

Лістинг 31. Перевірка конфігурації мережі контейнерів.

Малюнок 53. Перевірка конфігурації мережі контейнерів.

Що нового ми дізналися (мал. 53)? Обом контейнерам призначається однаковий мережевий інтерфейс, тобто bridge. За допомогою команди docker network lsми можемо побачити цей інтерфейс у списку, який використовує Docker Daemon.

Інтерфейс bridgeу Docker — це мережа за замовчуванням, яка дозволяє контейнерам спілкуватися один з одним на одному хості. Завдяки інтерфейсу bridgeконтейнери також можуть спілкуватися із зовнішнім світом. Інтерфейс bridgeстворюється під час встановлення Docker.

Під час запуску контейнера ми маємо можливість вказати, якому інтерфейсу чи інтерфейсам його слід призначити. Для цього використовується параметр network. Давайте проведемо наступний експеримент: ми створимо дві мережі: network1і network2(лістинг 31, малюнок 54), ми призначимо два вже діючі контейнери до network1, і ми додамо новостворений контейнер з іменем ubuntu3до network2(лістинг 32, малюнок 55).

docker network create network1
docker network create network2

Лістинг 31. Створення нових мереж.

Малюнок 54. Створення нової мережі.
docker network disconnect bridge ubuntu1
docker network disconnect bridge ubuntu2
docker network connect network1 ubuntu1
docker network connect network1 ubuntu2
docker network ls

Лістинг 32. Створення нової мережі.

Малюнок 55. Призначення контейнерів до мережі.

Тепер настав час для останньої частини головоломки, створення контейнера під назвою ubuntu3. Одразу під час його створення ми призначимо його вказаній мережі network2(лістинг 33, малюнок 56).

docker run -itd --network=network2 --name ubuntu3 ubuntu:22.04
docker inspect ubuntu3 --format '{{ .Name }}: {{ .NetworkSettings.Networks }}

Лістинг 33. Створення третього контейнера.

Малюнок 56. Створення контейнера ubuntu3.

Поки ніби все в порядку. Контейнери ubuntu1і ubuntu2працюють у network1, а ubuntu3функції в network2. Перевіримо тепер, чи ubuntu1здатний контейнер встановити ICMP-з’єднання ( ping) з контейнерами ubuntu2та ubuntu3(лістинг 34, малюнок 57).

# in the context of the Docker Host
docker container exec ubuntu2 hostname -I
docker container exec ubuntu3 hostname -I
docker exec -it ubuntu1 bash

# in the context of the ubuntu1 container
apt install -y iputils-ping

Лістинг 34. Перевірка IP-адрес контейнерів і встановлення пакета iputils-ping.

Малюнок 57. Встановлення необхідних пакетів.
Малюнок 58. Перевірка можливості встановлення з’єднання.

Успіх! Контейнер #1 може встановити з’єднання з контейнером #2 ( 172.18.0.3), але більше не може з’єднатися з контейнером #3 ( 172.19.0.2) (Малюнок 58). Завдяки такому підходу ми можемо ізолювати різні «середовища», якщо запускаємо різні проекти в одному демоні Docker.

Режим лише для читання

Docker дозволяє запускати контейнер у режимі лише для читання. Цей режим запобігає запису файлів на диск, створенню нових або зміні існуючих, навіть у каталогах, які зазвичай пов’язані з місцями, де запис завжди практично можливий (наприклад, /tmp). Щоб досягти цього ефекту, нам потрібно додати --read-onlyпрапорець до docker runкоманди, наприклад, так (лістинг 35, малюнок 59):

# Commands to be executed in the context of Docker Host
docker run -itd --read-only --name ubuntu-ro ubuntu:22.04
docker exec -it ubuntu-ro bash

# Command to be executed in the context of the ubuntu-ro container
touch /tmp/1

Лістинг 35. Запуск контейнера в режимі лише для читання.

Малюнок 59. Запуск контейнера в режимі лише для читання.

Використання цього режиму здається цікавим підходом, який може значно обмежити наслідки використання вразливостей безпеки програми, що працює в контейнері. Якщо зловмисник не має можливості створити новий файл на диску, це не закриє всі потенційні шляхи експлуатації, але значно обмежить його можливості. Звісно, ​​не кожен контейнер зможе працювати в такому режимі, але якщо це можливо, то обов’язково варто розглянути це.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.