Це друга частина статті про безпеку Docker. У цій частині ми розглянемо процеси надання та скасування дозволів для користувачів та додатків у Docker. Важливо знати, як правильно управляти доступом, щоб забезпечити максимальний рівень безпеки ваших контейнерів та захистити їх від несанкціонованого доступу.
За замовчуванням Docker запускає контейнери з певним стандартним набором дозволів. У таблиці представлено список цих дозволів.
Ці дозволи дозволяють контейнеру виконувати певні операції в контексті ядра системи (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).
Тепер ми запустимо контейнер вдруге, але цього разу зі скасуванням можливості 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 із контейнера.
Нам не потрібно вручну визначати, які дозволи для контейнера потрібно відкликати. Ми можемо скасувати всі дозволи. Це робиться за допомогою ALL
прапора, який означає, що якщо ми хочемо скасувати всі дозволи, ми можемо додати прапор --cap-drop=ALL
(лістинг 15, малюнок 24).
docker run -it --rm --cap-drop=ALL --name ubuntu-drop-all-cap ubuntu:22.04 bash
Лістинг 15. Запуск контейнера з усіма відкликаними дозволами.
Нам слід бути особливо обережними, якщо ми помічаємо конструкції, які використовують --cap-add
прапор – протилежність --cap-drop
, який використовується для надання дозволів. Нагадуємо, що в таблиці № 1 представлені лише дозволи за замовчуванням, надані Docker. Справжній список набагато ширший. Нашою метою має бути обмеження дозволів для контейнерів, тому надання додаткових дозволів має викликати тривогу.
Docker дозволяє запускати контейнери в привілейованому режимі. Це дещо протилежне команді --cap-drop=ALL
, яка надає контейнеру практично всі можливі привілеї.
Найпростіший спосіб пояснити це на прикладі. Спробуємо запустити два контейнери: один у привілейованому режимі, інший у стандартному (за замовчуванням). Потім я проведу серію перевірок, включаючи швидку розвідку, щоб побачити, чого я можу досягти на рівні контейнера з розширеним набором привілеїв.
Перш ніж продовжити, переконайтеся, що ви вимкнули механізм просторів імен Linux (файл/etc/docker/daemon.json
). В іншому випадку після виконання команди ви побачите повідомлення про помилку:docker: Error response from daemon: privileged mode is incompatible with user namespaces. You must run the container in the host namespace when running privileged mode
. Також не забудьте перезапустити Демон:sudo systemctl restart docker
.
# 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)? У разі запуску контейнера без прапора privileged
ми маємо доступ лише до обмеженого списку пристроїв у /dev
каталозі. Зовсім інакше виглядає ситуація для привілейованого контейнера. Як потенційний зловмисник може цим скористатися? Давайте шукати щось ще цікавіше. Особливо нас має цікавити наявність пристроїв з sd*
ідентифікаторами (в даному випадку sda
), під якими найчастіше розуміються жорсткі диски. Давайте подивимося, що ще можна знайти (мал. 26).
Каталог /dev/mapper
і його вміст припускають, що ми маємо справу з LVM
томами (Logical Volume Manager) на пристрої. LVM дозволяє створювати логічні томи, які можна легко змінювати та переносити між жорсткими дисками та розділами
За замовчуванням наш контейнер може не мати відповідних драйверів, необхідних для обробки LVM. З цієї причини нам потрібно їх встановити – apt install lvm2
(Малюнок 27).
Після встановлення всіх необхідних інструментів ми можемо завершити розвідку за допомогою команди lvscan
(Малюнок 28).
Ми щойно отримали доступ до файлів хосту Docker, які доступні лише користувачам із правами адміністратора!
Ми навіть можемо піти далі і за допомогою команди chroot
почати виконувати команди безпосередньо в контексті хосту 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. Процес побудови нового образу.
Час перевірки. Давайте створимо новий контейнер, використовуючи щойно створене зображення. Давайте також перевіримо, чи завершилася наша спроба підвищити привілеї успішно (лістинг 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)! Звичайно, «чудово» з точки зору того, хто хоче взяти під контроль вразливий контейнер. Ми підтвердили, що змогли підвищити дозволи до рівня користувача 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), цього разу спроба підвищення привілеїв не вдалася!
І те --security-opt="no-new-privileges=true"
, і інше --cap-drop=ALL
підвищує безпеку середовища Docker. Однак вони функціонують по-різному і можуть використовуватися разом для забезпечення додаткового рівня захисту. Коротше кажучи, no-new-privileges
запобігає підвищенню привілеїв, водночас --cap-drop=ALL
обмежує дозволи запущеного контейнера, відрізаючи всі привілеї.
Під час процесу захисту 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 bash
and ps -u
(Малюнок 33).
Як бачите, процеси, запущені в контейнері Docker, працюють у контексті користувача root. Незважаючи на те, що таке рішення не рекомендується, у деяких випадках воно необхідне. Як зазначалося раніше, існують процеси, які повинні працювати в цьому режимі.
Тепер ми використаємо іншу команду Docker, а саме docker container top ubuntu1
, щоб перевірити, як процеси, що виконуються в контейнері, зіставляються з процесами хосту Docker (Малюнок 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).
Все на своїх місцях!
Тепер ми повторимо вправу, пов’язану із запуском контейнера та виконанням кількох команд (лістинг 22).
docker run -itd --name ubuntu1 ubuntu:22.04 docker exec -it ubuntu1 bash ps u exit docker container top ubuntu1
Лістинг 22. Перезапуск контейнера.
Знову здається, що все на своїх місцях (мал. 36)!
Ми запустили ubuntu1
контейнер, а потім перевірили, чи запущені в контейнері процеси все ще виконуються в root
контексті користувача (всередині контейнера). Однак значні зміни відбулися після видачі docker container top ubuntu1
наказу. Ми спостерігаємо, що тепер, після змін, процес контейнера виконується на хості Docker у контексті новоствореного непривілейованого користувача dockeremap
(Малюнок 37).
Така конфігурація значно обмежує можливість підвищення привілеїв у системі Docker Host.