В двух словах о привилегиях Linux (capabilities)

Автор оригинала: SawАвтор — http://k3a.me
  • Перевод
Перевод статьи подготовлен специально для студентов курса «Администратор Linux».


Привилегии (capabilities) используются всё больше и больше во многом благодаря SystemD, Docker и оркестраторам, таким как Kubernetes. Но, как мне кажется, документация немного сложна для понимания и некоторые части реализации привилегий для меня оказались несколько запутанными, поэтому я и решил поделиться своими текущими знаниями в этой короткой статье.



Самая важная ссылка по привилегиям — это man-страница capabilities(7). Но она не очень хорошо подходит для первоначального знакомства.

Разрешения процессов (process capabilities)


Права обычных пользователей очень ограничены, в то время как права пользователя “root” очень обширны. Хотя процессам, запущенным под «root», часто не требуются все полномочия root.

Для уменьшения полномочий пользователя “root” разрешения POSIX (POSIX capabilities) предоставляют способ ограничить группы привилегированных системных операций, которые разрешено выполнять процессу и его потомками. По сути, они делят все «root»-права на набор отдельных привилегий. Идея capabilities была описана в 1997 году в черновике POSIX 1003.1e.

В Linux каждый процесс (задача) имеет пять 64-битных чисел (наборов), содержащих биты разрешений (до Linux 2.6.25 они были 32-битными), которые можно посмотреть в
/proc/<pid>/status
.

CapInh: 00000000000004c0
CapPrm: 00000000000004c0
CapEff: 00000000000004c0
CapBnd: 00000000000004c0
CapAmb: 0000000000000000

Эти числа (здесь показаны в шестнадцатеричной системе счисления) представляют собой битовые карты, в которых представлены наборы разрешений. Вот их полные имена:

  • Inheritable (наследуемые) — разрешения, которые могут наследовать потомки
  • Permitted (доступные) — разрешения, которые могут использоваться задачей
  • Effective (текущие, эффективные) — текущие действующие разрешения
  • Bounding (ограничивающий набор) — до Linux 2.6.25 ограничивающий набор был общесистемным атрибутом, общим для всех потоков, предназначенным для описания набора, за пределы которого разрешения расширяться не могут. В настоящее время это набор для каждой задачи и является лишь частью логики execve, подробности далее.
  • Ambient (наружные, начиная с Linux 4.3) — добавлены, чтобы легче предоставлять разрешения не-root пользователю, без использования setuid или файловых разрешений (подробности позже).

Если задача запрашивает выполнение привилегированной операции (например, привязку к портам < 1024), то ядро ​​проверяет действующий ограничивающий набор на наличие CAP_NET_BIND_SERVICE. Если он установлен, то операция продолжается. В противном случае операция отклоняется с EPERM (операция не разрешена). Эти CAP_-определены в исходном коде ядра и нумеруются последовательно, поэтому CAP_NET_BIND_SERVICE, равный 10, означает бит 1 << 10 = 0x400 (это шестнадцатеричная цифра “4” в моем предыдущем примере).

Полный человекочитаемый список полномочий, определенных на данный момент, можно найти в актуальной man-странице capabilities(7) (приведенный здесь список только для справки).

Кроме того, есть библиотека libcap для упрощения управления и проверки полномочий. В дополнение к API библиотеки в пакет входит утилита capsh, которая, помимо прочего, позволяет показать свои полномочия.

# capsh --print
Current: = cap_setgid,cap_setuid,cap_net_bind_service+eip
Bounding set = cap_setgid,cap_setuid,cap_net_bind_service
Ambient set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Здесь есть несколько запутанных моментов:

  • Current — отображает эффективные, наследуемые и доступные привилегии процесса capsh в формате cap_to_text(3). В этом формате права перечислены как группы разрешений “capability[,capability…]+(e|i|p)”, где “e” означает эффективные, “i” наследуемые и “p” доступные. Список не разделен символом “,”, как вы могли догадаться (cap_setgid+eip, cap_setuid+eip). Запятая разделяет разрешения в одной группе действий. Фактический список групп действий затем разделяется пробелами. Другим примером с двумя группами действий будет “= cap_sys_chroot+ep cap_net_bind_service+eip”. А также следующие две группы действий “= cap_net_bind_service+e cap_net_bind_service+ip” будут кодировать то же значение, что и одна “cap_net_bind_service+eip”.
  • Bounding set/Ambient set. Чтобы еще больше запутать, эти две строки содержат только список разрешений, заданных в этих наборах, разделенных пробелами. Здесь не используется формат cap_to_text, потому что он не содержит наборы доступных, эффективных и наследуемых разрешений, а только один (bounding/ambient) набор.
  • Securebits: отображает флаги securebits задачи в виде десятичных / шестнадцатеричных / в формате Verilog (да, все здесь ожидают, и это совершенно ясно по 'b, что каждый системный администратор программирует собственные FPGA и ASIC). Далее следует состояние securebits. Фактические флаги определены как SECBIT_* в securebits.h, а также описаны в capabilities(7).
  • Этой утилите не хватает отображения информации “NoNewPrivs”, которую можно посмотреть в
    /proc/<pid>/status
    . Она упоминается только в prctl(2), хотя и влияет напрямую на права при использовании вместе с файловыми разрешениями (более подробно далее). NoNewPrivs описывается следующим образом: “При значении no_new_privs, равном 1, execve(2) обещает не предоставлять привилегий на то, что не могло бы быть сделано без вызова execve(2) (например, обработка битов set-user-ID, set-group-ID и отключение обработки файловых разрешений). После установки атрибут no_new_privs не может быть сброшен. Значение этого атрибута наследуется потомками, созданными через fork(2) и clone(2), и сохраняется через execve(2).”. Kubernetes устанавливает этот флаг в 1, когда allowPrivilegeEscalation имеет значение false в pod securityContext.


При запуске нового процесса через execve(2), полномочия для дочернего процесса преобразуются с использованием формулы, указанной в capabilities(7):

P'(ambient)     = (file is privileged) ? 0 : P(ambient)

P'(permitted)   = (P(inheritable) & F(inheritable)) |
                  (F(permitted) & P(bounding)) | P'(ambient)

P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

P'(inheritable) = P(inheritable)    [i.e., unchanged]

P'(bounding)    = P(bounding)       [i.e., unchanged]

where:

     P()   denotes the value of a thread capability set before the
           execve(2) - значение набора разрешений потока до execve(2)

     P'()  denotes the value of a thread capability set after the
           execve(2) - значение набора разрешений потока после execve(2)

     F()   denotes a file capability set - файловые разрешения


Эти правила описывают действия, выполняемые для каждого бита во всех наборах разрешений (ambient/permitted/effective/inheritable/bounding). Используется стандартный синтаксис языка Си (& — для логического И, | — для логического ИЛИ). P’ — это дочерний процесс. P — текущий процесс, вызывающий execve(2). F — это, так называемые, “файловые разрешения” у файла, запущенного через execve.

Кроме того, процесс может программно изменить свои наследуемые, доступные и эффективные наборы с помощью libcap в любое время в соответствии со следующими правилами:

  • Если вызывающая сторона не имеет CAP_SETPCAP, новый наследуемый набор должен быть подмножеством P(наследуемый) & P(доступный)
  • (с Linux 2.6.25) Новый наследуемый набор должен быть подмножеством P(наследуемый) & P(ограничивающий)
  • Новый доступный набор должен быть подмножеством P(доступный)
  • Новый эффективный набор должен быть подмножеством P(эффективный)


Разрешения файлов (file capabilities)


Иногда пользователю с ограниченным набором прав необходимо запустить файл, который требует больше полномочий. Ранее это достигалось установкой бита setuid (chmod + s ./executable) в бинарном файле. Такой файл, если он принадлежит root, будет иметь полные права root при выполнении любым пользователем.

Но этот механизм предоставляет слишком много привилегий файлу, поэтому POSIX-разрешения реализовали концепцию, называемую “файловые разрешения”. Они хранятся в виде расширенного атрибута файла, называемого “security.capability”, поэтому вам нужна файловая система с поддержкой расширенных атрибутов (ext*, XFS, Raiserfs, Brtfs, overlay2, …). Для изменения этого атрибута необходимо разрешение CAP_SETFCAP (в доступном наборе разрешений процесса).

$ getfattr -m - -d `which ping`
# file: usr/bin/ping
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=

$ getcap `which ping`  
/usr/bin/ping = cap_net_raw+ep


Особенные случаи и замечания


Конечно, в реальности все не так просто, и есть несколько особых случаев, описанных в man-странице capabilities(7). Наверное, самыми важными из них являются:

  • Бит setuid и файловые разрешения игнорируются, если установлен NoNewPrivs или файловая система смонтирована с nosuid или процесс, вызывающий execve трассируется ptrace. Файловые разрешения также игнорируются, когда ядро ​​загружается с опцией no_file_caps.
  • “Глупый” файл (capability-dumb) — это бинарный файл, преобразованный из setuid-файла в файл с файловыми разрешениями, но без изменения его исходного кода. Такие файлы часто получаются путем установки на них разрешений +ep, например “setcap cap_net_bind_service+ep ./binary”. Важной частью является “е” — эффективный. После execve эти разрешения добавятся как к доступным, так и к действующим, поэтому исполняемый файл будет готов использовать привилегированную операцию. Напротив, ”умный” файл (capability-smart), который использует libcap или аналогичную функциональность, может использовать cap_set_proc(3) (или capset) для установки “эффективных” или “наследуемых” битов в любой момент, если это разрешение уже находится в ”доступном” наборе. Поэтому “setcap cap_net_bind_service+p ./binary” будет достаточно для “умного” файла, поскольку он сам сможет установить необходимые разрешения в эффективном наборе перед вызовом привилегированной операции. Смотрите пример кода.
  • Файлы с setuid-root продолжают работать, предоставляя все привилегии root при запуске не root пользователем. Но если у них установлены файловые разрешения, то будут предоставлены только они. Также можно создать setuid-файл с пустым набором разрешений, что сделает его выполнение под пользователем с UID 0 без каких-либо полномочий. Есть особые случаи для пользователя root при запуске файла с setuid-root и установки различных флагов securebits (см. man).
  • Ограничивающий набор (bounding set) маскирует доступные разрешения, но не наследуемые. Помните P '(доступные) = F (доступные) & P (ограничивающие). Если у потока есть разрешение в своем наследуемом наборе, которое не находится в его ограничивающем наборе, тогда он все еще может получить это разрешение в своем доступном наборе, запустив файл, который имеет разрешение в своем наследуемом наборе — P '(доступный) = P (наследуемый) & F (наследуемый).
  • Выполнение программы, которая изменяет UID или GID через биты set-user-ID, set-group-ID, или выполнение программы, для которой установлены какие-либо файловые разрешения, очистит окружающий набор (ambient set). Разрешения добавляются в окружающий набор, используя PR_CAP_AMBIENT prctl. Эти разрешения уже должны присутствовать как в доступных, так и в наследуемых наборах процесса.
  • Если процесс с UID, отличным от 0, выполняет execve(2), то все права, присутствующие в его доступных и действующих наборах, будут удалены.
  • Если не установлен SECBIT_KEEP_CAPS (или более широкий SECBIT_NO_SETUID_FIXUP), изменение UID с 0 на ненулевой удаляет все разрешения из наследуемого, доступного и эффективного наборов.


Итак…


Если официальный контейнер nginx, ingress-nginx или ваш собственный останавливается или перезапускается с ошибкой:

bind() to 0.0.0.0:80 failed (13: Permission denied)

… это означает, что была попытка слушать порт 80 под непривилегированным (не 0) пользователем, и в наборе текущих разрешений не было CAP_NET_BIND_SERVICE. Для получения этих прав необходимо использовать xattr и установить (с помощью setcap) для файла nginx разрешение, как минимум, cap_net_bind_service+ie. Это файловое разрешение будет объединено с унаследованным набором (заданным вместе с ограничивающим набором из pod SecurityContext/capability/add/NET_BIND_SERVICE), и также размещено в наборе доступных разрешений. В результате получится cap_net_bind_service+pie.

Это все работает до тех пор, пока securityContext/allowPrivilegeEscalation установлен в true и storage-драйвер docker/rkt (см. документацию docker) поддерживает xattrs.

Если бы nginx был умен по отношению к полномочиям, то cap_net_bind_service+i было бы достаточно. Затем он мог бы использовать libcap для расширения прав от доступного набора до эффективного. Получив в результате cap_net_bind_service+pie.

Помимо использования xattr, единственным способом получения cap_net_bind_service в не-root контейнере — это позволить Docker установить внешние разрешения (ambient capabilities). Но по состоянию на апрель 2019, это еще не реализовано.

Примеры кода


Здесь пример кода с использованием libcap для добавления CAP_NET_BIND_SERVICE в эффективный набор разрешений. Он требует наличия разрешения CAP_BIND_SERVICE+p для бинарного файла.

Ссылки (анг.):

OTUS. Онлайн-образование
583,54
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое