Представьте, что у вас есть две части рабочей нагрузки, которые не должны мешать друг другу. Один из вариантов — изолировать их так, чтобы они не знали о существовании друг друга; фактически именно это делают кон- тейнеры и виртуальные машины. Другой подход — ограничить доступные им действия, поэтому даже если одна из них знает о существовании другой, то просто ничем не может повлиять на нее. Изоляция приложений таким образом, что им доступна лишь часть ресурсов, называется помещением в «песочницу» (sandboxing). Контейнер, в котором запускается приложение, играет, по сути, роль «пе- сочницы». При каждом запуске контейнера известно, какой код приложения будет выполняться внутри него. В случае взлома приложения злоумыш- ленник может попытаться запустить код, поведение которого выходит за нормальные для этого приложения рамки. Механизм «песочницы» по- зволяет ограничить разрешаемые данному коду действия, таким образом ограничивая возможности злоумышленника повлиять на систему. Сначала мы рассмотрим механизм seccomp. ### Механизм seccomp Seccomp — механизм ограничения набора системных вызовов, разрешенных приложению. Вначале, после появления в ядре Linux в 2005 году, seccomp (сокращение от secure computing mode — «безопасный режим вычислений») означал, что переведенный в этот режим процесс может выполнять только несколько системных вызовов: * sigreturn (возврат из обработчика сигнала); ‰* exit (завершение выполнения процесса); ‰* read и write, но лишь при использовании дескрипторов файлов, уже от- крытых до перехода в безопасный режим. Ненадежный код можно спокойно выполнять в этом режиме, он не сможет совершить никаких вредоносных действий. К сожалению, есть побочный эф- фект — огромное количество кода тоже не сможет сделать ничего полезного в этом режиме. Такая «песочница» попросту слишком сильно ограничена. В 2012 году в ядро был добавлен новый подход, seccomp BFP, определяющий допустимость конкретного системного вызова на основе фильтров пакетов Беркли (Berkeley Packet Filters, BPF) с учетом профиля seccomp данного процесса. У каждого процесса может быть свой профиль. Фильтр seccomp BPF учитывает при решении о допустимости вызова данным профилем код операции системного вызова и его параметры. На самом деле все несколько сложнее: профиль указывает, что делать, если системный вызов соответствует конкретному фильтру, причем возможные действия включают возвращение ошибки, завершение процесса и вызов средства трассировки. Но в большинстве сценариев в мире контейнеров профиль либо разрешает вызов, либо возвращает ошибку, поэтому его можно считать своего рода белым списком (или черным списком) системных вызовов. Все вышеописанное может весьма пригодиться в мире контейнеров, посколь- ку в действительности некоторые системные вызовы контейнеризованному приложению выполнять незачем, разве что в исключительно редких ситуа- циях. Например, нежелательно, чтобы контейнеризованное приложение могло менять системное время на хост-компьютере, поэтому имеет смысл запретить ему доступ к системным вызовам clock_adjtime и clock_settime. Маловероятно, что контейнерам понадобится менять модули ядра, и потому им не нужно вызывать create_module, delete_module или init_module. В ядре Linux существует сервис keyring, не ограниченный пространством имен, вследствие чего лучше запретить контейнерам обращаться к request_key и keyctl. Возможно, вы захотите ограничить контейнер еще меньшим кругом си- стемных вызовов — в идеальном мире у каждого приложения был бы свой профиль, разрешающий ему только необходимый набор системных вы- зовов. Существует несколько различных подходов к созданию подобных профилей. ‰* Можно воспользоваться strace для трассировки всех системных вызовов, выполняемых приложением. * Более современный способ получить список системных вызовов — вос- пользоваться вспомогательной процедурой на основе eBPF. А поскольку seccomp ограничивает набор разрешенных системных вызовов с помощью BPF, то нет ничего удивительного в том, что eBPF (extended BPF — рас- ширенный BPF) позволяет получить список используемых системных вызовов. Для получения списка генерируемых контейнером системных вызовов можно применить такие утилиты, как falco2seccomp. Если вы не хотели бы тратить усилия на создание профилей seccomp са- мостоятельно, то, возможно, вам имеет смысл взглянуть на коммерческие утилиты, призванные обеспечить безопасность контейнеров. Некоторые из этих утилит умеют автоматически генерировать пользовательские профили seccomp на основе наблюдений за отдельными рабочими заданиями. ### Модуль AppArmor AppArmor (сокращение от Application Armor — «броня приложения») — один из нескольких модулей безопасности Linux (Linux security modules, LSM), которые можно включить в ядре Linux. В AppArmor профиль можно связать с исполняемым файлом и описать, что этому файлу можно, а что — нет, на языке привилегий и прав доступа файла. Напомню, что о них мы говорили в главе 2. Чтобы проверить, включен ли модуль AppArmor в вашем ядре, по- смотрите на файл /sys/module/apparmor/parameters/enabled — если найдете там y, значит, включен. AppArmor и прочие LSM реализуют мандатное управление доступом (mandatory access control)1. Оно настраивается главным администратором, после чего остальные пользователи не могут никоим образом изменить права доступа или передавать их другим пользователям. Управление же доступом в Linux является избирательным (discretionary) в том смысле, что если моя учетная запись пользователя является владельцем файла, то я могу предоставить доступ к нему другому пользователю (если эта возмож- ность не переопределена с помощью мандатного управления доступом) или запретить даже моей собственной учетной записи пользователя что-либо записывать в этот файл, чтобы случайно не испортить что-то. Благодаря мандатному управлению доступом администратор может гораздо тоньше контролировать происходящее в системе, причем отдельные пользователи не смогут переопределить заданные им настройки. AppArmor включает режим «претензий», при котором можно проверить исполняемый файл на соответствие профилю с занесением в журнал всех расхождений. Смысл в том, что можно обновлять профиль на основе этих журналов, чтобы в конечном итоге добиться отсутствия новых расхождений, после чего можно начинать обеспечивать соблюдение профиля. Создавать профили AppArmor для контейнеров можно с помощью bane (https://oreil.ly/Xe7YZ). Созданный профиль можно установить в каталог /etc/apparmor и запустить утилиту apparmor_parser для его загрузки. Просмотреть загруженные про- фили можно в /sys/kernel/security/apparmor/profiles. Если запустить контейнер с помощью команды docker run --security- opt="apparmor:<имя профиля>" ..., то его поведение будет ограничено вариан- тами, разрешенными профилем. Среды выполнения контейнеров Containerd и CRI-O также поддерживают AppArmor. ### Модуль SELinux SELinux (Security-Enhanced Linux — Linux с улучшенной безопасностью) — еще один тип LSM. SELinux позволяет ограничивать доступные процессам действия в смысле взаимодействий с файлами и прочими процессами. Каждый процесс вы- полняется в каком-либо домене SELinux (можете считать его своего рода контекстом работы процесса), а у каждого файла есть свой тип. Просмотреть информацию SELinux для файла можно с помощью команды ls -lZ; вдобавок вы можете по аналогии добавить -Z к команде ps, чтобы получить подробную информацию для процессов, связанную с SELinux. Ключевое отличие между правами доступа SELinux и обычными права- ми доступа DAC Linux (которые вы видели в главе 2) состоит в том, что в SELinux права никак не связаны с учетной записью пользователя — они описываются исключительно метками. Тем не менее эти системы прав делают одно дело, и потому любое действие должно быть разрешено как DAC, так SELinux. Чтобы обеспечивать реализацию стратегий, необходимо сначала марки- ровать все файлы на машине метками SELinux. Эти стратегии указывают права доступа процесса конкретного домена к файлам конкретного типа. На практике это означает следующее: можно сделать так, что приложение будет иметь доступ только к собственным файлам, а у остальных процес- сов никакого доступа к ним не будет. Взлом приложения в этом случае повлияет лишь на ограниченное множество файлов, даже если обычные избирательные права доступа его разрешают. При включенном модуле SELinux существует режим, при котором нарушения стратегий просто за- носятся в журнал, а не обеспечиваются принудительно (аналогично тому, что мы видели в AppArmor). Чтобы создать удачный профиль SELinux приложения, нужно знать все файлы, к которым ему может потребоваться доступ в случае как нормальной работы, так и возникновения ошибок, поэтому данную задачу лучше возло- жить на разработчика приложения. Некоторые поставщики предоставляют профили для своих приложений. ### «Песочница» gVisor «Песочница» gVisor от компании Google ограничивает контейнер, перехватывая системные вызовы примерно так же, как гипервизор перехватывает системные вызовы гостевой виртуальной машины. gVisor представляет собой «ядро для пользовательского пространства», что кажется мне терминологически противоречивым, но описывает реализацию системных вызовов Linux в пользовательском пространстве с помощью паравиртуализации. Для этого компонент Sentry «песочницы» gVisor перехватывает поступа- ющие от приложения системные вызовы. Sentry жестко ограничен с помощью seccomp, вследствие чего сам не может обращаться к ресурсам файловой системы. Если возникает необходимость выполнить системные вызовы, связанные с доступом к файлам, он передает их для выполнения совершенно отдельному процессу — Gofer. Даже системные вызовы, не имеющие отношения к доступу к файловой системе, не передаются непосредственно ядру хоста, а реализуются заново внутри Sentry. По сути, он представляет собой гостевое ядро, работающее в пользовательском пространстве. Проект gVisor (https://oreil.ly/cMROh) содержит исполняемый файл runsc, совместимый с комплектами в формате OCI, очень похожий на обычную среду выполнения OCI runc. При вы- полнении контейнера с помощью runsc можно легко видеть процессы gVisor, но если у вас уже есть файл config.json для runc, то, вероятно, вам придется сгенерировать его версию, совместимую с runsc.