Seccomp на практике. Профилирование syscall, фильтры и отладка

1168
Seccomp на практике. Профилирование syscall, фильтры и отладка

Seccomp ограничивает системные вызовы, которые процесс может отправлять ядру Linux. Фильтр проверяет номер syscall, архитектуру ABI и числовые аргументы, после чего разрешает вызов, возвращает ошибку, отправляет сигнал, записывает событие в журнал или передаёт решение пользовательскому процессу-наблюдателю.

Seccomp на практике.

Главная польза seccomp состоит в сокращении доступной поверхности ядра. Если атакующий захватит веб-сервер, обработчик документов или другой процесс, эксплойт не сможет обратиться к запрещённым интерфейсам ядра. Поведение фильтров и ограничения BPF подробно описывает документация ядра Linux.

Seccomp не является полноценной песочницей. Фильтр не управляет путями к файлам, сетевыми адресами внутри структур, пользователями, каталогами и объектами ядра. Seccomp нужно сочетать с Linux capabilities, AppArmor или SELinux, пространствами имён, непривилегированным пользователем и ограничением файловой системы.

Самая частая ошибка при настройке seccomp выглядит так. Администратор запускает strace на несколько минут, копирует увиденные syscall в allowlist и блокирует всё остальное. Профиль проходит один тест, но позже ломает обновление сертификата, ротацию журнала, перезагрузку конфигурации, DNS, обработку ошибки или запуск нового рабочего процесса.

Содержание

Как работает seccomp

Linux поддерживает Strict Mode и Filter Mode.

Режим Разрешённые операции Практическое применение
SECCOMP_MODE_STRICT read, write, _exit и sigreturn Небольшой процесс, который заранее открыл все дескрипторы и больше не выделяет память, не создаёт потоки и не открывает файлы
SECCOMP_MODE_FILTER Набор операций, заданный classic BPF-фильтром Контейнеры, серверы, службы, браузеры и большинство современных песочниц

Strict Mode разрешает именно _exit, а не библиотечную функцию exit. Современная libc часто завершает многопоточный процесс через exit_group, которого в строгом режиме нет. Динамически связанные программы также требуют openat, mmap, mprotect, close и других вызовов. Поэтому Strict Mode подходит только для специально спроектированного кода.

Filter Mode получает структуру с номером syscall, идентификатором архитектуры, адресом инструкции и шестью аргументами. Фильтр может проверять числовые значения и битовые маски. Например, правило способно запретить определённое семейство сокетов в прямом вызове socket или разрешить clone только без флагов создания новых пространств имён.

BPF-фильтр не разыменовывает указатели из памяти процесса. Поэтому seccomp не может прочитать путь, переданный в openat, содержимое структуры sockaddr или массив аргументов старого вызова socketcall. Для проверки файлов и сетевых объектов применяют LSM-механизмы, включая AppArmor, SELinux и Landlock.

Фильтр нельзя ослабить после установки

После включения seccomp процесс не может удалить действующий фильтр или заменить его менее строгим. Новые фильтры добавляются поверх старых. Ядро выполняет все применимые фильтры и выбирает действие с наибольшим приоритетом.

Дочерние процессы наследуют фильтр через fork и clone, если фильтр разрешает такие вызовы. Ограничения сохраняются после execve. Поэтому один неверный профиль может сломать не только основной бинарный файл, но и запускаемые им утилиты.

Зачем нужен No New Privileges

Непривилегированный процесс может самостоятельно установить seccomp-фильтр только после включения флага no_new_privs. Альтернативный вариант требует возможности CAP_SYS_ADMIN в пользовательском пространстве имён.

Флаг no_new_privs запрещает получать новые привилегии через последующий execve. Биты setuid, setgid и файловые capabilities больше не повышают права такого процесса. Флаг наследуется дочерними процессами и не отключается.

Как выбрать действие фильтра

Действие libseccomp Результат Когда применять
SCMP_ACT_ALLOW Ядро выполняет syscall Разрешённые операции рабочего профиля
SCMP_ACT_ERRNO Ядро не выполняет syscall и возвращает заданный errno Основной вариант на этапе внедрения
SCMP_ACT_LOG Ядро записывает событие и выполняет syscall Краткая диагностика на тестовом узле
SCMP_ACT_KILL_PROCESS Ядро завершает весь процесс Строгий профиль после полного тестирования
SCMP_ACT_KILL_THREAD Ядро завершает только вызвавший поток Почти никогда не подходит для многопоточных программ
SCMP_ACT_TRAP Ядро отправляет потоку SIGSYS Специализированная обработка нарушений внутри приложения
SCMP_ACT_NOTIFY Решение принимает пользовательский процесс-наблюдатель Контейнерные среды и сложные песочницы с отдельным супервизором

Для первого блокирующего профиля обычно выбирают SCMP_ACT_ERRNO. Приложение получает ошибку, а администратор может найти проблемную операцию без внезапного завершения процесса.

EPERM подходит не для каждого syscall

Большинство профилей возвращает EPERM, когда политика запрещает операцию. Однако некоторые библиотеки проверяют наличие нового syscall и переходят на старый интерфейс только после получения ENOSYS.

Известный пример связан с clone3. Старые seccomp-профили возвращали EPERM, после чего новые версии glibc не всегда переходили на clone. Приложение сообщало об ошибке создания потока, хотя старый syscall был разрешён. Docker использует для заблокированного clone3 значение ENOSYS, когда контейнер не получил нужные capabilities.

Не назначайте ENOSYS всем запрещённым вызовам. Программа может воспринять ошибку как отсутствие функции ядра и выбрать менее безопасный обходной путь. Выбирайте errno отдельно для syscall, который действительно поддерживает совместимый fallback.

Почему LOG не заменяет тестирование

SCMP_ACT_LOG разрешает операцию. Профиль с таким действием не проверяет поведение приложения при реальном запрете. Журналирование также может быстро заполнить журнал ядра, если фильтр регистрирует read, write, futex или другие частые вызовы.

Поддержка LOG зависит от версии ядра, libseccomp и контейнерной среды. Доступные действия показывает файл /proc/sys/kernel/seccomp/actions_avail.

Что проверить перед настройкой

Проверьте конфигурацию ядра

config="/boot/config-$(uname -r)"
 
 if [ -r "$config" ]; then
   grep -E '^CONFIG_SECCOMP=y$|^CONFIG_SECCOMP_FILTER=y$' "$config"
 elif [ -r /proc/config.gz ]; then
   zgrep -E '^CONFIG_SECCOMP=y$|^CONFIG_SECCOMP_FILTER=y$' /proc/config.gz
 else
   echo "Конфигурация текущего ядра недоступна"
 fi

Для Filter Mode нужны обе строки.

CONFIG_SECCOMP=y
 CONFIG_SECCOMP_FILTER=y

Некоторые дистрибутивы не публикуют конфигурацию ядра в /boot или /proc/config.gz. Отсутствие файла не доказывает отсутствие seccomp.

Посмотрите доступные действия

cat /proc/sys/kernel/seccomp/actions_avail 2>/dev/null

Современное ядро обычно выводит несколько значений, включая kill_process, kill_thread, trap, errno, user_notif, trace, log и allow. Конкретный список зависит от версии и конфигурации ядра.

Проверьте состояние процесса

grep -E '^(NoNewPrivs|Seccomp|Seccomp_filters):' /proc/$$/status
Значение Seccomp Значение
0 Фильтр не включён
1 Работает Strict Mode
2 Работает Filter Mode

Поле Seccomp_filters показывает количество фильтров и присутствует не во всех ядрах. Значение больше единицы может появиться, когда фильтры последовательно установили systemd, контейнерный runtime и само приложение.

Зафиксируйте среду тестирования

Запишите версии компонентов, которые влияют на набор системных вызовов.

uname -a
 ldd --version | head -n 1
 strace --version
 systemd --version | head -n 1
 docker version
 runc --version
 systemd-detect-virt

Один и тот же исходный код может использовать разные syscall после обновления glibc, musl, Go, Java, Python, OpenSSL, контейнерного образа или ядра. Профиль, созданный на x86_64, нельзя считать проверенным для arm64.

Как правильно собрать syscall приложения

Профилирование показывает наблюдавшееся поведение, но не доказывает, что список полный. Редкая ветка кода может сработать через неделю или только при аварии.

Запустите приложение под strace с самого начала

sudo strace -ff -tt -T -yy -s 256 
   -o /var/tmp/myapp.trace 
   -- /usr/local/bin/myapp --config /etc/myapp/config.yml
  • -f отслеживает создаваемые процессы и потоки.
  • -ff записывает каждый процесс в отдельный файл с PID в имени.
  • -tt добавляет точное время.
  • -T показывает длительность syscall.
  • -yy дополняет сведения о файловых дескрипторах.
  • -s 256 увеличивает длину выводимых строк.

-ff несовместим со сводным режимом -c. Для статистики запустите отдельный тест.

sudo strace -f -c -S calls 
   -o /var/tmp/myapp-syscalls.txt 
   -- /usr/local/bin/myapp --config /etc/myapp/config.yml

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

Не ограничивайтесь штатным трафиком

Во время трассировки выполните все доступные сценарии.

  • Запустите и штатно остановите приложение.
  • Подайте обычную и пиковую нагрузку.
  • Перезагрузите конфигурацию без остановки процесса.
  • Создайте новые рабочие процессы и потоки.
  • Проверьте ротацию журналов.
  • Обновите или перечитайте TLS-сертификаты.
  • Проверьте DNS, IPv4, IPv6 и Unix-сокеты.
  • Выполните healthcheck, резервное копирование и административные команды.
  • Отключите внешнюю базу данных или API.
  • Смоделируйте тайм-аут, обрыв соединения и нехватку места.
  • Проверьте завершение по SIGTERM и перезагрузку по SIGHUP.
  • Запустите все подключаемые модули, плагины и обработчики файлов.

Подключение к работающему процессу

Подключение через -p помогает исследовать уже запущенную службу, но не показывает загрузку библиотек и раннюю инициализацию.

pids=$(pgrep -x nginx)
 
 sudo strace -f 
   $(printf '%s
' "$pids" | sed 's/^/-p /') -o /var/tmp/nginx-attached.trace

Команда подключается к найденным процессам Nginx. Запуск вида strace -p $(pgrep nginx) ненадёжен, поскольку strace требует отдельный параметр -p перед каждым PID.

Учитывайте влияние strace

Запускайте трассировку на тестовом узле. Strace работает через ptrace, замедляет приложение и меняет временные характеристики. Журнал может содержать пути, имена файлов, адреса, фрагменты передаваемых данных и другую чувствительную информацию.

Профилирование контейнера может потребовать CAP_SYS_PTRACE, изменения политики Yama или временного диагностического профиля. Не переносите такие послабления в рабочую конфигурацию.

Почему нельзя слепо копировать список strace

Библиотечные функции и syscall не совпадают один к одному. Например, функция exit может вызвать exit_group, функция fork может использовать clone, а функция open в современной glibc обычно обращается к openat.

Выбор syscall зависит от архитектуры, версии libc и доступности новых интерфейсов ядра. Java, Go и другие среды выполнения также меняют системные вызовы после обновлений.

Как настроить seccomp в Docker

Docker применяет встроенный seccomp-профиль к обычному контейнеру, если администратор не указал пользовательский профиль и не отключил фильтрацию. Текущий принцип работы и список значимых ограничений описывает документация Docker.

Не начинайте с пользовательского профиля без причины. Встроенный профиль обновляется вместе с Docker Engine и может получить новые ограничения после обнаружения уязвимости ядра. Старая локальная копия останется без таких исправлений.

Проверьте встроенный профиль

docker info --format '{{json .SecurityOptions}}'

В выводе должен присутствовать seccomp. Затем проверьте состояние процесса внутри контейнера.

docker run --rm alpine 
   sh -c 'grep -E "^(NoNewPrivs|Seccomp|Seccomp_filters):" /proc/1/status'

Значение Seccomp: 2 подтверждает Filter Mode.

Проверьте, какой профиль назначен контейнеру

docker inspect 
   --format '{{json .HostConfig.SecurityOpt}}' 
   nginx-seccomp-test

Пустой список не обязательно означает отсутствие seccomp. Docker может применять встроенный профиль без явной записи в параметрах контейнера.

Когда нужен пользовательский профиль

Пользовательский профиль оправдан в нескольких случаях.

  • Контейнер обрабатывает недоверенные данные и требует более строгого allowlist.
  • Приложение не работает со встроенным профилем, а анализ подтвердил необходимость конкретного syscall.
  • Политика организации требует одинакового проверенного профиля на всех узлах.
  • Команда фиксирует профиль вместе с версией приложения и проверяет его в CI.

Не заменяйте встроенный профиль минимальным JSON, собранным по одному запуску Nginx. Текущий профиль Docker содержит архитектурные правила, проверки capabilities, аргументов syscall и специальные errno для совместимости.

Возьмите профиль из закреплённой версии

Не скачивайте файл из ветки main при каждом развёртывании. Закрепите тег или commit репозитория Moby Profiles и сохраните контрольную сумму. В июле 2026 года при подготовке статьи использовался релиз seccomp-профиля seccomp/v0.2.3.

После загрузки сохраните исходную копию.

install -d -m 0750 ./seccomp
 
 cp docker-default.json ./seccomp/docker-default.original.json
 cp docker-default.json ./seccomp/myapp.json
 
 sha256sum ./seccomp/docker-default.original.json 
   > ./seccomp/docker-default.original.sha256

Проверьте синтаксис JSON.

jq empty ./seccomp/myapp.json

Команда должна завершиться без вывода и вернуть код 0.

Не удаляйте карту архитектур

Профиль Docker обычно содержит archMap. Карта связывает основную архитектуру с совместимыми ABI, включая x86 и x32 на системе x86_64. Удаление карты создаёт риск обхода правил через альтернативный ABI или ломает 32-битные приложения.

Оставьте defaultAction, defaultErrnoRet и archMap из базового профиля. Меняйте только проверенные правила и фиксируйте разницу в системе контроля версий.

git diff --no-index 
   ./seccomp/docker-default.original.json 
   ./seccomp/myapp.json

Примените профиль к тестовому контейнеру

docker run --rm 
   --name nginx-seccomp-test 
   --security-opt seccomp="$PWD/seccomp/myapp.json" 
   -p 127.0.0.1:8080:80 
   nginx:alpine

В другом терминале проверьте ответ.

curl -fsS http://127.0.0.1:8080/ >/dev/null

Одного запроса недостаточно. Проверьте reload, завершение, TLS, журналирование, обработку ошибок и весь набор функций контейнера.

Подключите профиль через Docker Compose

services:
   web:
     image: nginx:alpine
     ports:
       - "127.0.0.1:8080:80"
     security_opt:
       - seccomp=./seccomp/myapp.json

Проверьте итоговую конфигурацию.

docker compose config

После изменения профиля пересоздайте контейнер.

docker compose up -d --force-recreate

docker restart не меняет параметры уже созданного контейнера.

Не используйте unconfined как постоянное решение

docker run 
   --security-opt seccomp=unconfined 
   myimage

Параметр полностью отключает seccomp для контейнера. Допустим только краткий диагностический запуск на изолированном тестовом узле. Если приложение работает лишь с unconfined, найдите конкретный заблокированный syscall и исправьте профиль.

Кейс CVE-2026-31431 и ограничение socketcall

Уязвимость Linux CVE-2026-31431, известная как Copy Fail, показала, почему seccomp нельзя рассматривать отдельно от AppArmor и SELinux. Эксплуатация использовала семейство сокетов AF_ALG, которое открывает пользовательским процессам доступ к криптографическому API ядра.

Прямой вызов socket передаёт семейство сокета числовым аргументом, поэтому seccomp может заблокировать AF_ALG. Старый 32-битный интерфейс socketcall передаёт аргументы через указатель. BPF не разыменовывает такой указатель и не видит семейство сокета.

Первая попытка закрыть обход через полный запрет socketcall сломала часть 32-битных программ. Позже Moby убрал общий запрет и перенёс защиту старого пути в AppArmor и SELinux, сохранив проверку аргумента прямого socket.

Патч ядра остаётся основной защитой. Seccomp и LSM могут уменьшить доступную поверхность, но не заменяют обновление уязвимого ядра. Пользовательский профиль и отключённый AppArmor способны убрать защиту, которую добавил контейнерный runtime.

Как настроить seccomp для systemd-сервиса

Systemd создаёт seccomp-фильтр через директиву SystemCallFilter. Группы syscall позволяют начать с подготовленного набора, а затем добавить операции, которые действительно нужны службе.

Проверьте версию systemd

systemd --version | head -n 1

SystemCallFilter доступен в старых выпусках systemd, но SystemCallLog появился только в systemd 247. Состав групп syscall также меняется между версиями и архитектурами.

Посмотрите состав группы system-service

systemd-analyze syscall-filter @system-service

Группа @system-service содержит вызовы, которые обычно нужны системным службам, но исключает специализированные интерфейсы для монтирования, перезагрузки, swap и изменения часов.

Список формируется во время сборки systemd. Новое ядро может содержать syscall, которого установленная версия systemd ещё не знает. Проверяйте фактический вывод на целевом сервере.

Проверьте архитектуру бинарных файлов

file /usr/sbin/nginx
 file /usr/lib/nginx/modules/*.so 2>/dev/null

Директива SystemCallArchitectures=native запрещает альтернативные ABI. Такое ограничение полезно против обходов, но сломает 32-битный бинарный файл или helper на 64-битном сервере.

Создайте drop-in-файл

sudo systemctl edit nginx.service

Добавьте базовый профиль.

[Service]
 NoNewPrivileges=yes
 SystemCallArchitectures=native
 SystemCallFilter=@system-service
 SystemCallErrorNumber=EPERM

SystemCallErrorNumber=EPERM заставляет ядро вернуть ошибку вместо немедленного завершения процесса через SIGSYS. Такой режим удобнее во время внедрения.

Systemd не требует явно перечислять некоторые вызовы, необходимые для запуска и завершения процесса. В список не нужно вручную добавлять execve, exit, exit_group, getrlimit, sigreturn, rt_sigreturn и базовые операции сна и чтения времени.

Проверьте конфигурацию до перезапуска

sudo systemd-analyze verify nginx.service

Затем примените настройки.

sudo systemctl daemon-reload
 sudo systemctl restart nginx.service
 sudo systemctl is-active nginx.service

Ожидаемый вывод последней команды выглядит как active.

Проверьте активные параметры

systemctl show nginx.service 
   -p NoNewPrivileges 
   -p SystemCallFilter 
   -p SystemCallArchitectures 
   -p SystemCallErrorNumber

Найдите PID основного процесса и проверьте состояние ядра.

pid=$(systemctl show 
   --property MainPID 
   --value nginx.service)
 
 sudo grep -E '^(NoNewPrivs|Seccomp|Seccomp_filters):' 
   "/proc/$pid/status"

Ожидаемый результат содержит Seccomp: 2.

Добавьте только подтверждённый syscall

Допустим, приложение использует memfd_create, а трассировка и документация приложения подтверждают необходимость вызова. Дополните строку.

[Service]
 NoNewPrivileges=yes
 SystemCallArchitectures=native
 SystemCallFilter=@system-service memfd_create
 SystemCallErrorNumber=EPERM

Не добавляйте syscall только потому, что strace показал попытку. Приложение может проверять необязательную функцию и нормально работать после отказа.

Проверьте общий уровень изоляции

systemd-analyze security nginx.service

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

Дополните seccomp другими ограничениями

[Service]
 NoNewPrivileges=yes
 PrivateTmp=yes
 PrivateDevices=yes
 ProtectSystem=strict
 ProtectHome=yes
 ProtectKernelTunables=yes
 ProtectKernelModules=yes
 ProtectControlGroups=yes
 RestrictSUIDSGID=yes
 LockPersonality=yes
 SystemCallArchitectures=native
 SystemCallFilter=@system-service
 SystemCallFilter=~@mount @reboot @swap
 SystemCallErrorNumber=EPERM

Не копируйте блок без проверки путей записи и функций службы. Например, ProtectSystem=strict потребует явно открыть каталоги журналов, кэша или состояния через ReadWritePaths, StateDirectory, CacheDirectory и LogsDirectory.

Как включить seccomp в Kubernetes

Kubernetes поддерживает три типа профиля.

Тип Поведение
RuntimeDefault Используется стандартный профиль containerd, CRI-O или другого runtime
Localhost Используется локальный JSON-профиль, установленный на узле
Unconfined Seccomp отключается

Включите RuntimeDefault на уровне Pod

apiVersion: v1
 kind: Pod
 metadata:
   name: web
 spec:
   securityContext:
     seccompProfile:
       type: RuntimeDefault
   containers:
     - name: nginx
       image: nginx:alpine

Профиль уровня контейнера может переопределить настройку Pod. Проверьте init containers и ephemeral containers отдельно, если манифест задаёт им собственный securityContext.

Подключите локальный профиль

apiVersion: v1
 kind: Pod
 metadata:
   name: web
 spec:
   securityContext:
     seccompProfile:
       type: Localhost
       localhostProfile: profiles/nginx.json
   containers:
     - name: nginx
       image: nginx:alpine

Файл должен находиться на каждом подходящем узле относительно каталога seccomp kubelet. При стандартном root-dir полный путь будет выглядеть как /var/lib/kubelet/seccomp/profiles/nginx.json.

Обычный Kubernetes не распространяет Localhost-профиль по узлам автоматически. Используйте систему управления конфигурациями, DaemonSet с контролируемой установкой или возможности конкретного runtime.

Проверьте применённую политику

kubectl get pod web 
   -o jsonpath='{.spec.securityContext.seccompProfile.type}{"
"}'

Ожидаемый результат выглядит как RuntimeDefault или Localhost.

Привилегированный контейнер несовместим с реальной seccomp-изоляцией. Не рассчитывайте на профиль, если контейнер запущен с privileged: true. Сначала устраните привилегированный режим.

Как найти заблокированный syscall

Начните с журнала приложения

sudo journalctl 
   -u nginx.service 
   --since "15 minutes ago"

Сообщения Operation not permitted, Permission denied, pthread_create failed или завершение с status=31/SYS могут указывать на seccomp. Такие же ошибки способны вызвать права файлов, capabilities, SELinux, AppArmor и пространства имён.

Проверьте журнал ядра

sudo journalctl -k 
   --since "15 minutes ago" 
   | grep -Ei 'seccomp|sigsys|audit'

На системе с auditd используйте отдельный поиск.

sudo ausearch -m SECCOMP -ts recent -i

Полезные поля записи включают syscall, arch, comm, exe, pid и code.

Преобразуйте номер syscall в имя

ausyscall 435

Пакет libseccomp может предоставлять другую утилиту.

scmp_sys_resolver 435

Номер зависит от ABI. Сначала проверьте поле arch в записи аудита. Номер 59 на x86_64 и номер 59 на другой архитектуре могут обозначать разные операции.

Повторите ошибку под strace

sudo strace -f 
   -e trace=all 
   -o /var/tmp/nginx-debug.trace 
   nginx -t

Найдите отказы.

grep -E 'EPERM|EACCES|ENOSYS' 
   /var/tmp/nginx-debug.trace

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

Проверьте LSM отдельно

sudo journalctl -k 
   --since "15 minutes ago" 
   | grep -Ei 'apparmor|avc|selinux'

Если журнал содержит AppArmor DENIED или SELinux AVC, изменение seccomp-профиля не исправит причину.

Сравните запуск с базовым профилем

Для Docker сначала верните встроенный профиль, а не unconfined. Если приложение заработало, проблема находится в пользовательском профиле. Если ошибка сохранилась, проверьте capabilities, LSM, файловую систему и конфигурацию приложения.

Неочевидные ограничения и ошибки

Симптом Причина Исправление
Программа не запускается Профиль блокирует динамический загрузчик, libc или раннюю инициализацию Трассировать процесс с самого старта и проверить openat, mmap, mprotect, arch_prctl, сигналы и управление памятью
Приложение запускается, но не создаёт потоки Заблокирован clone или clone3, возвращается неподходящий errno Проверить ожидаемый fallback библиотеки и правила Docker для clone3
Reload ломается, хотя обычные запросы работают Профилирование не охватило сигналы, fork, exec или открытие новой конфигурации Проверить kill, pidfd_send_signal, clone, wait4, openat и связанные операции
Правило работает на x86_64 и ломается на arm64 Архитектуры используют разные syscall и соглашения вызова Тестировать профиль на каждой целевой архитектуре
Запрет kill не мешает отправлять сигналы Ядро предоставляет похожие операции через tgkill, tkill и pidfd_send_signal Использовать allowlist вместо неполного blacklist
Seccomp не может ограничить конкретный файл Путь передаётся через указатель, который BPF не читает Применить AppArmor, SELinux, Landlock или изолированную файловую систему
Старый профиль стал слабее нового встроенного Пользовательский JSON не получил исправления из обновления runtime Сравнивать профиль после каждого обновления Docker, containerd, CRI-O или runc
Журнал seccomp пуст Действие LOG не поддерживается, отключён аудит или отказ вернул другой механизм Проверить actions_avail, auditd, AppArmor, SELinux и повторить ошибку под strace
Контейнер работает только с unconfined Профиль запрещает нужный syscall или аргумент Найти точный отказ и добавить минимальное правило вместо полного отключения seccomp
SystemCallArchitectures=native ломает службу Служба запускает 32-битный helper или библиотеку Проверить все бинарные файлы через file и разрешить нужную архитектуру только при обоснованной необходимости

Blacklist быстро устаревает

Ядро регулярно получает новые системные вызовы. Запрет одного старого интерфейса не блокирует новый syscall с похожими возможностями. Например, pidfd_send_signal частично дублирует операции kill, а новые mount API дополняют традиционный mount.

Allowlist автоматически блокирует неизвестные новые вызовы, пока администратор не добавит их после тестирования. Поэтому allowlist лучше подходит для долгоживущих служб.

Слишком строгий профиль может ослабить надёжность

Процесс иногда использует редкий syscall только для корректного завершения, сброса данных или записи диагностического сообщения. Блокировка такой операции не создаёт прямую уязвимость, но может привести к потере данных, зависанию или повреждению состояния.

Тестируйте не только основной успешный сценарий, но и обработку сбоев.

Интерпретируемые языки требуют более широкого покрытия

Python, PHP, Node.js и Java загружают модули, создают потоки, используют JIT, DNS, локали и файловые кэши. Набор syscall зависит от подключённых библиотек и конкретного запроса. Профиль для одного скрипта нельзя автоматически переносить на другой проект с той же средой выполнения.

Как откатить настройки

Откат Docker

Удалите пользовательский параметр seccomp и пересоздайте контейнер. Docker вернётся к встроенному профилю.

docker rm -f nginx-seccomp-test
 
 docker run --rm 
   --name nginx-seccomp-test 
   -p 127.0.0.1:8080:80 
   nginx:alpine

Для Compose удалите строку seccomp=... из security_opt.

docker compose up -d --force-recreate

Не используйте seccomp=unconfined как постоянный откат.

Откат systemd

Просмотрите созданные drop-in-файлы.

systemctl cat nginx.service

Удалите только файл, в котором настроен seccomp.

sudo rm 
   /etc/systemd/system/nginx.service.d/override.conf
 
 sudo systemctl daemon-reload
 sudo systemctl restart nginx.service
 sudo systemctl is-active nginx.service

Не удаляйте весь каталог drop-in. В каталоге могут находиться параметры окружения, лимиты ресурсов и другие рабочие настройки.

Откат Kubernetes

Для пользовательского Localhost-профиля верните RuntimeDefault и обновите workload.

spec:
   template:
     spec:
       securityContext:
         seccompProfile:
           type: RuntimeDefault

После изменения проверьте rollout.

kubectl rollout status deployment/web

Не переключайте рабочий Pod на Unconfined, кроме краткой диагностики в контролируемой среде.

Как сопровождать seccomp-профиль

  • Храните профиль рядом с кодом или манифестами приложения.
  • Закрепляйте версию базового профиля и его контрольную сумму.
  • Проверяйте JSON в CI.
  • Запускайте интеграционные тесты с включённым фильтром.
  • Тестируйте запуск, остановку, reload и аварийные сценарии.
  • Проверяйте каждую поддерживаемую архитектуру отдельно.
  • Сравнивайте пользовательский профиль с новым профилем runtime после обновлений.
  • Пересматривайте правила после обновления libc, языка, контейнерного образа и ядра.
  • Фиксируйте причину каждого нестандартного разрешения.
  • Удаляйте syscall, который больше не нужен приложению.

Полезный комментарий к правилу должен объяснять не название syscall, а функцию приложения, которая без него не работает. Например, «нужен для перезагрузки конфигурации через создание нового worker», а не «разрешён clone».

Контрольный список перед включением

  • Ядро поддерживает CONFIG_SECCOMP и CONFIG_SECCOMP_FILTER.
  • Профиль соответствует архитектуре и ABI приложения.
  • Профилирование охватило запуск, остановку, reload и обработку ошибок.
  • Проверены дочерние процессы, потоки, плагины и административные команды.
  • Для необязательных новых syscall выбран корректный errno.
  • Профиль протестирован с реальной версией libc и контейнерного runtime.
  • Проверены журналы seccomp, AppArmor и SELinux.
  • Подготовлена команда отката.
  • Пользовательский профиль не потерял свежие ограничения встроенного профиля.
  • Seccomp дополнен capabilities, LSM и файловой изоляцией.

FAQ

Нужно ли создавать свой seccomp-профиль для каждого Docker-контейнера?

Нет. Начните со встроенного профиля Docker. Создавайте пользовательский allowlist только для контейнеров с повышенным риском или специальными требованиями.

Можно ли построить готовый allowlist по одному запуску strace?

Нет. Strace показывает только выполненные ветки кода. Профиль должен пройти тесты запуска, остановки, reload, ошибок, TLS, DNS, ротации журналов и обновлений.

Что лучше возвращать при блокировке, EPERM или ENOSYS?

Обычно подходит EPERM. ENOSYS применяют для конкретных syscall, когда библиотека ожидает такой ответ и переходит на совместимый старый интерфейс.

Почему seccomp не может разрешить openat только для одного каталога?

Путь передаётся указателем на строку в памяти процесса. BPF-фильтр не разыменовывает указатели. Ограничьте файлы через AppArmor, SELinux, Landlock или mount namespace.

Можно ли использовать один профиль на x86_64 и arm64?

Можно хранить общий OCI-профиль с картой архитектур, но профиль нужно отдельно тестировать на каждой платформе. Наборы syscall и поведение libc различаются.

Почему приложение сломалось после обновления базового образа?

Новая libc, библиотека или среда выполнения могла перейти на другой syscall. Повторите интеграционные тесты и обновите профиль только после анализа нового вызова.

Достаточно ли SCMP_ACT_LOG для построения профиля?

Нет. LOG показывает вызовы и разрешает их, но не проверяет реакцию приложения на отказ. Журналирование также не гарантирует покрытие редких веток.

Защищает ли seccomp от всех побегов из контейнера?

Нет. Seccomp сокращает поверхность ядра, но разрешённый syscall тоже может содержать уязвимость. Обновляйте ядро и runtime, убирайте capabilities, включайте AppArmor или SELinux и не запускайте контейнер как privileged.

Как понять, что systemd действительно применил фильтр?

Найдите MainPID службы и проверьте /proc/PID/status. Значение Seccomp: 2 означает, что процесс работает в Filter Mode.

Можно ли временно отключить seccomp для диагностики?

Можно только на изолированном тестовом узле. Сначала лучше вернуть встроенный профиль runtime. Режим unconfined полностью убирает защиту и не подходит для постоянной эксплуатации.

```
Alt text
Обращаем внимание, что все материалы в этом блоге представляют личное мнение их авторов. Редакция SecurityLab.ru не несет ответственности за точность, полноту и достоверность опубликованных данных. Вся информация предоставлена «как есть» и может не соответствовать официальной позиции компании.
GARDA
06.08
Live · 11:00 МСК
Маскирование данных: битва мнений
Эксперты Гарды ответят на ваши вопросы в прямом эфире 6 августа в 11:00!
Принять участие
Реклама. ООО «Гарда Технологии», ИНН 5260443081. 16+