Безпека Docker – Контроль використання ресурсів (Частина 4)

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

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

Контроль використання ресурсів

Складовою безпеки системи, крім цілісності та конфіденційності, є також забезпечення доступності системи та даних. Коли мова йде про контейнери, дуже важливо контролювати кількість ресурсів хосту Docker, які може використовувати кожен контейнер. У системі Linux цей контроль можливий завдяки механізму під назвою cgroups(Групи керування). Це механізм ядра Linux, який використовується для обмеження, ізоляції та моніторингу системних ресурсів, які використовуються процесами або групами процесів. У контрольних групах є численні підсистеми, які відповідають за керування різними системними ресурсами та аспектами, зокрема:

  • blkio – контролює доступ до блокових пристроїв введення/виведення, дозволяючи контролювати та обмежувати пропускну здатність введення/виведення,

  • cpu – керування ЦП,

  • cpuset (cpus) – дозволяє призначати конкретні ЦП.

  • пристрої – контролює доступ до пристроїв за групами процесів.

  • Пам’ять – контролює використання пам’яті групами процесів, дозволяючи обмежити використання пам’яті та ізолювати його.

  • pids – контролює кількість процесів у групі, що дозволяє обмежити максимальну кількість процесів.

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

По-друге, існує ризик уразливості до атак типу «Відмова в обслуговуванні» (DoS) (не плутати з атаками Distributed Denial of Service або DDoS). Прикладом може бути Aa ReDoS-атака, яка може призвести до виснаження ресурсів і простою системи.

Вибрані нами обмеження можна визначити під час запуску контейнера. Також важливо відзначити, що існує можливість змінити обмеження використання ресурсів для контейнерів, які вже запущені. Такі параметри, як:

  • --memory=(lub -m) – відповідає за визначення верхньої межі оперативної пам’яті, яка може бути виділена контейнером (наприклад,  --memory=32m означає обмеження в 32 МБ пам’яті),

  • --memory-swap=– обмеження пам’яті SWAP ,

  • --cpus=– Визначає максимальний рівень використання ЦП. Наприклад, якщо Docker Host має 1 ядро, встановлення параметра ЦП на 0,5 ( --cpus=0.5) означатиме обмеження до 50% використання ресурсів ЦП,

  • --pids-limit= – Визначає, скільки процесів можна запустити в контексті конкретного контейнера (наприклад,  --pids-limit=5 означає, що ви не можете запустити більше 5 процесів у контейнері).

Повний список параметрів доступний у документації Docker .

Настав час практичного тесту. Ми запустимо новий контейнер ( ubuntu-limits), на якому встановимо пакет stress-ng (лістинг 36, малюнок 60). Stress-ng — це інструмент, який використовується для завантаження та тестування операційної системи різними способами, зокрема шляхом навантаження на процесор, оперативну пам’ять, диски та інші області, де ресурси можуть бути обмеженими. Це вдосконалена версія стрес-інструменту, яка пропонує складні та різноманітні варіанти тестування. Це дозволяє точно та гнучко генерувати навантаження, щоб оцінити, як система реагує на тиск, що особливо корисно для виявлення проблем продуктивності та дослідження стабільності системи.

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

# Commands to be executed in the context of the ubuntu-limits container
apt update && apt install -y stress-ng

Лістинг 36. Запуск нового контейнера та встановлення пакета stress-ng.

Малюнок 60. Запуск нового контейнера та встановлення stress-ng.

Давайте перевіримо, чи правильно встановлено пакет за допомогою команди stress-ng --version. Для зручності ми також відкриємо друге вікно терміналу, в якому будемо відстежувати рівень використання системних ресурсів під час виконання стрес-тестів.

Почнемо з тесту, спрямованого на перевірку використання процесора. Однак перш ніж це зробити, нам потрібно знати, скільки ядер має процесор, який використовує наш контейнер. Ми можемо отримати цю інформацію за допомогою nprocкоманди (мал. 61).

Малюнок 61. Запуск програми nproc.

Чому ця інформація важлива? Зокрема, ми передамо --cpu-loadпрограмі параметр stress-ng, який визначає, який відсоток рівня CPU-load stress-ng має надавати процесору. Другий параметр, який ми надамо, це --cpu, який визначає кількість процесів для використання. Отже, якщо ми встановимо перший параметр ( --cpu) на 1, а другий ( --cpu-load) на 100, це означатиме, що один процесор буде використано повністю, яке docker statsкоманда відобразить як значення, близьке до 100 у стовпці «CPU». Однак, якщо ми змінимо значення параметра --cpuна 2, зберігаючи значення --cpu-loadнезмінним, результат docker statsмає показувати значення, близьке до 200% (що означає, що два ядра використовуються на 100% кожне).

Крім того, під час цього тесту  спробуемо встановити обмеження до 150% за допомогою коду docker update ubuntu-limits --cpus=1.5. Ми побачимо, чи відобразиться це на результатах вимірювань (Відео 1).

Відео 1. Результати стрес-тесту.

Чудово! Все йде за планом.

Тепер давайте встановимо друге обмеження, пов’язане з оперативною пам’яттю. Ми можемо зробити це, виконавши команду з лістингу 37 на рівні хосту Docker.

docker update ubuntu-limits --memory=128m --memory-swap=256m

Лістинг 37. Встановлення обмежень пам’яті.

Ми можемо помітити, що обмеження, яке ми встановили 128 MB, було призначено виконанням docker statsкоманди (Мал. 62).

Малюнок 62. Встановлення обмежень на використання пам’яті.

Ще один швидкий тест (збільшені обмеження; лістинг 38):

docker update ubuntu-limits --memory=256m --memory-swap=256m
docker stats --no-stream

Лістинг 38. Наступна ітерація тесту.

Малюнок 63. Ще одна перевірка.

Припущення узгоджуються з ефектами (Малюнок 63).

Ми проведемо дві серії тестів. Перший передбачає спробу виділити пам’ять, яка не перевищує встановлений ліміт (наприклад, 200 МБ). Другий, однак, включатиме спробу виділити більший ліміт, ніж встановлений раніше (наприклад, 300 МБ або будь-яке значення на ваш вибір).

Ми виконуємо перший тест за допомогою команди з лістингу 39.

stress-ng --vm 1 --vm-bytes 200M --timeout 1m --vm-keep

Лістинг 39. Перша ітерація тестів пам’яті.

Через деякий час ми помітимо, що рівень виділеної пам’яті стабілізується на рівні приблизно 200 МБ (Малюнок 64).

Малюнок 64. Результат розподілу пам’яті за допомогою stress-ng.
stress-ng --vm 1 --vm-bytes 300M --timeout 1m --vm-keep

Лістинг 40. Друга ітерація тесту.

Відео 2. Друга ітерація тесту.

Як бачите, використання пам’яті коливається . Команда команда stress-ng --vm 1 --vm-bytes 300M --timeout 1m --vm-keepнамагається виділити 300 МБ пам’яті, що перевищує доступний ліміт. Тому система керування пам’яттю в контейнері Docker спробує впоратися з цією ситуацією, щоб не перевищити встановлений ліміт.

Проведемо ще один тест. Ми встановимо обмеження на можливість запуску процесів «всередині» контейнера. За замовчуванням у нашому контейнері запущено два процеси (Малюнок 65).

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

Отже, давайте встановимо обмеження, наприклад, на 5 процесів: docker update ubuntu-limits --pids-limit=5.

Малюнок 66. Встановлення обмеження процесу.

Тепер ми спробуємо запустити кілька процесів у фоновому режимі; наприклад, top &(Малюнок 67).

Малюнок 67. Запущені процеси у фоновому режимі.

Ми вичерпали ліміт… настільки ефективно, що навіть не можемо перевірити список запущених процесів.

Малюнок 68. Неможливість запустити команду ps.

Тепер ви засвоїли основи встановлення обмежень.

Зрештою запам’ятайте одну річ. Лише з метою демонстрації ми фактично встановили обмеження лише після запуску контейнера. Однак це не обов’язково – ви можете застосувати ці налаштування вже під час запуску контейнера, наприклад, за допомогою команди: docker run -it --pids-limit=5 --name ubuntu-test ubuntu:22.04 bash.

Підключення до віддаленого Docker Daemon

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

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

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

Спробуємо на першому кроці налаштувати підключення за допомогою SSH.

SSH

У рамках тестів  створюемо копію віртуальної машини, яка служить моїм хостом Docker. Цей процес може складатися з різних етапів, залежно від гіпервізора, який ви використовуєте. Важливо, що на додаток до оригінального хосту Docker – у моєму випадку це віртуальна машина з Ubuntu та IP-адресою 172.16.169.183– також має бути запущена друга система, на якій працює Docker. Знову ж таки, у моєму випадку це клон оригінальної машини з адресою 172.16.169.183. Обидві машини повинні мати можливість спілкуватися на рівні мережі.

ssh-keygen -t ed25519 -f .ssh/remote-dockerd -q -N ""
# make sure you provide the correct IP address and username!
ssh-copy-id -i .ssh/remote-dockerd.pub [email protected]

Лістинг 41. Генерація ключа SSH. 

Малюнок 69. Налаштування підключення SSH до віддаленого хосту.

Ми успішно налаштували безпечне з’єднання з другим сервером за допомогою автентифікації на основі ключа SSH (лістинг 41, малюнок 69). Настав час створити новий контекст Docker і встановити з’єднання з віддаленим сервером (лістинг 42, малюнок 70).

docker context show
docker context ls
# make sure to enter correct IP address 
docker context create --docker host=ssh://172.16.169.186 --description="remote dockerd" remote-dockerd
docker ps
docker context use remote-dockerd
docker ps

Лістинг 42. Створення нового контексту та віддалене підключення до хосту Docker.

Малюнок 70. Створення нового контексту та віддалене підключення до хосту Docker.

Успіх. Команда docker psвже виконана в контексті другого сервера.

TLS

Крім того, ми також можемо налаштувати з’єднання на основі TLS. Для цього нам потрібно виконати кілька команд на рівні хоста Docker (лістинг 43).

export HOST=reynardsec.com
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr

hostname -I

echo subjectAltName = DNS:$HOST,IP:172.16.169.183,IP:127.0.0.1 >> extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf
openssl genrsa -out key.pem 4096
echo extendedKeyUsage = clientAuth > extfile-client.cnf
openssl req -new -key key.pem -out client.csr
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile-client.cnf

Весь процес складається з кількох кроків:

  1. export HOST=reynardsec.com– Він визначає змінну середовища HOSTзі значенням «reynardsec.com».

  2. openssl genrsa -aes256 -out ca-key.pem 4096– 4096-бітний особистий ключ RSA, захищений шифруванням AES-256 і зберігає його у файлі ca-key.pem.

  3. openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem– Він створює новий сертифікат ЦС із закритого ключа ca-key.pem, який дійсний протягом 365 днів. Отриманий сертифікат зберігається у файлі ca.pem.

  4. openssl genrsa -out server-key.pem 4096– Він генерує 4096-бітний закритий ключ RSA та зберігає його у файлі server-key.pem.

  5. openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr– Він створює новий запит на підписання сертифіката (CSR) із закритого ключа server-key.pem. У цьому запиті Загальне ім’я (CN) встановлюється на значення змінної середовища HOST, а отриманий CSR зберігається у файлі server.csr.

  6. hostname -I– Відображає всі IP-адреси, налаштовані на мережевих інтерфейсах машини.

  7. echo subjectAltName = DNS:$HOST,IP:172.16.169.183,IP:127.0.0.1 >> extfile.cnf– Він додає до файлу додаткові доменні імена та IP-адреси extfile.cnf, які використовуватимуться як альтернативні імена для сервера.

  8. echo extendedKeyUsage = serverAuth >> extfile.cnf– Він визначає, що сертифікат використовуватиметься для автентифікації сервера, і додає цю інформацію до extfile.cnfфайлу.

  9. openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf– Він підписує запит на сертифікат server.csrза допомогою ключа CA та сертифіката, створюючи сертифікат сервера, дійсний протягом 365 днів, і зберігає його у файлі server-cert.pem.

  10. openssl genrsa -out key.pem 4096– Він генерує ще один 4096-бітний закритий ключ RSA та зберігає його у key.pemфайлі.

  11. echo extendedKeyUsage = clientAuth > extfile-client.cnf– Він створює файл конфігурації extfile-client.cnf, вказуючи, що сертифікат використовуватиметься для автентифікації клієнта.

  12. openssl req -new -key key.pem -out client.csr– Він генерує новий запит на сертифікат (CSR) із закритого ключа key.pemта зберігає отриманий CSR у файлі client.csr.

  13. openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile-client.cnf– Він підписує запит на сертифікат client.csrза допомогою ключа ЦС і сертифіката, створюючи сертифікат клієнта, дійсний протягом 365 днів, і зберігає його у файлі cert.pem.

Тепер ми спробуємо запустити Docker Daemon, щоб він прослуховував підключення, захищені TLS (Малюнок 71).

Малюнок 71. Запуск dockerd з параметрами, що відповідають за підключення TLS.

Здається, ми успішно запустили Docker Daemon. Тепер давайте спробуємо підключитися до нього з іншого хоста. Нам потрібно безпечно передати файли на інший хост ca.pem. У моєму випадку це буде сервер із адресою (лістинг 44, малюнок 72).cert.pemkey.pem172.16.169.186

# The command to be executed on the server where we generated the certificates
scp ca.pem cert.pem key.pem  [email protected]:~

Лістинг 44. Передача ключів і сертифікатів на віддалений сервер.

Малюнок 72. Переміщення файлів на інший сервер.

Тепер настав час увійти на наш віддалений сервер і спробувати підключитися до хосту Docker (лістинг 45, малюнок 73).

docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=172.16.169.183:2376 ps

Лістинг 45. Спроба підключення за допомогою TLS.

Малюнок 73. Спроба підключення за допомогою TLS.

З метою демонстрації ми запустили Docker Daemon ( dockerd) в «автономному» режимі. Однак це не зручно і не практично. Таким чином, ми також можемо остаточно зберегти конфігурацію, відредагувавши daemon.jsonфайл (лістинг 46).

{
  "icc": true,
  "tlsverify": true,
  "tlscacert": "/home/reynard/ca.pem",
  "tlscert": "/home/reynard/server-cert.pem",
  "tlskey": "/home/reynard/server-key.pem",
  "hosts": ["tcp://0.0.0.0:2376"]
}

Лістинг 46. Конфігурація TLS за допомогою файлу daemon.json.

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Лістинг 47. Вихідний рядок у файлі docker.service

ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock

Лістинг 48. Змінений файл docker.service.

Малюнок 74. Редагування конфігурації docker.service

Після впровадження цих змін нам потрібно перезавантажити служби, а потім перезапустити службу Docker (лістинг 49, малюнок 75).

sudo systemctl daemon-reload
sudo systemctl restart docker

Лістинг 49. Перезавантаження конфігурації та перезапуск служби.

Малюнок 75. Перезавантаження конфігурації служби та перезапуск Docker Daemon.

Настав час повернутися до нашого другого сервера і знову перевірити можливість підключення за протоколом TLS (Малюнок 76).

Малюнок 76. Підтвердження можливості підключення за допомогою TLS.

Скільки разів уже… успіху 🙂

Підсумовуючи, SSH і TLS ефективно захищають з’єднання за допомогою Docker Daemon. Однак TLS може виявитися більш придатним для великих, складніших і динамічних середовищ.

Журнал подій

За замовчуванням Docker використовує json-fileдрайвер журналювання для зберігання журналів із контейнерів. Журнали зберігаються у форматі JSON у файлі, розташованому на хості. Параметри за замовчуванням дозволяють переглядати журнали з контейнера за допомогою docker logsкоманди.

Драйвер json-fileстворює файл JSON для кожного контейнера, у який записуються всі журнали, що надходять із цього контейнера. Ці файли зберігаються в шляху /var/lib/docker/containers/<container-id>/, де <container-id>є ідентифікатор даного контейнера.

Журнали для певного контейнера можна переглянути, виконавши команду docker logs, наприклад: docker logs 84167e82e8cf(Малюнок 77).

Малюнок 77. Журнали, зібрані для вибраного контейнера.

З точки зору безпеки, зберігання журналів на одній машині з запущеними контейнерами не є найкращою практикою з точки зору безпеки. Рекомендується надсилати журнали на віддалений сервер, призначений для їх збору. На щастя, Docker підтримує кілька різних драйверів, які обробляють збір журналів, включаючи ті, які дозволяють надсилати дані на віддалений сервер. Список драйверів, підтримуваних за замовчуванням, доступний у документації (на вкладці «Драйвери журналювання»).

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