Традиционный разговор о песочницах в Linux всегда упирался в один неудобный факт: чтобы ограничить права процесса, нужно иметь права администратора. SELinux требует политик, написанных и загруженных root. AppArmor требует профилей, размещённых в системных директориях. seccomp-фильтры для сложных сценариев требуют привилегий или специальной конфигурации ядра. Получался парадокс: чтобы сделать систему безопаснее, сначала нужно открыть ей дверь с привилегированной стороны.
Landlock сломал этот парадокс. Начиная с ядра Linux 5.13, любой непривилегированный процесс может ограничить самого себя, установив правила доступа к файловым иерархиям, не запрашивая ничего у администратора, не требуя предустановленных профилей и не загружая модулей. Три системных вызова, и процесс живёт в клетке, которую сам себе построил. Снять её невозможно физически, только перезапуск процесса освобождает его от наложенных ограничений.
Как Landlock встраивается в стек безопасности ядра
Цель Landlock заключается в ограничении "ambient rights", то есть глобальных прав доступа к файловой системе, для набора процессов. Но в отличие от SELinux и AppArmor, которые работают как система принудительного контроля доступа с политиками, управляемыми администратором, Landlock является стекируемым LSM. Это означает, что его правила добавляются поверх уже существующих механизмов контроля доступа, не заменяя их и не конфликтуя с ними.
Убедиться, что Landlock активен в работающей системе:
dmesg | grep landlock
# Ожидаемый вывод:
# [ 0.043191] landlock: Up and running.
# Или через journald
journalctl -kb -g landlock
Проверить, что ядро собрано с поддержкой Landlock и включён в список LSM:
grep -E 'LANDLOCK|lsm=' /boot/config-$(uname -r)
# CONFIG_SECURITY_LANDLOCK=y
# Landlock должен присутствовать в параметре lsm=
# Если конфиг не в /boot
zgrep -iE 'landlock|lsm=' /proc/config.gz 2>/dev/null || \
grep -iE 'landlock|lsm=' /lib/modules/$(uname -r)/config
Если Landlock отсутствует в списке lsm=, его нужно добавить через параметры ядра. На системах с grub:
# В /etc/default/grub добавить landlock в параметр lsm
GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,apparmor"
grub2-mkconfig -o /boot/grub2/grub.cfg
Поскольку Landlock является стекируемым LSM, он позволяет создавать безопасные песочницы как новые уровни безопасности в дополнение к существующим системным средствам контроля доступа. Это важная деталь: Landlock не отменяет DAC-проверки файловой системы. Если процесс не имеет прав на файл по стандартным Unix-правам, Landlock не откроет к нему доступ. Он работает только в сторону ограничения, никогда в сторону расширения.
Три системных вызова и полная анатомия создания ruleset
Всё взаимодействие с Landlock сводится к трём системным вызовам. landlock_create_ruleset() создаёт новый ruleset и возвращает файловый дескриптор. landlock_add_rule() добавляет правило в ruleset. landlock_restrict_self() применяет ruleset к вызывающему потоку и с этого момента ограничения вступают в силу навсегда для этого потока и всех его потомков.
Минимальный пример на C, изолирующий процесс с доступом только к /usr и /tmp:
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
static inline int landlock_create_ruleset(
const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
static inline int landlock_add_rule(int ruleset_fd,
enum landlock_rule_type type,
const void *attr, __u32 flags)
{
return syscall(__NR_landlock_add_rule,
ruleset_fd, type, attr, flags);
}
static inline int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
int main(void)
{
/* Описываем, какие права файловой системы мы хотим контролировать */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE,
};
int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror("landlock_create_ruleset");
return 1;
}
/* Разрешить чтение и выполнение из /usr */
struct landlock_path_beneath_attr path_attr = {
.allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0) < 0) {
perror("landlock_add_rule /usr");
return 1;
}
close(path_attr.parent_fd);
/* Разрешить полный доступ к /tmp */
path_attr.allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE;
path_attr.parent_fd = open("/tmp", O_PATH | O_CLOEXEC);
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0) < 0) {
perror("landlock_add_rule /tmp");
return 1;
}
close(path_attr.parent_fd);
/* Запретить получение новых привилегий через execve */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
perror("prctl PR_SET_NO_NEW_PRIVS");
return 1;
}
/* Применить ruleset к текущему потоку */
if (landlock_restrict_self(ruleset_fd, 0) < 0) {
perror("landlock_restrict_self");
return 1;
}
close(ruleset_fd);
/* Теперь процесс изолирован */
printf("Sandboxed. Trying to read /etc/passwd...\n");
FILE *f = fopen("/etc/passwd", "r");
if (!f) printf("Access denied as expected.\n");
else fclose(f);
return 0;
}
Собрать и запустить:
gcc -o sandbox_demo sandbox_demo.c
./sandbox_demo
# Sandboxed. Trying to read /etc/passwd...
# Access denied as expected.
Ключевой момент, который легко пропустить: prctl(PR_SET_NO_NEW_PRIVS, 1, ...) обязателен перед landlock_restrict_self(), иначе системный вызов вернёт ошибку EPERM. Это намеренное требование ядра, гарантирующее, что sandboxed-процесс не сможет получить новые привилегии через setuid-бинарники после применения политики.
Версионирование ABI и совместимость с разными ядрами
Рекомендуемая практика, использовать подход best-effort: определить версию ABI системы, затем отключить функции, которые не поддерживаются, чтобы программа работала согласованно на разных ядрах. Это не просто рекомендация, а архитектурное требование для любого кода, претендующего на работу в продакшене.
Проверить поддерживаемую версию ABI:
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
perror("Landlock не поддерживается");
/* Деградировать без изоляции или завершить работу */
}
В ABI версии 1 переименование и перемещение файлов между директориями всегда запрещено, поэтому программы, полагающиеся на такие операции, совместимы только с Landlock ABI v2 и выше. Усечение файлов нельзя было ограничить до ABI v3. Поддержка сетевых ограничений появилась только в ABI v4. Ограничение ioctl(2) на устройствах стало возможным с ABI v5, а ограничения scope для сигналов и абстрактных Unix-сокетов появились в ABI v6.
Правильная инициализация с учётом версий:
struct landlock_ruleset_attr attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_REFER | /* ABI >= 2 */
LANDLOCK_ACCESS_FS_TRUNCATE | /* ABI >= 3 */
LANDLOCK_ACCESS_FS_IOCTL_DEV, /* ABI >= 5 */
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP | /* ABI >= 4 */
LANDLOCK_ACCESS_NET_CONNECT_TCP, /* ABI >= 4 */
};
switch (abi) {
case 1:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
/* fallthrough */
case 2:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
/* fallthrough */
case 3:
attr.handled_access_net = 0;
/* fallthrough */
case 4:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV;
/* fallthrough */
default:
break;
}
Наследование домена и стекирование политик
Каждый новый поток, созданный через clone(2), наследует ограничения Landlock-домена от своего родителя. Это поведение аналогично наследованию в seccomp и принципиально отличается от изменений credentials в POSIX-потоках: если один поток применяет Landlock-правила к себе, они не распространяются автоматически на другие потоки того же процесса. Это тонкое, но важное различие при работе с многопоточными приложениями.
Каждый раз, когда поток применяет ruleset к себе, он обновляет свой Landlock-домен новым слоем политики. Sandboxed-поток может безопасно добавлять новые ограничения к самому себе через новый ruleset. Один слой политики открывает доступ к пути, если хотя бы одно из его правил, встреченных на пути, этот доступ разрешает. Sandboxed-поток может получить доступ к пути только если все применённые слои политики одновременно разрешают этот доступ.
На практике это означает следующее: если родительский процесс разрешил чтение из /home/user/data, а дочерний процесс добавил слой, разрешающий только /home/user/data/readonly, итоговый доступ будет пересечением, то есть только /home/user/data/readonly. Ни один дочерний процесс не может расширить то, что ограничил родитель. После того как поток заблокирован через Landlock, нет способа убрать его политику безопасности; допускается только добавление новых ограничений.
Проверить, применены ли ограничения к процессу, можно через /proc:
# Посмотреть LSM-атрибуты процесса
cat /proc/<pid>/attr/current
# Проверить наличие no_new_privs (обязателен для Landlock)
cat /proc/<pid>/status | grep NoNewPrivs
Сетевые правила и scoping для IPC начиная с ABI v4
Начиная с ядра 6.7 и ABI v4, Landlock умеет ограничивать сетевые операции. Синтаксис аналогичен файловым правилам, но вместо файлового дескриптора используется номер порта.
Запретить процессу подключаться к любым TCP-портам, кроме 443:
/* Добавить сетевые права в ruleset_attr */
ruleset_attr.handled_access_net =
LANDLOCK_ACCESS_NET_CONNECT_TCP |
LANDLOCK_ACCESS_NET_BIND_TCP;
/* Разрешить connect только на порт 443 */
struct landlock_net_port_attr net_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
.port = 443,
};
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_NET_PORT, &net_attr, 0) < 0) {
perror("landlock_add_rule TCP 443");
}
Начиная с ABI v6, можно ограничить подключение sandboxed-процесса к абстрактным Unix-сокетам, созданным процессами вне связанного Landlock-домена, а также ограничить отправку сигналов процессам за пределами домена.
Включить IPC-scoping при создании ruleset:
struct landlock_ruleset_attr scoped_attr = {
.handled_access_fs = /* ... файловые права ... */,
.handled_access_net = /* ... сетевые права ... */,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL, /* ABI >= 6 */
};
Аудит отказов и диагностика политик в продакшене
Отказы в доступе по умолчанию логируются для sandboxed-программ, если включён audit. Это принципиально отличает Landlock от некоторых других механизмов изоляции: каждый заблокированный доступ оставляет след, который можно изучить.
Включить аудит и посмотреть события Landlock:
# Убедиться что auditd запущен
systemctl status auditd
# Смотреть события Landlock в реальном времени
auditctl -a always,exit -F arch=b64 \
-S landlock_restrict_self \
-k landlock_events
ausearch -k landlock_events | tail -30
# Или через journald с фильтром
journalctl -k --grep="landlock" | tail -20
Запись типа LANDLOCK_ACCESS показывает заблокированный запрос доступа к ресурсу ядра. Поле domain указывает идентификатор домена, заблокировавшего запрос. Поле blockers указывает причины отказа. Типичная запись в audit-логе выглядит так:
type=LANDLOCK_ACCESS domain=195ba459b blockers=fs.read_file
path="/etc/passwd" dev="sda1" ino=12345
type=LANDLOCK_DOMAIN domain=195ba459b status=allocated
mode=enforcing pid=1234 uid=1000 exe="/usr/bin/myapp"
Поле blockers точно указывает, какое именно право отсутствует в политике. Это делает отладку политик значительно быстрее, чем гадание по кодам ошибок в strace.
Готовые обёртки и интеграция без изменения кода приложения
Не каждое приложение можно перекомпилировать с поддержкой Landlock. Для таких случаев существуют обёртки, применяющие политику извне. Утилита landrun запускает произвольную команду в изолированной среде:
# Установить landrun
go install github.com/Zouuup/landrun/cmd/landrun@latest
# Запустить ls с доступом только к /usr и /home
landrun --rox /usr --rox /lib --ro /home ls /home
# Запустить curl только с доступом к порту 443
landrun --rox /usr --rox /lib \
--tcp-connect 443 \
curl https://example.com
Флаг --rox задаёт директорию в режиме read-only с правом execute, --ro только чтение без выполнения. Если не указать ни одного пути, landrun применяет максимальные ограничения, запрещая весь доступ.
Для Python-приложений существует библиотека python-landlock, позволяющая встраивать политики без написания C-кода:
from landlock import Ruleset, Access
with Ruleset(Access.FS_READ_FILE | Access.FS_READ_DIR) as rs:
rs.add_path("/usr", Access.FS_READ_FILE |
Access.FS_READ_DIR |
Access.FS_EXECUTE)
rs.add_path("/tmp", Access.FS_READ_FILE |
Access.FS_WRITE_FILE |
Access.FS_MAKE_REG)
rs.restrict_self()
# С этой точки процесс изолирован
Landlock занял уникальную нишу в экосистеме безопасности Linux. Там, где SELinux и AppArmor требуют администраторских усилий и централизованного управления политиками, Landlock даёт разработчику прямой рычаг: написать правила прямо в приложении, применить их перед обработкой недоверенных данных, и получить гарантию на уровне ядра, что даже при компрометации приложения атакующий останется в пределах явно очерченного периметра. Это не замена системным политикам безопасности, это дополнительный слой, который стоит ровно столько, сколько стоит добавить три системных вызова в код.
gLandlock LSM и непривилегированная изоляция процессов через правила файлового доступа
Традиционный разговор о песочницах в Linux всегда упирался в один неудобный факт: чтобы ограничить права процесса, нужно иметь права администратора. SELinux требует политик, написанных и загруженных root. AppArmor требует профилей, размещённых в системных директориях. seccomp-фильтры для сложных сценариев требуют привилегий или специальной конфигурации ядра. Получался парадокс: чтобы сделать систему безопаснее, сначала нужно открыть ей дверь с привилегированной стороны.
Landlock сломал этот парадокс. Начиная с ядра Linux 5.13, любой непривилегированный процесс может ограничить самого себя, установив правила доступа к файловым иерархиям, не запрашивая ничего у администратора, не требуя предустановленных профилей и не загружая модулей. Три системных вызова, и процесс живёт в клетке, которую сам себе построил. Снять её невозможно физически, только перезапуск процесса освобождает его от наложенных ограничений.
Как Landlock встраивается в стек безопасности ядра
Цель Landlock заключается в ограничении "ambient rights", то есть глобальных прав доступа к файловой системе, для набора процессов. Но в отличие от SELinux и AppArmor, которые работают как система принудительного контроля доступа с политиками, управляемыми администратором, Landlock является стекируемым LSM. Это означает, что его правила добавляются поверх уже существующих механизмов контроля доступа, не заменяя их и не конфликтуя с ними.
Убедиться, что Landlock активен в работающей системе:
dmesg | grep landlock
# Ожидаемый вывод:
# [ 0.043191] landlock: Up and running.
# Или через journald
journalctl -kb -g landlock
Проверить, что ядро собрано с поддержкой Landlock и включён в список LSM:
grep -E 'LANDLOCK|lsm=' /boot/config-$(uname -r)
# CONFIG_SECURITY_LANDLOCK=y
# Landlock должен присутствовать в параметре lsm=
# Если конфиг не в /boot
zgrep -iE 'landlock|lsm=' /proc/config.gz 2>/dev/null || \
grep -iE 'landlock|lsm=' /lib/modules/$(uname -r)/config
Если Landlock отсутствует в списке lsm=, его нужно добавить через параметры ядра. На системах с grub:
# В /etc/default/grub добавить landlock в параметр lsm
GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,apparmor"
grub2-mkconfig -o /boot/grub2/grub.cfg
Поскольку Landlock является стекируемым LSM, он позволяет создавать безопасные песочницы как новые уровни безопасности в дополнение к существующим системным средствам контроля доступа. Это важная деталь: Landlock не отменяет DAC-проверки файловой системы. Если процесс не имеет прав на файл по стандартным Unix-правам, Landlock не откроет к нему доступ. Он работает только в сторону ограничения, никогда в сторону расширения.
Три системных вызова и полная анатомия создания ruleset
Всё взаимодействие с Landlock сводится к трём системным вызовам. landlock_create_ruleset() создаёт новый ruleset и возвращает файловый дескриптор. landlock_add_rule() добавляет правило в ruleset. landlock_restrict_self() применяет ruleset к вызывающему потоку и с этого момента ограничения вступают в силу навсегда для этого потока и всех его потомков.
Минимальный пример на C, изолирующий процесс с доступом только к /usr и /tmp:
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
static inline int landlock_create_ruleset(
const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
static inline int landlock_add_rule(int ruleset_fd,
enum landlock_rule_type type,
const void *attr, __u32 flags)
{
return syscall(__NR_landlock_add_rule,
ruleset_fd, type, attr, flags);
}
static inline int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
int main(void)
{
/* Описываем, какие права файловой системы мы хотим контролировать */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE,
};
int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror("landlock_create_ruleset");
return 1;
}
/* Разрешить чтение и выполнение из /usr */
struct landlock_path_beneath_attr path_attr = {
.allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0) < 0) {
perror("landlock_add_rule /usr");
return 1;
}
close(path_attr.parent_fd);
/* Разрешить полный доступ к /tmp */
path_attr.allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_REMOVE_FILE;
path_attr.parent_fd = open("/tmp", O_PATH | O_CLOEXEC);
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0) < 0) {
perror("landlock_add_rule /tmp");
return 1;
}
close(path_attr.parent_fd);
/* Запретить получение новых привилегий через execve */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
perror("prctl PR_SET_NO_NEW_PRIVS");
return 1;
}
/* Применить ruleset к текущему потоку */
if (landlock_restrict_self(ruleset_fd, 0) < 0) {
perror("landlock_restrict_self");
return 1;
}
close(ruleset_fd);
/* Теперь процесс изолирован */
printf("Sandboxed. Trying to read /etc/passwd...\n");
FILE *f = fopen("/etc/passwd", "r");
if (!f) printf("Access denied as expected.\n");
else fclose(f);
return 0;
}
Собрать и запустить:
gcc -o sandbox_demo sandbox_demo.c
./sandbox_demo
# Sandboxed. Trying to read /etc/passwd...
# Access denied as expected.
Ключевой момент, который легко пропустить: prctl(PR_SET_NO_NEW_PRIVS, 1, ...) обязателен перед landlock_restrict_self(), иначе системный вызов вернёт ошибку EPERM. Это намеренное требование ядра, гарантирующее, что sandboxed-процесс не сможет получить новые привилегии через setuid-бинарники после применения политики.
Версионирование ABI и совместимость с разными ядрами
Рекомендуемая практика, использовать подход best-effort: определить версию ABI системы, затем отключить функции, которые не поддерживаются, чтобы программа работала согласованно на разных ядрах. Это не просто рекомендация, а архитектурное требование для любого кода, претендующего на работу в продакшене.
Проверить поддерживаемую версию ABI:
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
perror("Landlock не поддерживается");
/* Деградировать без изоляции или завершить работу */
}
В ABI версии 1 переименование и перемещение файлов между директориями всегда запрещено, поэтому программы, полагающиеся на такие операции, совместимы только с Landlock ABI v2 и выше. Усечение файлов нельзя было ограничить до ABI v3. Поддержка сетевых ограничений появилась только в ABI v4. Ограничение ioctl(2) на устройствах стало возможным с ABI v5, а ограничения scope для сигналов и абстрактных Unix-сокетов появились в ABI v6.
Правильная инициализация с учётом версий:
struct landlock_ruleset_attr attr = {
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_REFER | /* ABI >= 2 */
LANDLOCK_ACCESS_FS_TRUNCATE | /* ABI >= 3 */
LANDLOCK_ACCESS_FS_IOCTL_DEV, /* ABI >= 5 */
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP | /* ABI >= 4 */
LANDLOCK_ACCESS_NET_CONNECT_TCP, /* ABI >= 4 */
};
switch (abi) {
case 1:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
/* fallthrough */
case 2:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
/* fallthrough */
case 3:
attr.handled_access_net = 0;
/* fallthrough */
case 4:
attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV;
/* fallthrough */
default:
break;
}
Наследование домена и стекирование политик
Каждый новый поток, созданный через clone(2), наследует ограничения Landlock-домена от своего родителя. Это поведение аналогично наследованию в seccomp и принципиально отличается от изменений credentials в POSIX-потоках: если один поток применяет Landlock-правила к себе, они не распространяются автоматически на другие потоки того же процесса. Это тонкое, но важное различие при работе с многопоточными приложениями.
Каждый раз, когда поток применяет ruleset к себе, он обновляет свой Landlock-домен новым слоем политики. Sandboxed-поток может безопасно добавлять новые ограничения к самому себе через новый ruleset. Один слой политики открывает доступ к пути, если хотя бы одно из его правил, встреченных на пути, этот доступ разрешает. Sandboxed-поток может получить доступ к пути только если все применённые слои политики одновременно разрешают этот доступ.
На практике это означает следующее: если родительский процесс разрешил чтение из /home/user/data, а дочерний процесс добавил слой, разрешающий только /home/user/data/readonly, итоговый доступ будет пересечением, то есть только /home/user/data/readonly. Ни один дочерний процесс не может расширить то, что ограничил родитель. После того как поток заблокирован через Landlock, нет способа убрать его политику безопасности; допускается только добавление новых ограничений.
Проверить, применены ли ограничения к процессу, можно через /proc:
# Посмотреть LSM-атрибуты процесса
cat /proc/<pid>/attr/current
# Проверить наличие no_new_privs (обязателен для Landlock)
cat /proc/<pid>/status | grep NoNewPrivs
Сетевые правила и scoping для IPC начиная с ABI v4
Начиная с ядра 6.7 и ABI v4, Landlock умеет ограничивать сетевые операции. Синтаксис аналогичен файловым правилам, но вместо файлового дескриптора используется номер порта.
Запретить процессу подключаться к любым TCP-портам, кроме 443:
/* Добавить сетевые права в ruleset_attr */
ruleset_attr.handled_access_net =
LANDLOCK_ACCESS_NET_CONNECT_TCP |
LANDLOCK_ACCESS_NET_BIND_TCP;
/* Разрешить connect только на порт 443 */
struct landlock_net_port_attr net_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
.port = 443,
};
if (landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_NET_PORT, &net_attr, 0) < 0) {
perror("landlock_add_rule TCP 443");
}
Начиная с ABI v6, можно ограничить подключение sandboxed-процесса к абстрактным Unix-сокетам, созданным процессами вне связанного Landlock-домена, а также ограничить отправку сигналов процессам за пределами домена.
Включить IPC-scoping при создании ruleset:
struct landlock_ruleset_attr scoped_attr = {
.handled_access_fs = /* ... файловые права ... */,
.handled_access_net = /* ... сетевые права ... */,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL, /* ABI >= 6 */
};
Аудит отказов и диагностика политик в продакшене
Отказы в доступе по умолчанию логируются для sandboxed-программ, если включён audit. Это принципиально отличает Landlock от некоторых других механизмов изоляции: каждый заблокированный доступ оставляет след, который можно изучить.
Включить аудит и посмотреть события Landlock:
# Убедиться что auditd запущен
systemctl status auditd
# Смотреть события Landlock в реальном времени
auditctl -a always,exit -F arch=b64 \
-S landlock_restrict_self \
-k landlock_events
ausearch -k landlock_events | tail -30
# Или через journald с фильтром
journalctl -k --grep="landlock" | tail -20
Запись типа LANDLOCK_ACCESS показывает заблокированный запрос доступа к ресурсу ядра. Поле domain указывает идентификатор домена, заблокировавшего запрос. Поле blockers указывает причины отказа. Типичная запись в audit-логе выглядит так:
type=LANDLOCK_ACCESS domain=195ba459b blockers=fs.read_file
path="/etc/passwd" dev="sda1" ino=12345
type=LANDLOCK_DOMAIN domain=195ba459b status=allocated
mode=enforcing pid=1234 uid=1000 exe="/usr/bin/myapp"
Поле blockers точно указывает, какое именно право отсутствует в политике. Это делает отладку политик значительно быстрее, чем гадание по кодам ошибок в strace.
Готовые обёртки и интеграция без изменения кода приложения
Не каждое приложение можно перекомпилировать с поддержкой Landlock. Для таких случаев существуют обёртки, применяющие политику извне. Утилита landrun запускает произвольную команду в изолированной среде:
# Установить landrun
go install github.com/Zouuup/landrun/cmd/landrun@latest
# Запустить ls с доступом только к /usr и /home
landrun --rox /usr --rox /lib --ro /home ls /home
# Запустить curl только с доступом к порту 443
landrun --rox /usr --rox /lib \
--tcp-connect 443 \
curl https://example.com
Флаг --rox задаёт директорию в режиме read-only с правом execute, --ro только чтение без выполнения. Если не указать ни одного пути, landrun применяет максимальные ограничения, запрещая весь доступ.
Для Python-приложений существует библиотека python-landlock, позволяющая встраивать политики без написания C-кода:
from landlock import Ruleset, Access
with Ruleset(Access.FS_READ_FILE | Access.FS_READ_DIR) as rs:
rs.add_path("/usr", Access.FS_READ_FILE |
Access.FS_READ_DIR |
Access.FS_EXECUTE)
rs.add_path("/tmp", Access.FS_READ_FILE |
Access.FS_WRITE_FILE |
Access.FS_MAKE_REG)
rs.restrict_self()
# С этой точки процесс изолирован
Landlock занял уникальную нишу в экосистеме безопасности Linux. Там, где SELinux и AppArmor требуют администраторских усилий и централизованного управления политиками, Landlock даёт разработчику прямой рычаг: написать правила прямо в приложении, применить их перед обработкой недоверенных данных, и получить гарантию на уровне ядра, что даже при компрометации приложения атакующий останется в пределах явно очерченного периметра. Это не замена системным политикам безопасности, это дополнительный слой, который стоит ровно столько, сколько стоит добавить три системных вызова в код.