Docker AppArmor і seccomp: мінімальне жорстке налаштування, яке має значення

Було корисно?

Не потрібно «займатися безпекою» як театром відповідності. Потрібні контейнери, які поводяться належним чином, коли їх скомпрометовано — бо це рано чи пізно трапиться. Питання не в тому, чи є в вашому додатку баги. Питання в тому, чи перетвориться баг на нудний інцидент у тикеті або на багатогодинне «перехоплення заручників» з експлоїтом ядра й дуже самовпевненим нападником.

AppArmor і seccomp — дві з небагатьох механізмів захисту контейнерів, які системно змінюють радіус ураження в продакшні. Вони не блискучі й не вразять відділ закупівель. Але вони блокують реальні прийоми: дивні виклики системи, некоректний доступ до файлової системи і моменти «навіщо цьому API-контейнеру монтувати файлові системи?».

Що насправді означає «мінімальне укріплення»

У безпеці контейнерів «мінімальне укріплення» — це не «увімкнути всі можливості й молитися». Це невеликий набір контролів, які:

  • Застосовуються широко до різнорідних додатків.
  • Гучно сигналізують про помилки (ви можете виявити збої) і відпрацьовують безпечно (вони не знімають захист мовчки).
  • Зменшують поверхню атаки ядра — бо ядро це спільний ресурс, який ви не можете виправити, просто перепакувавши контейнер.
  • Операційно діагностуються під тиском о 02:00.

AppArmor і seccomp відповідають цим вимогам. Вони не замінюють запуск не від root, видалення Linux-можливостей, файлові системи лише для читання та належне управління секретами. Але якби мене попросили вибрати два контролі, які змінюють результат після компрометації, я б вибрав їх.

Мінімальне укріплення, яке має значення для більшості Docker-інфраструктур, виглядає так:

  • Використовуйте стандартний seccomp-профіль Docker (не відключайте його).
  • Використовуйте енфорсований AppArmor-профіль (не запускайте в unconfined).
  • Уникайте –privileged і уникати загальних seccomp=unconfined.
  • Коли додатку дійсно потрібні винятки — робіть їх явними, задокументованими й протестованими.

Є причина, чому ці два контролі згадуються в розборах інцидентів: вони часто відрізняють «зловмисник отримав shell всередині контейнера» від «зловмисник отримав root на хості».

Факти та історія, які мають значення (коротко, конкретно)

Контроль безпеки легше зрозуміти, коли знаєте, навіщо він існує. Кілька опорних пунктів:

  1. AppArmor з’явився в середині 2000-х і став мейнстрімом в Ubuntu задовго до появи контейнерів; він розроблявся для обмеження демонів за правилами, заснованими на шляхах.
  2. SELinux проти AppArmor — це здебільшого різниця в моделі політик і інструментах; Docker інтегрувався з обома, але AppArmor часто простіший в експлуатації в середовищах Debian/Ubuntu.
  3. seccomp починався як «secure computing mode» близько Linux 2.6.12, спочатку дуже обмежувальний; пізніше seccomp-bpf зробив його практичним через фільтри на основі BPF.
  4. Docker увімкнув seccomp за замовчуванням ще роки тому; багато команд вимикають його в період «просто змусити працювати» й потім забувають увімкнути назад.
  5. Ізоляція контейнерів — не межа VM; контейнери ділять ядро хоста. Ось чому фільтрація системних викликів має непропорційну цінність.
  6. Namespacing і cgroups — не політики безпеки; це механізми ізоляції. AppArmor і seccomp — це політики, які можуть сказати «ні».
  7. Більшість ескейпів з контейнера орієнтовані на ядро; якщо ви зменшите поверхню викликів системи й заблокуєте дивні інтерфейси ядра, ви підвищите свої шанси.
  8. AppArmor — «шляхово-залежний», що одночасно його сила (читабельність) і потенційна пастка (трюки з перейменуванням/монтуванням, якщо ви неакуратно працюєте з маунтами й медіацією).
  9. seccomp-фільтри призначені для процесу; якщо процес запущено без фільтра (або ви встановили unconfined), у вас не буде другої спроби пізніше.

Модель загроз: що ці контролі зупиняють (і що не зупиняють)

Чим корисний AppArmor

AppArmor — це обов’язковий контроль доступу (Mandatory Access Control). Він обмежує те, що процес може робити, навіть якщо процес вважає себе root. У контейнерному середовищі це корисно, коли пробій контейнера починається з «отримати root всередині контейнера» (що часто тривіально) і продовжується «торкнутися ресурсів хоста». AppArmor може блокувати читання/запис файлів, монтування, ptrace, сигнали й взаємодії з певними інтерфейсами ядра.

Також це хороший «політичний тригер» — коли ви бачите відмову, вона зазвичай корелює з поведінкою, яку слід хоча б зрозуміти.

Чим корисний seccomp

seccomp фільтрує системні виклики: він може дозволяти, забороняти або ловити виклики системи. Тут мова не про зупинку «rm -rf». Це про зупинку викликів, які часто використовуються в ланцюжках експлуатації: створення неймспейсів, keyrings, робота з raw-пакетами, perf events, монтування файлових систем тощо. Стандартний seccomp-профіль Docker блокує низку ризикових викликів, які типовим додаткам не потрібні.

Чого вони не роблять

  • Вони не виправляють вразливості вашого додатку. Вони їх містять.
  • Вони не замінюють принцип найменших привілеїв. Якщо ви даєте контейнеру маунти хоста або режим privileged, ви обходите значну частину цінності.
  • Вони не зупиняють ексфільтрацію даних через дозволені канали (HTTP, DNS тощо). Це справа мережевих політик і контролю egress.
  • Вони не зупиняють логічне зловживання типу «експортувати всі дані клієнтів», якщо ваш API це дозволяє.

Парафразована ідея (приписується): Werner Vogels відомий тим, що просуває ідею «все падає постійно», тож ви проєктуєте для стримування й відновлення. Такий підхід тут доречний: передбачайте компрометацію, дизайнуйте радіус ураження.

AppArmor в Docker: профілі, режими та здорові налаштування

Як Docker використовує AppArmor

На хостах з увімкненим AppArmor Docker може застосовувати AppArmor-профіль до кожного контейнера. Якщо ви нічого не робите, зазвичай отримуєте профіль за замовчуванням, наприклад docker-default (залежно від дистрибутива й пакування Docker). Якщо AppArmor вимкнено або Docker не може завантажити профілі, контейнери можуть працювати в unconfined.

«Unconfined» не означає «гаразд». Це означає, що ви зняли одну з небагатьох загороджень, яка працює навіть коли процес має root.

Enforce проти complain

AppArmor має два оперативно релевантні режими:

  • enforce: відхиляє порушення. Це те, що вам потрібно в продакшні.
  • complain: дозволяє, але журналить. Це спосіб зібрати дані для написання профілю, не зламавши робочі навантаження.

Якщо ви намагаєтесь впровадити AppArmor, почніть з режиму complain у стаджингу з реальним трафіком. Потім перекладайте в enforce з вікном на зміну й планом відкату.

Як виглядає «мінімально корисний» профіль

Для Docker «мінімально корисним» зазвичай є: зберегти Docker default і послаблювати тільки там, де є вагомі докази. Кастомні профілі — це можливість; вони також зобов’язують до підтримки.

Якщо ви мусите писати власний профіль, тримайте його жорстким щодо маунтів, raw-пристроїв, ptrace і небезпечних шляхів. Будьте явними щодо директорій, що дозволені для запису. Контейнери не повинні мати змогу записувати довільні частини файлової системи; якщо можуть — ви рано чи пізно дізнаєтесь про це суворим шляхом.

Операційні ознаки того, що AppArmor вас не захищає

  • docker info показує AppArmor вимкненим.
  • Контейнери показують AppArmorProfile: "" або unconfined.
  • Ніколи не з’являються відмови, навіть під час відомих поганих тестів (це підозріло, а не заспокоює).

Анекдот #1: AppArmor у режимі complain — як охоронець, який пише лише в щоденнику сильніший текст. Добре для досліджень, не дуже для зупинки нападників.

seccomp в Docker: фільтрація системних викликів без самошкоди

Стандартний seccomp-профіль Docker

Docker постачається зі стандартним seccomp-профілем, який блокує виклики системи, що історично вважаються ризиковими або рідко використовуються типовими контейнерними навантаженнями. Він не ідеальний і не мінімалістичний; він практичний. Стандартний профіль зазвичай ламає лише спеціалізовані робочі навантаження (деякі трасувальники, незвичні рантайми, певні інструменти налаштування БД, трюки з вкладеними контейнерами) — і навіть тоді зазвичай можна вирішити проблему, не переходячи в повний unconfined.

Як проявляються відмови seccomp

Коли seccomp блокує щось, зазвичай ви бачите одне з цього:

  • Процес отримує EPERM або EACCES і логує помилку на кшталт «operation not permitted».
  • Процес вбивається сигналом SIGSYS («bad system call»). Це трапляється коли фільтр налаштовано на kill.
  • Аудит-логи показують події seccomp, якщо налаштовано аудит.

Трюк при відлагодженні — корелювати помилки додатку з логами ядра/аудиту та конфігурацією контейнера. Якщо ви починаєте з випадкового відключення seccomp, ви «виправите» симптом, створивши регресію безпеки, яку ніхто не згадає через шість тижнів.

Коли варто кастомізувати seccomp

Кастомізуйте seccomp тільки якщо у вас стабільне навантаження і ви можете пояснити причину в одному реченні: «Цьому контейнеру потрібен системний виклик X, бо функція Y його використовує». Якщо ви не можете пояснити — ви здогадуєтесь. Здогадки приводять до того, що unconfined з’являється всюди.

Анекдот #2: Запуск контейнерів з seccomp=unconfined — це як зняти ремені безпеки, бо вони мнуть сорочку.

Практичні завдання: команди, виводи та рішення (12+)

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

Завдання 1 — Перевірити, чи Docker бачить AppArmor і seccomp

cr0x@server:~$ docker info --format 'apparmor={{.SecurityOptions}}'
apparmor=[name=apparmor] seccomp=[name=seccomp,profile=default] cgroupns

Що це означає: Docker бачить AppArmor і seccomp, а seccomp використовує профіль за замовчуванням.

Рішення: Добре. Якщо тут відсутні AppArmor або seccomp — зупиніться й виправте конфігурацію хоста перед «укріпленням контейнерів».

Завдання 2 — Підтвердити підтримку ядра AppArmor і статус

cr0x@server:~$ sudo aa-status
apparmor module is loaded.
55 profiles are loaded.
52 profiles are in enforce mode.
3 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

Що це означає: Модуль ядра завантажено і більшість профілів у режимі enforce.

Рішення: Якщо модуль не завантажений або профілі не в enforce — виправте це першочергово. «Прапорці Docker» не допоможуть, якщо AppArmor неактивний.

Завдання 3 — Перевірити, чи є якісь контейнери unconfined (AppArmor)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} AppArmor={{.AppArmorProfile}}'
/api AppArmor=docker-default
/worker AppArmor=docker-default
/metrics AppArmor=unconfined

Що це означає: Один контейнер працює без AppArmor-обмеження.

Рішення: Розглядайте це як дефект. Знайдіть, хто встановив --security-opt apparmor=unconfined (або чому Docker не зміг застосувати профіль) й виправте.

Завдання 4 — Перевірити режим seccomp для кожного контейнера

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Seccomp={{.HostConfig.SecurityOpt}}'
/api Seccomp=[]
/worker Seccomp=[]
/metrics Seccomp=[seccomp=unconfined]

Що це означає: Два контейнери використовують стандартні налаштування; один явно має seccomp unconfined.

Рішення: Приберіть виняток, якщо не можете його обґрунтувати. Якщо мусите зберегти — замініть на кастомний seccomp-профіль, що додає лише необхідні виклики.

Завдання 5 — Знайти, як був запущений контейнер (щоб відстежити джерело зміни)

cr0x@server:~$ docker inspect metrics --format 'Image={{.Config.Image}} SecurityOpt={{.HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}}'
Image=corp/metrics-exporter:2.7.1 SecurityOpt=[seccomp=unconfined apparmor=unconfined] Privileged=false

Що це означає: Хтось явно вимкнув обидва механізми.

Рішення: Поводьтеся як з вимкненням TLS-верифікації. Заведіть issue проти маніфесту розгортання, вимагайте обґрунтування й відповідального власника.

Завдання 6 — Перевірити, чи можливості контейнера не були «дбайливо» розширені

cr0x@server:~$ docker inspect api --format 'CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}'
CapAdd=[] CapDrop=[ALL]

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

Рішення: Коли ви суворо обмежуєте можливості, AppArmor/seccomp стають ще ефективнішими. Тримайте таку практику.

Завдання 7 — Виявити привілейовані контейнери (вони пробивають дірки скрізь)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Privileged={{.HostConfig.Privileged}}'
/api Privileged=false
/worker Privileged=false
/buildkit Privileged=true

Що це означає: Один контейнер є privileged. Це може бути навмисно (наприклад, інструменти збірки), але це високий ризик.

Рішення: Якщо це система збірки — ізолюйте її: виділені вузли, без продакшн-секретів, агресивний моніторинг, і по можливості використовуйте rootless або безпечніший підхід до збірки.

Завдання 8 — Перевірити логи ядра на відмови AppArmor

cr0x@server:~$ sudo dmesg -T | grep -i apparmor | tail -n 5
[Sat Jan  3 10:11:14 2026] audit: type=1400 audit(1704276674.123:781): apparmor="DENIED" operation="open" profile="docker-default" name="/proc/kcore" pid=29144 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

Що це означає: Процес у контейнері з docker-default намагався прочитати /proc/kcore і отримав відмову.

Рішення: Зазвичай це хороша новина: означає, що обмеження працює. Розслідуйте, чи очікувався доступ (рідко) або він підозрілий (частіше).

Завдання 9 — Перевірити логи аудиту на порушення seccomp

cr0x@server:~$ sudo ausearch -m SECCOMP -ts recent | tail -n 3
time->Sat Jan  3 10:12:01 2026
type=SECCOMP msg=audit(1704276721.443:812): auid=4294967295 uid=0 gid=0 ses=4294967295 subj==unconfined pid=29210 comm="node" exe="/usr/local/bin/node" sig=31 arch=c000003e syscall=319 compat=0 ip=0x7f2c8e2f1a9d code=0x0

Що це означає: Процес викликав подію seccomp для syscall 319 (приклад). За потреби зіставте номери syscall з іменами, але подія — це підказка.

Рішення: Корелюйте з контейнером і розгортанням. Якщо це ваш додаток, який падає, потрібно цільове налаштування seccomp, а не загальне відключення.

Завдання 10 — Перевірити, який seccomp-профіль Docker використовує за замовчуванням (на рівні демона)

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald",
  "live-restore": true
}

Що це означає: Немає перевизначення; Docker використовуватиме свій вбудований стандартний seccomp, якщо контейнер не запитує інакше.

Рішення: Добре. Якщо ви бачите "seccomp-profile": "unconfined" або схожі патерни (залежно від інструментів), поводьтеся з цим як з аварійним відкочуванням безпекової позиції.

Завдання 11 — Запустити канарковий контейнер, щоб перевірити, що примус не зламався мовчки

cr0x@server:~$ docker run --rm --name aa-canary alpine:3.19 sh -c 'cat /proc/kcore; echo ok'
cat: can't open '/proc/kcore': Permission denied
ok

Що це означає: Відмова очікувана при типових Docker/AppArmor-налаштуваннях; контейнер продовжує працювати.

Рішення: Тримайте це як швидкий тест під час ініціалізації вузла або після оновлень ядра. Якщо раптом воно почне проходити, швидше за все ви втратили AppArmor-обмеження.

Завдання 12 — Довести, що seccomp активний викликом системи, яку зазвичай блокує

cr0x@server:~$ docker run --rm alpine:3.19 sh -c 'apk add --no-cache strace >/dev/null; strace -e trace=bpf true'
strace: syscall bpf denied

Що це означає: Системний виклик bpf зазвичай блокується стандартним seccomp-профілем; strace повідомляє про відмову.

Рішення: Якщо це несподівано проходить — хтось відключив seccomp або змінив профіль. Якщо вашому легітимному додатку потрібен bpf, це нішова ситуація — документуйте її й ізолюйте.

Завдання 13 — Визначити AppArmor-профіль, застосований до процесу працюючого контейнера

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' api); sudo cat /proc/$pid/attr/current
docker-default (enforce)

Що це означає: Ядро показує, що процес обмежено під docker-default в режимі enforce.

Рішення: Це сильний крок верифікації, коли метадані Docker вводять в оману або ви підозрюєте відмінності під час виконання.

Завдання 14 — Виявити контейнери з ризиковими маунтами, що підривають політику

cr0x@server:~$ docker inspect api --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/api/config","Destination":"/app/config","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Що це означає: Використано bind-маунт лише для читання для конфігурації. Це розумно.

Рішення: Якщо бачите маунти до /, /var/run/docker.sock, /proc або записувані шляхи хоста з широким охопленням — виправте це першочергово; жодне тонке налаштування профілю не зробить це безпечним.

Завдання 15 — Помітити «docker.sock всередині контейнера» як фатальну помилку

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} {{range .Mounts}}{{.Destination}} {{end}}' | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock

Що це означає: Контейнер має доступ до Docker socket, що в більшості конфігурацій фактично дає root на хості.

Рішення: Поводьтеся з цим як з привілейованим навантаженням. Ізолюйте його, обмежте хто може його розгортати і не змішуйте з продакшн-даними.

Швидкий план діагностики

Коли щось ламається після увімкнення AppArmor/seccomp, ваша мета — знайти вузьке місце за хвилини, а не години. Ось порядок, що економить час.

Перше: підтвердити, що це справді AppArmor/seccomp (не мережа, не DNS, не права)

  • Шукайте «Operation not permitted» або «Bad system call» у логах контейнера.
  • Перевірте опції безпеки контейнера й профілі (docker inspect).
  • Перевірте логи ядра/аудиту на відмови (dmesg, ausearch).

Друге: ідентифікувати, яка дія блокується

  • Відмови AppArmor зазвичай містять операцію і шлях (наприклад, operation="open" name="/proc/kcore").
  • Події seccomp згадують номер syscall (іноді й ім’я) і часто SIGSYS.

Третє: вирішити категорію виправлення

  • Виправити додаток, якщо він робить щось непотрібне (часто).
  • Виправити конфіг контейнера (маунти, можливості, користувач), якщо поведінка додатку нормальна, але контейнер надто обмежений у неправильному місці.
  • Виправити профіль лише якщо це стабільна, обґрунтована потреба.
  • Ізолювати навантаження, якщо воно потребує небезпечних прав (системи збірки, eBPF-інструменти, вкладені рантайми).

Четверте: протестувати з найменшим можливим винятком

Ніколи не переходьте від дефолту до unconfined. Для seccomp дозволяйте один syscall. Для AppArmor дозволяйте один шлях або одну можливість. Потім перевпровадьте й перевірте, що відмова зникла й інші речі не регресували.

Три корпоративні міні-історії з практики

1) Інцидент через хибну думку: «Воно всередині контейнера, тож не може торкнутися хоста»

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

Команда вважала, що контейнеризація сама по собі достатня. AppArmor був встановлений у дистрибутиві, але не застосовувався до контейнерів; seccomp був відключений місяцями тому, бо інший сервіс спіткнувся об нього під час поспішного міграційного етапу. Ніхто не записав причину. Вимкнення стало дефолтом.

Зловмисник використав баг парсера, щоб отримати виконання коду в контейнері. Звідти вони перелічили маунти і знайшли записуваний bind-маунт у директорію хоста, що використовувалася для спільних завантажень. Та директорія містила випадковий SSH-ключ, залишений старим автоматизованим скриптом. Ключ належав обліковому запису, що хоч і не був root, але входив у групу, яка могла говорити з Docker socket на деяких машинах.

З «RCE в контейнері» вони перейшли до «контролю за демоном Docker», а далі все було закінчено: запустити privileged-контейнер, змонтувати файлову систему хоста, залишити бекдор. Розбір інциденту повертався до однієї думки: «Ми припускали, що контейнери — це межа».

Після цього виправлення були не геройськими. Вони були нудними: знову увімкнути дефолтний seccomp, застосувати AppArmor, прибрати маунти docker.sock і запровадити політику, що будь-який виняток потребує власника й терміну дії. Наступна спроба експлуатації все ще дала shell у контейнері, але другий стрибок був заблокований. Оце й був виграш.

2) Оптимізація, що зіграла злий жарт: «Наш кастомний seccomp швидший»

Група, зосереджена на продуктивності, вирішила, що стандартний seccomp Docker «надто великий» і значить «має бути повільним». Вони написали кастомний профіль, який дозволяв лише ті системні виклики, які використовували їхні Go-сервіси. Початкові тести виглядали добре: латентність на штучному навантаженні трохи покращилася, і в стаджингу ніхто не бачив помилок.

Потім настав продакшн. Перший симптом не був безпековим; це була доступність. Частина контейнерів почала падати під більш високим навантаженням, але лише на вузлах з новішим ядром. Підпис аварії був сумішшю «bad system call» й криптичних помилок рантайму. Інженер on-call зробив те, що роблять на виклику: вимкнув seccomp для падаючих сервісів, щоб відновити працездатність. Інциденти припинилися. «Виправлення» закріпилося.

Причина була прозаїчна: рантайм Go і libc робили виклики, яких не було в їхньому вузькому тесті, включно з викликами для керування потоками й новими інтерфейсами ядра. Їхній профіль було «оптимізовано» під знімок поведінки, а не під реальну поведінку на різних версіях ядра, змінах libc і режимах операцій, як DNS і TLS.

Довгостроковий урок не був «ніколи кастомізувати». Це був «не кастомізуйте, якщо не можете це підтримувати». Вони повернулися до дефолтного seccomp для загальних навантажень і зберегли ретельно підтримуваний кастомний профіль для одного спеціалізованого сервісу, яким опікувалася команда, що його потребувала, і тестувала в CI на кількох ядрах. Незначний виграш у бенчмарку не вартий операційної крихкості.

3) Нудна, але правильна практика, яка врятувала день: канаркові тести й примусові дефолти

Інша організація керувала десятками кластерів Kubernetes, але також мала невелику флотилію Docker-only хостів для спадкових батч-робіт. Ці хости часто оновлювалися — патчі ядра, оновлення Docker, звичний хаос. SRE-команда мала просту звичку: під час bootstrap кожного вузла запускати канарковий контейнер, який пробував кілька «повинні бути заборонені» дій і підтверджував, що очікувані відмови з’являються в логах.

Одного тижня, після нібито рутинного оновлення ОС, канарка почала дозволяти дії, які мали бути заблоковані. Ніяких тривог ще не спрацювало, бо продакшн-завдання все ще працювали. Але перевірка канарки провалила admission вузла, тож нові навантаження не запланувалися там. Хост було автоматично поміщено в карантин.

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

Команда виправила конфігурацію завантаження, знову увімкнула AppArmor і зняла карантин з вузла. Ніякого інциденту, ніякого впливу на клієнтів. Просто черговий невидимий виграш, що існує тільки тому, що хтось наполягав на нудному контрольному циклі й не погоджувався на «ймовірно працює».

Типові помилки: симптом → корінна причина → виправлення

1) Симптом: контейнер виходить одразу з «Bad system call»

Корінна причина: seccomp заблокував syscall, і дія налаштована на kill або процес не коректно обробляє EPERM.

Виправлення: Підтвердьте, що seccomp активний, і перевірте логи аудиту (ausearch -m SECCOMP). Додайте конкретний syscall до кастомного seccomp-профілю для цього навантаження або оновіть додаток/рантайм. Не ставте seccomp=unconfined як «тимчасове» виправлення без плану на видалення.

2) Симптом: додаток не може прочитати файл, який раніше читав (хоча Unix-права виглядають правильними)

Корінна причина: AppArmor заборонив доступ до шляху, навіть якщо Unix-права дозволяють.

Виправлення: Перевірте dmesg на відмови AppArmor, ідентифікуйте профіль, потім або налаштуйте профіль, або перемістіть файл у дозволений шлях (часто кращий варіант). Перевірте маунти: інший шлях всередині контейнера може спричинити невідповідність політики.

3) Симптом: «mount: permission denied» всередині контейнера

Корінна причина: AppArmor і/або seccomp заблокували операції, пов’язані з монтуванням; також, ймовірно, бракує можливостей типу CAP_SYS_ADMIN.

Виправлення: Запитайте, навіщо контейнеру робити монтування. Більшість не повинні. Для легітимних випадків (рідко) ізолюйте навантаження і явно надайте тільки те, що потрібно, з вузьким охопленням.

4) Симптом: відлагоджувальні інструменти не працюють (strace, perf, ptrace)

Корінна причина: Часто це момент, коли AppArmor і seccomp обмежують інтерфейси трасування, бо вони потужні в експлойтах.

Виправлення: Використовуйте спеціальні образи для відлагодження в ізольованому середовищі, або запускайте інструменти на хості з належним доступом. Якщо потрібно увімкнути трасування в продакшні, робіть це для короткочасного debug-контейнера з аудитом доступу й чітким планом завершення.

5) Симптом: «працює на одному вузлі, падає на іншому» після укріплення

Корінна причина: Різні версії ядра, різні версії Docker або різні набори AppArmor-профілів. Кастомні seccomp-профілі особливо чутливі до варіацій рантайму.

Виправлення: Стандартизуйте образи вузлів, фіксуйте й тестуйте профілі, додавайте канаркові перевірки. Розходження хостів — це питання безпеки й надійності, а не лише гігієни.

6) Симптом: все працює, але ви ніде не бачите жодної відмови

Корінна причина: Логування/аудит не ввімкнено або AppArmor/seccomp фактично не застосовано (контейнери unconfined).

Виправлення: Перевірте через /proc/<pid>/attr/current, docker info і канаркові тести. Тиша — не доказ безпеки.

7) Симптом: хтось «виправляє» інцидент, додаючи –privileged

Корінна причина: Відсутність операційної дисципліни і відсутній шлях ескалації для тонкої настройки профілів. Privileged стає лазівкою.

Виправлення: Зробіть розгортання privileged доступним тільки за явним схваленням, ізолюйте privileged-навантаження і побудуйте швидкий шлях для цільових змін профілів з тестами.

Контрольні списки / покроковий план

Покроково: мінімальний rollout, що не зіпсує вам тиждень

  1. Інвентаризація поточної позиції. Ідентифікуйте контейнери з apparmor=unconfined, seccomp=unconfined, у privileged режимі та з маунтами docker.sock.
  2. Виправте найгірші речі першими. Приберіть docker.sock маунти з не-build навантажень. Усуньте privileged-контейнери в продакшн-данаплейні.
  3. Увімкніть контролі на хості. Переконайтеся, що AppArmor завантажено і в enforce. Переконайтеся, що Docker звітує про увімкнений seccomp за замовчуванням.
  4. Канаровий тест на кожному вузлі. Використовуйте швидкі перевірки відмов (типу /proc/kcore і заблокований syscall) під час bootstrap і після оновлень.
  5. Широке розгортання дефолтного seccomp. Стандартний seccomp має бути уключений скрізь, якщо немає документованого винятку.
  6. Запровадження AppArmor enforce. Переконайтеся, що контейнери не працюють в unconfined. Віддавайте перевагу docker-default, якщо немає сильної причини інакше.
  7. Обробляйте винятки з власником. Кожен unconfined або кастомний профіль потребує власника, причини й періодичного перегляду.
  8. Автоматизуйте перевірку. CI-перевірки для манифестів розгортання: фейл, якщо ставлять unconfined без allowlist.
  9. Операційна готовність для відлагодження. Навчіть on-call де шукати відмови й події seccomp; напишіть рукбук, що починається з логів, а не з гадань.

Контрольний список: опції запуску контейнера, на які варто звертати увагу

  • Не використовуйте --privileged, якщо вузол не виділений і навантаження не явно високодовірене.
  • Не використовуйте --security-opt seccomp=unconfined, крім тимчасового пом’якшення з тикетом і терміном дії.
  • Не використовуйте --security-opt apparmor=unconfined в продакшні. Виправте профіль натомість.
  • Віддавайте перевагу --read-only з явними writable-маунтами для стану.
  • За замовчуванням відкидайте можливості; додавайте лише те, що можете пояснити.
  • Уникайте PID/мережевих неймспейсів хоста, якщо ви не пишете node-агенти і не розумієте наслідків.

Контрольний список: що фіксувати в запиті на виняток

  • Точний образ контейнера й версію.
  • Точні логи відмов (AppArmor) або номери/події syscall (seccomp).
  • Бізнес-обґрунтування поведінки.
  • Запропонований найменший виняток (один шлях, один syscall, одна можливість).
  • План ізоляції (виділені вузли, зменшені секрети, обмежена мережа).
  • План тестування і план відкату.

Питання й відповіді

1) Чи потрібні мені обидва — AppArmor і seccomp? Хіба одного не достатньо?

Використовуйте обидва. Вони покривають різні шари: AppArmor — контроль доступу (файли, можливості, операції), seccomp — поверхню системних викликів. Перекриття нормальне; надлишковість — це мета.

2) Якщо мій контейнер працює не від root, чи все одно вони потрібні?

Так. Робота не від root дуже допомагає, але експлуатація ядра й ескейпи контейнерів не потребують старту процесу з root. Захист у глибину важливий, бо нападники складають ланцюги слабкостей.

3) Чому б просто не запускати все в privileged і покладатися на мережеві контроли?

Тому що мережеві контролі не зупинять локальне підвищення привілеїв, баги ядра або випадкову шкоду хосту. Privileged-контейнери — це по суті «процеси з відділом маркетингу».

4) Яка найбезпечніша дефолтна позиція seccomp у Docker?

Використовуйте стандартний seccomp-профіль Docker і відхиляйтеся від нього лише для конкретних навантажень з документованою потребою. Дефолт — прагматичний баланс для багатьох робочих навантажень.

5) Як дізнатися, чи контейнер фактично обмежено (а не тільки «сконфігуровано»)?

Перевірте атрибут процесу: /proc/<pid>/attr/current для AppArmor. Для seccomp шукайте події SECCOMP під час відомої заблокованої дії або перевірте конфіг рантайму й протестуйте канаркою.

6) Мій додаток потребує ptrace/strace у продакшні. Що робити?

Перше питання: чи справді це потрібно? Зазвичай потрібно краща телеметрія, а не ptrace. Якщо справді потрібно — ізолюйте навантаження, звужуйте його контекст і прийміть підвищений ризик. Не розширюйте безпеку всього флоту без вагомих підстав.

7) Чи зашкодить увімкнення цих контролів продуктивності?

У більшості реальних навантажень накладні витрати неістотні в порівнянні з мережею, диском і логікою додатку. Операційні витрати виникають через винятки й відладку, а не через CPU-цикли.

8) Чому відмови з’являються лише після оновлення бібліотеки?

Тому що рантайми еволюціонують. Новіші libc, JVM, версії Go або node можуть використовувати інші syscalls або звертатися до нових інтерфейсів ядра. Ось чому кастомні «мінімальні» seccomp-профілі вимагають підтримки й тестування на різних ядрах.

9) Чи достатньо AppArmor на Ubuntu, а SELinux на RHEL?

Використовуйте те, що ваша платформа підтримує добре. AppArmor поширений на Ubuntu/Debian; SELinux — на RHEL-похідних системах. Головне — забезпечити enforcement і операційну зрілість, а не займатися ідеологією.

10) Який найшвидший виграш, якщо я можу виправити лише одну річ цього тижня?

Припиніть запуск unconfined і використання privileged режиму. Застосуйте стандартний seccomp скрізь. Ці зміни закривають багато «легких дверей» для втечі.

Наступні кроки, що справді знижують ризик

Якщо ви хочете мінімальне укріплення, яке має значення, не починайте з написання 500-рядкової політики. Почніть із перевірки, що дефолти реальні і застосовуються:

  • Переконайтеся, що AppArmor завантажено й enforce на кожному вузлі.
  • Переконайтеся, що Docker використовує стандартний seccomp і контейнери не unconfined.
  • Приберіть privileged-контейнери й маунти docker.sock з продакшн-данаплейнів.
  • Додайте простий канарковий тест до bootstrap вузла, який доводить, що відмови відбуваються.
  • Операціоналізуйте діагностику: навчіть on-call читати відмови й події seccomp перед тим, як тягнути за unconfined.

Зробіть ці п’ять речей — і ви перетворите «безпеку контейнерів» зі слайд-деку в контрольний цикл. Оце та нудна робота, що зберігає системи — і вихідні — цілими.

← Попередня
Спеціальні малі блоки ZFS: як прискорити навантаження з дрібними файлами
Наступна →
Планування місткості ZFS: проєктування зростання без перебудови

Залишити коментар