Безпека Docker – Надання та скасування дозволів (Частина 2)

15 травня 2024 3 хвилин Автор: D2-R2

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

Надання та скасування дозволів

За замовчуванням Docker запускає контейнери з певним стандартним набором дозволів. У таблиці представлено список цих дозволів.

Таблиця 1. Список стандартних дозволів, які надаються контейнеру

Ці дозволи дозволяють контейнеру виконувати певні операції в контексті ядра системи (Docker Host). Поточний список усіх дозволів міститься в документації .

Наприклад, за замовчуванням кожному контейнеру надається привілей NET_RAW. Це означає, що з рівня контейнера ми зможемо надіслати, серед іншого, пакет ICMP, використовуючи команду pingабо traceroute(лістинг 13).

# commands to be executed in the context of Docker Host
docker run -it --rm --name ubuntu-ping ubuntu:22.04 bash

# commands to be executed in the context of ubuntu-ping containter
apt update && apt install -y iputils-ping
ping -c 3 8.8.8.8

Лістинг 13. Запуск контейнера, встановлення пакетів і виконання команди ping.

Малюнок 21. Надсилання пакета ICMP.

Просте завдання. Результат відповідає очікуванням (мал. 21).

Тепер ми запустимо контейнер вдруге, але цього разу зі скасуванням можливості NET_RAW, тобто з прапором --cap-drop=CAP_NET_RAW(лістинг 14).

# Command to be executed in the context of Docker Host.
docker run -it --rm --cap-drop=CAP_NET_RAW --name ubuntu-ping ubuntu:22.04 bash

# Commands to be executed in the context of the ubuntu-ping container.
apt update && apt install -y iputils-ping
ping -c 3 8.8.8.8

Лістинг 14. Перезапуск контейнера, цього разу зі скасованими дозволами.

Малюнок 22. Запуск контейнера з відкликаним дозволом.
Малюнок 23. Підтвердження неможливості відправити пакет ICMP.

Знову все пішло за планом (мал. 22, мал. 23). Ми позбавили дозволів, необхідних для надсилання, зокрема, пакетів ICMP із контейнера.

Нам не потрібно вручну визначати, які дозволи для контейнера потрібно відкликати. Ми можемо скасувати всі дозволи. Це робиться за допомогою ALLпрапора, який означає, що якщо ми хочемо скасувати всі дозволи, ми можемо додати прапор --cap-drop=ALL(лістинг 15, малюнок 24).

docker run -it --rm --cap-drop=ALL --name ubuntu-drop-all-cap ubuntu:22.04 bash

Лістинг 15. Запуск контейнера з усіма відкликаними дозволами.

Малюнок 24. Запуск контейнера зі скасованими дозволами.

Нам слід бути особливо обережними, якщо ми помічаємо конструкції, які використовують --cap-addпрапор – протилежність --cap-drop, який використовується для надання дозволів. Нагадуємо, що в таблиці № 1 представлені лише дозволи за замовчуванням, надані Docker. Справжній список набагато ширший. Нашою метою має бути обмеження дозволів для контейнерів, тому надання додаткових дозволів має викликати тривогу.

Уникнення привілейованого режиму

Docker дозволяє запускати контейнери в привілейованому режимі. Це дещо протилежне команді --cap-drop=ALL, яка надає контейнеру практично всі можливі привілеї.

Найпростіший спосіб пояснити це на прикладі. Спробуємо запустити два контейнери: один у привілейованому режимі, інший у стандартному (за замовчуванням). Потім я проведу серію перевірок, включаючи швидку розвідку, щоб побачити, чого я можу досягти на рівні контейнера з розширеним набором привілеїв.

# Commands to be executed in the context of Docker Host.
docker run -itd --privileged --name ubuntu-privileged ubuntu:22.04
docker run -itd --name ubuntu-unprivileged ubuntu:22.04

docker exec -it ubuntu-unprivileged bash

# Commands to be executed in the context of the ubuntu-unprivileged container.
ls /dev
ls /dev | grep sda
exit

# Command to be executed in the context of Docker Host.
docker exec -it ubuntu-privileged bash

# Commands to be executed in the context of the ubuntu-privileged container.
ls /dev
ls /dev | grep sda
exit

Лістинг 16. Запуск контейнерів у стандартному та привілейованому режимах.

Малюнок 25. Порівняння дозволів контейнера, запущеного в стандартному та привілейованому режимах.

Чи бачите ви суттєву різницю (Малюнок 25)? У разі запуску контейнера без прапора privilegedми маємо доступ лише до обмеженого списку пристроїв у /devкаталозі. Зовсім інакше виглядає ситуація для привілейованого контейнера. Як потенційний зловмисник може цим скористатися? Давайте шукати щось ще цікавіше. Особливо нас має цікавити наявність пристроїв з sd*ідентифікаторами (в даному випадку sda), під якими найчастіше розуміються жорсткі диски. Давайте подивимося, що ще можна знайти (мал. 26).

Малюнок 26. Продовження розвідки.

Каталог /dev/mapperі його вміст припускають, що ми маємо справу з LVMтомами (Logical Volume Manager) на пристрої. LVM дозволяє створювати логічні томи, які можна легко змінювати та переносити між жорсткими дисками та розділами

За замовчуванням наш контейнер може не мати відповідних драйверів, необхідних для обробки LVM. З цієї причини нам потрібно їх встановити – apt install lvm2(Малюнок 27).

Малюнок 27. Встановлення пакетів, необхідних для взаємодії з томами LVM.

Після встановлення всіх необхідних інструментів ми можемо завершити розвідку за допомогою команди lvscan(Малюнок 28).

Малюнок 28. Розвідка за допомогою lvscan.

Ми щойно отримали доступ до файлів хосту Docker, які доступні лише користувачам із правами адміністратора!

Ми навіть можемо піти далі і за допомогою команди chrootпочати виконувати команди безпосередньо в контексті хосту Docker.

Малюнок 29. Контроль над хостом Docker.

З практичної точки зору ми взяли під контроль Docker Host (Малюнок 29)!

Доступ до пристроїв

Ми повинні бути пильними не тільки на --privilegedпараметр. Інші конструкції також можуть бути небезпечними, особливо ті, що використовують параметр --device.

Параметр --deviceу Docker дозволяє відображати пристрої від хоста до контейнера. Це використовується, коли програма всередині контейнера потребує прямого доступу до фізичного обладнання хост-системи. Це може стосуватися різних типів пристроїв, таких як графічні процесори (GPU), жорсткі диски, принтери та інші периферійні пристрої.

Використовуючи цю опцію, програми в контейнері можуть взаємодіяти з певним пристроєм так, ніби вони працюють безпосередньо на хост-системі. Це особливо корисно у випадках, коли продуктивність і доступ до спеціалізованих апаратних функцій є ключовими.

Якщо ми використовуємо таку конструкцію для надання доступу до дисків, ми фактично досягнемо того самого ефекту, що й той, який ми обговорювали в контексті параметра --privileged.

Блокування можливості «надання» (отримання) дозволів

Параметр no-new-privilegesу Docker використовується для керування наданням дозволів у контейнері. Якщо для цього параметра встановлено значення true, процеси в контейнері не зможуть отримати нові дозволи, крім тих, які їм надано під час запуску. Це може допомогти підвищити безпеку системи, обмеживши можливість ескалації привілеїв потенційно шкідливими процесами.

Давайте подивимося, як це працює на практиці. Давайте підготуємо тестове середовище. Ми будемо використовувати виконуваний файл оболонки bash для підвищення привілеїв, який використовує root користувача setuid.

setuid— це позначка дозволів в операційних системах сімейства Linux і Unix, яка дозволяє запускати програми з дозволами іншого користувача, зазвичай користувача root. Це означає, що коли програма з setuidпрапорцем запускається, вона працює з дозволами власника файлу, а не користувача, який її запустив.

Давайте підготуємо простий Dockerfile (лістинг 17).

FROM ubuntu:22.04
RUN cp /bin/bash /bin/givemeroot
RUN chmod 4755 /bin/givemeroot
RUN useradd -ms /bin/bash unprivilegeduser
USER unprivilegeduser
CMD ["/bin/bash"]

Лістинг 17. Docker-файл, підготовлений з метою демонстрації ескалації привілеїв.

Підготовлений нами Dockerfile створює образ на основі Ubuntu 22.04, копіює його /bin/bashв новий файл /bin/givemerootі призначає setuidдозволи для цього файлу. Це дає змогу запускати його з правами root. unprivilegeduserПотім створюється новий користувач із назвою bashоболонки за замовчуванням. Контекст користувача перемикається на unprivilegeduser(з USERкомандою), що означає, що всі подальші інструкції виконуватимуться з правами цього користувача.

Час збереження файлу на диску, в будь-якому каталозі. Пам’ятайте про правильну назву цього файлу, яка є Dockerfile(лістинг 18, ​​малюнок 30).

mkdir test-priv-esc && cd test-priv-esc
nano Dockerfile
cat Dockerfile
docker build -t ubuntu-setuid-escalation .

Лістинг 18. Процес побудови нового образу.

Малюнок 30. Побудова нового образу.

Час перевірки. Давайте створимо новий контейнер, використовуючи щойно створене зображення. Давайте також перевіримо, чи завершилася наша спроба підвищити привілеї успішно (лістинг 19).

# Instruction to be executed in the context of Docker Host
docker run --rm -it ubuntu-setuid-escalation bash

# Instruction to be executed in the context of container
id
head -n 1 /etc/shadow
/bin/givemeroot -p
id
head -n 1 /etc/shadow

Лістинг 19. Запуск контейнера з використанням попередньо створеного образу.

Малюнок 31. Підтвердження можливості підвищення привілеїв.

Чудово (малюнок 31)! Звичайно, «чудово» з точки зору того, хто хоче взяти під контроль вразливий контейнер. Ми підтвердили, що змогли підвищити дозволи до рівня користувача root (у контексті контейнера).

Тепер давайте спробуємо захистити себе від такої можливості. Ми запустимо інший контейнер, але цього разу з додатковим параметром, тобто --security-opt="no-new-privileges=true"(лістинг 20).

# Commands to be executed in the context of the "old" container.
exit
exit

# Command to be executed in the context of Docker Host.
docker run --rm -it --security-opt="no-new-privileges=true" ubuntu-setuid-escalation bash

# Commands to be executed in the context of the new container.
id
/bin/givemeroot -p
id

Лістинг 20. Запуск контейнера з накладеними обмеженнями.

Малюнок 32. Запуск другої версії контейнера.

Як видно (мал. 32), цього разу спроба підвищення привілеїв не вдалася!

І те --security-opt="no-new-privileges=true", і інше --cap-drop=ALLпідвищує безпеку середовища Docker. Однак вони функціонують по-різному і можуть використовуватися разом для забезпечення додаткового рівня захисту. Коротше кажучи, no-new-privilegesзапобігає підвищенню привілеїв, водночас --cap-drop=ALLобмежує дозволи запущеного контейнера, відрізаючи всі привілеї.

Ескалація привілеїв і простори імен Linux

Під час процесу захисту Docker одним із найважливіших аспектів є обмеження ризику підвищення привілеїв на хості Docker, яке може виникнути через можливі помилки в конфігурації Docker Daemon. Ми вже згадували про це в кількох попередніх параграфах. Механізм просторів імен Linux , вбудований у ядро ​​системи Linux, є ефективним інструментом для цієї мети.

Простори імен у системі Linux дозволяють ізолювати та відокремлювати системні ресурси для процесів, що, у свою чергу, дозволяє мати ефективний захист хост-системи від потенційно шкідливого впливу програм, що працюють у контейнерах. Кожен простір імен функціонує як незалежний рівень, обмежуючи видимість і доступ до ресурсів системи для процесів, що працюють в ньому.

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

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

Давайте перевіримо, про що йдеться.

Ми запустимо перший контейнер, ввівши команду docker run -itd --name ubuntu1 ubuntu:22.04.

Ми можемо отримати доступ до системної оболонки та перевірити список запущених процесів. Для цього потрібно ввести наступні команди: docker exec -it ubuntu1 bashand ps -u(Малюнок 33).

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

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

Тепер ми використаємо іншу команду Docker, а саме docker container top ubuntu1, щоб перевірити, як процеси, що виконуються в контейнері, зіставляються з процесами хосту Docker (Малюнок 34).

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

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

Щоб активувати цей механізм, нам потрібно використати параметр конфігурації userns-remapта зберегти відповідне значення у daemon.jsonфайлі. Варто пам’ятати, що за замовчуванням цей файл може не існувати, тому може знадобитися створити його в шляху /etc/docker/daemon.json. Користувачі Docker Desktop зможуть знайти файл у шляху $HOME/.docker/daemon.json. Правильна конфігурація представлена ​​в лістингу 21 ( cat /etc/docker/daemon.json).

{
  "userns-remap": "default"
}

Лістинг 21. Вміст файлу daemon.json

Згідно з документацією Docker, після встановлення userns-remapпараметра defaultта перезапуску Docker система автоматично створить користувача з іменем dockremap. Контейнери запускатимуться у своєму контексті, а не як користувач root.

Після перезапуску служби Docker ( sudo service docker restart) доцільно перевірити, чи dockremapсправді було створено користувача та чи було збережено конфігурацію простору імен у файлах конфігурації хосту Docker. В першу чергу це стосується /etc/subuidфайлу (мал. 35).

Малюнок 35. Налаштування файлу subuid.

Все на своїх місцях!

Тепер ми повторимо вправу, пов’язану із запуском контейнера та виконанням кількох команд (лістинг 22).

docker run -itd --name ubuntu1 ubuntu:22.04
docker exec -it ubuntu1 bash

ps u
exit

docker container top ubuntu1

Лістинг 22. Перезапуск контейнера.

Малюнок 36. Запуск контейнера після внесення змін.

Знову здається, що все на своїх місцях (мал. 36)!

Ми запустили ubuntu1контейнер, а потім перевірили, чи запущені в контейнері процеси все ще виконуються в rootконтексті користувача (всередині контейнера). Однак значні зміни відбулися після видачі docker container top ubuntu1наказу. Ми спостерігаємо, що тепер, після змін, процес контейнера виконується на хості Docker у контексті новоствореного непривілейованого користувача dockeremap(Малюнок 37).

Малюнок 37. Підтвердження успішного впровадження змін.

Така конфігурація значно обмежує можливість підвищення привілеїв у системі Docker Host.

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