Це четверта частина статті про безпеку 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.
Давайте перевіримо, чи правильно встановлено пакет за допомогою команди stress-ng --version
. Для зручності ми також відкриємо друге вікно терміналу, в якому будемо відстежувати рівень використання системних ресурсів під час виконання стрес-тестів.
Почнемо з тесту, спрямованого на перевірку використання процесора. Однак перш ніж це зробити, нам потрібно знати, скільки ядер має процесор, який використовує наш контейнер. Ми можемо отримати цю інформацію за допомогою nproc
команди (мал. 61).
Чому ця інформація важлива? Зокрема, ми передамо --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).
Ще один швидкий тест (збільшені обмеження; лістинг 38):
docker update ubuntu-limits --memory=256m --memory-swap=256m docker stats --no-stream
Лістинг 38. Наступна ітерація тесту.
Припущення узгоджуються з ефектами (Малюнок 63).
Ми проведемо дві серії тестів. Перший передбачає спробу виділити пам’ять, яка не перевищує встановлений ліміт (наприклад, 200 МБ). Другий, однак, включатиме спробу виділити більший ліміт, ніж встановлений раніше (наприклад, 300 МБ або будь-яке значення на ваш вибір).
Ми виконуємо перший тест за допомогою команди з лістингу 39.
stress-ng --vm 1 --vm-bytes 200M --timeout 1m --vm-keep
Лістинг 39. Перша ітерація тестів пам’яті.
Через деякий час ми помітимо, що рівень виділеної пам’яті стабілізується на рівні приблизно 200 МБ (Малюнок 64).
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).
Отже, давайте встановимо обмеження, наприклад, на 5 процесів: docker update ubuntu-limits --pids-limit=5
.
Тепер ми спробуємо запустити кілька процесів у фоновому режимі; наприклад, top &
(Малюнок 67).
Ми вичерпали ліміт… настільки ефективно, що навіть не можемо перевірити список запущених процесів.
Тепер ви засвоїли основи встановлення обмежень.
Зрештою запам’ятайте одну річ. Лише з метою демонстрації ми фактично встановили обмеження лише після запуску контейнера. Однак це не обов’язково – ви можете застосувати ці налаштування вже під час запуску контейнера, наприклад, за допомогою команди: docker run -it --pids-limit=5 --name ubuntu-test ubuntu:22.04 bash
.
Демон Docker не завжди працюватиме на тій самій машині, що й клієнт Docker. Можуть бути ситуації, коли нам знадобиться підключитися до Docker Daemon з локальної станції, яка працює на віддаленому сервері.
Зв’язок із віддаленим хостом Docker Daemon має бути захищеним, оскільки недостатній захист з’єднання може становити серйозну загрозу безпеці. Docker Daemon має повний контроль над операційною системою Docker Host (у стандартній конфігурації). Віддалене керування без належних захисних заходів може призвести до несанкціонованого доступу, втрати даних, порушення конфіденційності та інших проблем, пов’язаних із безпекою. Передача даних між клієнтом і Docker Daemon відкрито, без шифрування, може призвести до їх перехоплення та маніпулювання неавторизованими особами.
SSH і TLS є двома широко використовуваними методами захисту віддаленого зв’язку. SSH простий у налаштуванні та використанні, пропонуючи надійне шифрування та автентифікацію. Крім того, TLS також забезпечує надійне шифрування та автентифікацію, але його налаштування може виявитися складнішим. Однак TLS є більш гнучким і масштабованим, що особливо часто використовується в середовищах, де потрібне керування кількома сертифікатами та автентифікація кількох користувачів або служб.
Спробуємо на першому кроці налаштувати підключення за допомогою 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.
Ми успішно налаштували безпечне з’єднання з другим сервером за допомогою автентифікації на основі ключа 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.
Успіх. Команда docker ps
вже виконана в контексті другого сервера.
Крім того, ми також можемо налаштувати з’єднання на основі 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
Весь процес складається з кількох кроків:
export HOST=reynardsec.com
– Він визначає змінну середовищаHOST
зі значенням «reynardsec.com».
openssl genrsa -aes256 -out ca-key.pem 4096
– 4096-бітний особистий ключ RSA, захищений шифруванням AES-256 і зберігає його у файлі ca-key.pem.
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
– Він створює новий сертифікат ЦС із закритого ключаca-key.pem
, який дійсний протягом 365 днів. Отриманий сертифікат зберігається у файліca.pem
.
openssl genrsa -out server-key.pem 4096
– Він генерує 4096-бітний закритий ключ RSA та зберігає його у файліserver-key.pem
.
openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr
– Він створює новий запит на підписання сертифіката (CSR) із закритого ключаserver-key.pem
. У цьому запиті Загальне ім’я (CN) встановлюється на значення змінної середовища HOST, а отриманий CSR зберігається у файліserver.csr
.
hostname -I
– Відображає всі IP-адреси, налаштовані на мережевих інтерфейсах машини.
echo subjectAltName = DNS:$HOST,IP:172.16.169.183,IP:127.0.0.1 >> extfile.cnf
– Він додає до файлу додаткові доменні імена та IP-адресиextfile.cnf
, які використовуватимуться як альтернативні імена для сервера.
echo extendedKeyUsage = serverAuth >> extfile.cnf
– Він визначає, що сертифікат використовуватиметься для автентифікації сервера, і додає цю інформацію до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
– Він підписує запит на сертифікатserver.csr
за допомогою ключа CA та сертифіката, створюючи сертифікат сервера, дійсний протягом 365 днів, і зберігає його у файліserver-cert.pem
.
openssl genrsa -out key.pem 4096
– Він генерує ще один 4096-бітний закритий ключ RSA та зберігає його уkey.pem
файлі.
echo extendedKeyUsage = clientAuth > extfile-client.cnf
– Він створює файл конфігураціїextfile-client.cnf
, вказуючи, що сертифікат використовуватиметься для автентифікації клієнта.
openssl req -new -key key.pem -out client.csr
– Він генерує новий запит на сертифікат (CSR) із закритого ключаkey.pem
та зберігає отриманий CSR у файлі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
– Він підписує запит на сертифікатclient.csr
за допомогою ключа ЦС і сертифіката, створюючи сертифікат клієнта, дійсний протягом 365 днів, і зберігає його у файліcert.pem
.
openssl req -new -key key.pem -out client.csr
. Цей крок тут не пропущено 🙂Тепер ми спробуємо запустити Docker Daemon, щоб він прослуховував підключення, захищені TLS (Малюнок 71).
Здається, ми успішно запустили Docker Daemon. Тепер давайте спробуємо підключитися до нього з іншого хоста. Нам потрібно безпечно передати файли на інший хост ca.pem
. У моєму випадку це буде сервер із адресою (лістинг 44, малюнок 72).cert.pem
key.pem
172.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. Передача ключів і сертифікатів на віддалений сервер.
Тепер настав час увійти на наш віддалений сервер і спробувати підключитися до хосту Docker (лістинг 45, малюнок 73).
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=172.16.169.183:2376 ps
Лістинг 45. Спроба підключення за допомогою 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.
Після впровадження цих змін нам потрібно перезавантажити служби, а потім перезапустити службу Docker (лістинг 49, малюнок 75).
sudo systemctl daemon-reload sudo systemctl restart docker
Лістинг 49. Перезавантаження конфігурації та перезапуск служби.
Настав час повернутися до нашого другого сервера і знову перевірити можливість підключення за протоколом TLS (Малюнок 76).
Скільки разів уже… успіху 🙂
Підсумовуючи, 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).
З точки зору безпеки, зберігання журналів на одній машині з запущеними контейнерами не є найкращою практикою з точки зору безпеки. Рекомендується надсилати журнали на віддалений сервер, призначений для їх збору. На щастя, Docker підтримує кілька різних драйверів, які обробляють збір журналів, включаючи ті, які дозволяють надсилати дані на віддалений сервер. Список драйверів, підтримуваних за замовчуванням, доступний у документації (на вкладці «Драйвери журналювання»).