В этой статье поговорим о том, как использовать API Landlock для защиты Linux-приложений, ограничивая доступ к файловой системе и сети.

Два часа ночи. Вас будит уведомление: хакер нашёл уязвимость в вашем приложении и теперь может украсть учётные данные для доступа к системам ваших клиентов. Как предотвратить такой сценарий?

До сих пор у нас был только вариант защищать приложения со стороны системы, используя решения вроде SELinux или AppArmor. Мы также можем настраивать права пользователей и групп или фильтровать системные вызовы через seccomp. А что, если разработчики могли бы управлять правами приложения самостоятельно?

Здесь и появляется Landlock — механизм безопасности ядра Linux, который позволяет приложениям добровольно ограничивать собственные привилегии. Без root и без сложной настройки. И очень просто: всего три новых системных вызова.

Что такое Landlock?

Из документации ядра Linux:

Landlock — это модуль безопасности Linux (LSM), который позволяет непривилегированным процессам добровольно ограничивать свои права доступа.

Landlock — модуль безопасности Linux, появившийся в ядре 5.13, который позволяет обычным процессам добровольно ограничивать свой доступ к системным ресурсам. Он использует контроль доступа на основе путей, то есть вы точно задаёте, какие пути и какие операции разрешены.

Зачем нужен Landlock?

Основные преимущества:

  1. Работает без root

  2. Нужно освоить всего 3 системных вызова:
    landlock_create_ruleset() — создаёт набор правил (ruleset)
    landlock_add_rule() — добавляет конкретные правила доступа
    landlock_restrict_self() — применяет ограничения к процессу

  3. Приложения работают и на старых ядрах (с ослабленной защитой или вовсе без неё)

  4. Используется в systemd, Chromium, Pacman

Landlock работает по принципу «запрещено по умолчанию»: по умолчанию блокируется всё. Вы должны явно указать, что разрешено. Это противоположно обычному поведению Unix, где всё разрешено, пока вы это не запретите.

Чтобы защитить приложение, нужно сделать три шага:

  1. Создать набор правил → Определить, какие типы доступа вы хотите контролировать.

    - Handled_access — какие права вы вообще хотите контролировать (например, чтение, запись).

    2. Добавить правила → Указать конкретные пути и разрешённые операции.

    - Allowed_access — что именно разрешено для заданного пути

    3. Применить правила → Включить ограничения для процесса.

    - Правила начинают применяться после landlock_restrict_self(), а ограничения наследуются дочерними процессами (fork, exec).

Псевдокод:

// Хочу контролировать чтение и запись
ruleset = create_ruleset(READ | WRITE);

// /usr можно читать
add_rule(ruleset, "/usr", READ);

// /tmp можно читать и писать
add_rule(ruleset, "/tmp", READ | WRITE);

// Всё остальное: заблокировано!
restrict_self(ruleset);

После restrict_self() процесс (и его дочерние процессы) может только читать из /usr и читать/писать в /tmp. Любая попытка обратиться к /home или /etc завершится ошибкой EACCES.

Давайте соберём песочницу

Приложение позволяет запускать программы из командной строки и задавать разрешения через конфигурационный файл. Полный код доступен здесь: Landlock — Sandbox. В main мы загружаем конфигурацию и параметры, переданные через командную строку. Затем создаём набор правил, добавляем разрешения для путей и сети и активируем Landlock. Наконец, запускаем программу, для которой хотим ограничить доступ.

// sandbox.cpp
int main(int argc, char* argv[], char *const *const envp) {
    ...
    landlock.create_ruleset(fs_restrictions, net_restrictions);

    for (auto& it : path_perms) {
        landlock.add_rule(it.first, it.second);
    }
    ...
    if (net_port >= 0) {
        landlock.add_net_rule(static_cast<__u64>(net_port), net_permissions);
    ...
    landlock.restrict_self(no_new_priv);
    ...
    execvpe(cmd_args_c[0], cmd_args_c.data(), envp);
}

Теперь можно перейти к файлу landlock.cpp и разобрать реализацию. Сначала нам нужен заголовок landlock.h.

#include <linux/landlock.h>

Ниже приведены все доступные права вплоть до ABI v5. Рекомендую прочитать комментарии в заголовке landlock.h — там отлично описаны все символы.

...
static const std::map<std::string, __u64> LANDLOCK_FS_MAP = {
    {"execute",         LANDLOCK_ACCESS_FS_EXECUTE},
    {"read_file",       LANDLOCK_ACCESS_FS_READ_FILE},
    {"write_file",      LANDLOCK_ACCESS_FS_WRITE_FILE},
    {"read_dir",        LANDLOCK_ACCESS_FS_READ_DIR},
    {"remove_dir",      LANDLOCK_ACCESS_FS_REMOVE_DIR},
    {"remove_file",     LANDLOCK_ACCESS_FS_REMOVE_FILE},
    {"make_char",       LANDLOCK_ACCESS_FS_MAKE_CHAR},
    {"make_dir",        LANDLOCK_ACCESS_FS_MAKE_DIR},
    {"make_reg",        LANDLOCK_ACCESS_FS_MAKE_REG},
    {"make_sock",       LANDLOCK_ACCESS_FS_MAKE_SOCK},
    {"make_fifo",       LANDLOCK_ACCESS_FS_MAKE_FIFO},
    {"make_block",      LANDLOCK_ACCESS_FS_MAKE_BLOCK},
    {"make_sym",        LANDLOCK_ACCESS_FS_MAKE_SYM},

    /* ABI v2 */
    {"refer",           LANDLOCK_ACCESS_FS_REFER},

    /* ABI v3 */
    {"truncate",        LANDLOCK_ACCESS_FS_TRUNCATE},

    /* ABI v5 */
    {"ioctl_dev",       LANDLOCK_ACCESS_FS_IOCTL_DEV},
};

static const std::map<std::string, __u64> LANDLOCK_NET_MAP = {
    /* ABI v4 */
    {"bind_tcp",         LANDLOCK_ACCESS_NET_BIND_TCP},
    {"connect_tcp",       LANDLOCK_ACCESS_NET_CONNECT_TCP},
};

Далее у нас есть три новых системных вызова: landlock_create_ruleset, landlock_add_rule, landlock_restrict_self.

...
static inline int sys_create_ruleset(
    const struct landlock_ruleset_attr *attr,
    size_t attr_size,
    __u32 flags
) {
    return syscall(__NR_landlock_create_ruleset, attr, attr_size, flags);
}

static inline int sys_add_rule(
    int ruleset_fd,
    enum landlock_rule_type rule_type,
    const void *rule_attr,
    __u32 flags
) {
    return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
}

static inline int sys_restrict_self(
    int ruleset_fd,
    __u32 flags
) {
    return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}

Чтобы создать новый набор правил, необходимо передать структуру landlock_ruleset_attr, содержащую битовые маски прав доступа, которые должны контролироваться по умолчанию. В зависимости от версии ABI доступны поля для прав доступа к файловой системе и сети. Важный шаг — адаптация прав под конкретную версию ABI. Системный вызов без параметров возвращает доступную версию ABI.

...
int Landlock::create_ruleset(
        const std::vector<std::string>& fs_restr,
        const std::vector<std::string>& net_restr) {
        ...
            struct landlock_ruleset_attr ruleset_attr = {
        .handled_access_fs = fs_access,
        .handled_access_net = net_access,
    };

    int abi = sys_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (abi < 0) {
        /* Корректная деградация, если Landlock не поддерживается. */
        std::cerr << "Текущее ядро не поддерживает API Landlock\n";
        return 0;
    }

    switch (abi) {
    case 1:
        /* Удаляем LANDLOCK_ACCESS_FS_REFER для ABI < 2 */
        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
        __attribute__((fallthrough));
    case 2:
        /* Удаляем LANDLOCK_ACCESS_FS_TRUNCATE для ABI < 3 */
        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
        __attribute__((fallthrough));
    case 3:
        /* Удаляем поддержку сети для ABI < 4 */
        ruleset_attr.handled_access_net &=
            ~(LANDLOCK_ACCESS_NET_BIND_TCP |
            LANDLOCK_ACCESS_NET_CONNECT_TCP);
        __attribute__((fallthrough));
    case 4:
        /* Удаляем LANDLOCK_ACCESS_FS_IOCTL_DEV для ABI < 5 */
        #ifdef LANDLOCK_ACCESS_FS_IOCTL_DEV
        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV;
        #endif
        break;
    }

    ruleset_fd = sys_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
    ...
}

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

int Landlock::add_rule(
        const std::string path,
        std::vector<std::string>& fs_perms) {
    int path_fd;
    struct landlock_path_beneath_attr path_beneath = {0};

    if (open_path(path, path_fd) < 0) {
        return -1;
    }

    __u64 fs_access = make_allowed_mask(fs_perms);
    path_beneath.parent_fd = path_fd;
    path_beneath.allowed_access = fs_access;

    if (sys_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0) < 0)
    ...
}

Сетевые разрешения добавляются в набор правил.

sys_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, &net_port, 0)

Наконец, мы вызываем landlock_restrict_self. Этот системный вызов вернёт ошибку, если для процесса не установлен флаг PR_SET_NO_NEW_PRIVS. Без этого флага процесс может повысить свои привилегии уже после применения ограничений, например запустив бинарник с флагом setuid.

...
int Landlock::restrict_self(bool no_new_privs) {
    /* Устанавливаем no_new_privs (обязательно перед landlock_restrict_self) */
    ...
        if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
    ...
    /* Применяем набор правил */
    if (sys_restrict_self(ruleset_fd, 0) < 0)
    ...
}

Как встроить Landlock в приложение

Стратегия выбора разрешений:

  • Начните с минимума: только READ_FILE + READ_DIR

  • Постепенно добавляйте права по мере необходимости приложению

  • Проверьте, что всё работает с включёнными ограничениями

  • Зафиксируйте в документации, зачем нужно каждое разрешение

Версии ABI

Landlock развивался в нескольких версиях ядра. Каждая версия ABI добавляет новые возможности:

ABI

Ядро

Ключевая возможность

1

5.13

Базовые ограничения доступа к файловой системе

2

5.19

REFER — контроль операций, создающих/перемещающих ссылки на объекты (link/rename и связанные операции)

3

6.2

TRUNCATE — контроль усечения файлов

4

6.7

Сеть — контроль bind/connect для TCP

5

6.10

IOCTL_DEV — контроль ioctl на устройствах

6

6.12

IPC — сигналы и абстрактные сокеты

Ловушка файловых дескрипторов

Файловый дескриптор, открытый до применения ограничений, остаётся доступным.

int fd = open("/etc/passwd", O_RDONLY);  // Открываем ДО включения песочницы
apply_landlock_sandbox();
// Это по-прежнему работает! Дескриптор уже открыт
char buf[1024];
read(fd, buf, sizeof(buf));

Решение: закрыть все дескрипторы перед вызовом restrict_self или использовать флаг O_CLOEXEC при вызовах open*.

Как протестировать приложение?

Всё просто. Скомпилируйте приложение, доступное здесь: Landlock — Sandbox. Проверьте, как настройки влияют на поведение программы.

Приложение vulnerable_server.py позволяет выполнять команды над системными файлами, что упрощает изучение поведения API.

./sandbox -- python3 vulnerable_server.py

Поддерживает ли моё ядро Landlock?

Проверьте на своей системе:

dmesg | grep landlock
uname -r

Подведем итоги

Landlock меняет подход к безопасности Linux-приложений, позволяя разработчикам напрямую задавать ограничения доступа к путям и сокетам. Развитие Landlock продолжается, и в будущем ожидаются новые улучшения.

Надеюсь, материал был полезен. Спасибо за прочтение!

Документация и ресурсы:

Если вы только заходите в Linux и хотите разобраться с нуля, важно построить базу: файловая модель, права, процессы, сеть, инструменты диагностики. На специализации Administrator Linux эти основы раскладывают по полочкам и доводят до практики — чтобы потом уверенно применять вещи вроде Landlock, а не копировать команды вслепую. Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:

  • 26 февраля 20:00. «С Windows на Linux: первый шаг системного администратора». Записаться

  • 4 марта 20:00. «GREP и другие регулярные выражения Linux». Записаться

  • 12 марта 20:00. «Перенаправление потоков в Linux». Записаться

Для тех, кто хочет быстро подтянуть основы, рекомендуем мини-видеокурс «Linux для начинающих», сейчас всего за 10 рублей.