Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Мережі й безпека контейнерів»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

Мережевий драйвер визначає, як контейнер підключений до мережі хоста й до інших контейнерів.

bridge (за замовчуванням) - контейнер отримує власну мережеву «картку» у віртуальній мережі всередині хоста. Ззовні він недоступний, доки порт не опубліковано (-p 8080:80). Контейнери в одній bridge-мережі бачать один одного.

host - контейнер використовує мережу хоста напряму, без ізоляції:

docker run --network host nginx   # nginx слухає порт 80 самого хоста
  • плюс: немає накладних витрат на трансляцію адрес, корисно для програм з великим мережевим навантаженням чи багатьма портами;
  • мінус: немає мережевої ізоляції, порти контейнера конфліктують з портами хоста, -p ігнорується. На Docker Desktop (macOS, Windows) працює з обмеженнями, бо «хост» - це віртуальна машина.

none - лише локальний інтерфейс (lo), жодної мережі. Для задач, яким мережа не потрібна й не повинна бути доступною: обробка файлів, генерація звітів з недовірених даних.

Інші драйвери:

  • overlay - мережа поверх кількох хостів (Docker Swarm), контейнери на різних серверах спілкуються як в одній мережі;
  • macvlan / ipvlan - контейнер отримує власну адресу в фізичній мережі, як окремий пристрій. Для інтеграції зі старими системами, що очікують окремий IP;
  • плагіни сторонніх постачальників.
docker network ls
docker network create app-net
docker run --network app-net --name db postgres:18

Практичне правило: для застосунків - власні bridge-мережі (Compose створює їх автоматично). host - лише коли є виміряна потреба, бо він прибирає один із шарів ізоляції. none - для ізольованої обробки даних.

Докладніше в документації: Мережеві драйвери Docker

Публікація порту (-p) відкриває доступ до контейнера ззовні: трафік на порт хоста перенаправляється в контейнер. На відміну від EXPOSE у Dockerfile, який лише документує порт, -p справді змінює мережеві правила хоста.

docker run -p 8080:80 nginx              # 0.0.0.0:8080 - на ВСІХ інтерфейсах хоста
docker run -p 127.0.0.1:8080:80 nginx    # лише з самого хоста
docker run -p 10.0.0.5:8080:80 nginx     # лише на конкретному інтерфейсі

Ключова різниця: без вказаної адреси порт публікується на всіх мережевих інтерфейсах (0.0.0.0 і ::) - тобто доступний з інтернету, якщо сервер має публічну адресу.

Типова аварія: на сервері в compose.yaml бази даних залишили з розробки:

services:
  postgres:
    ports:
      - "5432:5432"   # база доступна всьому інтернету

Сканери знаходять відкриті бази й Redis за хвилини. Через те, що Docker керує правилами фаєрвола сам, ufw таку публікацію не блокує.

Як правильно:

  • сервісам, до яких звертаються лише інші контейнери (база, Redis, черги), - не публікувати порти взагалі: контейнери в одній мережі звертаються один до одного за іменем сервісу (postgres:5432) без публікації;
  • доступ з хоста для розробки чи адміністрування - 127.0.0.1:5432:5432, а на сервері - SSH-тунель;
  • назовні - лише зворотний проксі (Nginx, Caddy, Traefik) на 80/443.
services:
  postgres:
    ports:
      - "127.0.0.1:5432:5432"

Перевірити, що реально опубліковано: docker ps (колонка PORTS) чи ss -tlnp на хості.

Докладніше в документації: Публікація портів

Docker створює мережу з назвою bridge автоматично, і docker run без --network підключає контейнери саме до неї. Але для застосунків документація Docker радить власні (user-defined) bridge-мережі.

docker network create app-net
docker run -d --network app-net --name db postgres:18
docker run -d --network app-net --name web myapp

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

1. DNS за іменами контейнерів. У власній мережі контейнер web звертається до бази як db:5432 - вбудований DNS Docker розв'язує ім'я в поточну IP-адресу. У мережі за замовчуванням імен немає - лише IP, які змінюються після перезапуску (застарілий --link для цього вже не рекомендований).

2. Ізоляція. Усі контейнери без явної мережі потрапляють в одну спільну мережу bridge і можуть «бачити» один одного. Власні мережі відокремлюють проєкти й групи сервісів: контейнер з однієї мережі не дістанеться до бази в іншій.

3. Підключення «на льоту». Контейнер можна приєднати до власної мережі чи від'єднати без перезапуску: docker network connect app-net web.

4. Налаштування (підмережа, MTU, внутрішня мережа без виходу в інтернет) задаються для кожної мережі окремо.

Docker Compose робить це автоматично: для проєкту створюється мережа <проєкт>_default, і сервіси звертаються один до одного за іменами сервісів. Тому в Laravel-проєкті з Compose у .env пишуть DB_HOST=mysql, а не IP.

Корисний прийом безпеки - кілька мереж:

services:
  proxy:
    networks: [frontend]
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]

networks:
  frontend:
  backend:
    internal: true   # без виходу в інтернет

Зворотний проксі не має доступу до бази, а база не може сама звертатися в інтернет.

Докладніше в документації: Драйвер bridge

За замовчуванням процес у контейнері працює від root (UID 0). Перевірити просто: docker run --rm alpine id - покаже uid=0(root).

Чому це небезпечно, якщо контейнер ізольований? Ізоляція не абсолютна:

  • root у контейнері - той самий root ядра, що й на хості (без user namespaces). Вразливість у ядрі чи середовищі виконання, помилкова конфігурація (--privileged, змонтований docker.sock, змонтовані каталоги хоста) - і процес отримує права root на хості;
  • змонтовані каталоги: root у контейнері може змінювати й видаляти файли хоста в bind mount, створювати файли, які потім не видалиш без sudo;
  • зламаний застосунок (RCE через вразливість у коді) отримує повні права всередині контейнера: встановити інструменти, змінити бінарні файли, читати всі файли.

Як запустити від звичайного користувача:

RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app
USER app

або під час запуску:

docker run --user 1000:1000 myapp
services:
  app:
    user: "1000:1000"

Що зазвичай ламається і як виправити:

  • порти нижче 1024 - звичайний користувач їх не відкриє. Застосунок слухає 8080, а зовні публікується -p 80:8080;
  • права на каталоги для запису (storage, bootstrap/cache у Laravel) - chown при збиранні образу;
  • bind mount у розробці - UID у контейнері має збігатися з UID користувача на хості, інакше файли належатимуть «чужому» користувачу. Laravel Sail для цього використовує змінні WWWUSER/WWWGROUP.

Офіційні образи: php-fpm запускає робочі процеси від www-data, хоча головний процес стартує від root; nginx має варіанти nginx-unprivileged. Для власних образів USER - стандартна практика, а сканери безпеки й Kubernetes-політики (runAsNonRoot) перевіряють це автоматично.

Докладніше в документації: Запуск контейнерів

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

Два головні механізми ядра Linux:

1. Простори імен (namespaces) - що процес бачить:

  • PID - власне дерево процесів; процес контейнера має PID 1 всередині й не бачить процесів хоста;
  • network - власні інтерфейси, адреси, таблиці маршрутизації, порти;
  • mount - власна файлова система (образ + томи);
  • UTS - власне ім'я хоста;
  • IPC - власні черги повідомлень і спільна пам'ять;
  • user (опційно) - відображення користувачів: root усередині може бути звичайним користувачем ззовні.

2. Контрольні групи (cgroups) - скільки ресурсів процес може використати: пам'ять, процесор, кількість процесів, ввід-вивід. Саме cgroups реалізують --memory, --cpus, --pids-limit.

Додаткові шари захисту: обмежені можливості (capabilities) root, профіль seccomp (заборонені системні виклики), AppArmor/SELinux.

Що з цього випливає:

  • контейнери легкі: старт - мілісекунди, накладні витрати мінімальні, бо немає окремого ядра й гостьової ОС;
  • ізоляція слабша, ніж у ВМ: вразливість у ядрі хоста потенційно доступна з будь-якого контейнера. Тому недовірений код (код користувачів, багатоорендні платформи) часто запускають у пісочницях з додатковою ізоляцією - gVisor, Kata Containers, Firecracker;
  • ядро одне: Linux-контейнер не запуститься на ядрі Windows напряму. Docker Desktop на macOS і Windows запускає контейнери всередині легкої Linux-віртуальної машини;
  • «контейнер - це межа безпеки» з обережністю: для довірених застосунків вона достатня, але не варто покладатися на неї як на єдиний захист.

Перевірити простори імен процесу: ls -l /proc/<pid>/ns на хості.

Докладніше в документації: Безпека Docker Engine