Многие уверены, что контейнеры Docker или OCI сами по себе дают хорошую изоляцию. На деле cgroups и Linux namespaces — лишь кирпичики, настоящей глубокой защиты они не строят. Главная проблема: классический демон Docker (или rootful Podman) работает от root. Если злоумышленник вырвется из контейнера через дыру в ядре, баг рантайма или неосторожный bind mount — он сразу получит root на хосте. Это не теория, свежий пример — CVE-2025-9074. Rootless-контейнеры меняют картину: при таком же breakout атакующий окажется на хосте всего лишь непривилегированным пользователем.
Базовая механика rootless держится на user namespaces и диапазонах подчинённых UID/GID из /etc/subuid и /etc/subgid. Скажем, пользователю andreu отдано 65536 UID начиная с 100000. Rootless Podman строит маппинг: uid 0 внутри контейнера отображается в реального непривилегированного пользователя на хосте, остальные контейнерные UID — в подчинённые. Второй важный момент: Podman по умолчанию работает без демона, так что никакого root-ового сокета, через который можно атаковать систему. Запуск и супервизию сервисов отдаём systemd.
Разберём на примере PostgreSQL от имени служебного пользователя librarian. Чтобы сервис жил без активных пользовательских сессий, один раз включаем lingering: loginctl enable-linger librarian. Сам unit-файл кладём в /home/librarian/.config/containers/systemd/db.container. В нём описываем контейнер: образ, отправку логов в journald, привязку порта только к приватному IP. Критичная настройка — UserNS=keep-id:uid=999,gid=999. Она фиксирует маппинг, чтобы файлы данных на хосте принадлежали самому librarian, а не случайным high-UID, и оставались читаемыми для бэкапов. Опция :U в Volume при старте делает chown точки монтирования под контейнерного пользователя, поэтому свежеинициализированный каталог данных сразу получает правильного владельца. Секреты вынесены в отдельный файл через EnvironmentFile, чтобы не светить в логах и тексте unit-а.
Для управления логинимся под librarian, прописываем XDG_RUNTIME_DIR и DBUS_SESSION_BUS_ADDRESS, а дальше — привычные команды вроде systemctl --user start db (имя unit-а без .container). Второй пример — Anubis, эдакая tarpit-ловушка. Здесь добавляем ужесточения: ReadOnly=true делает всю файловую систему контейнера read-only, NoNewPrivileges=true запрещает setuid-бинарникам повышать привилегии, а DropCapability=ALL сбрасывает все capabilities (порты выше 1024 их и не требуют). Файл политик монтируется с флагом :ro, так что даже rootless-процесс читает его без изменения владельца.
Сеть в rootless-режиме преподносит свои сюрпризы: привилегированные порты младше 1024, сохранение source IP клиента и правила файрвола работают иначе. Эти тонкости автор пообещал разобрать в следующей части.