Часть 1: Установка пакетов из Интернета без sudo
Привет!
Перед вами серия статей, в которых на подробно разобранных примерах показано, как удовлетворить некоторые базовые потребности пользователя ПК под управлением ОС Линукс, при помощи контейнеризации. В основном, это базовые потребности в безопасности, то есть, сохранении целостности системы и данных/идентичности пользователя, при повседневном использовании ПК. Надеюсь, что после прочтения статьи, вы спросите себя: "почему я не делал/а так всегда?". Если вы хотите получить быстрый старт в стиле использования ОС Линукс, за который вам не будет стыдно перед самим/ой собой, тогда прошу под кат.
Podman это менеджер контейнеров, который сегодня стоит использовать вместо популярного ранее Docker. Docker уступает Podmanу тем,
что официально нагрубил нам в мае 2024го требует дополнительных действий, чтобы запускать контейнеры без повышенных привилегий (sudo/root). То есть, Podman по умолчанию безопаснее, чем Docker. Для пользователей ОС семейства RedHat и вовсе было бы странным пользоваться Docker тогда как Podman уже предустановлен и отлично взаимодействует с SELinux.
Если вы уже знаете команды для Docker (например docker run
), то знаете и для Podman. Простая подстановка podman
вместо docker
в знакомые команды, в большинстве случаев, будет работать так, как вы ожидаете.
Автор является пользователем Debian на архитектуре amd64 с файловой системой xfs, поэтому все примеры выполнены для подобного моему хоста. Впрочем, вам их будет легко адаптировать для любой комбинации дистрибутива/архитектуры процессора/файловой системы, на которой может работать Podman.
Что касается корневого образа Debian, который я использую в контейнерах, то используйте его, как есть, на вашем хосте, будь то CentOS, Arch, т.д., т.п.
Приступим
Проверьте, установлен ли на вашей системе Podman (введите команду podman
). Если нет, то установите его из репозитория вашего дистрибутива, вместе с uidmap
. Пример для Debian/Ubuntu:
sudo apt update && sudo apt upgrade -y && sudo apt install podman uidmap -y
Это первый и последний шаг, который мы выполняем с повышенными привилегиями. И всё. Podman для того и создавался, чтобы быть безопаснее Docker.
В общем случае, в Debian/Ubuntu, пакеты .deb устанавливаются с правами суперпользователя, например sudo dpkg -i имя_пакета.deb
. Это оттого, что приложению бывает нужно поместить себя в одну из папок, которые выводит команда echo $PATH
, а эти папки принадлежат
суперпользователю. Иногда приложение создаёт системный
systemd-сервис, который запускается при запуске системы, поскольку это часть приложения. Но ведь может сделать и гораздо больше: если мы говорим о том, что команда оболочки может сделать с повышенными привилегиями, то легче будет перечислить то, чего команда сделать не сможет, чем сможет. В работе, вам может потребоваться часто устанавливать программы из Интернета, и не всегда в .deb-пакете скрипты инсталляции, которым dpkg передаёт управление от лица суперпользователя, такие же этичные, как у Microsoft.
Поэтому, сейчас мы научимся делать это безопасно, для любых пакетов.
Учиться будем на .deb пакете Visual Studio Code
Создайте новую папку, в которой мы будем производить все дальнейшие действия (далее - рабочая папка) и войдите в неё. Скачайте в рабочую папку установочный файл .deb с официального сайта. В нашем случае, скачанный файл имеет неопределённое имя, так как оно содержит номер версии, а они выпускаются регулярно, поэтому мы будем копировать файл в файловую систему контейнера по вайлдкард: COPY code_*_amd64.deb /root/vs.deb
.
Создайте в рабочей папке файл с именем Dockerfile
, следующего содержания:
FROM docker.io/library/debian:latest
RUN apt update && apt upgrade -y
&& apt install-y xfce4 xdg-utils
&& apt clean && rm -rf /var/lib/apt/lists/*
RUN apt install -y git vim curl
python3 python3-dev python3-venv python3-pip pylint pipx python3-virtualenv
build-essential libssl-dev libffi-dev libpq-dev
&& apt clean && rm -rf /var/lib/apt/lists/*
ARG GROUP_ID=9898
ARG USER_ID=9898
ARG USRNME=testpilot
ARG GIT_EMAIL=ваш_емейл@mail.ru
ARG GIT_NAME='"Ваше имя"'
RUN groupadd -g $GROUP_ID --system $USRNME &&
useradd -m --gid $GROUP_ID --system -u $USER_ID $USRNME
&& mkdir -p /home/$USRNME/.config
&& mkdir -p /home/$USRNME/.cache
&& mkdir -p /home/$USRNME/.local/share
&& chown --recursive $USRNME:$USRNME /home/$USRNME
COPY code_*_amd64.deb /root/vs.deb
WORKDIR /root
RUN dpkg -i vs.deb
RUN apt -f install
COPY entrypoint.sh /entrypoint.sh
RUN chmod ugo+x /entrypoint.sh
WORKDIR /home/$USRNME
RUN JDK23_SHA256SUM=$(curl https://download.java.net/java/GA/jdk23.0.1/c28985cbf10d4e648e4004050f8781aa/11/GPL/openjdk-23.0.1_linux-x64_bin.tar.gz.sha256)
&& curl -o jdk23.tar.gz https://download.java.net/java/GA/jdk23.0.1/c28985cbf10d4e648e4004050f8781aa/11/GPL/openjdk-23.0.1_linux-x64_bin.tar.gz
&& JDK23_ACTUAL_SHA256SUM=$(sha256sum jdk23.tar.gz)
&& if [ $JDK23_ACTUAL_SHA256SUM != $JDK23_SHA256SUM ]; then
echo "Error: The sha256sum of the file does not match the expected value.";
echo "Expected: $JDK23_SHA256SUM";
echo "Actual: $JDK23_ACTUAL_SHA256SUM";
exit 1;
fi
RUN tar -xzvf jdk23.tar.gz -C .
RUN chown -R 9898:9898 .
USER $USRNME
RUN git config --global user.email $GIT_EMAIL
RUN git config --global user.name $GIT_NAME
ENV usrnme=${USRNME}
ENTRYPOINT /entrypoint.sh $usrnme
Dockerfile
- это проект создаваемого образа контейнера.
Как видите, при использовании Podman, вам придётся прибавлять в инструкции FROM префикс docker.io/library/
к названию корневого образа из докер-хаба, так как Docker делает это за пользователя по умолчанию, если не указан другой репозиторий.
Контейнеризация десктопных приложений - в чём-то искусство. Десктопные приложения могут требовать графического окружения или каких-то библиотек. Как правило, о том, что требуется приложению для его нормальной работы, оно подскажет вам в логах, например при выполнении podman run имя_образа_контейнера
из командной строки. Этим, в частности, объясняется доустановка пакетов xfce4
и xdg-utils
в нашем примере.
Образ контейнера (далее - образ) создаётся послойно. Каждая инструкция в Dockerfile порождает новый слой, за исключением инструкций CMD, ENTRYPOINT, HEALTHCHECK, SHELL и STOPSIGNAL.
Для слоя образа вычисляется хеш-сумма, на основании хеш-суммы предшествующего слоя и его самого, подобно тому, как это делается для коммита системы контроля версий текста git. На основании равенства хеш-сумм для слоя, при компиляции нового образа, Podman определяет, что такой слой уже создавался ранее для другого образа, и что его можно использовать для вновь создаваемого.
Так как я хочу, чтобы списки репозитория/пакеты всё-таки обновились
заново при работе с другим, не связанным с нашим, образом, то я прибавляю в нашу инструкцию с apt upgrade
, через оператор последовательного выполнения &&
, остальные пакеты, специфичные для нашего образа (xfce4
, т.д.).
В то же время, я доустанавливаю другие пакеты второй инструкцией RUN apt
потому, что некоторые пакеты (иксы, в частности) устанавливаются очень долго, а во время разработки Dockerfile, часто выясняется, что нужно ещё что-то доустановить в образ. При дописывании названия пакета во вторую инструкцию RUN apt
, вычисляться заново будет только она, первая же будет взята из кеша.
Совет
Не удаляйте неудачные образы до тех пор, пока не получите финальную версию. Слои, вычисленные в предыдущие запуски podman build
и не претерпевшие изменения своей хеш-суммы, будут взяты из кеша при следующей компиляции. Так как каждый слой вычисляется на основании предыдущего, то все слои, следующие после строчки с изменённой инструкцией, получат новые хеш-суммы, то есть, их придётся вычислять заново.
Обратите внимание на то, что обе инструкции
RUN apt
заканчиваются очисткой от установочных файловapt clean
и удалением кэша списков репозиториевrm -rf /var/lib/apt/lists/*
. Это из-за того, что каждый слой образа занимает место на диске (их полный список у меня хранится в домашней папке, в/home/имя_пользователя/.local/share/containers/storage/vfs/dir/
). Список же созданных образов можно посмотреть командойpodman images
.
Спойлер второй статьи данной серии
В общем случае, GUI-приложения не рассчитаны на запуск с правами суперпользователя. Приведу один пример: браузер Chromium возможно запустить с правами суперпользователя только с флагом отключения песочницы: chromium --no-sandbox
. Таким браузером я бы не стал пользоваться даже на виртуалке. Уж безопаснее будет пользоваться обычным способом установленным браузером из репозитория вашего дистрибутива. Почему - отвечу в следующей статье. К слову, в поставку
VSCode входит Chromium, поэтому, без вышеназванного флага, под рутом VSCode тоже запустить не получится.
В контейнере, мы будем работать под пользователем с ненулевыми UID и GID. Значение 9898 выбрано произвольно, так же как и имя пользователя. Не забудьте поменять значения GIT_EMAIL и GIT_NAME на свои собственные.
Фундаментом безопасности Unix-подобных систем являются числа UID и GID. Это бОльшая часть того, на чём держится безопасность Linux. На этих идентификаторах основаны права на файл (755, 644, т.д.), при том, что в Unix-подобных ОС всё является файлом. Например, процессы это файлы в папке /proc. Выполните любопытства ради команду
ls -hl /proc
и посмотрите, кому принадлежат там папки с номерами процессов (это те самые номера процессов и их владельцы, которые вы видите выводеtop
). На основании равенства своего UID, процесс пользуется правами владельца на файлы.
Совокупность отображений одних идентификаторов (0, то есть root, в контейнере) на другие (100001, к примеру) составляет альтернативные пользовательские пространства имён (user namespaces), которые Podman может создавать для каждого контейнера, используя пакет uidmap. Запуск приложения в контейнере, в пространстве имён вашего обычного пользователя, мало что добавляет к безопасности пользования ПК.
Завершим рассмотрение Dockerfile
Инструкция RUN выполняет /bin/sh -c 'всё_что_в_инструкции_RUN'
, поэтому создание образа будет прервано, если код возврата будет > 0, чем мы и пользуемся при сверке контрольной суммы устанавливаемой в наш контейнер JDK (exit 1
).
WORKDIR это смена директории в файловой системе контейнера.
USER это смена пользователя.
ENV это аналог команды оболочки export ИМЯ_ПЕРЕМЕННОЙ_ОКРУЖЕНИЯ=значение_переменной_окружения_оболочки
.
ENTRYPOINT - точка входа в контейнер - это то первое, что будет выполнено при его запуске.
COPY уже упоминалась выше.
Создайте в рабочей папке файл с названием entrypoint.sh
, следующего содержания:
#!/bin/bash
set -e
export PATH=$(echo $PATH):/home/$1/jdk-23.0.1/bin
code --verbose
Заметьте, что этот баш-скрипт использует первый переданный ему аргумент $1, а в нашем Dockerfile видно, что это имя пользователя внутри контейнера.
Теперь, всё готово для создания образа! Придумайте, как вы его назовёте (я назвал vscode), и поехали:
Находясь в рабочей папке, выполните:
podman build -t vscode .
Точка в конце значит "эта директория", -t имя_образа
называется "тегом", по аналогии с git.
Дождитесь окончания сборки образа. Найдите готовый образ в списке, который вернёт команда podman images
.
Создайте в любой удобной папке файл run.sh
, следующего содержания:
#!/bin/bash
CONTAINER=vscode
podman run
--uidmap=0:19:1
--uidmap=9898:20:1
--gidmap=0:19:1
--gidmap=9898:20:1
--rm -it
-v /tmp/.X11-unix:/tmp/.X11-unix:ro
-v /home/$(whoami)/Code:/home/testpilot/Code:rw
-v /home/$(whoami)/.cache/Code:/home/testpilot/.cache:rw
-v /home/$(whoami)/.local/share/Code:/home/testpilot/.local/share:rw
-v /home/$(whoami)/.config/Code:/home/testpilot/.config:rw
-v /home/$(whoami)/.vscode:/home/testpilot/.vscode:rw
-e DISPLAY=$DISPLAY
$CONTAINER
Поговорим про инструкции --uidmap
и --gidmap
. Для этого, откройте в текстовом редакторе файл /etc/subuid
. Установка пакета uidmap создала этот файл и добавила в него строчку:
имя_пользователя:100000:65536
Это означает, что у имя_пользователя
есть дополнительно 65536 UIDов
(говорим UID, подразумеваем пользовательское пространство имён), с 100000го по 65535й, включительно. В файле /etc/subgid
должны быть те же маппинги, только для идентификаторов групп.
С какой целью запускать приложения в контейнерах? Чтобы сделать для них невидимой остальную часть файловой системы? С этим справится и профиль AppArmor, который предустановлен на Debian. А вот запуск каждого приложения в отдельном пользовательском пространстве имён - это стандарт безопасного запуска приложений.
Помните, как грамотные вебмастера до появления Docker не только
помещали свой веб-сервер в chroot-тюрьму, но и запускали этот веб-сервер под отдельным пользователем, созданным специально для веб-сервера? Это делалось специально, для того, чтобы, если вдруг злоумышленнику станет известна уязвимость данной версии веб-сервера, позволяющая через http-запрос заставить этот веб-сервер запустить нежелательный процесс в ОС, то чтобы этот процесс не имел прав на данные/процесс, скажем, базы данных, работающей на той же машине, так как она была бы запущена в другом пользовательском пространстве имён.
Сегодня, благодаря пакету uidmap и контейнеризации, имея в системе лишь одного обычного пользователя, мы можем запускать на одной и той же системе тысячи веб-серверов и БД с уникальными UID и GID, то есть, каждый в своём собственном пространстве имён.
Podman работает с uidmap и именно в этом раскрывается весь потенциал этого механизма, с точки зрения безопасности.
Инструкцию --uidmap=0:19:1 читаем как: отобразить UID 0 контейнера на UID из файла /etc/subuid для имени пользователя, запустившего этот контейнер, начиная с 19го, в количестве 1 штука. То есть, для записи имя_пользователя:100000:65536, вне контейнера (в top, например) это будет выглядеть как UID 100018 (так как первый для имя_пользователя это 100000й). Так же читаем --gidmap.
+1 к безопасности добавляет параметр --rm, что значит "удалить контейнер по завершении работы". То есть, даже при случайном повреждении контейнера (допустим, вы случайно повредили ту JDK, которую мы добавили в контейнер, а она вам нужна), вам всего-навсего нужно закрыть и открыть приложение-контейнер заново, чтобы оно было воссоздано из образа, целостность которого гарантирована, подобно коммиту в git, хеш-суммой. Создание контейнера из образа происходит на удивление быстро: на моём Dell с i7-6600U, контейнеризированные приложения запускаются всего лишь в 3-4 раза дольше, чем обычные, учитывая, что они все создаются из образов заново. То есть, 3-4 секунды, вместо 1. Для меня это приемлемо.
Параметр -e
добавляет в контейнер переменную окружения, а -v
отображает папку файловой системы хоста в контейнер, с указанными через :
правами. Для того чтобы размер шрифта, скачанные плагины и тему оформления не приходилось устанавливать заново каждый раз при запуске VSCode, мы создаем в папках ~/.cache
, ~/.config
и ~/.local
папку Code (сделайте это), также и ~/.vscode
, и при помощи механизма Access Control List выдаём права на чтение, исполнение и запись для пользователя с UID 100019 (то есть, для пользователя с UID 9898 в контейнере), следующими командами:
setfacl -R -m u:100019:rwx /home/$(whoami)/.config/Code
setfacl -R -m u:100019:rwx /home/$(whoami)/.cache/Code
setfacl -R -m u:100019:rwx /home/$(whoami)/.config/Code
setfacl -R -m u:100019:rwx /home/$(whoami)/.local/share/Code
setfacl -R -m u:100019:rwx /home/$(whoami)/.vscode
ACL даёт возможность другому пользователю, не являющемуся владельцем, работать с файлами и папками, по правам.
Не забудьте таким же образом дать права контейнерному пользователю на папку с вашими проектами, например так:
setfacl -R -m u:100019:rwx /home/$(whoami)/мои_проекты
Файл /tmp/.X11-unix/X0
это розетка (socket) оконного менеджера для подключения внешних клиентов, каковым является юзер с UID 100019.
Комбинация флагов -i
(интерактивно) и -t
(телетайп оболочка) позволит закрывать приложение комбинацией Ctrl+C, в случае, если вы запустили скрипт run.sh
не через ярлык на рабочем столе, а из эмулятора терминала. Это особенно удобно при дебаге создаваемых вами образов, хотя и не является необходимым при штатном использовании.
Сделайте созданный скрипт исполняемым:
chmod +x путь_к_файлу/run.sh
Мы близки к запуску, но осталось несколько важных шагов.
Видеть в top
и ls -hl
вместо имён пользователей UIDы может быть не очень удобно. Давайте присвоим нашему UID/GID имя, создав просроченного пользователя c именем "pod_vscode":
sudo groupadd -g 100019 -r -o pod_vscode && sudo useradd -g pod_vscode -r -o -M -e 1970-01-01 -u 100019 -s /usr/sbin/nologin pod_vscode
Теперь, у нас есть дополнительный бонус - наш файл /etc/passwd
служит нам справочником UID наших контейнеров. А это критически важно для изоляции пользовательских пространств имён - не путать эти идентификаторы, следить за тем, чтобы каждый контейнер запускался со своими UID/GID, чтобы сохранить изоляцию времени выполнения. Также и в файлах /etc/subuid
и /etc/subgid
, интервалы для разных пользователей не должны пересекаться.
Для того чтобы десктопное приложение в контейнере запустилось, ему нужно право рисовать на экране. Это право - авторизация в X-сервере. По умолчанию, такое право есть у вашего пользователя, от имени которого запущены "иксы" (Xfce, KDE, Gnome и т.д), а у пользователя pod_vscode - нет. Дадим ему такое право:
xhost +si:localuser:pod_vscode
ВНИМАНИЕ: если вы не добавите эту команду в автозагрузку, то при следующем включении компьютера, нужно будет выполнить эту команду вновь, для того, чтобы запустить VSCode. Кстати, команды xhost
не понимают UID, а только имена. Если бы мы не присвоили таким оригинальным образом нашему UID имя, то нам пришлось бы давать права на отрисовку на экране всем локальным несетевым пользователям командой:
xhost +local:
Впрочем, это удобно при дебаге. В обеих командах xhost
, если заменить +
на -
, права отнимутся.
Осталось добавить ярлык на рабочий стол. Это важно, чтобы прочувствовать контейнеризацию, как стиль пользования ПК. Пусть ярлык запускает созданный нами файл run.sh
. Иконку для ярлыка можно скачать этой командой:
curl -o vscicons.zip https://code.visualstudio.com/assets/branding/visual-studio-code-icons.zip && unzip vscicons.zip
Вы также можете захотеть добавить эту иконку в иконки системы, например так:
sudo cp visual-studio-code-icons/vscode.svg /usr/share/icons/hicolor/scalable/vscode.svg
Если в вашем дистрибутиве иконки лежат где-то не в
/usr/share
, то можно сперва выяснить, где они лежат:find / -iname "*.png"
Наконец, запустите приложение!
Признаюсь, статья получилась несколько длиннее, чем планировалось. Но важные основы, изложенные здесь, далее повторяться не будут. В следующей статье, мы запустим в контейнере браузер Chromium.
Автор: vaclav1986