Landlock LSM встроен в ядро Linux начиная с 5.13 и разрешает непривилегированному процессу самому ограничить доступ к файловой системе. Для этого не нужны права root, глобальные политики или контейнер. Процесс создаёт ruleset, добавляет правила для каталогов и применяет их к себе через landlock_restrict_self. После вызова он и его потомки видят только те пути и операции, которые вы явно разрешили.
В руководстве разберём проверку поддержки в ядре, модель прав, минимальный код на C и Python, запуск через systemd и диагностику ошибок. Материал построен как пошаговая инструкция, чтобы вы могли внедрить Landlock в рабочую среду без поломки уже работающих сервисов.
Если вы параллельно усиливаете защиту Linux-сервера, посмотрите практический hardening и аудит Linux с чек-листом и сканерами уязвимостей.
Зачем нужен Landlock LSM: изоляция файловой системы без root и контейнеров
Landlock закрывает узкую задачу: изолирует доступ отдельного процесса к файловой системе изнутри. Это self-restriction: процесс сам ограничивает себя, поэтому не может расширить права после применения.
Landlock, AppArmor, seccomp и контейнеры: в чём разница
| Механизм | Требует root | Объект контроля | Зона действия | Накладные расходы |
|---|---|---|---|---|
| Landlock | нет | пути и файловые операции | процесс и потомки | минимальные |
| AppArmor/SELinux | да | глобальные политики, пути, сеть, capabilities | система или профиль | низкие |
| seccomp | нет | системные вызовы, без понимания путей | процесс и потомки | минимальные |
| Контейнеры | обычно да | namespaces, cgroups, файловая система, сеть | полная среда | выше |
Landlock подходит для точечной изоляции обработчиков загрузки, утилит конвертации и тестовых скриптов. Он не управляет сетью, CPU и памятью, но закрывает доступ к файлам и каталогам.
Ограничения Landlock, которые важно знать до внедрения
- Landlock управляет только файловой системой. Сеть, память и CPU остаются без ограничений.
- После
landlock_restrict_selfправа можно только ужесточать. Повторный вызов не вернёт доступ. - Набор прав зависит от ABI ядра. Например,
LANDLOCK_ACCESS_FS_TRUNCATEиLANDLOCK_ACCESS_FS_REFERподдерживаются не во всех ядрах. - Правила строятся по иерархии каталогов
path_beneath. Файл вне явно разрешённой иерархии недоступен.
Landlock не заменяет полноценную песочницу. Используйте его как инструмент точечного контроля доступа к файловой системе.
Проверка поддержки Landlock в ядре и дистрибутиве
Перед написанием кода проверьте три условия: версия ядра, конфигурация CONFIG_SECURITY_LANDLOCK и наличие системного вызова.
Минимальные версии ядра и конфигурация
Минимальное ядро Linux 5.13. В RHEL 9, Ubuntu 22.04 и Debian 12 поддержка обычно включена, но проверка обязательна.
uname -r
grep LANDLOCK /boot/config-$(uname -r)
cat /sys/kernel/security/lsm
Если файла /boot/config нет, выполните zcat /proc/config.gz | grep LANDLOCK или загрузите модуль configs командой modprobe configs. Список активных LSM в /sys/kernel/security/lsm должен содержать landlock.
Проверка доступности системного вызова
Мини-тест на C вызывает landlock_create_ruleset с нулевыми аргументами. Если syscall существует, вернётся дескриптор или другая ошибка, но не ENOSYS.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
int main(void) {
int fd = syscall(SYS_landlock_create_ruleset, NULL, 0, 0);
if (fd < 0) {
if (errno == ENOSYS) {
puts(\"Landlock syscall отсутствует\");
return 1;
}
printf(\"syscall есть, ошибка: %s\\n\", strerror(errno));
return 0;
}
close(fd);
puts(\"Landlock доступен\");
return 0;
}
Модель правил Landlock: rights, layers и path_beneath
Ruleset определяет, какие файловые операции Landlock будет контролировать. Если право не включено в handled_access_fs, Landlock его не ограничивает. Правила path_beneath задают разрешённые действия в иерархии каталогов. Слои позволяют комбинировать ограничения от нескольких источников.
Права доступа к файловой системе и их значение
LANDLOCK_ACCESS_FS_READ_FILE: чтение содержимого файлаLANDLOCK_ACCESS_FS_WRITE_FILE: запись в файлLANDLOCK_ACCESS_FS_EXECUTE: выполнение файлаLANDLOCK_ACCESS_FS_READ_DIR: чтение содержимого каталогаLANDLOCK_ACCESS_FS_REMOVE_DIR/LANDLOCK_ACCESS_FS_REMOVE_FILE: удаление каталога или файлаLANDLOCK_ACCESS_FS_MAKE_REG/LANDLOCK_ACCESS_FS_MAKE_DIR/LANDLOCK_ACCESS_FS_MAKE_SOCK: создание объектовLANDLOCK_ACCESS_FS_REFER: связывание или переименованиеLANDLOCK_ACCESS_FS_TRUNCATE: усечение файла
Перед добавлением TRUNCATE или REFER сверяйтесь с ABI версией ядра. На старых ядрах вызов вернёт EINVAL.
Как правила наследуются и почему нельзя ослабить ограничения
Первый вызов landlock_restrict_self включает ограничения для текущего процесса и всех дочерних. Повторные вызовы только ужесточают. Если ruleset запретил запись в /home, последующая попытка добавить правило записи в /home не вернёт доступ. Планируйте права всего процесса заранее.
Минимальный рабочий ruleset на C: ограничиваем доступ к каталогу
Пример создаёт ruleset, разрешает чтение из /var/lib/myapp и выполнение из /usr/bin, затем применяет ограничения и запускает оболочку.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>
static int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, __u32 flags) {
return syscall(SYS_landlock_create_ruleset, attr, size, flags);
}
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, const void *rule_attr, __u32 flags) {
return syscall(SYS_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
}
static int landlock_restrict_self(int ruleset_fd, __u32 flags) {
return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
}
int main(void) {
struct landlock_ruleset_attr ruleset_attr = {0};
struct landlock_path_beneath_attr path_attr = {0};
int ruleset_fd, fd;
__u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_EXECUTE;
ruleset_attr.handled_access_fs = handled;
ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror(\"landlock_create_ruleset\");
return 1;
}
fd = open(\"/var/lib/myapp\", O_PATH | O_CLOEXEC);
if (fd < 0) {
perror(\"open /var/lib/myapp\");
close(ruleset_fd);
return 1;
}
path_attr.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG;
path_attr.parent_fd = fd;
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_attr, 0) != 0) {
perror(\"landlock_add_rule\");
close(fd);
close(ruleset_fd);
return 1;
}
close(fd);
if (landlock_restrict_self(ruleset_fd, 0) != 0) {
perror(\"landlock_restrict_self\");
close(ruleset_fd);
return 1;
}
close(ruleset_fd);
execl(\"/bin/sh\", \"sh\", NULL);
perror(\"execl\");
return 1;
}
Компиляция и запуск тестового приложения
gcc -Wall -o landlock-demo landlock-demo.c
strace -e trace=landlock_restrict_self,openat ./landlock-demo
После запуска попробуйте прочитать файл в разрешённом каталоге и в /etc. Первое действие выполнится, второе вернёт EACCES.
Разбор кода: от ruleset_attr до restrict_self
struct landlock_ruleset_attr: размерsizeof(struct landlock_ruleset_attr)сообщает ядру ABI-версию. Передавайте его вlandlock_create_ruleset.landlock_add_ruleсLANDLOCK_RULE_PATH_BENEATH:parent_fdобязан быть открыт сO_PATH.landlock_restrict_selfcflags=0: применяет ruleset. Вызывайте доexec, чтобы правила унаследовались потомками.- Ошибка
EINVALобычно указывает на неверный размер ABI или неподдерживаемое право.E2BIGпоявляется при слишком большом наборе правил.
Landlock в Python: тот же сценарий без компиляции C
Для Python достаточно ctypes. Определяем структуры и вызываем syscalls напрямую.
import ctypes
import os
libc = ctypes.CDLL(None, use_errno=True)
class LandlockRulesetAttr(ctypes.Structure):
_fields_ = [("handled_access_fs", ctypes.c_uint64)]
class LandlockPathBeneathAttr(ctypes.Structure):
_fields_ = [("allowed_access", ctypes.c_uint64), ("parent_fd", ctypes.c_int)]
LANDLOCK_RULE_PATH_BENEATH = 1
LANDLOCK_ACCESS_FS_EXECUTE = 1 << 0
LANDLOCK_ACCESS_FS_WRITE_FILE = 1 << 1
LANDLOCK_ACCESS_FS_READ_FILE = 1 << 2
LANDLOCK_ACCESS_FS_READ_DIR = 1 << 3
LANDLOCK_ACCESS_FS_MAKE_REG = 1 << 8
LANDLOCK_ACCESS_FS_TRUNCATE = 1 << 14
SYS_landlock_create_ruleset = 444
SYS_landlock_add_rule = 445
SYS_landlock_restrict_self = 446
def restrict(catalog="/tmp/allowed"):
ruleset_attr = LandlockRulesetAttr()
ruleset_attr.handled_access_fs = (
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG
)
ruleset_fd = libc.syscall(SYS_landlock_create_ruleset, ctypes.byref(ruleset_attr), ctypes.sizeof(ruleset_attr), 0)
if ruleset_fd < 0:
err = ctypes.get_errno()
raise OSError(err, os.strerror(err))
fd = os.open(catalog, os.O_PATH | os.O_CLOEXEC)
path_attr = LandlockPathBeneathAttr()
path_attr.allowed_access = (
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG
)
path_attr.parent_fd = fd
rc = libc.syscall(SYS_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, ctypes.byref(path_attr), 0)
os.close(fd)
if rc != 0:
err = ctypes.get_errno()
raise OSError(err, os.strerror(err))
rc = libc.syscall(SYS_landlock_restrict_self, ruleset_fd, 0)
if rc != 0:
err = ctypes.get_errno()
raise OSError(err, os.strerror(err))
os.close(ruleset_fd)
print("Landlock applied")
if __name__ == "__main__":
restrict()
print(open("/tmp/allowed/test.txt").read())
try:
print(open("/etc/passwd").read())
except PermissionError as e:
print("Blocked:", e)
Если syscall номера на вашей архитектуре отличаются, сверьтесь с /usr/include/asm/unistd_64.h. Для готовой библиотеки подойдёт python-landlock.
Типовые ошибки Python-обёртки и их устранение
- Неверный размер структуры из-за отсутствия полей. Используйте
ctypes.sizeof, а не ручной расчёт. - Передача обычного
fdвместоO_PATH.parent_fdобязан быть открыт сO_PATH. - Неподдерживаемые права на старых ядрах. Не добавляйте
TRUNCATEилиREFERбез проверки ABI.
Интеграция Landlock с systemd: изоляция сервисов без контейнеров
systemd напрямую не управляет правилами Landlock. Практичный путь: написать обёртку landlock-enforce, которая читает профиль, создаёт ruleset, вызывает landlock_restrict_self и запускает приложение через execvp.
Обёртка для запуска сервисов с Landlock
Обёртка должна быть простой и идемпотентной. Она принимает профиль и команду. Пример профиля /etc/landlock/myapp.json:
{
"handled_access_fs": ["READ_FILE", "WRITE_FILE", "READ_DIR", "MAKE_REG", "EXECUTE"],
"rules": [
{"path": "/var/lib/myapp", "access": ["READ_FILE", "READ_DIR", "WRITE_FILE", "MAKE_REG"]},
{"path": "/usr/bin", "access": ["EXECUTE", "READ_FILE"]},
{"path": "/etc", "access": ["READ_FILE", "READ_DIR"]}
]
}
Логика обёртки: разобрать профиль, создать ruleset с поддержкой ABI, добавить правила, вызвать restrict_self, затем execvp.
Минимальный unit-файл с Landlock-изоляцией
[Service]
User=myapp
NoNewPrivileges=yes
ExecStart=/usr/local/bin/landlock-enforce --profile=/etc/landlock/myapp.json -- /usr/bin/app
ProtectSystem=strict
RestrictSUIDSGID=yes
NoNewPrivileges=yes запрещает эскалацию привилегий, которая могла бы обойти ограничения. Опции ProtectSystem=strict и ReadOnlyPaths добавляют защиту на уровне systemd. Если сервис получает пароли или токены, безопаснее передавать их через systemd credentials, как показано в руководстве по управлению секретами в systemd.
Диагностика и типичные ошибки Landlock
При внедрении чаще всего встречаются ENOSYS, EINVAL, E2BIG, EACCES и EPERM.
Таблица ошибок: причина и решение
| errno | Симптом | Вероятная причина | Решение |
|---|---|---|---|
| ENOSYS | syscall отсутствует | ядро ниже 5.13 или LSM не включён | обновить ядро, включить CONFIG_SECURITY_LANDLOCK |
| EINVAL | create/add/restrict вернул -1 | неверный размер ABI, неподдерживаемое право, неверный flags | передавать sizeof struct, убрать TRUNCATE/REFER, проверить ABI |
| E2BIG | add_rule или restrict_self | слишком много правил или слоёв | уменьшить число правил |
| EACCES | операция с файлом вернула EACCES | доступ запрещён правилами | проверить allowed_access и path_beneath |
| EPERM | restrict_self | процесс уже ограничен или seccomp блокирует syscall | проверить порядок вызовов |
Отладка с strace и проверка активных ограничений
strace -f -e trace=landlock_create_ruleset,landlock_add_rule,landlock_restrict_self,openat ./app
Убедитесь, что landlock_restrict_self вернул 0. Тестовое чтение запрещённого пути должно дать EACCES. Проверяйте в отдельном подпроцессе, чтобы не ограничить текущую сессию.
Поэтапное внедрение Landlock в рабочую среду без поломок
Начинайте с read-only сценария, затем добавляйте запись. В staging сравнивайте поведение сервиса с ограничениями и без.
Сценарий: изолируем обработчик загрузок
Обработчику загрузок разрешён /var/upload с READ_FILE|WRITE_FILE|READ_DIR|MAKE_REG, чтение /usr/share/app, запрещены /etc, /home, /root. Проверьте: загрузка файла работает, чтение /etc/passwd не работает, создание файла в /home не работает.
Для тестов можно поднять отдельный VDS в Timeweb Cloud и прогнать сценарий в изолированной среде, не трогая боевой сервер.
Контрольный список перед деплоем
- Проверено ядро и ABI:
uname -r,grep LANDLOCK /boot/config-$(uname -r) - Код протестирован в staging с включёнными strace-логами
- Подготовлен откат к предыдущему unit-файлу
- Дочерние процессы не требуют больше прав, чем у родителя
- Права описаны заранее, повторный
landlock_restrict_selfне добавляет доступ - Если доступ пользователей и групп задаётся через LDAP, сверьтесь с руководством по RBAC через LDAP