B4. Контейнер руками
Модуль 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 усередині раптом
дозволений. Тоді перевіряють, які простори й обмеження отримав процес.
Завдання
Section titled “Завдання”Результат роботи — скрипт mycontainer.sh, який запускає оболонку
в підготовленому кореневому каталозі з увімкненими всіма механізмами
ізоляції, і звіт із таблицею «механізм → що приховалося».
sudo ./mycontainer.sh ./rootfs /bin/shСкрипт із покроковим виводом: який механізм вмикається і що це змінює.
Що зробити:
- Кореневий образ: каталог із файлами через
debootstrapабо розпакованийalpine-minirootfs. - Власний простір монтувань із
pivot_rootна цей каталог і змонтованими/proc,/sys,/dev. - Простори PID, UTS, IPC і мережі; для мережі пара
veth, щобpingназовні проходив. - Корінь через overlayfs: спільний
lowerтільки для читання і власнийupperдля кожного запуску. - cgroup із обмеженням пам’яті на процес контейнера, як у B3.
- Обрізані привілеї:
capsh --dropі профіль seccomp, що забороняє принаймніmount,rebootіkexec_load. - Простір користувачів:
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власний; - ви можете підтвердити командою кожне з восьми тверджень таблиці «Що треба вміти підтвердити»;
- звіт містить п’ять пунктів вище.
Перед початком
Section titled “Перед початком”- Прочитайте в модулі 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) і записуйте, що змінилося.
Таблиця «механізм → що приховалося» і є головним результатом роботи.
-
Кореневий образ.
debootstrap --variant=minbaseабо розпакованийalpine-minirootfs. Це просто каталог із файлами. -
Простір монтувань і
pivot_root.unshare --mount, потімpivot_rootна ваш каталог, змонтувати/proc,/sys,/dev.Спробуйте спершу
chrootзамістьpivot_rootі знайдіть, чому з нього можна вийти; різниця між ними цілком практична. -
Простір PID.
unshare --pid --fork --mount-proc. Теперps -eпоказує один процес, і це вашshпід номером 1.Перевірте зворотне: з господаря той самий процес видно під звичайним PID. Ізоляція міняє лише погляд: процес існує так само, як і до неї.
-
Простори UTS, IPC і мережі.
unshare --utsдозволяє змінитиhostname, не чіпаючи господаря.unshare --netдає порожній стек із самимlo.З’єднайте контейнер із господарем парою
vethі добийтесяpingназовні: так робить Docker під капотом. -
overlayfs.
Змонтуйте корінь як
lowerтільки для читання плюс власнийupperдля запису. Створіть файл усередині й знайдіть його в каталозіupperна господарі (модуль 15).Запустіть два контейнери з одного
lowerі переконайтеся, що вони не бачать змін одне одного. -
Обмеження ресурсів.
cgroup із B3 на процес контейнера.
-
Обрізання привілеїв.
capsh --drop=cap_sys_admin,cap_net_admin,...і профільseccomp, що забороняє хоча бmount,rebootіkexec_load(модуль 16).Перевірте, що заборонений виклик тепер справді не проходить.
-
Простір користувачів.
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 зовні |
Часті помилки
Section titled “Часті помилки”/proc не перемонтовано. ps показує процеси господаря, хоча простір
PID у вас уже власний. Лікується прапорцем --mount-proc або монтуванням
вручну.
pivot_root не спрацював. Новий корінь мусить бути точкою монтування,
і часто досить зробити mount --bind каталогу на себе.
Мережа не працює після --net. Так і має бути, простір же порожній.
Потрібна пара veth, адреси з обох боків і маршрут.
Усе робиться від root господаря. Тоді ви зібрали привілейований
контейнер. Восьмий етап тут обов’язковий, у ньому суть роботи.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Запустити всередині справжній образ із Docker Hub, розпакувавши шари
руками. Або порівняти свій скрипт із runc — еталонною реалізацією OCI.