Пользователь
46,8
рейтинг
5 ноября 2015 в 17:11

Разработка → Изолируем демоны с 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.
@ValdikSS
карма
631,0
рейтинг 46,8
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (33)

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

    Недавно открыл для себя классную штуку — firejail. Умеет не только это, но и все остальное, перечисленное в статье + сетевые ограничения.
    • +1
      Да, про firejail я хочу написать следующую статью. Он очень мощный и отлично подходит, в том числе, для изоляции десктопных приложений.
      • 0
        Осваиваю firejail, но что он творит с сетевыми неймспейсами?

        Например запускаю для теста что-нибудь:
        firejail --net=eth0 bash


        Всё ок, сеть отдельно, IP адрес назначает отдельный. При этом:
        
        # ip netns list
        # ip netns identify 26957
        #
        


        Пусто!
        • 0
          ip netns identify [PID] — Report network namespaces names for process

          This command walks through /var/run/netns and finds all the network namespace names for network namespace of the specified
          process, if PID is not specified then the current process will be used.
          Никто не говорил, что информацию о namespace нужно обязательно писать в /var/run/netns. Это, в общем-то, только ip так и делает, больше никто.
          • 0
            Так как посмотреть-то извне контейнера? Пока нужна, в общем-то фича — посмотреть/поменять iptables изолированного процесса (внутри все пермишны порезаны).
            • +2
              # ln -s /proc/PID/net/ns /var/run/netns/firejail
              # ip netns exec firejail ip a
              • +2
                Есть еще nsenter:
                # nsenter -t PID -n
              • 0
                Чуть иначе:
                ln -s /proc/PID/ns/net /var/run/netns/firejail
                


                но это то, что надо! Спасибо.
  • +1
    Штука прекрасная, но скорее для admin-like или infrastructure-like процессов.

    Тот же docker дает прекрасные и управляемые среды для публикации приложений, клонирования.
    • +1
      Ну, если уж на то пошло, то в systemd есть свои средства для управления контейнерами, когда вам нужны именно контейнеры — systemd-run и machinectl.
      • 0
        Само собой. Скорее тут вопрос, что будет менее затратно.
      • +1
        systemd-nspawn*
  • +2
    Для не пользующих systemd есть готовые альтернативы. Упоминаемый выше firejail действительно хорош, но не единственный.
    И большой тред в тему на hacker news.
    • +5
      Ну, на самом деле, подобных вещей прямо дохрена. В этой статье я хотел использовать именно стандартные средства systemd, вторая, если кого-то заинтересует развитие темы, будет более общая.

      Есть еще такая классная страничка-сборник всего, подобная той, ссылку на которую вы предоставили: doger.io
      • 0
        За ссылочку спасибо.
        Если я не ошибаюсь, я с ее автором немного общался на реддите по поводу namespaces и python.
        Он уже 3 проект переписывает с нуля…
  • +2
    Я очень удивляюсь, когда говоря о плюсах systemd забывают упомянуть его возможности изоляции демонов. А что самое интересное — очень многие админы просто не знают о такой полезной фиче.
  • +1
    Может быть, тебе будет интересен porto — это такая система иерархических контейнеров, используемая в Яндексе, но заточненная в первую очередь не на изоляцию FS (как Docker), а на изоляцию других неймспейсов и использование cgroups. Тут вот ребята рассказали, зачем это полезно.
    • 0
      Спасибо, посмотрю.
  • 0
    Проблема знаете в чём? В том, что большинство разработчиков заявляют: «Это надо разбираться, документацию читать / админов просить. А там раз раз и готово».
    • +1
      Вопрос только — чья это проблема? Я всё же думаю, что systemd. За это многие и не любят его, что он такой сложный и многогранный. Но что делать, зато сколько возможностей)
      • 0
        Проблема того, кто потом всё разгребает
    • +3
      Ну, так-то демоны можно запускать и от имени пользователя, используя systemctl --user, и храня юниты в ~/.config/systemd/user/. Но, в целом, вы правы, тут горе от ума и нежелания читать документацию и осваивать новое. systemd очень многогранен, это и хорошо, и плохо, но я считаю, что это лучшее, что случалось с линуксом.
      • 0
        Просто с докером методология «xy*k-xy*k и в продакшн» намного проще реализуется и позволяет экономить на админах. В этом и для руководства и для разработчиков не first-class проектов есть серьёзное преимущество.

        > нежелания читать документацию и осваивать новое
        А тут, думаю, информационная интоксикация. Когда этот монструозный systemd осваивать, когда каждую неделю — новый фреймворк, который обещает помочь ещё быстрее «xy*k-xy*k и в продакшн»? :)
        • +1
          Так и каждый фреймворк нужно осваивать, а чтобы вообще дойти до его освоения, еще и выбрать нужно, что конкретно использовать!
          Вон, полюбуйтесь, сколько средств для управления докером уже понаделали!
          github.com/veggiemonk/awesome-docker#dev-tools
          stackoverflow.com/questions/18285212/how-to-scale-docker-containers-in-production

          Для меня в этом разобраться заметно сложнее, чем в systemd, а я из systemd использую далеко не только саму init-систему.
  • +1
    Для большинства приложений и такого хватит с головой:

    useradd  -s /bin/bash user
    passwd user
    


    /usr/lib/systemd/system/my-app.service:

    [Unit]
    Description=<Описание>
    
    [Service]
    Restart=always
    EnvironmentFile=-/home/user/env.txt
    WorkingDirectory=/home/user/app
    ExecStart=/home/user/app/myexe
    LimitNOFILE=131072
    LimitNPROC=131072
    User=user
    Group=user
    
    [Install]
    WantedBy=multi-user.target
    


    Это практически все, можно управлять этим сервисом:

    systemctl start my-app
    systemctl enable my-app # добавляем в автозагрузку
    
    • +3
      Нет, не хватит. Представьте, что у вас взломали такой сервис и получили возможность выполнения кода от пользователя user. Самое простое — они могут быстро, эффективно и достаточно долго (до тех пор, пока этого не заметят) перебирать пароль root, используя su, дампить памяти других процессов, запущенных от этого же пользователя (если приложение, например, использует fork, а взломать удалось только один из форков). Если вдруг у вас используется ядро с уязвимостью, позволяющее поднять привилегии, то их с большой вероятностью успешно поднимут.
      Различные namespaces и ограничения capabilities, как минимум, сильно усложнят задачу.

      В общем, если у вас сервис, который особо никому не нужен, то и так сойдет, но если у вас какой-нибудь Tor, который интересен и спецслужбам тоже, то этого явно недостаточно.
      • 0
        В общем, если у вас сервис, который особо никому не нужен, то и так сойдет

        Ну я же и написал — в большинстве случаев.
  • 0
    Разве эти задачи не должны решаться с помощью selinux? Буду очень признателен, если кто-нибудь объяснит, в чем разница между этими механизмами.
    • 0
      Эти задачи можно решить и с помощью selinux тоже, я думаю, но, вероятно, сложнее (я его ни разу всерьез не использовал).
      • 0
        Меня просто смутило, что мы даем приложению возможность открывать любой порт, а не только тот, который ему нужен. Мне кажется, что selinux гибче, но я им не пользовался вообще.
    • +2
      Я apparmor для ограничения skype использовал — в целом, интересная штука.
      Но нюанс в том, что selinux\apparmor\etc нужно понимать и настраивать по типу «что не разрешено — запрещено». А это значит, что нужно хорошо понимать ту программу, которую запускаешь.
      Контейнер же создать гораздо проще — прописал ограничения и используй себе…
      В идеале нужно использовать и то, и то. И большинство песочниц используют как ограничения cgroups & namespaces, так и capabilities & apparmor\selinux
    • +2
      Вы правы — SELinux создавался как раз именно для этих целей. SELinux — это FLASK. FLASK — это крупный проект, к которому очень плотно приложились специалисты одной небезызвестной организации. Он изначально разрабатывался с оглядкой на требования TCSEC, поэтому на выходе получился хоть и очень гибкий и секьюрный инструмент, но достаточно запутанный для неподготовленного админа. Конечно, над его юзабилити непрерывно работают и настроить его под типовое применение с targerted policy не сложнее AppArmor или того же systemd (я про возможности изоляции), однако, если Вам вдруг захочется MLS\MCS — добро пожаловать в чудный мир SELinux. Мир, в котором без бутылки не разберешься :)

      А вообще штука хорошая, да. И с ним стоит подружиться.
  • 0
    Довольно интересная статья, спасибо. Попробую завтра реализовать

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