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

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

Питання з реальних співбесід з відповідями: 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 - для ізольованої обробки даних.

Докладніше в документації: Мережеві драйвери 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

Класична продакшен-пастка на 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"]

Це закриває більшість сценаріїв закріплення зловмисника в контейнері після злому застосунку.

Докладніше в документації: Монтування tmpfs

Найпоширеніший спосіб передати пароль бази чи ключ 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.

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

Образ містить не лише ваш код, а й базову ОС, системні бібліотеки, 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) для базових образів і залежностей роблять процес постійним, а не авральним.

Докладніше в документації: Docker Scout

/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 в образі (непривілейований користувач усередині контейнера) - базова практика.

Докладніше в документації: Режим rootless

--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, маніфести) і сканерами конфігурації.

Докладніше в документації: Профілі seccomp

За замовчуванням 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 має не з'єднатися - сегментація працює лише тоді, коли її перевірили.

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