Landlock LSM в Linux: ограничиваем доступ приложений к файловой системе без root и контейнеров | AdminWiki

Landlock LSM в Linux: ограничиваем доступ приложений к файловой системе без root и контейнеров

28 августа 2026 8 мин. чтения
Содержание статьи

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_self c flags=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СимптомВероятная причинаРешение
ENOSYSsyscall отсутствуетядро ниже 5.13 или LSM не включёнобновить ядро, включить CONFIG_SECURITY_LANDLOCK
EINVALcreate/add/restrict вернул -1неверный размер ABI, неподдерживаемое право, неверный flagsпередавать sizeof struct, убрать TRUNCATE/REFER, проверить ABI
E2BIGadd_rule или restrict_selfслишком много правил или слоёвуменьшить число правил
EACCESоперация с файлом вернула EACCESдоступ запрещён правиламипроверить allowed_access и path_beneath
EPERMrestrict_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
Поделиться:
Сохранить гайд? В закладки браузера