Перейти до вмісту

B4. Контейнер руками

середнійспирається на модуль 16, модуль 17

Модуль 17 стверджує, що контейнер не є сутністю ядра, а лише комбінацією незалежних механізмів: просторів імен, cgroups, overlayfs, capabilities і seccomp. У цій роботі ви вмикаєте їх по черзі, без Docker, і після кожного дивитеся, що змінилося для процесу всередині. Наприкінці у вас буде скрипт на сотню рядків, який робить те саме, що docker run, і ви зможете сказати, чого він не робить.

Після цієї роботи ви зможете:

  • зібрати з unshare, pivot_root і mount середовище, у якому оболонка має PID 1, власне ім’я вузла і порожній мережевий стек;
  • пояснити, чим pivot_root відрізняється від chroot і чому з другого можна вийти;
  • показати на господарі процес, який усередині вважає себе єдиним, і довести через /proc/self/ns/*, що простори різні;
  • обрізати процесу привілеї через capabilities і seccomp і перевірити, що заборонений виклик не проходить;
  • пояснити, що дає простір користувачів і чому Ubuntu типово забороняє його непривілейованим процесам.

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

Результат роботи — скрипт mycontainer.sh, який запускає оболонку в підготовленому кореневому каталозі з увімкненими всіма механізмами ізоляції, і звіт із таблицею «механізм → що приховалося».

Terminal window
sudo ./mycontainer.sh ./rootfs /bin/sh

Скрипт із покроковим виводом: який механізм вмикається і що це змінює.

Що зробити:

  1. Кореневий образ: каталог із файлами через debootstrap або розпакований alpine-minirootfs.
  2. Власний простір монтувань із pivot_root на цей каталог і змонтованими /proc, /sys, /dev.
  3. Простори PID, UTS, IPC і мережі; для мережі пара veth, щоб ping назовні проходив.
  4. Корінь через overlayfs: спільний lower тільки для читання і власний upper для кожного запуску.
  5. cgroup із обмеженням пам’яті на процес контейнера, як у B3.
  6. Обрізані привілеї: capsh --drop і профіль seccomp, що забороняє принаймні mount, reboot і kexec_load.
  7. Простір користувачів: root усередині відповідає звичайному користувачеві зовні.

Чого робити не треба. Формат образу й шари з хешами, мережевий міст із NAT, реєстр образів і керування життєвим циклом до роботи не входять: це внесок середовища виконання, і у звіті ви поясните, навіщо він потрібен.

Обмеження. Без Docker, Podman чи іншого готового середовища виконання: кожен механізм вмикається окремою командою, щоб було видно, що він змінює.

Що записати у звіт:

  • таблицю «механізм → що приховалося»: вивід ps -e, hostname, ip link, ls /, id після кожного етапу;
  • чому з chroot можна вийти, а з pivot_root ні;
  • значення readlink /proc/self/ns/pid усередині й на господарі для одного й того самого процесу;
  • компроміс простору користувачів: навіщо Ubuntu забороняє його через AppArmor і чим ви ризикуєте, вимикаючи заборону;
  • перелік того, чого ваш контейнер не робить порівняно з docker run, власними словами.

Готово, коли:

  • скрипт запускає оболонку командою вище, і всередині ps -e показує лише процеси контейнера, findmnt / показує overlay, hostname власний;
  • ви можете підтвердити командою кожне з восьми тверджень таблиці «Що треба вміти підтвердити»;
  • звіт містить п’ять пунктів вище.
  • Прочитайте в модулі 17 розділи «З чого зроблено контейнер» і «Образи й OCI», у модулі 16 розділ «Скільки перевірок проходить виклик» (частину про capabilities і seccomp), а в модулі 15 розділ «overlayfs і FUSE».
  • Зробіть спершу B3: етап з обмеженням ресурсів спирається на групу звідти.
  • Потрібні права root. Підходить віртуальна машина або контейнер з увімкненою вкладеністю просторів імен (див. огляд). У Vagrant-машині з архіву вже є debootstrap, uidmap, libcap2-bin і libseccomp-dev; заготовок і check.sh для цієї роботи немає.

Після кожного етапу виконуйте всередині одні й ті самі команди (ps -e, hostname, ip link, ls /, id) і записуйте, що змінилося. Таблиця «механізм → що приховалося» і є головним результатом роботи.

  1. Кореневий образ.

    debootstrap --variant=minbase або розпакований alpine-minirootfs. Це просто каталог із файлами.

  2. Простір монтувань і pivot_root.

    unshare --mount, потім pivot_root на ваш каталог, змонтувати /proc, /sys, /dev.

    Спробуйте спершу chroot замість pivot_root і знайдіть, чому з нього можна вийти; різниця між ними цілком практична.

  3. Простір PID.

    unshare --pid --fork --mount-proc. Тепер ps -e показує один процес, і це ваш sh під номером 1.

    Перевірте зворотне: з господаря той самий процес видно під звичайним PID. Ізоляція міняє лише погляд: процес існує так само, як і до неї.

  4. Простори UTS, IPC і мережі.

    unshare --uts дозволяє змінити hostname, не чіпаючи господаря. unshare --net дає порожній стек із самим lo.

    З’єднайте контейнер із господарем парою veth і добийтеся ping назовні: так робить Docker під капотом.

  5. overlayfs.

    Змонтуйте корінь як lower тільки для читання плюс власний upper для запису. Створіть файл усередині й знайдіть його в каталозі upper на господарі (модуль 15).

    Запустіть два контейнери з одного lower і переконайтеся, що вони не бачать змін одне одного.

  6. Обмеження ресурсів.

    cgroup із B3 на процес контейнера.

  7. Обрізання привілеїв.

    capsh --drop=cap_sys_admin,cap_net_admin,... і профіль seccomp, що забороняє хоча б mount, reboot і kexec_load (модуль 16).

    Перевірте, що заборонений виклик тепер справді не проходить.

  8. Простір користувачів.

    unshare --user --map-root-user: усередині ви root, зовні — звичайний користувач. Це те, що відрізняє непривілейований контейнер від привілейованого, і найважливіший рівень захисту з усіх перелічених.

Що треба вміти підтвердити

Section titled “Що треба вміти підтвердити”

Для звірки: у правильно зібраному контейнері hostname власний, ps -e бачить 4 процеси замість 144 на господарі, оболонка має PID 1, findmnt / показує overlay, мережевих інтерфейсів нуль.

Твердження Чим доводиться
Усередині власний простір PID ps -e показує кілька процесів замість сотень на господарі
Той самий процес видно з господаря ps -ef | grep на господарі
Простори різні порівняти readlink /proc/self/ns/pid там і там
Корінь підмінено, а не chroot findmnt / усередині
Запис іде в upper знайти створений файл на господарі
Ресурси обмежені cat <група>/memory.max
Привілеї обрізані capsh --print усередині
root усередині ≠ root зовні id усередині, ps -o user зовні

/proc не перемонтовано. ps показує процеси господаря, хоча простір PID у вас уже власний. Лікується прапорцем --mount-proc або монтуванням вручну.

pivot_root не спрацював. Новий корінь мусить бути точкою монтування, і часто досить зробити mount --bind каталогу на себе.

Мережа не працює після --net. Так і має бути, простір же порожній. Потрібна пара veth, адреси з обох боків і маршрут.

Усе робиться від root господаря. Тоді ви зібрали привілейований контейнер. Восьмий етап тут обов’язковий, у ньому суть роботи.

Запустити всередині справжній образ із Docker Hub, розпакувавши шари руками. Або порівняти свій скрипт із runc — еталонною реалізацією OCI.