Добавлен перевод статьи "Dynamic Users with systemd"
This commit is contained in:
685
s4a.tex
685
s4a.tex
@@ -18,6 +18,8 @@ pdfauthor={Lennart Poettering, Sergey Ptashnick}}
|
||||
%\setcounter{tocdepth}{1} % А почему бы и нет?
|
||||
% Несколько сокращений
|
||||
\newcommand{\sectiona}[1]{\section*{#1}\addcontentsline{toc}{section}{#1}}
|
||||
\newcommand{\subsectiona}[1]{\subsection*{#1}%
|
||||
\addcontentsline{toc}{subsection}{#1}}
|
||||
\newcommand{\hreftt}[2]{\href{#1}{\texttt{#2}}}
|
||||
\newenvironment{caveat}[1][]{\smallskip\par\textbf{Предупреждение#1: }}%
|
||||
{\smallskip\par}
|
||||
@@ -26,6 +28,8 @@ pdfauthor={Lennart Poettering, Sergey Ptashnick}}
|
||||
\newcommand{\qna}[1]{\medskip\par\textbf{Вопрос: #1}\nopagebreak\par Ответ:}
|
||||
\newcommand\yousaywtf[1]{\emph{#1}}
|
||||
\newcommand\yousaywtfsk[1]{\yousaywtf{#1}\medskip\par}
|
||||
\newcommand\llquote{\texorpdfstring{<<}{"}}
|
||||
\newcommand\rrquote{\texorpdfstring{>>}{"}}
|
||||
% Настройка макета страницы
|
||||
\setlength{\hoffset}{-1.5cm}
|
||||
\addtolength{\textwidth}{2cm}
|
||||
@@ -2762,6 +2766,7 @@ PrivateNetwork=yes
|
||||
котором настраивается только интерфейс обратной петли.
|
||||
|
||||
\subsection{Предоставление службам независимых каталогов \texttt{/tmp}}
|
||||
\label{sec:privatetmp}
|
||||
|
||||
Еще одна простая, но мощная опция настройки служб~--- +PrivateTmp=+:
|
||||
\begin{Verbatim}
|
||||
@@ -5234,7 +5239,7 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
(network namespace), иерархию монтирования (mount namespace, также использовался
|
||||
термин filesystem namespace), иерархию контрольных групп (cgroup namespace),
|
||||
очереди сообщений POSIX и интерфейсы SysV IPC (IPC namespace), имя хоста (UTS
|
||||
namespace), независимые списки индентификаторов процессов (PID namespace) и
|
||||
namespace), независимые списки идентификаторов процессов (PID namespace) и
|
||||
пользователей/групп (user namespace). Лежат в основе систем контейнерной
|
||||
изоляции, таких как LXC и Docker. Также широко используются в systemd для
|
||||
обеспечения безопасности~--- см. главы \ref{sec:chroots} и
|
||||
@@ -5262,6 +5267,684 @@ IPC)\footnote{Прим. перев.: Как и в предыдущем случ
|
||||
директивы в поставляемые по умолчанию юнит-файлы, так как эти люди лучше знают,
|
||||
какие именно привилегии минимально необходимы для функционирования их демонов.
|
||||
|
||||
\sectiona{Динамические пользователи}
|
||||
|
||||
\emph{Коротко о главном: теперь вы можете настроить systemd таким образом, чтобы
|
||||
он автоматически выделял новый идентификатор пользователя (UID) для службы при
|
||||
ее запуске, и автоматически освобождал его при остановке службы. Такой подход
|
||||
абсолютно безопасен и может также применяться в сочетании с другими технологями
|
||||
systemd, в частности, одноразовыми службами (transient services), службами с
|
||||
активацией через сокет и экземплярами служб.}
|
||||
|
||||
В выпуске
|
||||
\href{https://lists.freedesktop.org/archives/systemd-devel/2017-October/039589.html}%
|
||||
{systemd 235}, помимо прочих улучшений, значительно расширена поддержка
|
||||
\emph{динамических пользователей} (базовая функциональность для которой
|
||||
появилась еще в версии 232). Это мощная и удобная, но пока малоизвестная
|
||||
технология, и в данной статье я попытаюсь исправить последний недостаток,
|
||||
рассказав о ней более подробно.
|
||||
|
||||
Понятие \emph{пользователя} является одной из ключевых концепций безопасности
|
||||
POSIX-совместимых операционных систем. Большинство созданных впоследствии
|
||||
механизмов обеспечения безопасности (SELinux и другие системы мандатного
|
||||
контроля доступа, capabilities, пространства имен пользователей и т.д.) так или
|
||||
иначе связаны или взаимодействуют с концепцией пользователя. Даже если вы хотите
|
||||
пересобрать ядро Linux, полностью отключив все возможные механизмы безопасности,
|
||||
от пользователей вы, скорее всего, не~избавитесь.
|
||||
|
||||
Изначально, понятие пользователя было введено при создании многопользовательских
|
||||
систем, рассчитанных на одновременную работу нескольких
|
||||
пользователей-\emph{людей}, обеспечивая взаимную изоляцию и разделение ресурсов.
|
||||
Однако, большинство современных UNIX-систем используют концепцию пользователей
|
||||
иначе: несмотря на то, что реальный пользователь у них может быть только один
|
||||
(или вообще ни одного!), в их списках пользователей (то есть файлах
|
||||
+/etc/passwd+), записей намного больше. В наше время, большинство пользователей
|
||||
типичной UNIX-системы являются \emph{системными}~--- они соответствуют
|
||||
не~сидящим перед компьютером живым людям, а наборам полномочий (security
|
||||
identity), с которыми запускаются системные службы (т.е. программы). Хотя
|
||||
традиционные многопользовательские системы постепенно теряют свою актуальность,
|
||||
лежащее в их основе понятие <<пользователь>> стало одним из краеугольных камней
|
||||
в технологиях защиты UNIX-систем. Большинство служб в современной системе
|
||||
работает от имени своего пользователем, которому даны минимально возможные
|
||||
полномочия.
|
||||
|
||||
Создатели ОС Android хорошо понимали значимость концепции пользователя для
|
||||
безопасности системы, и пошли еще дальше: в Android пользователи создаются
|
||||
не~только для системных служб, но и для каждого графического приложения, что
|
||||
позволяет обеспечить разделение ресурсов и защиту процессов разных приложений
|
||||
друг от друга.
|
||||
|
||||
Тем не~менее, в традиционных Linux-системах концепции пользователей пока
|
||||
уделяется меньше внимания. Несмотря на то, что она являтся ключевой технологией
|
||||
обеспечения безопасности UNIX-системы, механизмы создания пользователей и
|
||||
управления ими пока не~обладают достаточной гибкостью. В большинстве случаев,
|
||||
если вы устанавливаете службу, которая выполняется от своего пользователя,
|
||||
работу по созданию учетных записей берут на себя установочные скрипты RPM или
|
||||
DEB-пакетов. После этого, созданный пользователь останется в системе уже
|
||||
навсегда, даже если вы удалите соответствующий пакет. Большинство дистрибутивов
|
||||
Linux используют для идентификаторов системных пользователей диапазон от 0 до
|
||||
1000~--- не~так уж и много. Следовательно, создание пользователей является
|
||||
<<дорогой>> операцией: количество идентификаторов ограничено, и не~существует
|
||||
механизма для их автоматического освобождения после использования. Если вы
|
||||
будете активно использовать концепцию системных пользователей, вы рано или
|
||||
поздно исчерпаете доступный лимит.
|
||||
|
||||
У вас может возникнуть вопрос: почему системные пользователи не~удаляются при
|
||||
удалении создавшего их пакета (как минимум, в большинстве дистрибутивов)?
|
||||
Причиной этому является одно из важных свойств концепции пользователя (его можно
|
||||
даже назвать \emph{ошибкой дизайна}): идентификаторы пользователей
|
||||
сохраняются в атрибутах файлов (и некоторых других объектов, в частности,
|
||||
элементов IPC). Если служба, работающая из-под отдельного системного
|
||||
пользователя, создаст где-либо новый файл, то даже после остановки службы и
|
||||
удаления ее пакета, числовой идентификатор ее пользователя (UID) останется в
|
||||
метаданных этого файла. Если бы наша система удаляла системных пользователей
|
||||
вместе с их пакетами, то рано или поздно этот идентификатор заняла бы
|
||||
какая-нибудь другая служба. И это уже можно считать уязвимостью, так как файл с
|
||||
данными одной службы становится доступен для другой. Как следствие, дистрибутивы
|
||||
склонны избегать повторного использования UID, и каждый созданный системный
|
||||
пользователь остается в системе навсегда.
|
||||
|
||||
Выше мы рассматривали сложившуюся к настоящему моменту ситуацию. А теперь
|
||||
посмотрим, что нового вносит наша концепция динамических пользователей, и какие
|
||||
проблемы она может решить.
|
||||
|
||||
\subsectiona{Что такое \llquote{}динамический пользователь\rrquote{}?}
|
||||
|
||||
Реализованный в systemd механизм динамических пользователей нацелен на упрощение
|
||||
и удешевление операций создания пользователей <<на лету>>, и в перспективе
|
||||
позволит значительно расширить область примнения концепции пользователей в
|
||||
целом.
|
||||
|
||||
При создании или редактировании service-файла вашей службы, вы можете включить
|
||||
для нее механизм динамических пользователей, добавив в секцию +[Service]+
|
||||
директиву
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#DynamicUser=}%
|
||||
{DynamicUser=yes}. После этого, при каждом запуске данной службы, для нее будет
|
||||
на лету создаваться системный пользователь. При остановке службы он будет
|
||||
автоматически удаляться. UID такого пользователя выбирается из диапазона
|
||||
61184--65519 (при этом производится автоматическая проверка, исключающая
|
||||
использование UID, занятых кем-либо еще).
|
||||
|
||||
Вы спросите, как же в таком случае решается вышеописанная проблема привязки
|
||||
идентификаторов пользователей к файлам? Существуют как минимум два очевидных
|
||||
подхода к ее решению:
|
||||
\begin{enumerate}
|
||||
\item Запретить службе создавать файлы, каталоги и объекты IPC.
|
||||
\item Автоматически удалять созданные службой файлы, каталоги и объекты
|
||||
IPC при ее остановке.
|
||||
\end{enumerate}
|
||||
|
||||
systemd реализует одновременно обе стратегии, но применяет их к различным
|
||||
областям рабочего окружения службы. А именно:
|
||||
\begin{enumerate}
|
||||
\item Установка +DynamicUser=yes+ также применяет директивы
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#ProtectSystem=}%
|
||||
{ProtectSystem=strict} и
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#ProtectHome=}%
|
||||
{ProtectHome=read-only}. В результате служба теряет возможность
|
||||
что-либо писать практически во все каталоги системы, за
|
||||
исключением специальных файловых систем (+/dev+, +/proc+ и
|
||||
+/sys+) и временных каталогов (+/tmp+ и +/var/tmp+). (Кстати,
|
||||
включать эти опции имеет смысл даже для служб, которые
|
||||
не~используют режим +DynamicUser=yes+, так как это сильно
|
||||
снижает возможный ущерб при взломе службы.)
|
||||
\item Установка +DynamicUser=yes+ автоматически применяет директиву
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#PrivateTmp=}%
|
||||
{PrivateTmp=yes}. В результате для этой службы создаются свои
|
||||
собственные, изолированные от других служб экземпляры каталогов
|
||||
+/tmp+ и +/var/tmp+\footnote{Прим. перев.: Технически это
|
||||
реализовано следующим образом: в системных +/tmp+ и +/var/tmp+
|
||||
создаются подкаталоги со специальными именами (построенными на
|
||||
основе имени службы и (псевдо)случайных последовательностей
|
||||
символов), принадлежащие руту и имеющие права доступа 0700 (это
|
||||
граничные каталоги~--- принцип их работы описан в данной статье
|
||||
ниже), а внутри них создаются подкаталоги +tmp+ с правами 0777.
|
||||
Для самой службы создается пространство имен монтирования (mount
|
||||
namespace), в котором эти подкаталоги bind-монтируются в +/tmp+
|
||||
и +/var/tmp+ соответственно. См. также раздел
|
||||
\ref{sec:privatetmp}.}, причем их жизненный цикл привязан к
|
||||
жизненному циклу службы: при остановке службы удаляется
|
||||
не~только ее пользователь, но и ее временные каталоги. (Опять же
|
||||
замечу, что эту директиву имеет смысл применять и без
|
||||
+DynamicUser=yes+, так как она тоже повышает безопасность вашей
|
||||
системы.)
|
||||
\item Установка +DynamicUser=yes+ автоматически применяет директиву
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#RemoveIPC=}%
|
||||
{RemoveIPC=yes}, которая обеспечивает автоматическое удаление
|
||||
объектов межпроцессного взаимодействия (IPC) SysV и POSIX (общая
|
||||
память, очереди сообщений, семафоры), принадлежащих службе, при
|
||||
ее остановке. Как следствие, жизненный цикл IPC-объектов также
|
||||
привязан к жизненному циклу службы и ее пользователя. (И снова:
|
||||
эту директиву имеет смысл применять и для обычных служб!)
|
||||
\end{enumerate}
|
||||
|
||||
Использование этих четырех опций позволяет практически полностью изолировать
|
||||
службу с динамическим пользователем от основной системы. Она не~может создавать
|
||||
файлы или каталоги где-либо, кроме +/tmp+ и +/var/tmp+, где они будут удалены
|
||||
при ее остановке, как и созданные ею объекты IPC. Таким образом, проблема
|
||||
владения файлами, каталогами и объектами IPC, успешно решена.
|
||||
|
||||
Если вам нужно немного приоткрыть <<железный занавес>> и дать службе возможность
|
||||
взаимодействовать с другими программами, это можно сделать при помощи параметра
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#RuntimeDirectory=}%
|
||||
{RuntimeDirectory=}. Подкаталог с указанным в этом параметре именем создается в
|
||||
каталоге +/run+ при запуске службы, принадлежит ее пользователю, и тоже
|
||||
автоматически удаляется при ее остановке. Однако, при этом он открыт для доступа
|
||||
других программ, так что служба может размещать в нем различные интерфейсные
|
||||
объекты (например, UNIX-сокеты), жизненный цикл которых также будет привязан к
|
||||
жизненному циклу службы. Например, если вы зададите для своей службы
|
||||
+RuntimeDirectory=foobar+, то увидите, что при ее запуске создается каталог
|
||||
+/run/foobar+, а при ее остановке он удаляется. (Как и другие описанные здесь
|
||||
директивы, +RuntimeDirectory=+ прекрасно может работать и без +DynamicUser=+,
|
||||
предоставляя для вашей службы автоматически создаваемый и удаляемый каталог с
|
||||
правильным владельцем.)
|
||||
|
||||
\subsectiona{Долговременное хранение данных}
|
||||
|
||||
Вышеописанный способ изоляции службы, хотя и может оказаться полезен в некоторых
|
||||
случаях, имеет одно существенное ограничение: служба не~может сохранить
|
||||
какие-либо данные так, чтобы получить к ним доступ при последующих запусках.
|
||||
Почти все системное дерево каталогов доступно ей только для чтения, а все
|
||||
доступные на запись каталоги удаляются при остановке и перезапуске.
|
||||
|
||||
В выпуске systemd 235 это ограничение было снято: мы добавили три новых
|
||||
опции~---
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#RuntimeDirectory=}%
|
||||
{StateDirectory=}, +LogsDirectory=+ и +CacheDirectory=+. По большей части они
|
||||
действуют аналогично вышеописанной опции +RuntimeDirectory=+, но создают
|
||||
подкаталоги не~в +/run+, а в +/var/lib+, +/var/log+ и +/var/cache+
|
||||
соответственно. Но самое главное их отличие состоит в том, что эти подкаталоги
|
||||
\emph{не~удаляются} при остановке службы, что позволяет использовать их для
|
||||
долговременного хранения данных.
|
||||
|
||||
Очевидный вопрос: как при этом решается вышеописанная проблема с динамическими
|
||||
идентификаторами пользователей?
|
||||
|
||||
Для решения этой задачи мы обратились к опыту систем контейнерной изоляции.
|
||||
При работе с контейнерами возникает схожая проблема: контейнеры и хост
|
||||
используют частично или полностью перекрывающиеся наборы числовых идентификаторов
|
||||
пользователей, а значит, пользователи хоста могут получить доступ к
|
||||
файлам контейнера, которые принадлежат пользователям с таким же UID~--- а ведь
|
||||
это может быть совсем другой пользователь, никак не~связанный со своим хостовым
|
||||
<<двойником>> (кроме случаев, когда используются пространства имен
|
||||
пользователей\footnote{Прим. перев.: В таких случаях используется механизм
|
||||
отображения UID (user-remap), когда пользователям, работающим с user
|
||||
namespaces (например, +dockremap+ в Docker), выдаются диапазоны UID хоста (см.
|
||||
\hreftt{http://man7.org/linux/man-pages/man5/subuid.5.html}{/etc/subuid}), на
|
||||
которые отображаются UID гостевой системы. Это решает проблему пересечения UID
|
||||
хоста и гостя. Без присвоения такого диапазона гость будет иметь только один
|
||||
UID (в пространстве гостя он может быть любым, даже нулевым), который для хоста
|
||||
(и его файловой системы) будет виден как UID пользователя контейнера.}).
|
||||
Особенно неприятно, если это происходит с исполняемым файлом, имеющим
|
||||
+suid+-бит~--- тогда речь идет уже не~просто о доступе к чужим данным, а о
|
||||
потенциальной возможности повышения привилегий. Чтобы предотвратить подобные
|
||||
ситуации, системы управления контейнерами обычно помещают корневые системы
|
||||
гостей в специальный \emph{граничный} каталог, имеющий ограниченные права
|
||||
доступа (обычно: права 0700, владелец +root:root+). При этом,
|
||||
непривилигерованные пользователи хоста уже не~могут добраться до файлов,
|
||||
принадлежащих их контейнерным <<двойникам>>, просто потому, что не~имеют доступа
|
||||
ко всему, что лежит в граничном каталоге. В UNIX, чтобы получить доступ к файлу,
|
||||
вы должны иметь доступ ко всем каталогам по пути к нему, начиная с корневого.
|
||||
|
||||
Как это работает для наших служб с динамическими пользователями? Предположим,
|
||||
что вы указали +StateDirectory=foobar+, но \emph{не~включали} режим
|
||||
+DynamicUser=+. В момент запуска такой службы, для нее будет создан каталог
|
||||
+/var/lib/foobar+, принадлежащий пользователю, от которого запускается
|
||||
служба\footnote{Прим. перев.: Если режим +DynamicUser=+ отключен, пользователь
|
||||
службы определяется директивой
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#User=}%
|
||||
{User=}. Более подробно ее роль при включенном и выключенном режиме
|
||||
динамических пользователей рассмотрена \hyperref[itm:setuser]{ниже}.}. Этот
|
||||
каталог и его содержимое \emph{не~удаляются} после завершения службы. Если же мы
|
||||
включим для нашей службы режим +DynamicUser=+, алгоритм станет немного
|
||||
сложнее~--- теперь +/var/lib/foobar+ уже не~каталог, а принадлежащая руту
|
||||
символьная ссылка на каталог +/var/lib/private/foobar+, который принадлежит
|
||||
динамическому пользователю, выделенному для службы. Каталог +/var/lib/private+
|
||||
играет роль граничного: он принадлежит +root:root+, и имеет права 0700. При
|
||||
остановке службы, и символьная ссылка, и каталог, на который она указывает,
|
||||
не~удаляются. Каталог продолжает принадлежать теперь уже освобожденному
|
||||
идентификатору пользователя, однако граничный каталог не~позволит получить к
|
||||
нему доступ ни пользователям хоста, ни другим службам, получившим тот же UID.
|
||||
|
||||
Следующий вопрос: если граничный каталог столь успешно защищает подкаталог с
|
||||
данными службы, то как сама служба сможет получить к ним доступ? Для этого,
|
||||
служба запускается в специально модифицированном пространстве имен точек
|
||||
монтирования (mount namespace): помимо уже упомянутых хитростей с монтированием
|
||||
+/tmp+ и +/var/tmp+\footnote{Прим. перев.: Строго говоря, <<хитростей>> там
|
||||
гораздо больше~--- упомянутые выше директивы +ProtectSystem=strict+ и
|
||||
+ProtectHome=read-only+ предполагают перемонтирование целого ряда каталогов,
|
||||
включая корневой. Но к теме это действительно не~относится.}, добавляется
|
||||
+tmpfs+, смонтированная в +/var/lib/private+ и доступная только на чтение, с
|
||||
правами, разрешающими чтение из этого каталога (0755). Внутри этой файловой
|
||||
системы создан каталог +foobar+, в который bind-монтируется каталог
|
||||
+/var/lib/private/foobar+ с хоста. В результате и для хоста, и для службы
|
||||
содержимое этого каталога находится по одному и тому же пути, но при этом <<с
|
||||
точки зрения>> службы каталог +/var/lib/private+ уже не~является
|
||||
ограничительным, и в нем отсутствуют подкаталоги, принадлежащие другим службам,
|
||||
что обеспечивает полную изоляцию служб друг от друга. Символьная сслыка
|
||||
+/var/lib/foobar+ позволяет не~задумываться о том, используется граничный
|
||||
каталог или нет: и при включенном, и при выключенном режиме динамических
|
||||
пользователей, каталог с данными программы доступен по указанному пути.
|
||||
|
||||
Назревает очередной вопрос. Предположим, что мы запустили службу от динамического
|
||||
пользователя и с настроенным каталогом для хранения данных. Она получила
|
||||
некоторый UID (назовем его $X$), которому принадлежит этот каталог. Потом мы
|
||||
перезапустили службу, и она получила новый UID $Y\neq X$. Что тогда произойдет?
|
||||
Неужели каталог и его содержимое будут все еще принадлжать UID $X$, и наша
|
||||
служба уже не~сможет получить к ним доступ? Конечно же, нет~--- systemd
|
||||
рекурсивно поменяет владельца для каталога и его содержимого.
|
||||
|
||||
Разумеется, операция рекурсивной смены владельца (+chown()+) для дерева
|
||||
каталогов может оказаться весьма затратной (хотя, по моему личному опыту, для
|
||||
большинства служб это не~так критично, как кажется на первый взгляд), и поэтому
|
||||
мы ввели две оптимизации, сводящие к минимуму вероятность такой операции.
|
||||
Во-первых, systemd начинает подбор подходящего UID с некоторого значения,
|
||||
полученного путем хеширования имени службы. Таким образом, если имя службы
|
||||
не~поменялось, то при следующем запуске она, скорее всего, получит тот же UID. В
|
||||
результате, необходимость в +chown()+ отпадает (разумеется, после
|
||||
соответствующих проверок). Во-вторых, если указанный каталог уже существует, и
|
||||
его владельцем является неиспользуемый UID из динамического диапазона, то службе
|
||||
присваивается именно этот UID, что также увеличивает шансы избежать +chown()+.
|
||||
(На самом деле, выделенный сейчас диапазон на четыре с лишним тысячи UID не~так
|
||||
уж и велик, и при активном применении динамических пользователей рано или поздно
|
||||
появятся ситуации, когда обойтись без +chown()+ уже не~удастся.)
|
||||
|
||||
Директивы +CacheDirectory=+ и +LogsDirectory=+ работают по аналогии со
|
||||
+StateDirectory=+. Единственное их отличие состоит в том, что они управляют
|
||||
подкаталогами в +/var/cache+ и +/var/log+, и используют граничные каталоги
|
||||
+/var/cache/private+ и +/var/log/private+ соответственно.
|
||||
|
||||
\subsectiona{Примеры}
|
||||
|
||||
Итак, мы ознакомились с теорией. Попробуем теперь посмотреть, как все это
|
||||
работает на практике. Простой пример:
|
||||
\begin{Verbatim}
|
||||
# cat > /etc/systemd/system/dynamic-user-test.service <<EOF
|
||||
[Service]
|
||||
ExecStart=/usr/bin/sleep 4711
|
||||
DynamicUser=yes
|
||||
EOF
|
||||
# systemctl daemon-reload
|
||||
# systemctl start dynamic-user-test
|
||||
# systemctl status dynamic-user-test
|
||||
dynamic-user-test.service
|
||||
Loaded: loaded (/etc/systemd/system/dynamic-user-test.service; static; vendor preset: disabled)
|
||||
Active: active (running) since Fri 2017-10-06 13:12:25 CEST; 3s ago
|
||||
Main PID: 2967 (sleep)
|
||||
Tasks: 1 (limit: 4915)
|
||||
CGroup: /system.slice/dynamic-user-test.service
|
||||
└─2967 /usr/bin/sleep 4711
|
||||
|
||||
Okt 06 13:12:25 sigma systemd[1]: Started dynamic-user-test.service.
|
||||
# ps -e -o pid,comm,user | grep 2967
|
||||
2967 sleep dynamic-user-test
|
||||
# id dynamic-user-test
|
||||
uid=64642(dynamic-user-test) gid=64642(dynamic-user-test) groups=64642(dynamic-user-test)
|
||||
# systemctl stop dynamic-user-test
|
||||
# id dynamic-user-test
|
||||
id: ‘dynamic-user-test’: no such user
|
||||
\end{Verbatim}
|
||||
|
||||
Мы создали юнит-файл службы с включенным режимом +DynamicUser=+, запустили эту
|
||||
службу, убедились, что она работает нормально, запросили данные о пользователе,
|
||||
от которого она запущена (в нашем случае его имя совпадает с именем службы~---
|
||||
systemd использует его автоматически, если оно соответствует синтаксису имени
|
||||
пользователя, и вы не~указали явно другого имени), остановили службу и
|
||||
убедились, что такого пользователя больше не~существует.
|
||||
|
||||
Уже неплохо, правда? Следующий шаг~--- повторим то же самое с интерактивной
|
||||
\emph{одноразовой} службой. Для тех, кто не~очень хорошо разбирается в тонкостях
|
||||
systemd: одноразовая служба (transient service) создается и запускается <<на
|
||||
лету>>, без каких-либо конфигурационных файлов~--- например, с помощью программы
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd-run.html}%
|
||||
{systemd-run}, которую можно запустить непосредственно из командной оболочки.
|
||||
Короче: запуск службы без предварительного создания юнит-файла.
|
||||
\begin{Verbatim}
|
||||
# systemd-run --pty --property=DynamicUser=yes --property=StateDirectory=wuff /bin/sh
|
||||
Running as unit: run-u15750.service
|
||||
Press ^] three times within 1s to disconnect TTY.
|
||||
sh-4.4$ id
|
||||
uid=63122(run-u15750) gid=63122(run-u15750) groups=63122(run-u15750) context=system_u:system_r:initrc_t:s0
|
||||
sh-4.4$ ls -al /var/lib/private/
|
||||
total 0
|
||||
drwxr-xr-x. 3 root root 60 6. Okt 13:21 .
|
||||
drwxr-xr-x. 1 root root 852 6. Okt 13:21 ..
|
||||
drwxr-xr-x. 1 run-u15750 run-u15750 8 6. Okt 13:22 wuff
|
||||
sh-4.4$ ls -ld /var/lib/wuff
|
||||
lrwxrwxrwx. 1 root root 12 6. Okt 13:21 /var/lib/wuff -> private/wuff
|
||||
sh-4.4$ ls -ld /var/lib/wuff/
|
||||
drwxr-xr-x. 1 run-u15750 run-u15750 0 6. Okt 13:21 /var/lib/wuff/
|
||||
sh-4.4$ echo hello > /var/lib/wuff/test
|
||||
sh-4.4$ exit
|
||||
exit
|
||||
# id run-u15750
|
||||
id: ‘run-u15750’: no such user
|
||||
# ls -al /var/lib/private
|
||||
total 0
|
||||
drwx------. 1 root root 66 6. Okt 13:21 .
|
||||
drwxr-xr-x. 1 root root 852 6. Okt 13:21 ..
|
||||
drwxr-xr-x. 1 63122 63122 8 6. Okt 13:22 wuff
|
||||
# ls -ld /var/lib/wuff
|
||||
lrwxrwxrwx. 1 root root 12 6. Okt 13:21 /var/lib/wuff -> private/wuff
|
||||
# ls -ld /var/lib/wuff/
|
||||
drwxr-xr-x. 1 63122 63122 8 6. Okt 13:22 /var/lib/wuff/
|
||||
# cat /var/lib/wuff/test
|
||||
hello
|
||||
\end{Verbatim}
|
||||
|
||||
В приведенном примере, мы запускаем интерактивную оболочку +/bin/sh+ как
|
||||
одноразовую службу +run-u15750.service+ (+systemd-run+ выбрал это имя
|
||||
автоматически, так как мы не~указали имя службы явно\footnote{Прим. перев.: Это
|
||||
можно было бы сделать параметром +systemd-run+ +--unit=+.}) под динамическим
|
||||
пользователем, имя которого, как и в предыдущем примере, унаследовано от имени
|
||||
службы. Так как мы задали +StateDirectory=wuff+, то каталог для долговременного
|
||||
хранения данных нашей службы должен быть доступен под именем +/var/lib/wuff+. В
|
||||
интерактивной оболочке, запущенной в рамках службы, команда +ls+ показывает
|
||||
граничный каталог +/var/lib/private+ и его содержимое, а также символьную ссылку
|
||||
+/var/lib/wuff+, указывающую на его подкаталог +wuff+. Наконец, перед
|
||||
завершением оболочки, мы создаем там тестовый файл. Вернувшись в нашу исходную
|
||||
оболочку, мы проверяем, существует ли еще пользователь, выделенный для нашей
|
||||
службы~--- нет, он автоматически удален в момент завершения службы (оболочки).
|
||||
При помощи аналогичных команд +ls+ мы снова проверяем каталог долговременного
|
||||
хранения данных службы, на этот раз с хоста. Видим мы почти то же самое, с
|
||||
двумя исключениями: во-первых, пользователь и группа, владеющие каталогом и его
|
||||
содержимым, отображаются уже в виде числовых идентификатов, а не~имен. Это
|
||||
обусловлено тем, что пользователь (то есть ассоциация числового идентификатора с
|
||||
некоторым именем) был удален в момент завершения службы. Во-вторых, отличаются
|
||||
права доступа к граничному каталогу: 0755 (читать могут все) изнутри службы, и
|
||||
0700 (читать может только владелец, т.е. рут)~--- с хоста.
|
||||
|
||||
А теперь попробуем запустить еще одну одноразовую службу, указав ей тот же
|
||||
каталог с данными:
|
||||
\begin{Verbatim}
|
||||
# systemd-run --pty --property=DynamicUser=yes --property=StateDirectory=wuff /bin/sh
|
||||
Running as unit: run-u16087.service
|
||||
Press ^] three times within 1s to disconnect TTY.
|
||||
sh-4.4$ cat /var/lib/wuff/test
|
||||
hello
|
||||
sh-4.4$ ls -al /var/lib/wuff/
|
||||
total 4
|
||||
drwxr-xr-x. 1 run-u16087 run-u16087 8 6. Okt 13:22 .
|
||||
drwxr-xr-x. 3 root root 60 6. Okt 15:42 ..
|
||||
-rw-r--r--. 1 run-u16087 run-u16087 6 6. Okt 13:22 test
|
||||
sh-4.4$ id
|
||||
uid=63122(run-u16087) gid=63122(run-u16087) groups=63122(run-u16087) context=system_u:system_r:initrc_t:s0
|
||||
sh-4.4$ exit
|
||||
exit
|
||||
\end{Verbatim}
|
||||
|
||||
Как видим, +systemd-run+ сгенерировал для этой службы уже другое имя, которое
|
||||
перешло и к ее пользователю, однако числовой идентификатор пользователя остался
|
||||
прежним~--- systemd подхватил UID владельца каталога с данными (предварительно
|
||||
убедившись, что он больше никем не~используется). Этим иллюстрируется
|
||||
вышеописанная оптимизация алгоритма выбора UID (цикл выбора идентификатора
|
||||
начинается с UID владельца существующего каталога с данными): рекурсивного
|
||||
выполнения +chown()+ удалось избежать.
|
||||
|
||||
Надеюсь, вышеприведенные примеры помогли вам разобраться и в самой идее, и в
|
||||
особенностях ее реализации.
|
||||
|
||||
\subsectiona{Практическое применение}
|
||||
|
||||
Итак, мы рассмотрели, как включить механизм динамических пользователей для
|
||||
юнита, и разобрались, как он реализован. Самое время попытаться понять, где и
|
||||
для чего он может применяться.
|
||||
|
||||
\begin{itemize}
|
||||
\item Одно из главных достоинств технологии динамического выделения
|
||||
UID~--- возможность запускать службы с урезаннмыми привилегиями
|
||||
(то есть, от отдельного пользователя), не~оставляя в системе
|
||||
никаких артефактов. Системный пользователь создается и
|
||||
используется, но после применения автоматически удаляется, и
|
||||
повторное применение его UID не~несет никаких рисков для
|
||||
безопасности. Мы можем одной командой запускать одноразовые
|
||||
службы для выполнения каких-либо операций, изолируя их под
|
||||
отдельным идентификатором пользователя, без необходимости
|
||||
вручную создавать и удалять учетную запись, и не~расходуя
|
||||
доступные UID попусту.
|
||||
\item Во многих случаях, запуск службы уже не~требует предварительной
|
||||
подготовки со стороны пакетного менджера. Другими словами,
|
||||
большинство операций +useradd+/+mkdir+/+chown+/+chmod+,
|
||||
выполняемых пост-инсталляционными скриптами пакетов, а также
|
||||
дополнительные конфигурационные файлы в
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/sysusers.d.html}%
|
||||
{sysusers.d} и
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html}%
|
||||
{tmpfiles.d} становятся необязательными, так как эти операции
|
||||
выполняются автоматически благодаря директивам +DynamicUser=+ и
|
||||
+StateDirectory=+/+CacheDirectory=+/+LogsDirectory=+, причем
|
||||
не~при установке/удалении пакета, а непосредственно при
|
||||
запуске/остановке службы.
|
||||
\item Сочетание технологий динамических пользователей и одноразовых
|
||||
служб предоставляет простой механизм изоляции приложений.
|
||||
Например, предположим, что мы не~доверяем бинарнику +sort+.
|
||||
Мы можем поместить его в простую и надежную песочницу
|
||||
динамического пользователя при помощи +systemd-run+, и при этом
|
||||
сохранить возможность подключать ее через каналы (pipelines) к
|
||||
другим программам. Простенький пример конвейера, второй элемент
|
||||
которого запущен от динамического пользователя (который
|
||||
уничтожается после завершения конвейера):
|
||||
\begin{Verbatim}
|
||||
# cat some-file.txt | systemd-run --pipe --property=DynamicUser=1 sort -u | \
|
||||
grep -i foobar > some-other-file.txt
|
||||
\end{Verbatim}
|
||||
\item Сочетая технологию динамических пользователей и экземпляров
|
||||
служб\footnote{Прим перев.: Более подробно работа с экземплярами
|
||||
служб рассмотрена в главе~\ref{sec:instances}.}, можно получить
|
||||
гибкой и полностью автоматический механизм управления
|
||||
идентификаторами пользователей. Допустим, мы создаем шаблон
|
||||
службы +/etc/systemd/system/foobard@.service+:
|
||||
\begin{Verbatim}
|
||||
[Service]
|
||||
ExecStart=/usr/bin/myfoobarserviced
|
||||
DynamicUser=1
|
||||
StateDirectory=foobar/%i
|
||||
\end{Verbatim}
|
||||
Теперь предположим, что вы запускаете экземпляр этой службы
|
||||
для одного из своих клиентов:
|
||||
\begin{Verbatim}
|
||||
# systemctl enable foobard@customerxyz.service --now
|
||||
\end{Verbatim}
|
||||
Готово! (Надеюсь, понятно, что эту операцию можно повторять
|
||||
многократно, подставляя каждый раз вместо +customerxyz+
|
||||
идентификаторы различных клиентов.)
|
||||
\item Сочетая технологии динамических пользователей и сокет-активации,
|
||||
вы легко можете получить систему, где каждое входящее соединение
|
||||
обслуживается экземпляром процесса, работающим в песочнице
|
||||
динамически выделенного UID\footnote{Прим перев.: Здесь идет
|
||||
речь о сокет-активации <<в стиле inetd>>, когда на каждое
|
||||
соединение создается экземпляр службы. Более подробно она
|
||||
обсуждается в главе~\ref{sec:inetd}.}. Пример конфигурации
|
||||
сокета +waldo.socket+:
|
||||
\begin{Verbatim}
|
||||
[Socket]
|
||||
ListenStream=2048
|
||||
Accept=yes
|
||||
\end{Verbatim}
|
||||
И соответствующего ему шаблона службы +waldo@.service+:
|
||||
\begin{Verbatim}
|
||||
[Service]
|
||||
ExecStart=-/usr/bin/myservicebinary
|
||||
DynamicUser=yes
|
||||
\end{Verbatim}
|
||||
В результате, systemd будет слушать TCP-порт 2048, и на каждое
|
||||
входящее соединение создавать новый экземпляр +waldo@.service+,
|
||||
каждый раз с новым идентификатором пользователя, обеспечивающим
|
||||
его изоляцию от остальных экземпляров.
|
||||
\item Динамическое выделение пользователей хорошо сочетается с
|
||||
<<системами без состояния>> (state-less systems), то есть
|
||||
системами, которые запускаются с пустыми каталогами +/etc+ и
|
||||
+/var+. Динамическим выделение UID и директивы
|
||||
+StateDirectory=+, +CacheDirectory=+, +LogsDirectory=+ и
|
||||
+RuntimeDirectory=+ позволяет автоматических создавать
|
||||
пользователя и необходимые службе каталоги непосредственно перед
|
||||
ее запуском.
|
||||
\end{itemize}
|
||||
|
||||
Динамическое выделение пользователей~--- масштабная и глобальная концепция, и ее
|
||||
применение, разумеется, не~ограничивается приведенным списком. Этот список~---
|
||||
всего лишь попытка разбудить ваше воображение, дать начальный импульс к
|
||||
размышлениям.
|
||||
|
||||
\subsectiona{Рекомендации сопровождающим пакетов}
|
||||
|
||||
Я уверен, что для значительной доли служб, поставляемых в современных
|
||||
дистрибутивах, опции +DynamicUser=+, +StateDirectory=+ и т.д., могут оказаться
|
||||
весьма полезны. Во многих случаях они позволят вообще отказаться от +post-inst+
|
||||
скриптов, а также конфигурационных файлов в +sysusers.d+ и +tmpfiles.d+,
|
||||
объединив все необходимые настройки непосредственно в юнит-файле. Так что, если
|
||||
вы сопровождаете какой-либо пакет со службой, пожалуйста, рассмотрите
|
||||
возможность использования этих директив. Тем не~менее, существует ряд ситуаций,
|
||||
когда данные директивы неэффективны или неприменимы:
|
||||
\begin{enumerate}
|
||||
\item Службы, которым нужно писать куда-либо за пределами разрешенного
|
||||
списка каталогов (+/run/<package>+, +/var/lib/<package>+,
|
||||
+/var/cache/<package>+, +/var/log/<package>+, +/var/tmp+, +/tmp+,
|
||||
+/dev/shm+) не~совместимы с описанным подходом. Например, демон,
|
||||
обновляющий систему~--- ему, как минимум, необходим доступ на
|
||||
запись в +/usr+.
|
||||
\item Службы, которые управляют набором процессов, запущенных от
|
||||
различных пользователей, например, некоторые SMTP-серверы. Если
|
||||
ваша служба построена по принципу \emph{суперсервера}, то
|
||||
управление идентификаторами пользователей для своих процессов
|
||||
она должна осуществлять сама~--- systemd не~должен в это
|
||||
вмешиваться.
|
||||
\item Службы, запускаемые от рута, и вообще требующие расширенных
|
||||
привилегий.
|
||||
\item Службы, котороые должны запускаться в пространстве имен
|
||||
монтирования хоста (например, если служба должна создавать точки
|
||||
монтирования, видимые для всей системы). Как уже упоминалось
|
||||
выше, +DynamicUser=+ задействует механизмы +ProtectSystem=+,
|
||||
+PrivateTmp=+ и т.д., которые основаны на запуске службы в
|
||||
отдельном пространстве имен монтирования.
|
||||
\item В вашем дистрибутиве пока нет свежих версий systemd: 232
|
||||
(поддержка +DynamicUser=+) или 235 (поддержка +StateDirectory=+
|
||||
и аналогичных ей опций).
|
||||
\item Правила создания пакетов для вашего дистрибутива не~разрешают
|
||||
подобный подход. Уточните эти моменты в правилах и, при
|
||||
необходимости, обсудите данный вопрос в рассылке вашего
|
||||
дистрибутива.
|
||||
\end{enumerate}
|
||||
|
||||
\subsectiona{Дополнительные замечания}
|
||||
|
||||
Еще несколько замечаний, непосредственно относящихся к обсуждаемой теме:
|
||||
\begin{enumerate}
|
||||
\item Обратите внимание, что процесс выделения и удаления динамических
|
||||
пользователей никак не~затрагивает +/etc/passwd+. Добавление
|
||||
пользователя в базу данных оусществляется при помощи NSS-модуля
|
||||
glibc
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/nss-systemd.html}%
|
||||
{nss-systemd}\footnote{Прим. перев.: Разумеется, чтобы
|
||||
преобразование <<имя-идентификатор>> для пользователя и его
|
||||
группы работало корректно, это модуль должен быть указан в
|
||||
строках +passwd:+ и +group:+ файла +/etc/nsswitch.conf+. Примеры
|
||||
приведены на странице руководства модуля.}, и эта информация
|
||||
никогда не~попадает на диск.
|
||||
\item В традиционных UNIX-системах, демоны сбрасывают привилегии с рута
|
||||
до обычного пользователя самостоятельно, в то время как механизм
|
||||
динамических пользователей предполагает, что этим должен
|
||||
заниматься systemd. В версии systemd 235 добавлена возможность
|
||||
совместить механизм динамических пользователей с самостоятельным
|
||||
сбросом привилегий процессом службы. Для этого, включите опцию
|
||||
+DynamicUser=+, а в опции
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#User=}%
|
||||
{User=} укажите имя пользователя, в которого ваша служба
|
||||
перевоплотится (+setuid()+) после инициализации. В результате, в
|
||||
момент запуска службы systemd создаст динамического пользователя
|
||||
с указанным именем. Далее, в директиве
|
||||
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.service.html\#ExecStart=}%
|
||||
{ExecStart=} непосредственно перед командой запуска укажите
|
||||
символ <<+!+>>. После этого, пользователь для службы будет
|
||||
создаваться, но запускаться она будет от рута~--- systemd
|
||||
будет считать, что служба сама сбросит полномочия в ходе
|
||||
инициализации. Например: +ExecStart=!/usr/bin/mydaemond+. При
|
||||
этом, регистрация соответствия имени и идентификатора
|
||||
пользователя в базе данных производится, как и прежде, при
|
||||
запуске службы, и поэтому процесс демона сможет без проблем
|
||||
преобразововать имя пользователя в UID.
|
||||
\item У вас может возникнуть вопрос: почему для динамического выделения
|
||||
UID выбран именно диапазон 61184--65519 (в шестнадцатеричной
|
||||
записи 0xEF00--0xFFEF)? Он был выбран потому, что большинство
|
||||
дистрибутивов (например, Fedora) используют для обычных
|
||||
пользователей идентификаторы ниже 60000, и мы не~хотим
|
||||
переступать эту границу. Также мы делаем небольшой отступ от
|
||||
65535, так как некоторые UID вблизи этого значения имеют
|
||||
специальное значение (65535 часто трактуется как
|
||||
<<некорректный>> или <<отсутствующий>> UID, так как является
|
||||
представлением числа $-1$ в 16-битном целом типе; 65534 обычно
|
||||
соответствует пользователю +nobody+, и некоторые подсистемы ядра
|
||||
отображают в это значение <<посторонние>>
|
||||
идентификаторы\footnote{Прим. перев.: Например, изнутри
|
||||
пространства имен пользователей, все пользователи хоста, кроме
|
||||
создателя пространства, выглядят как +nobody+.}). И наконец, мы
|
||||
не~хотим выходить за пределы 16-битного целого типа. Даже с
|
||||
распространением технологии пространств имен пользователей,
|
||||
контейнерам все равно не~нужен весь диапазон значений,
|
||||
предоставляемый 32-битным целым типом, который используется в
|
||||
ядре Linux для UID. Там не~менее, очевидно, что контейнеры
|
||||
должны поддерживать весь 16-битный диапазон~--- как минимум,
|
||||
из-за +nobody+. (Если честно, я считаю выделение 64 тысяч
|
||||
идентификаторов на контейнер оптимальным вариантом: верхние 16
|
||||
бит из 32-битного UID можно использовать как идентификатор
|
||||
контейнера, в том время как нижние будут соответствовать
|
||||
идентификатору пользователя в этом контейнере\ldots{} Надеюсь,
|
||||
вы не~потеряли нить рассуждений.) И, не~дожидаясь вашего
|
||||
вопроса: пока нет никакого способа изменить этот диапазон~---
|
||||
его границы заданы в исходном коде. Но когда-нибудь мы
|
||||
обязательно добавим соответствующие настройки.
|
||||
\item Вы можете поинтересоваться, что произойдет, если вы уже
|
||||
используете идентификаторы из диапазона 61184--65519 для других
|
||||
целей? systemd должен обработать такую ситуацию корректно, если
|
||||
эти идентификаторы зарегистрированы в базе даных пользователей:
|
||||
выбрав UID, systemd проверят, не~используется ли он кем-то, и
|
||||
если он занят, выбирает другой~--- до тех пор, пока не~найдет
|
||||
свободный. Проверка производится прежде всего при помощи
|
||||
функций NSS. Также просматриваются списки объектов IPC, и
|
||||
их владельцы проверяются на совпадение с нашим кандидатом. Таким
|
||||
образом, systemd избегает использования UID, занятых кем-то еще.
|
||||
Тем не~менее, это сокращает набор доступных идентификаторов, и в
|
||||
худшем случае выделение пользователя может завершиться ошибкой
|
||||
из-за отсутствия свободных UID в рабочем диапазоне.
|
||||
\item Если имя для выделяемого пользователя не~указано явно, systemd
|
||||
пытается вывести его из имени службы. Однако, далеко не~каждое
|
||||
корректное имя службы является также корректным именем
|
||||
пользователя, и чтобы обойти это, используется случайное имя.
|
||||
Возможно, вам будет удобнее задать имя пользователя вручную~---
|
||||
используйте для этого директиву +User=+.
|
||||
\item \label{itm:setuser}
|
||||
Если вы используете +User=+ в сочетании с +DynamicUser=on+, но
|
||||
пользователь с указанным именем уже существует, то для службы
|
||||
будет использован именно он, а механизм динамического выделения
|
||||
пользователя для этой службы автоматически отключится. Таким
|
||||
образом, упрощается переход между статическими и динамическими
|
||||
пользователями: вы указываете нужное вам имя в +User=+, и пока
|
||||
этот пользователь существует в системной базе, система будет
|
||||
использовать его, и лишь при отутствии такого пользователя он
|
||||
будет создаваться в динамическом режиме. Также это может быть
|
||||
полезно в других ситуациях, например, чтобы подготовить службы,
|
||||
использующие динамических пользователей, к возможности перехода
|
||||
на статические UID, скажем, чтобы применить к ним квоты файловой
|
||||
системы.
|
||||
\item systemd всегда выделяет вместе с пользователем еще и группу, с
|
||||
тем же самым значением идентификатора (UID = GID).
|
||||
\item Если бы ядро Linux имело механизм наподобие +shiftfs+, то есть
|
||||
способ смонтировать существующий каталог куда-либо с подменой
|
||||
UID/GID по некоторому правилу, задаваемому при монтировании, это
|
||||
значительно упростило бы реализацию работы +StateDirectory=+ в
|
||||
сочетании с +DynamicUser=+, в частности, позволив отказаться от
|
||||
рекурсивной смены владельца, и просто монтировать каталог с
|
||||
хоста в пространство имен гостя, подменив владельца каталога на
|
||||
UID/GID службы. Однако я не~питаю больших надежд на подобный
|
||||
вариант, так как все работы в этой области сейчас завязаны на
|
||||
пространство имен пользователей~--- механизм, который
|
||||
\emph{никак не~используется} в обсуждаемой технологии (есть
|
||||
мнение, что он создает гораздо больше проблем, чем решает, хотя
|
||||
вы можете с этим и не~согласиться).
|
||||
\end{enumerate}
|
||||
|
||||
На сегодня все!
|
||||
|
||||
\appendix
|
||||
|
||||
\section{FAQ (часто задаваемые вопросы)\sfnote{Перевод статьи
|
||||
|
||||
Reference in New Issue
Block a user