Landlock LSM trên Linux: Sandbox ứng dụng không cần root với tính năng bảo mật kernel tích hợp

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

Bối cảnh: Khi SELinux vẫn chưa đủ tiện

Mình đã dùng SELinux trên Fedora server được khoảng 2 năm, và phải thú nhận — viết policy cho từng ứng dụng tốn thời gian khủng khiếp. AppArmor dễ hơn nhưng vẫn cần root để deploy profile. Điểm chung của cả hai: muốn sandbox ứng dụng mà không có root, thì bó tay.

Landlock LSM ra đời từ kernel 5.13 và đánh đúng vào điểm đó — ứng dụng có thể tự sandbox chính nó, không cần admin, không cần policy file, không cần cài thêm gì. Toàn bộ nằm sẵn trong kernel.

Mình bắt đầu nghiêm túc với Landlock sau một incident nhỏ — script Python bị khai thác, cố đọc /etc/passwd. Server đang quản lý là Ubuntu 22.04, 4GB RAM. Bật thêm auditd, Landlock denial hiện ngay trong log — process nào vừa thử chạm file ngoài vùng cho phép, không cần đoán mò. Nửa năm vận hành, đây là phần đáng ghi lại nhất.

Kiểm tra kernel có hỗ trợ Landlock không

Trước khi code bất cứ thứ gì, kiểm tra kernel có bật Landlock không. Không phải distro nào cũng compile sẵn.

# Kernel phải >= 5.13
uname -r

# Kiểm tra Landlock được compile vào kernel
grep LANDLOCK /boot/config-$(uname -r)
# Cần thấy: CONFIG_SECURITY_LANDLOCK=y

# Kiểm tra ABI version đang active
cat /sys/kernel/security/landlock/abi
# Output: 1, 2, 3, hoặc 4 — số càng cao càng nhiều tính năng

ABI v1 (kernel 5.13) và v2 (kernel 5.19) xử lý filesystem restriction cơ bản. V3 (kernel 6.2) thêm giới hạn truncate. Muốn sandbox cả TCP connection thì cần ABI v4 từ kernel 6.7 trở lên. Ubuntu 22.04 với kernel HWE 5.15 dừng ở v2 — đủ cho phần lớn những gì bài này đề cập.

Nâng kernel để có ABI cao hơn

# Upgrade lên HWE stack mới nhất trên Ubuntu 22.04 (kernel 6.8 → ABI v4)
sudo apt install linux-generic-hwe-22.04
sudo reboot

# Xác nhận sau reboot
cat /sys/kernel/security/landlock/abi

Cấu hình Landlock chi tiết

Ba syscall tạo nên toàn bộ cơ chế: landlock_create_ruleset, landlock_add_rule, và landlock_restrict_self. Điều quan trọng nhất — gọi restrict_self xong là sandbox active ngay và không thể revoke. Ngay cả process đó cũng không gỡ ra được nữa.

Ví dụ 1: Sandbox Python script bằng ctypes

Cách này không cần thư viện ngoài, chạy được trên bất kỳ Python 3.8+ nào:

#!/usr/bin/env python3
"""
Landlock sandbox — giới hạn file access không cần root.
Dùng ctypes gọi trực tiếp Linux syscall.
"""
import ctypes
import ctypes.util
import os
import struct

# Access flags (từ linux/landlock.h)
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)

# Syscall numbers (x86_64)
NR_LANDLOCK_CREATE_RULESET = 444
NR_LANDLOCK_ADD_RULE        = 445
NR_LANDLOCK_RESTRICT_SELF   = 446
PR_SET_NO_NEW_PRIVS         = 38

libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)


def _create_ruleset(handled_access_fs):
    attr = struct.pack("=II", handled_access_fs, 0)
    buf = ctypes.create_string_buffer(attr)
    fd = libc.syscall(NR_LANDLOCK_CREATE_RULESET, buf, len(attr), 0)
    if fd < 0:
        raise OSError(ctypes.get_errno(), "landlock_create_ruleset failed")
    return fd


def _add_path_rule(ruleset_fd, path, allowed_access):
    path_fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
    try:
        attr = struct.pack("=QI4x", allowed_access, path_fd)
        buf = ctypes.create_string_buffer(attr)
        ret = libc.syscall(NR_LANDLOCK_ADD_RULE, ruleset_fd, 1, buf, 0)
        if ret < 0:
            raise OSError(ctypes.get_errno(), f"add_rule failed: {path}")
    finally:
        os.close(path_fd)


def apply_sandbox(read_paths=None, write_paths=None):
    """
    Áp dụng Landlock sandbox cho process hiện tại.
    Sau khi gọi, mọi truy cập file ngoài danh sách đều bị block.
    """
    # Bắt buộc: không cho phép leo thang đặc quyền
    if libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0:
        raise OSError("prctl PR_SET_NO_NEW_PRIVS failed")

    handled = (
        LANDLOCK_ACCESS_FS_EXECUTE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_READ_DIR
    )

    ruleset_fd = _create_ruleset(handled)
    try:
        ro = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR
        rw = ro | LANDLOCK_ACCESS_FS_WRITE_FILE

        for path in (read_paths or []):
            _add_path_rule(ruleset_fd, path, ro)

        for path in (write_paths or []):
            _add_path_rule(ruleset_fd, path, rw)

        ret = libc.syscall(NR_LANDLOCK_RESTRICT_SELF, ruleset_fd, 0)
        if ret < 0:
            raise OSError(ctypes.get_errno(), "landlock_restrict_self failed")
    finally:
        os.close(ruleset_fd)


if __name__ == "__main__":
    apply_sandbox(
        read_paths=["/usr", "/lib", "/lib64", "/proc/self"],
        write_paths=["/tmp", "/var/data"]
    )
    print("Sandbox active — chỉ đọc được /usr, /lib, ghi được /tmp và /var/data")

    # Kiểm tra: ghi /tmp phải hoạt động
    open("/tmp/ok.txt", "w").write("test")
    print("/tmp/ok.txt: OK")

    # Kiểm tra: /etc/passwd phải bị block
    try:
        open("/etc/passwd").read()
        print("CẢNH BÁO: /etc/passwd không bị block!")
    except PermissionError:
        print("/etc/passwd: bị block đúng cách")

Ví dụ 2: Sandbox với landlockrun (không cần sửa source code)

Không có source code để chỉnh? landlock-sandboxer từ kernel source cho phép wrap bất kỳ binary nào mà không cần đụng vào code gốc:

# Build từ kernel source (một lần)
apt install linux-source gcc
tar xf /usr/src/linux-source-*.tar.bz2
cd linux-source-*/samples/landlock
make
sudo cp sandboxer /usr/local/bin/landlock-sandboxer

# Chạy ứng dụng trong sandbox
# Chỉ đọc /usr, /lib — ghi vào /tmp và /var/myapp
LL_FS_RO="/usr:/lib:/lib64:/etc/ssl" \
LL_FS_RW="/tmp:/var/myapp" \
landlock-sandboxer python3 /opt/myapp/process.py

Ví dụ 3: Sandbox service qua systemd

Đây là cách mình đang dùng trên production — kết hợp systemd native hardening với Landlock ở application layer:

# /etc/systemd/system/myapp.service
[Unit]
Description=My App (Landlock sandboxed)
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python app.py

# systemd hardening — complement Landlock ở kernel level
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/myapp /opt/myapp/data

# Giới hạn syscall — chỉ những gì app cần
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl restart myapp
sudo systemctl status myapp

ProtectSystem=strict kết hợp ReadWritePaths tạo mount namespace riêng cho service — khác cơ chế với Landlock syscall nhưng bổ sung tốt. Landlock hoạt động ở process level, systemd hardening ở service level. Hai lớp, hai góc độ.

Kiểm tra & Monitoring

Verify sandbox bằng strace

# Theo dõi các lần bị block — EACCES là dấu hiệu Landlock đang chặn
strace -e trace=openat,open -f python3 myapp_sandboxed.py 2>&1 | grep -E "EACCES|EPERM"

# Output mẫu:
# openat(AT_FDCWD, "/etc/shadow", O_RDONLY) = -1 EACCES (Permission denied)
# openat(AT_FDCWD, "/root/.ssh/id_rsa", O_RDONLY) = -1 EACCES (Permission denied)

Script test tự động

#!/bin/bash
# test_sandbox.sh

python3 - <<'PYEOF'
import sys
from landlock_wrapper import apply_sandbox

apply_sandbox(
    read_paths=["/usr", "/lib", "/lib64", "/tmp"],
    write_paths=["/tmp"]
)

tests = [
    ("/tmp/write_test.txt", "w", True,  "Ghi /tmp"),
    ("/etc/passwd",         "r", False, "Đọc /etc/passwd"),
    ("/root/.bashrc",       "r", False, "Đọc /root/.bashrc"),
]

all_ok = True
for path, mode, allowed, label in tests:
    try:
        open(path, mode).close()
        status = "PASS" if allowed else "FAIL (không bị block!)"
        if not allowed: all_ok = False
    except (PermissionError, OSError):
        status = "FAIL (bị block)" if allowed else "PASS (bị block đúng)"
        if allowed: all_ok = False
    print(f"  [{status}] {label}: {path}")

sys.exit(0 if all_ok else 1)
PYEOF

[ $? -eq 0 ] && echo "Sandbox hoạt động đúng" || echo "Có vấn đề — kiểm tra lại"

Monitoring với auditd

# Cài auditd để ghi lại mọi lần bị block
sudo apt install auditd
sudo systemctl enable --now auditd

# Xem Landlock denials hôm nay
sudo ausearch -m LANDLOCK --start today

# Real-time monitoring
sudo tail -f /var/log/audit/audit.log | grep -i landlock

# Tóm tắt: process nào bị block nhiều nhất
sudo ausearch -m LANDLOCK --start today | grep -oP 'comm="\K[^"]+' | sort | uniq -c | sort -rn

Khi nào dùng Landlock, khi nào không

Mình thấy Landlock phát huy rõ nhất trong mấy trường hợp này:

  • Ứng dụng có code nguồn và có thể thêm vài dòng init sandbox
  • Deploy trên môi trường shared hosting không có quyền root
  • Muốn defense-in-depth: ứng dụng bị compromise vẫn không đọc được credential file
  • Chạy code không tin tưởng (user script, plugin từ bên ngoài)

Nhưng cũng phải thực tế — Landlock không phải giải pháp cho mọi thứ:

  • Không thay thế được network firewall — network restriction chỉ có từ ABI v4 (kernel 6.7+) và vẫn còn nhiều giới hạn
  • Không bảo vệ được nếu attacker đã có root
  • Không sandbox syscall — cần kết hợp với seccomp cho coverage đầy đủ hơn

Hiện tại mình chạy cả ba lớp cho các service quan trọng: Landlock kiểm soát filesystem trong code, systemd hardening tạo mount namespace, seccomp lọc syscall. Chúng bổ sung cho nhau tốt — và điểm mình thích nhất là không lớp nào cần root để bật ở application level.

Share: