Питання на співбесіді: Мережі й безпека контейнерів
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
14 питань
Мережевий драйвер визначає, як контейнер підключений до мережі хоста й до інших контейнерів.
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 на хості.
Класична продакшен-пастка на Ubuntu й Debian:
sudo ufw default deny incoming
sudo ufw allow 22
sudo ufw allow 443
# фаєрвол налаштовано, «все закрито»
А потім docker run -p 6379:6379 redis - і Redis доступний з інтернету, попри правила ufw.
Чому так: Docker сам керує правилами фаєрвола (iptables чи nftables) - без них не працювали б мережі bridge і публікація портів. Трафік на опублікований порт перенаправляється в контейнер у таблиці NAT ще до того, як потрапить у ланцюжки, якими керує ufw. Документація Docker прямо попереджає: Docker і ufw використовують правила фаєрвола несумісно.
Як захиститися:
1. Не публікувати те, що не має бути публічним - найнадійніше:
services:
redis:
# без ports - доступний лише іншим контейнерам у мережі
postgres:
ports:
- "127.0.0.1:5432:5432" # лише з самого хоста
2. Ланцюжок DOCKER-USER - Docker пропускає через нього трафік до контейнерів перед власними правилами. Туди додають обмеження (наприклад, дозволити порт лише з певних адрес). Правила в інших ланцюжках, створених Docker, змінювати не можна - Docker перезаписує їх.
3. Фаєрвол на рівні мережі чи хмари - security groups у AWS, фаєрвол провайдера (Hetzner Cloud Firewall) працюють поза хостом і не залежать від правил, які створює Docker.
4. Не вимикати керування фаєрволом Docker ("iptables": false) без глибокого розуміння - зламаються мережі контейнерів.
Перевірка ззовні, а не з самого сервера:
nmap -p 1-65535 ваш-сервер
і docker ps для переліку опублікованих портів.
Висновок для співбесіди: фаєрвол хоста не захищає опубліковані порти Docker; захищає правильна публікація (127.0.0.1 чи без ports) і фаєрвол на рівні мережі.
У Linux права root розділені на окремі можливості (capabilities): змінювати власника файлів (CAP_CHOWN), відкривати порти нижче 1024 (CAP_NET_BIND_SERVICE), налаштовувати мережу (CAP_NET_ADMIN), завантажувати модулі ядра (CAP_SYS_MODULE) тощо.
Docker за замовчуванням дає контейнеру обмежений набір - близько півтора десятка можливостей (CHOWN, SETUID, NET_BIND_SERVICE, KILL та інші), а небезпечні (SYS_ADMIN, NET_ADMIN, SYS_MODULE) прибирає. Тому root у контейнері слабший за root на хості.
Принцип найменших привілеїв - прибрати все й додати лише потрібне:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
services:
app:
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
security_opt:
- no-new-privileges:true
docker run --rm --cap-drop ALL alpine chown nobody /tmp
# chown: /tmp: Operation not permitted - навіть від root
no-new-privileges забороняє процесу отримати нові права через setuid-бінарні файли (наприклад, sudo, su) - навіть якщо вони є в образі.
Скільки можливостей потрібно типовому вебзастосунку? Часто - жодної, якщо він працює від звичайного користувача й слухає порт вище 1024. Процеси, що стартують від root і перемикаються на іншого користувача (nginx, php-fpm master), потребують SETUID, SETGID, CHOWN. Підібрати мінімальний набір - експериментально: прибрати все й додавати те, без чого контейнер не запускається.
Небезпечні можливості, які додають «щоб працювало»:
SYS_ADMIN- фактично майже повний root (монтування, багато адміністративних операцій) - один із класичних шляхів виходу з контейнера;NET_ADMIN- керування мережею хоста;SYS_PTRACE- відстеження інших процесів.
Якщо документація сторонньої програми вимагає таких можливостей, варто зрозуміти навіщо, а не додавати бездумно.
Перевірити ефективні можливості в контейнері: grep CapEff /proc/self/status і розшифрувати capsh --decode=....
Докладніше в документації: Запуск контейнерів: привілеї й можливості
--read-only робить кореневу файлову систему контейнера доступною лише для читання. Запис дозволено тільки в явно змонтовані томи й tmpfs.
docker run --rm --read-only alpine touch /x
# touch: /x: Read-only file system
docker run --rm --read-only --tmpfs /tmp alpine sh -c 'touch /tmp/y && echo ok'
# ok
Навіщо:
- зламаний застосунок не може змінити себе: зловмисник, що отримав виконання коду, не запише веб-шелл у каталог з кодом, не підмінить бінарні файли, не встановить інструменти через пакетний менеджер;
- незмінність: контейнер гарантовано відповідає образу - жодного «дрейфу» конфігурації через ручні зміни;
- видно всі місця запису: кожен каталог, куди застосунок пише, треба оголосити явно - це документує поведінку застосунку.
Що зазвичай потрібно для запису й чим це закрити:
- тимчасові файли (
/tmp,/var/run, кеш PHP-сесій) -tmpfs(у пам'яті, зникає при зупинці); - дані, що мають жити - іменовані томи;
- для Laravel:
storage/framework(кеш, сесії, скомпільовані шаблони),storage/logs,bootstrap/cache- tmpfs або томи. Логи краще писати вstderr(LOG_CHANNEL=stderr), тоді каталог логів не потрібен.
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/www/html/storage/framework:uid=1000,gid=1000
volumes:
- uploads:/var/www/html/storage/app
Особливості tmpfs:
- зберігається в пам'яті й зараховується до ліміту пам'яті контейнера - великі тимчасові файли (обробка відео) там не місце;
- вміст зникає при зупинці контейнера;
- права й розмір задаються опціями монтування.
Поєднання захисних прапорців - стандартний «посилений» запуск:
read_only: true
user: "1000:1000"
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
Це закриває більшість сценаріїв закріплення зловмисника в контейнері після злому застосунку.
Найпоширеніший спосіб передати пароль бази чи ключ API в контейнер - змінна оточення. Але змінні оточення легко витікають:
docker inspectпоказує всі змінні контейнера відкритим текстом - будь-хто з доступом до Docker на хості їх бачить;/proc/<pid>/environ- процеси з достатніми правами читають оточення інших процесів;- дочірні процеси успадковують оточення: сторонній бінарний файл, запущений застосунком, отримує всі секрети;
- логи й звіти про помилки: сторінки налагодження,
phpinfo(), дампи оточення в системах моніторингу; ENVу Dockerfile зберігається в образі - будь-хто з доступом до образу (реєстр) прочитаєdocker history/docker inspectобразу.
Секрети Compose монтують значення файлом у /run/secrets/<назва>:
services:
app:
image: myapp
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt # або environment: DB_PASSWORD з оточення хоста
- значення не видно в
docker inspectконтейнера; - доступ лише для сервісів, яким секрет явно надано;
- багато офіційних образів підтримують змінні з суфіксом
_FILE(POSTGRES_PASSWORD_FILE,MYSQL_ROOT_PASSWORD_FILE) - читають секрет з файлу.
Застосунок має вміти читати секрет з файлу. Для Laravel - невеликий код у конфігурації чи завантаження змінних з файлу на старті контейнера (скрипт-обгортка, що читає /run/secrets/* і експортує їх лише для процесу PHP).
Що варто пам'ятати:
- секрети Compose без Swarm - це по суті bind mount файлу: захищають від
inspectі випадкового витоку, але не від root на хості; - у продакшені - менеджери секретів (Vault, AWS Secrets Manager, Doppler) чи механізми оркестратора (Kubernetes Secrets з шифруванням);
- на етапі збирання образу секрети передаються через
RUN --mount=type=secret, а неARGчиENV; - файл
.envз секретами - у.dockerignoreі.gitignore.
Образ містить не лише ваш код, а й базову ОС, системні бібліотеки, PHP і розширення, Composer- і npm-залежності. У кожному шарі можуть бути відомі вразливості (CVE).
Інструменти сканування:
docker scout cves myapp:1.4 # Docker Scout (вбудований у Docker CLI)
docker scout quickview myapp:1.4 # коротке зведення з порадами
trivy image myapp:1.4 # Trivy від Aqua Security - відкритий і популярний у CI
grype myapp:1.4 # Grype від Anchore
Сканер визначає пакети в образі (за базами пакетного менеджера ОС і файлами залежностей мов) і зіставляє їх з базами вразливостей.
Як вбудувати в процес:
- у CI на кожне збирання: падати на вразливостях рівня
CRITICAL/HIGH, для яких є виправлення (trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1); - регулярне сканування вже зібраних образів - нові CVE з'являються щодня, а образ, чистий тиждень тому, сьогодні може бути вразливим;
- SBOM (перелік компонентів образу) -
docker scout sbomчиsyft: швидко зрозуміти, де використовується вразлива бібліотека, коли виходить нове повідомлення.
Що робити з результатами - не намагатися виправити все:
- оновити базовий образ - найчастіше прибирає більшість знахідок. Закріплюйте мінорну версію (
php:8.5-fpm-alpine) і регулярно перезбирайте; - менший образ - менше вразливостей: Alpine,
-slim, distroless, multi-stage без інструментів збирання у фінальному образі; - оцінювати досяжність: вразливість у бібліотеці, яку застосунок не викликає, чи в пакеті, що потрапив в образ випадково, - менш пріоритетна, ніж у вебсервері;
- документувати свідомі винятки (VEX, файл ігнорування з причиною й датою перегляду), щоб звіт лишався корисним, а не з сотнею проігнорованих рядків.
Пастки:
- «нуль CVE» - не мета: багато знахідок у базових ОС не мають виправлень або не стосуються вашого використання;
- сканер бачить лише відомі пакети: бінарні файли, скопійовані вручну (
curl ... | tar), часто невидимі для нього; - автоматичні оновлення (Dependabot, Renovate) для базових образів і залежностей роблять процес постійним, а не авральним.
/var/run/docker.sock - сокет, через який клієнти керують демоном Docker. Демон працює від root і виконує будь-які команди API: створює контейнери з будь-якими параметрами.
Хто має доступ до сокета - той має root на хості. Наприклад:
docker run -v /:/host --rm -it alpine chroot /host sh
Новий контейнер монтує кореневу файлову систему хоста, і chroot дає повноцінну оболонку root на хості: читання /etc/shadow, SSH-ключів, зміна системних файлів, встановлення бекдора.
Що це означає на практиці:
1. Монтування сокета в контейнер (-v /var/run/docker.sock:/var/run/docker.sock) - популярне для Traefik, Portainer, агентів моніторингу, CI-раннерів, Watchtower. Якщо такий контейнер зламано (вразливість у вебінтерфейсі, ланцюжку постачання), зловмисник отримує весь хост.
2. Група docker на хості - користувач у ній фактично root без sudo. Додавати в неї варто лише тих, кому довіряєте як адміністраторам.
3. TCP-доступ до демона без TLS (-H tcp://0.0.0.0:2375) - відкритий root для всього інтернету. Такі демони масово знаходять і використовують для криптомайнінгу.
Як зменшити ризик:
- не монтувати сокет без крайньої потреби; якщо інструмент може працювати інакше (файловий провайдер Traefik замість Docker-провайдера) - обрати інший спосіб;
- проксі сокета з білим списком дозволених викликів API (наприклад,
tecnativa/docker-socket-proxy): Traefik отримує лише читання списку контейнерів, а не створення нових; - монтування
:roне допомагає - це лише права на файл сокета, а не на дії через API; - rootless Docker - демон працює від звичайного користувача, і доступ до сокета дає права цього користувача, а не root;
- CI-збирання - замість Docker-in-Docker через сокет хоста використовувати ізольовані раннери чи збирання без демона (BuildKit rootless, Kaniko, Buildah);
- віддалений доступ до демона - лише через SSH (
DOCKER_HOST=ssh://...) чи TLS з клієнтськими сертифікатами.
На співбесіді це питання перевіряє розуміння, що Docker API - привілейований інтерфейс, а не «просто інструмент розробника».
Докладніше в документації: Безпека Docker Engine: поверхня атаки демона
У звичайній конфігурації демон Docker працює від root, і root усередині контейнера - це root ядра (UID 0). Вихід з контейнера через вразливість чи помилку конфігурації дає права root на хості. Два механізми зменшують цей ризик.
1. User namespace remapping (userns-remap) - демон лишається від root, але користувачі контейнера відображаються на непривілейований діапазон UID хоста:
// /etc/docker/daemon.json
{ "userns-remap": "default" }
Root (UID 0) у контейнері насправді є, наприклад, UID 100000 на хості. Якщо процес вирветься з контейнера, на хості він - звичайний користувач без прав.
2. Rootless mode - і демон, і контейнери працюють від звичайного користувача:
- демон не має прав root узагалі - вразливість у самому демоні не дає root на хості;
- доступ до сокета Docker дає лише права цього користувача (а не «фактично root», як у звичайному режимі);
- кожен користувач може мати власний Docker.
Обмеження й компроміси:
- порти нижче 1024 без додаткового налаштування недоступні (
net.ipv4.ip_unprivileged_port_start); - мережа реалізована в просторі користувача - продуктивність нижча, а справжня адреса клієнта може бути недоступна без додаткового налаштування;
- обмеження ресурсів через cgroups потребують cgroup v2 і делегування;
- деякі драйвери сховища й можливості недоступні;
- права на bind mount: файли, створені в контейнері, на хості належать відображеним UID - треба розуміти відображення, щоб не отримати «чужі» файли;
- з
userns-remapдеякі опції (--privileged, спільні простори імен з хостом) не працюють чи потребують вимкнення відображення для конкретного контейнера.
Альтернатива - Podman: працює без демона і за замовчуванням без root; CLI сумісний з Docker.
Коли варто:
- багатокористувацькі сервери й CI, де контейнери запускають люди чи процеси з різним рівнем довіри;
- посилена безпека продакшен-хостів, де можна прийняти обмеження;
- для локальної розробки rootless зазвичай зайвий, а Docker Desktop і так ізолює контейнери у віртуальній машині.
Незалежно від режиму USER в образі (непривілейований користувач усередині контейнера) - базова практика.
--privileged знімає майже всі обмеження контейнера:
- надає всі можливості (capabilities), включно з
SYS_ADMIN; - вимикає профілі seccomp і AppArmor/SELinux;
- дає доступ до всіх пристроїв хоста (
/dev) - дисків, модулів ядра.
Процес у привілейованому контейнері може змонтувати диск хоста й прочитати чи змінити будь-які файли, завантажити модуль ядра - тобто це фактично root на хості. Ізоляція контейнера тоді майже лише формальна.
Коли його застосовують - Docker-in-Docker, деякі системні агенти (керування мережею, сховищем), експерименти. Майже завжди є вужча альтернатива: окремі можливості (--cap-add), конкретні пристрої (--device), зовнішній демон через SSH чи безпечні інструменти збирання.
Seccomp - фільтр системних викликів ядра. Docker за замовчуванням застосовує профіль, що забороняє кілька десятків небезпечних викликів (наприклад, mount, reboot, kexec_load, add_key, створення нових просторів імен у більшості випадків). Більшості застосунків вони не потрібні, а для зловмисника це інструменти виходу з контейнера.
docker run --security-opt seccomp=./custom-profile.json myapp
docker run --security-opt seccomp=unconfined myapp # вимкнути - лише для налагодження
Власний, вужчий профіль (лише ті виклики, що реально використовує застосунок) - ще сильніший захист, але вимагає ретельного тестування.
AppArmor (Ubuntu/Debian) / SELinux (RHEL/Fedora) - мандатний контроль доступу: обмежує, до яких файлів, мереж і можливостей процес має доступ, незалежно від прав користувача. Docker застосовує профіль docker-default для AppArmor; власні профілі задають через --security-opt apparmor=....
Багатошаровий захист - кожен механізм закриває свою частину:
| Механізм | Що обмежує |
|---|---|
| простори імен | що процес бачить |
| cgroups | скільки ресурсів використовує |
| capabilities | які привілейовані дії може робити root |
| seccomp | які системні виклики ядра доступні |
| AppArmor/SELinux | до яких ресурсів є доступ |
| непривілейований користувач | права всередині контейнера |
Правило для продакшену: жодного --privileged і seccomp=unconfined без задокументованої причини; перевіряти це в рев'ю конфігурацій (Compose, маніфести) і сканерами конфігурації.
За замовчуванням Compose підключає всі сервіси проєкту до однієї мережі default. Кожен контейнер бачить кожен: зворотний проксі може під'єднатися до бази, а сервіс обробки зображень - до Redis із сесіями.
Якщо будь-який сервіс зламано (вразливість у бібліотеці обробки файлів, сторонній образ), зловмисник отримує мережевий доступ до всіх інших.
Сегментація - кілька мереж за принципом «хто з ким має говорити»:
services:
proxy:
image: caddy
ports: ["443:443"]
networks: [edge]
app:
image: myapp
networks: [edge, data]
worker:
image: myapp
command: php artisan queue:work
networks: [data, egress]
postgres:
image: postgres:18
networks: [data]
redis:
image: redis:8
networks: [data]
networks:
edge:
data:
internal: true # без маршруту в інтернет
egress: # окрема мережа з виходом назовні
Що це дає:
- проксі бачить лише застосунок - не базу й не Redis;
- база й Redis у мережі
internal: true- не можуть самі звертатися в інтернет (зламана база не завантажить шкідливий код і не відправить дані назовні); - воркер має вихід в інтернет для сторонніх API, але не стоїть «на краю» - до нього не звертаються ззовні.
Додаткові прийоми:
- без
portsдля всього, крім точки входу; - окремі облікові записи бази для застосунку й воркерів з мінімальними правами - мережа обмежує, хто може під'єднатися, права - що можна зробити після під'єднання;
- аліаси мереж - сервіс може мати різні імена в різних мережах.
Обмеження: мережі Docker - це сегментація на рівні з'єднань (L3/L4). Вони не замінюють автентифікацію між сервісами: Redis і база мають паролі навіть у внутрішній мережі. Для політик на рівні окремих потоків і шифрування між сервісами - оркестратори з мережевими політиками (Kubernetes NetworkPolicy) чи service mesh.
Перевірка: docker compose exec proxy nc -zv postgres 5432 має не з'єднатися - сегментація працює лише тоді, коли її перевірили.