Часть 2: Браузер со звуком и веб-камерой в контейнере
Привет, Хабр!
Это вторая статья о контейнеризации как стиле повседневного использования графических приложений в ОС Линукс. Здесь мы научимся безопасно запускать браузер и менеджер паролей, так как это близкие потребности. Здесь не будут повторяться инструкции и объяснения, данные в первой статье, так как предполагается, что они известны читателю. Посему, без долгих предисловий, ныряем под кат!
Темой изоляции приложений друг от друга я озаботился, когда мне нужно было распаковать свой файл паролей .kbdx
приложением KeePass2, чтобы ввести пароль на сайте. И это при том, что в браузере в тот момент было открыто несколько разных вкладок. Ведь браузер представляет собой не только интерпретатор HTML-разметки, но и компилятор JavaScript-кода. Представьте себе, что процесс от имени вашего пользователя, в пространстве имён вашего пользователя, со всеми присущими правами доступа, исполняет присланный сервером код. И это при штатном использовании, без эксплуатации багов.
Историческая справка: Именно таким опасным был выход в Интернет до конца 2008 года. 11 декабря 2008 года, в версии Google Chromium 1.0.154.36 была добавлена песочница, которая изолировала исполняемый браузером код от остальных процессов ОС. По прошествии многих лет развития этой технологии, вроде бы, песочница Chromium закрыла вышеозначенную потребность безопасного открытия менеджера паролей при открытых сайтах. Тем не менее мы знаем, что такие технологии,
благодарянесмотря на десятилетия разработки, всё же имеют уязвимости нулевого дня. Тема «побега из песочницы Chromium» все эти годы остаётся актуальной. Так, в одном только 2024м году было обнаружено шесть таких уязвимостей, эксплуатируемых в промышленных масштабах. Поэтому, мы хотим всегда запускать наш браузер в контейнере, без привилегий.
Браузер это мультимедийное приложение, которое мы используем для игр и видеозвонков, поэтому нам потребуются видеокарта, веб-камера, звуковая карта. Качество звука для нас очень важно, поэтому мы также уделим внимание настройке PulseAudio.
Создайте в отдельной папке файл Dockerfile
следующего содержания:
FROM --platform=linux/amd64 docker.io/library/debian:latest RUN apt update && apt upgrade -y \ && apt install alsa-oss alsa-utils pulseaudio ca-certificates ffmpeg bluetooth bluez chromium -y \ && apt clean \ && rm -rf /var/lib/apt/lists/* ARG GROUP_ID=9898 ARG USER_ID=9898 ARG USR_NAME=chromeuser RUN echo "default-sample-format = s32le # Варианты s16le, s32le, float32le" > /etc/pulse/daemon.conf RUN echo "default-sample-rate = 192000 # как вариант 48000, 96000, 192000, 320000" >> /etc/pulse/daemon.conf RUN echo "alternate-sample-rate = 44100 # как вариант 44100, 88200" >> /etc/pulse/daemon.conf RUN echo "default-sample-channels = 2" >> /etc/pulse/daemon.conf RUN echo "default-channel-map = front-left,front-right" >> /etc/pulse/daemon.conf RUN echo "default-fragments = 2 # по умолчанию 4" >> /etc/pulse/daemon.conf RUN echo "default-fragment-size-msec = 125 # по умолчанию 25" >> /etc/pulse/daemon.conf RUN echo "resample-method = soxr-vhq # Варианты src-sinc-best-quality, soxr-vhq, copy, speex-float-1" >> /etc/pulse/daemon.conf RUN echo "remixing-produce-lfe=no # yes для 2+1, 5+1 и т.д." >> /etc/pulse/daemon.conf RUN echo "remixing-consume-lfe=no # yes для 2+1, 5+1 и т.д." >> /etc/pulse/daemon.conf RUN echo "high-priority = yes" >> /etc/pulse/daemon.conf RUN echo "nice-level = -11" >> /etc/pulse/daemon.conf RUN echo "realtime-scheduling = yes" >> /etc/pulse/daemon.conf RUN echo "realtime-priority = 9 # по умолчанию 5" >> /etc/pulse/daemon.conf RUN echo "rlimit-rtprio = 9" >> /etc/pulse/daemon.conf RUN echo "daemonize = no" >> /etc/pulse/daemon.conf RUN echo "PULSE DAEMON:" && cat /etc/pulse/daemon.conf RUN echo "autospawn = no" > /etc/pulse/client.conf RUN echo "daemon-binary = /bin/true" >> /etc/pulse/client.conf RUN echo "enable-shm = false" >> /etc/pulse/client.conf RUN echo "default-server = unix:/run/user/$USER_ID/pulse/native" >> /etc/pulse/client.conf RUN echo "PULSE CLIENT:" && cat /etc/pulse/client.conf RUN groupadd -g $GROUP_ID --system ${USR_NAME} && \ useradd -m --gid $GROUP_ID --system -u $USER_ID -G audio,video,bluetooth ${USR_NAME} \ && mkdir -p /home/${USR_NAME}/reports \ && mkdir -p /home/${USR_NAME}/.config \ && mkdir -p /home/${USR_NAME}/.cache \ && mkdir -p /home/${USR_NAME}/.local/share \ && chown --recursive ${USR_NAME}:${USR_NAME} /home/${USR_NAME} USER ${USR_NAME} RUN mkdir -p /tmp/xdg CMD "chromium"
Здесь для вас многое должно быть знакомым из предыдущей статьи. Новым является:
-
Аргумент
--platform=linux/amd64
командыFROM
позволяет явно задавать архитектуру, для которой был создан образ. Здесь он указан в образовательных целях. -
Большое количество слоёв образа — конкатенации конфигурационных файлов звукового сервера PulseAudio.
-
Добавление пользователя, от имени которого будет запускаться браузер, в группы
audio
,video
иbluetooth
. -
Вместо ENTRYPOINT можно использовать CMD, что будет интерпретировано как
sh -c
при запуске контейнера .
Эту конфигурацию звука я смог составить благодаря статье на OpenNet. Заметьте, что данная конфигурация оптимальна для моего оборудования. Вы можете выяснить поддерживаемые форматы передачи звука и и битрейты вашим железом командой aplay --device hw /dev/urandom --dump-hw-params
. В тему настройки звука на Линукс далее мы углубляться не будем.
По причине того, что нам в данном примере нужно будет создавать образы и запускать контейнеры несколькими разными способами, то мы будем использовать для запуска контейнеров утилиту make
. Установим её:
sudo apt update && sudo apt upgrade -y && sudo apt install build-essential
В той же папке, где мы создали Dockerfile
, создайте файл Makefile
следующего содержания:
SHELL=/bin/bash DOCKER=podman UUID=$(shell id -u) GUID=$(shell id -g) ME=$(shell whoami) MY_UID=$(shell id -u) USRNME=chromeuser IMG_NAME=chromium build: @ echo "Собираем образ Chromium для запуска в ином пространстве имён" && \ ${DOCKER} build -t ${IMG_NAME} . build-keep-uid: @ echo "Собираем образ Chromium для запуска от имени пользователя ${ME}" && \ ${DOCKER} build -t ${IMG_NAME}${MY_UID} --build-arg USER_ID=${MY_UID} --build-arg GROUP_ID=${MY_UID} --build-arg USR_NAME=${ME} . # Запустить образ Chromium в ином пространстве имён run: @ ${DOCKER} run \ --log-level info \ --log-driver json-file \ -it --rm \ --uidmap=0:7:1 \ --uidmap=9898:8:1 \ --gidmap=0:7:1 \ --gidmap=9898:8:1 \ --gidmap=29:6:1 \ --gidmap=44:5:1 \ --gidmap=108:4:1 \ --cpus 3.5 \ --memory 4096m \ --shm-size 2G \ -v /opt/${IMG_NAME}/config:/home/${USRNME}/.config:rw \ -v /opt/${IMG_NAME}/cache:/home/${USRNME}/.cache:rw \ -v /opt/${IMG_NAME}/reports:/home/${USRNME}/reports:rw \ -v /opt/${IMG_NAME}/local:/home/${USRNME}/.local:rw \ -v /home/${ME}/Downloads:/home/${USRNME}/Downloads:rw \ -v /tmp/.X11-unix:/tmp/.X11-unix:ro \ -v /run/user/${UUID}/pulse/native:/tmp/xdg/pulse/native:rw \ --device /dev/dri/card0 \ --device /dev/snd \ --device /dev/video0 \ -e DISPLAY=${DISPLAY} \ -e PULSE_SERVER=unix:path=/tmp/xdg/pulse/native \ -e XDG_RUNTIME_DIR=/tmp/xdg \ ${IMG_NAME} # Запустить образ Chromium от имени обычного пользователя run-keep-uid: @ ${DOCKER} run \ --log-level info \ --log-driver json-file \ -it --rm \ --userns=keep-id \ --cpus 3.5 \ --memory 4096m \ -v /opt/${IMG_NAME}${MY_UID}/config:/home/${ME}/.config:rw \ -v /opt/${IMG_NAME}${MY_UID}/cache:/home/${ME}/.cache:rw \ -v /opt/${IMG_NAME}${MY_UID}/reports:/home/${ME}/reports:rw \ -v /opt/${IMG_NAME}${MY_UID}/local:/home/${ME}/.local:rw \ -v /home/${ME}/Downloads:/home/${ME}/Downloads:rw \ -v /tmp/.X11-unix:/tmp/.X11-unix:ro \ -e DISPLAY=${DISPLAY} \ --device /dev/snd \ --device /dev/dri/card0 \ --device /dev/video0 \ -e XDG_RUNTIME_DIR=/tmp \ -e PULSE_SERVER=unix:/tmp/pulse/native \ -v ${XDG_RUNTIME_DIR}/pulse:/tmp/pulse:ro \ -e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket \ -v /run/dbus:/run/dbus:rw \ ${IMG_NAME}${MY_UID} # Запустить анонимный агностический браузер в ином пространстве имён run-anon: @ ${DOCKER} run \ --log-level info \ --log-driver json-file \ -it --rm \ --uidmap=0:7:1 \ --uidmap=9898:8:1 \ --gidmap=0:7:1 \ --gidmap=9898:8:1 \ --gidmap=29:6:1 \ --gidmap=44:5:1 \ --gidmap=108:4:1 \ --cpus 3.5 \ --memory 4096m \ -v /home/${ME}/Downloads:/home/${USRNME}/Downloads:rw \ -v /tmp/.X11-unix:/tmp/.X11-unix:ro \ -e DISPLAY=${DISPLAY} \ ${IMG_NAME}
В этом Makefile
мы видим 2 цели для создания контейнеров и 3 цели для запуска.
Рассмотрим цели подробнее:
-
Цель
build
создаёт образ из нашего Dockerfile, как есть, ничего не меняя. -
Цель
build-keep-uid
создаёт образ из нашего Dockerfile, назначая пользователю в контейнере те же имя и uid/gid, которые имеет наш хостовый пользователь. -
Цель
run
запускает браузер в ином пространстве имён, со звуком и веб-камерой, который будет сохранять куки, кеши и плагины в папку/opt/chromium
. -
Цель
run-keep-uid
запускает браузер в пространстве имён хостового пользователя, в котором будет всё то же самое, что и в цели run, плюс вывод звука на bluetooth-устройства. Эта цель собирает контейнер из образа, созданного цельюbuild-keep-uid
. -
Цель
run-anon
запускает анонимный агностический браузер в отдельном пространстве имён.
Анонимный браузер не содержит цифрового отпечатка — уникального набора куков сайтов, набора расширений, сведений о железе и ОС, которые формируют идентичность посетителя сайта.
Обсудим более подробно представленные целями make
команды podman run
:
-
--log-level info
задаёт наименьший приоритет (уровень) логирования, включая который и выше, сообщения от программы журналируются. -
--log-driver json-file
задаёт формат журналирования. -
Мы дополнительно отображаем группы с gid
29
(audio),44
(video) и108
(bluetooth) на uid100005
,100004
и100003
хоста, соответственно. -
Инструкцией
--cpus 3.5
мы ограничиваем доступный ресурс ЦПУ для данного контейнера. Например, если процессор 4х-ядерный, то4.0
означает, что каждое ядро сможет быть задействовано на 100%. Соответственно,3.5
будет ограничением в 87.5% для каждого ядра. А это значит, что баг на сайте или майнер не смогут перегреть наш ЦП. -
В Linux есть разные способы организации обмена данными между процессами, и один из них — разделяемая память. Это каталог
/dev/shm
, содержимое которого находится на плашке ОЗУ, а не на жёстком диске. К сожалению, многие сайты архитектурно зависят от этого механизма, и вообще не работают без него vk.com или работают частично. Ввиду этого, нам придётся выделить контейнеру разделяемую память в объёме 2Гб параметром--shm-size 2G
.
Отображение локальных папок в /opt/имя_контейнера
, а также папки Downloads
и папки с экранами мониторов моей сессии X-сервера /tmp/.X11-unix/
должно быть понятно из предыдущей статьи. Рекомендую их владельцем иметь хостового пользователя, а пользователю с uid 100007
выдавать на них права через ACL командой setfacl -R -m u:100007:rwx /opt/имя_контейнера
.
Зато следующее отображение -v /run/user/${UUID}/pulse:/tmp/xdg/pulse:ro
и объявления констант окружения -e PULSE_SERVER=unix:path=/tmp/xdg/pulse/native
и -e XDG_RUNTIME_DIR=/tmp/xdg
требуют пояснения: /run/user/${UUID}/pulse/native
это сокет звукового сервера PulseAudio. Константа XDG_RUNTIME_DIR
задаёт директорию для сокетов и пайпов времени выполнения пользовательской сессии. Константа PULSE_SERVER
объявляет путь к сокету звукового сервера.
Пробросить сокет не является достаточным, требуется проброс устройств:
-
Видеокарта
--device /dev/dri/card0
-
Звуковые устройства подключаются так
--device /dev/snd
, что означает «все символьные I/O устройства в этой папке» -
Веб-камеру подключаем так
--device /dev/video0
Имена ваших файлов типаc
, то есть символьных устройств, могут отличаться, поэтому рекомендую изучить содержимое вашей папки/dev/
ВНИМАНИЕ: В Линукс всё является файлом, в том числе и устройства символьного ввода-вывода, как мы видели только что. Однако, эти файлы не хранятся на жёстком диске, а создаются каждый раз при запуске ОС. Для пользования данными файлами-устройствами, пользователь должен иметь соответствующие права доступа. Поэтому, после включения вашего ПК и перед запуском браузера, придётся выполнять с root-правами скрипт следующего содержания:
#!/bin/bash setfacl -R -m u:100007:rw /dev/snd setfacl -m u:100007:rw /dev/dri/card0 setfacl -m u:100007:rw /dev/video0
Как видно из определения цели run
нашего Makefile
, мы выбрали uid/gid 100007
для пользователя дочернего пространства имён suduid/subgid, под которым будет запущен процесс браузера.
Создайте пользователя с uid 100007
для дальнейшего использования с командой xhost
, как это было показано в предыдущей статье.
В определении цели run-keep-uid
мы видим ранее не встречавшийся параметр --userns=keep-id
, что означает «запустить в пространстве имён хостового пользователя», то есть, с сохранением его UID. Обратите внимание, что именно для этого мы создаём отдельный образ make-целью build-keep-uid
.
Справедливым вопросом будет: Зачем нам запускать браузер в том же пространстве имён, если от этого мы хотим уйти? Дело в том, что у меня покамест не получилось выводить звук на bluetooth-устройства из-под пользователя в другом пространстве имён. На обычные динамики или гарнитуру, подключенную через мини джек звук идёт, а на bluetooth-устройство — нет. Если у кого-то получится это сделать, дайте знать в комментариях — решим эту задачу вместе.
Если вы, также, как и я, захотите использовать многие приложения исключительно в контейнерах, то подобный Makefile
может служить единой точкой запуска всех контейнеризированных приложений, которые вы часто используете.
Доселе, примеры контейнеризированных браузеров, которые я находил в Интернете, скорее вредили пользователю, чем помогали. Например, в Dockerfile
отсутствовала смена
пользователя с рутового на обычного, что вынуждало в точке входа запускать Chromium
с флагом --no-sandbox
. А это, в свою очередь, отключало описанный в начале статьи механизм изоляции JavaScript-рантайма.
Историческая справка: Дело в том, что в декабре 2017 года произошла ещё одна революция в безопасности браузеров:
В Chrome 63 была добавлена изоляция среды исполнения между вкладками.
До этого, если у вас было открыто две вкладки, на одной из которых вы прошли аутентификацию в почту, а на второй открыли нехороший сайт, то JavaScript с нехорошего сайта мог украсть токен доступа к сессии почтового ящика с первой вкладки и пополнить спам-базу своего неэтичного владельца адресами ваших контактов, к примеру. Как видите, контейнеризация браузера никак не могла бы защитить от такой опасности.
Именно таким опасным было пользование Интернетом до конца 2017 года для пользователей Chromium, а для пользователей Firefox и вовсе вплоть до июня 2021го, когда наконец Project Fisson прошёл стадию бета-тестирования и в Firefox 97 данный механизм изоляции вкладок был уже включен по умолчанию.
Механизм изоляции вкладок в Chromium является составной частью песочницы Chromium.
То есть, отключаем песочницу — отключаем изоляцию вкладок. Уж куда безопаснее будет запускать браузер на хосте, без контейнера, чем в контейнере с флагом--no-sandbox
.
В завершение данной статьи, хочу привести содержимое Dockerfile
и скрипта запуска менеджера паролей, которым пользуюсь сам:
-
Dockerfile
FROM docker.io/library/debian:bookworm-slim RUN apt update && apt upgrade -y \ && apt install keepass2 -y \ && rm -rf /var/lib/apt/lists/* ARG GROUP_ID=9898 ARG USER_ID=9898 ARG USRNME=testpilot 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 \ && chown --recursive $USRNME:$USRNME /home/$USRNME USER $USRNME CMD ["keepass2"]
-
скрипт запуска:
#!/bin/bash CONTAINER=$1 podman run \ --uidmap=0:119:1 \ --uidmap=9898:120:1 \ --gidmap=0:119:1 \ --gidmap=9898:120:1 \ --rm -it \ --net=none \ -v /tmp/.X11-unix:/tmp/.X11-unix:ro \ -v /opt/keepass:/home/testpilot/:rw \ -e DISPLAY=$DISPLAY \ $CONTAINER
Данный скрипт запуска принимает в качестве аргумента имя образа.
Допустим, что у нас есть опасение относительно KeePass2, что по желанию владельца кода, эта программа в один прекрасный день может отправить пароли куда-то в далёкое зарубежье. Чтобы этого не могло произойти, используем параметр --net=none
, так что наш контейнер не будет иметь доступа к сети.
По умолчанию, хорошей практикой будет максимально ограничивать контейнер в доступных ресурсах системы, так что отключать сеть менеджеру паролей нужно в любом случае, если только вы не используете LDAP или другое сетевое средство аутентификации.
В завершение:
На этом, я планирую завершить серию статей о запуске графических приложений в контейнерах, ввиду однотипности дальнейших примеров.
Предполагаю, что вам также понадобится контейнер с okular
для открытия PDF-документов. Okular желательно также запускать с параметром --net=none
, так как PDF-файлы это PostScript-документы, которые похожи на HTML тем, что в них можно добавлять JavaScript код. А JavaScript-вирусы, как правило, докачивают вредоносный код из Интернета.
Важный оффтопик: Советую на хосте задать приложением по умолчанию для открытия PDF-файлов Thunar или другой файловый менеджер, или вовсе текстовый редактор типа
gedit
. Иначе, риск слишком велик — случайный клик по ярлыку случайно скачанного PDF-файла может оказаться запуском трояна вашими собственными руками.
В Dockerfile
для Okular установите попутно KDE, чтобы была доступна тёмная тема Breeze Dark, таким образом:
apt install -y okular kde-plasma-desktop
Чтобы тёмную тему не пришлось настраивать каждый раз после запуска, создайте и отображайте в скрипте запуска домашнюю папку для контейнерного пользователя:
-v /opt/okular:/home/username:rw
Заметьте, что целиком свою домашнюю папку отображать в контейнер с точки зрения безопасности было бы грубой ошибкой, так как в ~/.local/share/containers
хранятся те самые образы контейнеров, на целостность которых мы во многом рассчитываем.
Не забывайте, что UID/GID пользователей для процессов контейнера на хосте ни в коем
случае не должны пересекаться друг с другом. Также, этим пользователям необходимо выдавать права на отрисовку на экране после каждого запуска компьютера. Это делается без повышенных привилегий (в отличие от скрипта sound.sh
для выдачи прав на запись в звуковую карту через ACL).
Мой скрипт xhost.sh
выглядит примерно так:
#!/bin/bash xhost +si:localuser:pod_chrom xhost +si:localuser:pod_kee xhost +si:localuser:pod_vs xhost +si:localuser:pod_okular #... и так далее
Мой скрипт sound.sh
выглядит примерно так:
#!/bin/bash # Дать chromium звук & вебку & GPU setfacl -R -m u:100007:rw /dev/snd setfacl -m u:100007:rw /dev/dri/card0 setfacl -m u:100007:rw /dev/video0 # Дать какому-то другому приложению звук & вебку & GPU setfacl -R -m u:100021:rw /dev/snd setfacl -m u:100021:rw /dev/dri/card0 setfacl -m u:100021:rw /dev/video0 #... и так далее
В качестве послесловия, отметим, что приведённый в этих двух статьях стиль пользования ОС Линукс является выражением потребности в более современной архитектуре ОС, которая бы делала невозможной столь лёгкую компрометацию системы (как например через открытие PDF-файла). В Линукс всё файл, и доступ любому файлу определяется правами
доступа POSIX или ACL. И такое однообразие в обращении ко всему в системе открывает простор для фантазий на тему эксплойта нецелевого использования. Выражаясь проще, «защита от дурака» это ещё и защита от злоумышленника, а в Линукс она отсутствует. Это и неудивительно, ведь в 1969 году Кен Томпсон и Деннис Ричи вряд ли могли представить себе все эти опасности, которых мы учимся избегать в данных статьях, в частности. Они имплементировали самые лучшие идеи того времени — многопользовательская и многозадачная архитектура.
Тем не менее с годами выявлялись новые потребности, которые оформлялись в пожелания к архитектуре ОС. Так, в начале 2010х годов началась разработка Phantom OS — операционной системы, в которой всё суть объект.
Phantom OS архитектурно, из коробки, закрывает потребности в безопасном, изолированном запуске приложений, а это то, ради чего мы здесь научились делать так много нетривиальных действий. Хочется надеяться на то, что этот проект поскорее станет зрелым и пойдёт в массы, и что мы вскоре сможем пользоваться ОС с современной архитектурой, учитывающей выявленные в последние годы потребности.
Покамест же, запускаем десктопные приложения в контейнерах.
ссылка на оригинал статьи https://habr.com/ru/articles/868772/
Добавить комментарий