Многие компании используют контейнеры в качестве фундаментальной технологии для управления своими приложениями и их выполнения. Если вы уже имеете опыт работы с контейнерами, то понимаете их мотивацию: контейнеры обеспечивают совершенно новые уровни портируемости и масштабируемости. Однако использование контейнеров, как и любой другой технологии, также означает и появление новых способов применения эксплойтов приложений.
При определённых конфигурациях контейнеров эксплойт приложения в конце концов может привести к компрометации хоста, на котором запущен контейнер. Также стоит учесть и другие вопросы, например, секреты, хранящиеся в контейнерах как переменные окружения, и то, к чему контейнеры имеют доступ. Если вы хотите больше узнать о рекомендациях по обеспечению безопасности контейнеров Docker, то можете воспользоваться полезной шпаргалкой.
Сложившийся жизненный цикл разработки ПО уже включает в себя процессы обеспечения безопасности, например, сканирование уязвимостей и анализ состава ПО, но требуется и нечто большее. Большинство существующих технологий обеспечения безопасности приложений предотвращают уязвимость приложений, но не многие способны предотвратить урон, вызванный успешным эксплойтом приложения. Я изучил новый способ защиты приложений в контейнерах после внедрения эксплойтов. В этом посте я расскажу, что это и как безболезненно интегрировать его в уже сложившиеся процессы разработки ПО. В качестве дополнительной защиты я использовал Seccomp-BPF, поэтому прежде чем вдаваться в подробности, нужно немного рассказать о нём.
▍ Вводная информация
Выполняемые на компьютерах программы активно используют функции операционной системы. В современных языках программирования такие задачи, как открывание файлов и создание новых процессов, абстрагированы, однако внутри кода они выполняются при помощи запросов к ядру, называемых системными вызовами (syscall). Насколько важны syscall для работы программы? В ядре Linux есть примерно четыреста syscall, и даже в простой программе «Hello, World!», написанной на C, используются два из них: write и exit.
Код, выполняемый в так называемом «пользовательском пространстве», не может сделать ничего, не попросив сделать это ядро. Умные разработчики ядра Linux решили воспользоваться этим и создать мощную функцию защиты. В июле 2012 года была выпущена версия Linux 3.5, в которой была добавлена поддержка Seccomp-BPF.
Seccomp-BPF — это функция ядра Linux, позволяющая благодаря созданию специального фильтра ограничить список системных вызовов, которые может выполнять процесс.Теоретически можно создать фильтр Seccomp-BPF, позволяющий процессу выполнять только те syscall, которые необходимы для его работы, и ничего больше. Это будет полезно в случае, если приложение оказалось уязвимым для эксплойтов таким образом, что злоумышленник может создавать дополнительные процессы. Если Seccomp не позволяет процессу выполнять новые syscall, есть большая вероятность, что это помешает нападающему.
Seccomp — очень крутая штука, она даже интегрирована в среду выполнения контейнера и такие инструменты управления Docker и Kubernetes. Возникает вопрос: «Почему Seccomp не используют везде?» Думаю, причина в том, что не хватает ресурсов для восполнения пробела между низкоуровневой функцией ядра наподобие Seccomp и современными процессами разработки ПО. Не у каждой организации есть разработчик низкоуровневого кода, подробно знающий тему syscall. К тому же нужно тратить дополнительные ресурсы на то, чтобы разобраться, какие системные вызовы нужны вашей программе и дополнять её с каждой новой фичей, реализуемой в коде.
Я думал о том, как решить эту проблему, и пришёл к такой мысли: «Что, если записывать те syscall, которые выполняет программа в процессе своей работы?» Я рассказал о своей идее одному из коллег, и на следующий день он прислал мне ссылку на инструмент на GitHub. Оказалось, разработчики Red Hat уже создали инструмент под названием
oci-seccomp-bpf-hook
, выполняющий именно эту задачу!▍ Создание фильтра Seccomp-BPF
Инструмент
oci-seccomp-bpf-hook
был создан для работы с Linux-контейнерами. OCI расшифровывается как «Open Container Initiative», это набор стандартов для сред выполнения контейнеров, определяющий, какие виды интерфейсов они должны иметь возможность предоставлять. Соответствующие требованиям OCI среды выполнения (например, Docker) имеют механизм под названием «хуки», позволяющий запускать код до запуска контейнера и после завершения его работы. Вместо того чтобы объяснять, как инструмент Red Hat использует эти хуки, лучше будет показать это на примере.Red Hat разработал
oci-seccomp-bpf-hook
для использования с его средой выполнения контейнеров podman. Podman во многом обратно совместим с Docker, поэтому, если вы работали с Docker, синтаксис в моих примерах будет казаться вам знакомым. Кроме того, хук OCI в настоящее время доступен только в связанных с Red Hat DNF-репозиториях, или его можно установить их исходников. Чтобы не усложнять демо, я просто воспользуюсь сервером Fedora (если у вас нет среды Fedora, то рекомендую запустить виртуальную машину Fedora на чём-нибудь наподобие Virtualbox или VMware).Первым делом для использования
oci-seccomp-bpf-hook
нужно убедиться, что он установлен вместе с podman. Для этого можно выполнить следующую команду:sudo dnf install podman oci-seccomp-bpf-hook
Теперь, когда у нас есть podman и хук OCI, мы можем наконец приступить к генерации фильтра Seccomp-BPF. В readme можно узнать, что синтаксис выглядит так:
sudo podman run --annotation io.containers.trace-syscall="if:[absolute path to the input file];of:[absolute path to the output file]" IMAGE COMMAND
Давайте выполним команду
ls
в простом контейнере и передадим вывод на /dev/null
. При этом мы будем записывать системные вызовы, выполняемые командой ls
, и сохранять их в файл /tmp/ls.json
.sudo podman run --annotation io.containers.trace-syscall=of:/tmp/ls.json fedora:35 ls / > /dev/null
Так как мы передаём вывод команды
ls
в /dev/null
, в терминале вывода быть не должно. Однако после выполнения команды мы можем взглянуть на файл, в который сохраняли системные вызовы. В нём мы видим, что команда сработала и syscall были записаны:cat /tmp/ls.json
{"defaultAction":"SCMP_ACT_ERRNO","architectures":["SCMP_ARCH_X86_64"],"syscalls":[{"names":["access","arch_prctl","brk","capset","chdir","close","close_range","dup2","execve","exit_group","fchdir","fchown","fstatfs","getdents64","getegid","geteuid","getgid","getrandom","getuid","ioctl","lseek","mmap","mount","mprotect","munmap","newfstatat","openat","openat2","pivot_root","prctl","pread64","prlimit64","pselect6","read","rt_sigaction","rt_sigprocmask","seccomp","set_robust_list","set_tid_address","sethostname","setresgid","setresuid","setsid","statfs","statx","umask","umount2","write"],"action":"SCMP_ACT_ALLOW","args":[],"comment":"","includes":{},"excludes":{}}]}
Этот файл и есть наш фильтр Seccomp, теперь мы можем использовать его с любой средой выполнения, которая его поддерживает. Давайте попробуем использовать фильтр с той же контейнированной командой
ls
, которую мы только что выполняли:sudo podman run --security-opt seccomp=/tmp/ls.json fedora ls / > /dev/null
Нет ни вывода, ни ошибок, это означает, что команда была успешно выполнена с применённым фильтром Seccomp. А теперь начинается самое интересное. Мы добавим контейнеру возможностей, которых не было, когда мы записывали системные вызовы для создания фильтра Seccomp. Достаточно будет добавить команде
ls
флаг -l
.sudo podman run --security-opt seccomp=/tmp/ls.json fedora ls -l / > /dev/null
ls: /: Operation not permitted
ls: /proc: Operation not permitted
ls: /root: Operation not permitted
…
Как видите, на сей раз мы получаем серию ошибок, сообщающих, что нельзя выполнить операцию, которую пытается выполнить наша команда. Добавление флага
-l
к команде ls
добавила процессу несколько новых syscall, которые отсутствуют в белом списке фильтра Seccomp. Если мы сгенерируем новый фильтр Seccomp с командой ls -l
, то увидим, что новый фильтр работает, потому что теперь в нём есть все требуемые syscall.sudo podman run --annotation io.containers.trace-syscall=of:/tmp/lsl.json fedora ls -l / > /dev/null
sudo podman run --security-opt seccomp=/tmp/lsl.json fedora ls -l / > /dev/null
Как видите, использование фильтров Seccomp с контейнерами сильно ограничивает их возможности. В ситуации, когда нападающий может использовать эксплойт вашего приложения, это может помешать ему нанести урон или вообще применять эксплойты.
Благодаря хуку OCI Red Hat, для создания фильтра Seccomp вам больше не нужно иметь глубоких знаний системных вызовов ядра Linux. Вы легко можете создать фильтр для конкретного приложения, не позволяющий контейнеру делать ничего сверх того, что он должен делать. Это серьёзный шаг для восполнения пробела между возможностями ядра и высокоуровневой разработкой ПО.
▍ В заключение
Как бы ни был прекрасен
oci-seccomp-bpf-hook
, сам по себе он не полностью исполнил мою мечту интегрировать Seccomp в сложившийся процесс разработки ПО. Всё равно необходимы дополнительные ресурсы, связанные с работой этого инструмента, а разработчики не будут гореть желанием тратить время на ручное изменение фильтра Seccomp под каждое обновление приложения. Чтобы окончательно восполнить этот пробел и максимально упростить использование Seccomp в энтерпрайз-приложениях, нам нужно найти способ автоматизировать генерацию фильтров Seccomp-BPF. К счастью, взглянув на современный процесс разработки ПО, мы находим идеальное место для внедрения этой автоматизации: вовремя Continuous Integration (CI).Процессы CI уже стали неотъемлемой частью сложившегося жизненного цикла разработки ПО. Если вы незнакомы с CI, то скажу, что она позволяет использовать такие возможности, как автоматизированное юнит-тестирование и сканирование безопасности кода, при каждом коммите в репозиторий git. Существует множество инструментов для CI, поэтому это идеальный этап для автоматизации генерации фильтра Seccomp для контейнированного приложения.
Вскоре я напишу ещё один пост с демонстрацией того, как создать процесс CI, генерирующий фильтр Seccomp при каждом обновлении кода. Благодаря этому, вы, наконец, сможете воспользоваться преимуществами ограничений syscall при помощи Seccomp и защитить свои приложения!
Telegram-канал и уютный чат для обсуждений