В последнее время я вижу, как довольно большое количество людей применяет контейнерную виртуализацию только для того, чтобы запереть потенциально небезопасное приложение внутри контейнера. Как правило, используют для этого 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.