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 - для ізольованої обробки даних.
Публікація порту (-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 # без виходу в інтернет
Зворотний проксі не має доступу до бази, а база не може сама звертатися в інтернет.
За замовчуванням процес у контейнері працює від 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 на хості.