Изолируем демоны с systemd или «вам не нужен Docker для этого!»

В последнее время я вижу, как довольно большое количество людей применяет контейнерную виртуализацию только для того, чтобы запереть потенциально небезопасное приложение внутри контейнера. Как правило, используют для этого Docker из-за его распространенности, и не знают ничего лучше. Действительно, многие демоны первоначально запускаются от имени root, а далее либо понижают свои привилегии, либо master-процесс порождает обрабатывающие процессы с пониженными привилегиями. А есть и такие, которые работают исключительно от root. Если в демоне обнаружат уязвимость, которая позволяет получить доступ с максимальными привилегиями, будет не очень приятно обнаружить злоумышленников, уже успевших скачать все данные и оставить вирусов.

Контейнеризация, предоставляемая Docker и другим подобным ПО, действительно спасает от этой проблемы, но также и привносит новые: необходимо создавать контейнер для каждого демона, заботиться о сохранности измененных файлов, обновлять базовый образ, да и сами контейнеры часто основаны на разных ОС, которые необходимо хранить на диске, хотя они вам, в общем-то, и не особо нужны. Что делать, если вам не нужны контейнеры как таковые, в Docker Hub приложение собрано не так, как нужно вам, да и версия устарела, SELinux и AppArmor кажутся вам слишком сложными, а вам бы хотелось запускать его в вашем окружении, но используя такую же изоляцию, которую использует Docker?

Capabilities


В чем отличие обычного пользователя от root? Почему root может управлять сетью, загружать модули ядра, монтировать файловые системы, убивать процессы любых пользователей, а обычный пользователь лишен таких возможностей? Все дело в capabilities — средстве для управления привилегиями. Все эти привилегии даются пользователю с UID 0 (т.е. root) по умолчанию, а у обычного пользователя нет ни одного из них. Привилегии можно как дать, так и отобрать. Так, например, привычная команда ping требует создания RAW-сокета, что невозможно сделать от имени обычного пользователя. Исторически, на ping ставили SUID-флаг, который просто запускал программу от имени суперпользователя, но сейчас все современные дистрибутивы выставляют CAP_NET_RAW capability, которая позволяет запускать ping из-под любого аккаунта.

Получить список установленных capabilities файла можно командой getcap из состава libcap.

% getcap $(which ping)
/usr/bin/ping = cap_net_raw+ep


Флаг p здесь означает permitted, т.е. у приложения есть возможность использовать заданную capability, e значит effective — приложение будет ее использовать, и есть еще флаг iinheritable, что дает возможность сохранять список capabilities при вызове функции execve().

Capabilities можно задать как на уровне ФС, так и просто у отдельного потока программы. Получить capability, которая не была доступна с момента запуска, нельзя, т.е. привилегии можно только понижать, но не повышать.

Также существуют биты безопасности (Secure Bits), их три: KEEP_CAPS позволяет сохранить capability при вызове setuid, NO_SETUID_FIXUP отключает перенастройку capability при вызове setuid, и NOROOT запрещает выдачу дополнительных привилегий при запуске suid-программ.

Namespaces


Возможность поместить приложение в свои namespaces (пространства имен) — еще одна возможность ядра Linux. Отдельные пространства имен могут быть заданы для:

  • Файловой системы
  • UTS (имя хоста)
  • System V IPC (межпроцессорное взаимодействие)
  • Сети
  • PID
  • Пользователей


Если мы поместим приложение, например, в отдельное сетевое пространство, оно не сможет увидеть наши сетевые адаптеры, которые видны с хоста. То же самое можно проделать и с файловой системой.

systemd


К счастью, systemd поддерживает все необходимое для изоляции приложений и разграничения прав.

Эти возможности мы и будем использовать, но сначала немного подумаем над тем, какие права нужны нашему приложению.

Итак, какие бывают демоны? Есть те, которым права суперпользователя в целом не требуются, а используют они их лишь для того, чтобы слушать порт ниже 1024. Таким программам достаточно выдать capability CAP_NET_BIND_SERVICE, который позволит им слушать любые порты без ограничений, и сразу запускать их от непривилегированного пользователя. Установить capability на файл можно командой setcap. В качестве подопытного «сервиса» у нас будет ncat из состава nmap, который будет выдавать shell-доступ любому желающему — хуже не придумаешь:

% sudo setcap CAP_NET_BIND_SERVICE=ep /usr/bin/ncat
% getcap /usr/bin/ncat
/usr/bin/ncat = cap_net_bind_service+ep


Теперь пишем простейший systemd unit, который будет запускать ncat с необходимыми параметрами на порту 81 от имени пользователя nobody:

[Unit]
Description=Vuln

[Service]
User=nobody
ExecStart=/usr/bin/ncat --exec /bin/bash -l 81 --keep-open --allow ::1


Сохраняем его в /etc/systemd/system/vuln.service и запускаем привычным sudo systemctl start vuln.

Подключаемся к нему:

% ncat ::1 81
whoami
nobody


Работает, отлично!

Настало время защищать наш сервис, для этого у systemd есть следующие директивы:

  • CapabilityBoundingSet= — управляет capabilities. Устанавливает только те, что были переданы в этом параметре, или наоборот, забирает переданные, если перед первым стоит символ тильда "~".
  • SecureBits= — задает биты безопасности.
  • Capabilities= — тоже управляет capabilities, но таким образом, что преимущество имеют capabilities, прописанные в файле на уровне ФС, так что практически бесполезно.
  • ReadWriteDirectories=, ReadOnlyDirectories=, InaccessibleDirectories= — управляют пространством имен файловой системы. Перемонтируют ФС внутри пространства имен демона таким образом, что заданные директории доступны для чтения и записи, только для чтения, либо вообще недоступны (становятся пустыми).
  • PrivateTmp= — перемонтирует /tmp и /var/tmp в свои собственные tmpfs внутри namespace.
  • PrivateDevices= — отбирает доступ к устройствам из /dev, оставляя доступ только к стандартным устройствам, вроде /dev/null, /dev/zero, /dev/random и прочим.
  • PrivateNetwork= — создает пустое сетевое пространство имен с одним интерфейсом lo.
  • ProtectSystem= — монтирует /usr и /boot в режим только для чтения, а при передаче аргумента «full», делает то же самое еще и с /etc.
  • ProtectHome= — делает недоступными директории /home, /root и /run/user, либо перемонтирует их в режим только для чтения с параметром «read-only»
  • NoNewPrivileges= — позволяет удостовериться, что приложение не получит дополнительных привилегий. По заявлениям авторов, более мощна, чем соответствующая capability.
  • SystemCallFilter= — фильтрует системные вызовы с использованием технологии seccomp. Об этом чуть позже.


Давайте перепишем наш unit-файл с применением этих опций:

[Unit]
Description=Vuln

[Service]
User=nobody
ExecStart=/usr/bin/ncat --exec /bin/bash -l 81 --keep-open --allow ::1
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
InaccessibleDirectories=/sys
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=full


Итак, мы выдали нашему приложению одно capability CAP_NET_BIND_SERVICE, создали отдельные /tmp и /var/tmp, отобрали доступ к устройствам и домашним директориям, перемонтировали /usr, /boot и /etc в режим только для чтения, и отдельно заблокировали /sys, т.к. типичный демон туда вряд ли полезет, а все это выполняется от имени пользователя.

Следует отметить, что CapabilityBoundingSet не дает заполучить дополнительные capabilities даже suid-приложениям вроде su или sudo, поэтому мы не сможем получить доступ от имени другого пользователя или рута, даже зная их пароли, т.к. ядро не даст выполнить вызовы setuid и setgid:

% ncat ::1 81           
python -c 'import pty; pty.spawn("/bin/bash")'   # создает новый pty, без него не получится использовать sudo или su
[nobody@valaptop /]$ sudo -i    # запрет setuid() и setgid()
sudo: unable to change to root gid: Operation not permitted
sudo: unable to initialize policy plugin
[nobody@valaptop /]$ ping   # запрет получения capability cap_net_raw
bash: /usr/sbin/ping: Operation not permitted
[nobody@valaptop /]$ cd /home
bash: cd: /home: Permission denied
[nobody@valaptop /]$ ls -lad /home
d--------- 2 root root 40 Nov  3 11:46 /home
[nobody@valaptop tmp]$ ls -la /tmp
total 4
drwxrwxrwt  2 root root   40 Nov  5 00:31 .
drwxr-xr-x 19 root root 4096 Nov  3 22:28 ..


Рассмотрим второй тип демонов, те, которые запускаются от root и понижают свои привилегии. Такой подход используется для многих целей: считывание конфиденциальных файлов, которые доступны только от суперпользователя (например, приватного ключа для использования TLS веб-сервером), ведение логов, которые не будут доступны в случае компрометации не-root форка, и просто приложения, которые произвольно меняют UID (ssh-серверы, ftp-серверы). Если такие программы не изолировать, то самое страшное, что может случиться — злоумышленник получит полный доступ от имени суперпользователя. Хоть и отсутствие сapabilities, присущих root, делают из него практически обычного непривилегированного пользователя, root все равно остается root'ом с кучей файлов, принадлежащих ему, которые он может читать, поэтому нам нужно дополнительно убедиться в недоступности отдельных директорий, где могут храниться ключи и конфигурационные файлы, которые не должны быть прочитаны:

[Unit]
Description=Vuln

[Service]
ExecStart=/usr/bin/ncat --exec /bin/bash -l 81 --keep-open --allow ::1
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID
NoNewPrivileges=yes
InaccessibleDirectories=/sys
InaccessibleDirectories=/etc/openvpn
InaccessibleDirectories=/etc/strongswan
InaccessibleDirectories=/etc/nginx
ReadOnlyDirectories=/proc
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=full


Здесь мы добавили capability CAP_SETUID и CAP_SETGID для того, чтобы наш демон мог понижать привилегии, использовали NoNewPrivileges, чтобы он не мог повысить себе capabilities, заблокировали доступ к директориям, которые он читать не должен, и разрешили доступ к /proc только на чтение, чтобы нельзя было использовать sysctl. Можно также монтировать сразу весь корень в read-only, а права на запись давать только в те директории, которые использует программа.

Следует отдельно убедиться в правах доступа к файлу /etc/shadow. В современных дистрибутивах он не доступен на чтение даже для root, а для работы с ним применяется capability CAP_DAC_OVERRIDE, которая позволяет игнорировать права доступа.

% ls -la /etc/shadow
---------- 1 root root 1214 ноя  3 19:57 /etc/shadow


Проверяем наши настройки!

python -c 'import pty; pty.spawn("/bin/bash")'   # создает новый pty
[root@valaptop /]# whoami
root
[root@valaptop /]# ping   # запрет получения capability cap_net_raw
bash: /usr/sbin/ping: Operation not permitted
[root@valaptop /]# cat /etc/shadow   # нет CAP_DAC_OVERRIDE
cat: /etc/shadow: Permission denied
[root@valaptop /]# cd /etc/openvpn
bash: cd: /etc/openvpn: Permission denied
[root@valaptop /]# /suid   # SUID shell
[root@valaptop /]# cat /etc/shadow   # уже из-под нового shell, прав не прибавилось
cat: /etc/shadow: Permission denied


К сожалению, systemd (пока) не умеет работать с PID namespace, так что наш root-демон может убивать остальные программы, выполняющиеся из-под root.

В целом, на этом можно и закончить, capabilities и настройки пространств имен хорошо выполняют свою работу по изоляции приложений, но есть еще одна вещь, которую было здорово бы настроить.

seccomp


Технология seccomp запрещает программе выполнять определенные системные вызовы, сразу убивая ее при попытке это сделать. Хоть seccomp появился давно, в 2005 году, по-настоящему использовать его стали сравнительно недавно, с выпуском Chrome 20, vsftpd 3.0 и OpenSSH 6.0.

Существует два подхода к использованию seccomp: черный список и белый список. Составить черный список потенциально опасных вызовов заметно проще белого, поэтому этот подход используют чаще. Проект firejail по умолчанию запрещает выполнять программам следующие syscall'ы (тильда включает режим черного списка):

SystemCallFilter=~mount umount2 ptrace kexec_load open_by_handle_at init_module \
finit_module delete_module iopl ioperm swapon swapoff \
syslog process_vm_readv process_vm_writev \
sysfs_sysctl adjtimex clock_adjtime lookup_dcookie \
perf_event_open fanotify_init kcmp add_key request_key \
keyctl uselib acct modify_ldt pivot_root io_setup \
io_destroy io_getevents io_submit io_cancel \
remap_file_pages mbind get_mempolicy set_mempolicy \
migrate_pages move_pages vmsplice perf_event_open


В systemd до версии 227 включительно имеется баг, который требует установку NoNewPrivileges=true для использования seccomp.

Белый список можно составить следующим образом:

  1. Запускаем требуемую программу под strace:
    % strace -qcf nginx
    

    Получаем большую таблицу syscall'ов:
     time     seconds  usecs/call     calls    errors syscall
    ------ ----------- ----------- --------- --------- ----------------
      0.00    0.000000           0        24           read
      0.00    0.000000           0        27           open
      0.00    0.000000           0        32           close
      0.00    0.000000           0         6           stat
    …
      0.00    0.000000           0         1           set_tid_address
      0.00    0.000000           0         4           epoll_ctl
      0.00    0.000000           0         3           set_robust_list
      0.00    0.000000           0         2           eventfd2
    

  2. Переписываем их все, устанавливаем в качестве SystemCallFilter. Скорее всего, ваше приложение упадет, т.к. strace нашел не все вызовы. Смотрим, при выполнении какого вызова приложение завершилось, в логах демона audit:
    type=SECCOMP msg=audit(1446730375.597:7943724): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=11915 comm="(nginx)" exe="/usr/lib/systemd/systemd" sig=31 arch=40000003 syscall=191 compat=0 ip=0xb75e5be8 code=0x0
    
    Номер нужного нам syscall — 191. Открываем таблицу вызовов и ищем название этого вызова по номеру.
  3. Добавляем его в разрешенные вызовы. В случае падения, возвращаемся к пункту 2.


Tips & Tricks


Проверить текущие привилегии и возможность их повышения можно командой captest.

filecap выведет вам список файлов с установленными capabilities.

С помощью netcap можно получить список запущенных сетевых программ, имеющих хотя бы один сокет и одну capability, а pscap выведет не только сетевое запущенное ПО.

Не обязательно целиком редактировать systemd unit и отслеживать его изменения при обновлении, а лучше добавить необходимые директивы через systemctl edit.

© Habrahabr.ru