Docker PID 1: Чому exec критично важливий для безпеки та стабільності контейнерів

 Основні висновки

  • exec замінює bash процес на ваш застосунок, роблячи його PID 1
  • Без exec сигнали Docker не доходять до вашого застосунку
  • PID 1 має особливу поведінку в Linux і Docker
  • Зомбі-процеси можуть заблокувати систему без правильного init процесу
  • Практика: завжди використовуйте exec в entrypoint скриптах

Проблема: Коли Docker контейнер не вмирає

Уявіть ситуацію: ви запускаєте docker stop mycontainer, чекаєте… і контейнер все ще працює. Через 10 секунд Docker втрачає терпіння і вбиває контейнер за допомогою SIGKILL. Дані втрачаються.

Причина? Ваш застосунок ніколи не отримав сигнал SIGTERM для graceful shutdown.

Типовий антипаттерн

Що відбувається:

  1. Bash скрипт отримує PID 1
  2. Node.js застосунок отримує інший PID (наприклад, PID 7)
  3. docker stop надсилає SIGTERM до PID 1 (bash)
  4. Bash не передає сигнал до дочірніх процесів
  5. Node.js ніколи не дізнається про shutdown
  6. Docker чекає 10 секунд і вбиває всіх SIGKILL

PID 1: Особливий процес з особливими правилами

Чому PID 1 особливий?

PID 1 в Linux має спеціальну поведінку: він ігнорує будь-які сигнали, якщо для них не встановлено явного обробника. Це зроблено навмисно – ви не хочете випадково вбити init процес, оскільки це має серйозні наслідки для всієї системи.

Відмінності PID 1:

  • Ігнорує сигнали без явних обробників
  • Не можна вбити стандартними сигналами
  • Відповідальний за reaping зомбі-процесів
  • Повинен adoptувати orphaned процеси (взяти опікунство над цими процесами)

Демонстрація проблеми

Як PID 1:

Як звичайний процес:


Shell Form vs Exec Form: Критична відмінність

Shell Form (Небезпечний)

Команда виконується через /bin/sh -c, створюючи додатковий shell процес:

Exec Form (Правильний)

Команда виконується безпосередньо як PID 1 без shell:


Зомбі-процеси: Тихий вбивця ресурсів

Що таке зомбі-процес?

Зомбі-процес – це процес, який завершився, але його батьківський процес не викликав waitpid() для отримання його статусу виходу. Процес “мертвий”, але запис про нього залишається в системній таблиці процесів.

Реальний приклад атаки через зомбі

У реальному проекті Stormkit зомбі-процеси призвели до вичерпання пам’яті сервера та Redis помилок.

Чому це небезпечно?

  1. Вичерпання PID: Система може закінчити PID’и
  2. Споживання пам’яті: Кожен зомбі займає місце в kernel
  3. Маскування атак: Інструменти моніторингу можуть плутати зомбі з живими процесами
  4. DoS атаки: Злумисник може створити тисячі зомбі

exec: Магічне рішення

Як працює exec в bash

Що відбувається під капотом

Без exec:

З exec:

Команда exec замінює поточний процес на новий, зберігаючи той же PID.


Практичні рішення та Best Practices

1. Використовуйте exec в entrypoint скриптах

2. Обробляйте сигнали правильно

3. Використовуйте Init системи для складних випадків

4. Docker –init флаг


Вразливості та Сценарії атак

1. PID Exhaustion Attack

Сценарій: Зловмисник знаходить endpoint, який створює процеси без reaping

Атака:

Результат: Система не може створювати нові процеси

2. Container Escape через Signal Handling

Проблема: Неправильна обробка сигналів може дозволити вихід за межі контейнера

Атака:

3. Resource Exhaustion

Зомбі-процеси займають запис у таблиці процесів ядра, що може привести до вичерпання системних ресурсів:


Інструменти діагностики та моніторингу

1. Перевірка PID процесу

2. Моніторинг зомбі-процесів

3. Тестування signal handling

4. Process tree аналіз


Реальні кейси з практики

Кейс 1: Node.js застосунок у production

Проблема: NPM не є process manager і не передає сигнали до застосунку

Кейс 2: Kubernetes Pod Termination

У Kubernetes під час видалення pod’а надсилається SIGTERM сигнал кожному контейнеру. Якщо процес не обробляє цей сигнал правильно, він може ігнорувати його.

Симптоми:

  • Pod’и “висять” під час deployment
  • Тривалий час завершення
  • Corrupted дані

Рішення:

Кейс 3: Multi-stage Docker Build


Checklist: Аудит вашого Docker setup

✅ Dockerfile Review

  • [ ] Використовується exec form для ENTRYPOINT/CMD
  • [ ] Немає npm start в production образах
  • [ ] Shell скрипти використовують exec
  • [ ] Встановлено правильні signal handlers

✅ Runtime Tests

  • [ ] docker stop займає < 2 секунд
  • [ ] PID 1 – це ваш застосунок, не bash
  • [ ] Немає зомбі-процесів після навантаження
  • [ ] Graceful shutdown зберігає дані

✅ Security Checks

  • [ ] Немає виконання довільних команд в signal handlers
  • [ ] Логи не містять “killed” повідомлень
  • [ ] Моніторинг показує стабільну кількість процесів
  • [ ] Init система налаштована для multi-process контейнерів

Висновки та рекомендації

Основні принципи

  1. Завжди використовуйте exec в shell entrypoint скриптах
  2. Уникайте shell form в Dockerfile інструкціях
  3. Обробляйте SIGTERM у ваших застосунках
  4. Використовуйте init системи для складних сценаріїв
  5. Тестуйте signal handling на всіх етапах розробки

Інструменти для впровадження

  • Tini: Мінімальна init система для контейнерів
  • dumb-init: Альтернатива від Yelp
  • Docker –init: Вбудоване рішення
  • s6-overlay: Повноцінна init система з supervision

Заключна думка

Правильна обробка сигналів не є менш важливою коли ваш застосунок працює всередині Docker контейнера. Різниця між exec node app.js та node app.js здається мінімальною, але вона критично важлива для стабільності, безпеки та правильного функціонування ваших контейнеризованих застосунків.

Не дозволяйте простій помилці у одному рядку коду зруйнувати надійність вашого production середовища. Використовуйте exec – ваші контейнери скажуть вам спасибі! 🐳


Додаткові ресурси

Стаття написана на основі досліджень та аналізу real-world кейсів з production середовищ.

Опубліковано в Docker

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *