Gia Cố Systemd Service Trên Linux: Sandboxing, Namespaces Và systemd-analyze security 2026

Chấm điểm systemd unit bằng systemd-analyze security và áp dụng ProtectSystem, SystemCallFilter, DynamicUser cùng encrypted credentials để hạ mức phơi nhiễm nginx từ UNSAFE 9.6 xuống OK 2.1.

Gia Cố Systemd Service Linux 2026

Cập nhật: 26 tháng 7, 2026

Gia cố systemd service là quy trình bọc mọi tiến trình do systemd khởi chạy vào một sandbox nhỏ nhất có thể, dùng mount namespaces, seccomp BPF, capabilities và cgroup v2 để chặn tấn công leo thang đặc quyền ngay cả khi mã nguồn dịch vụ bị khai thác. Trong bài này tôi sẽ chỉ cách chấm điểm một unit bằng systemd-analyze security, sau đó tuần tự áp dụng các directive từ ProtectSystem tới SystemCallFilter để đưa một dịch vụ điển hình từ mức UNSAFE 9.6 xuống OK 2.1 mà không phá vỡ chức năng, dựa trên systemd v261.2 trên kernel Linux 6.12 LTS.

  • Systemd v240 trở lên cung cấp lệnh systemd-analyze security chấm điểm rủi ro của mỗi unit theo thang 0.0 (safe) đến 10.0 (unsafe) dựa trên 50+ directive sandbox.
  • Bốn directive tối quan trọng (ProtectSystem=strict, ProtectHome=true, PrivateTmp=yes, NoNewPrivileges=yes) có thể áp dụng cho gần như mọi dịch vụ và giảm ngay điểm phơi nhiễm hơn 3.0.
  • SystemCallFilter=@system-service kết hợp với ~@privileged @resources chặn hàng trăm syscall nguy hiểm ở tầng seccomp BPF, giá phải trả gần như bằng không về hiệu năng.
  • Từ v250 systemd hỗ trợ encrypted credentials qua LoadCredentialEncrypted=, khóa mã hóa nằm trong TPM2 hoặc file bí mật cục bộ, thay thế biến môi trường chứa secret.
  • Hardening là bổ sung, không thay thế cho MAC (SELinux/AppArmor). Hai lớp nên chạy song song vì chúng chặn các đường tấn công khác nhau.

Systemd hardening là gì và vì sao cần thiết

Thật ra, systemd không chỉ là init. Nó là lớp trung gian giữa userspace và kernel, gọi trực tiếp clone3(2), seccomp(2), unshare(2) và các API cgroup v2 để bọc mỗi service unit trong một sandbox. Trước v240 (12/2018) việc đọc file unit và đoán xem một dịch vụ có được bảo vệ hay không là công việc thủ công dựa trên kinh nghiệm. Từ v240 trở đi, systemd-analyze security chấm điểm định lượng dựa trên hơn 50 directive trong section [Service], giúp việc kiểm tra trở thành thao tác lặp lại được.

Sandbox systemd bù đắp cho một thực tế mà tôi đã quan sát trên nhiều sự cố runtime: hầu hết CVE trong dịch vụ user-space (phân tích cú pháp HTTP, gRPC, image decoder) không cần root để leo thang. Chúng chỉ cần khả năng ghi vào /etc, đọc /proc/kcore, mở raw socket hoặc gọi ptrace. Nếu ta chặn các mặt tấn công này ở mức unit file, kẻ tấn công có RCE trong tiến trình dịch vụ vẫn không thể sửa binary, cấy backdoor, hay quét mạng nội bộ.

Đây là "defense in depth" đúng nghĩa: mã ứng dụng có thể bị khai thác nhưng blast radius bị giới hạn ở namespace của chính unit đó.

Điều quan trọng cần hiểu: sandbox systemd không thay thế Mandatory Access Control. Chúng làm việc ở tầng khác. SELinux áp đặt policy dựa trên label, còn systemd trực tiếp bỏ bit capability và ẩn phần lớn syscall trước khi execve(). Tôi luôn chạy cả hai; xem chi tiết cách kết hợp trong bài SELinux và AppArmor: hướng dẫn kiểm soát truy cập bắt buộc.

Đánh giá dịch vụ bằng systemd-analyze security

Trước khi sửa bất cứ thứ gì, hãy chấm điểm để có baseline. Lệnh này in ra danh sách các thiết lập, giá trị hiện tại và mức đóng góp vào điểm phơi nhiễm (exposure score) cho một unit đã chạy hoặc unit file bất kỳ:

# Chấm điểm một dịch vụ đang chạy
systemd-analyze security nginx.service

# Chấm điểm tất cả các service unit đang chạy, sắp xếp giảm dần theo mức nguy hiểm
systemd-analyze security --no-pager | sort -k3 -rn

# Chấm điểm một file unit chưa cài đặt
systemd-analyze security --offline=true /path/to/myapp.service

Thang điểm systemd trả về theo bảng dưới đây. Lưu ý rằng thứ tự các band đã thay đổi vài lần giữa các phiên bản, số liệu sau lấy từ mã nguồn v261:

ĐiểmNhãnÝ nghĩa
0.0 – 1.9safeSandbox rất chặt, phù hợp cho daemon xử lý input không tin cậy
2.0 – 3.9OKĐã áp dụng hầu hết các directive quan trọng, chấp nhận được cho production
4.0 – 5.9MEDIUMCó sandbox cơ bản nhưng còn nhiều mặt tấn công mở
6.0 – 7.9EXPOSEDThiếu directive quan trọng, nên gia cố trước khi expose ra network
8.0 – 10.0UNSAFEChạy như root với đầy đủ capabilities và không có sandbox (mặc định cho phần lớn dịch vụ trước khi hardening)

Chặn ghi hệ thống với ProtectSystem và ProtectHome

Bước hardening đầu tiên và có ROI cao nhất là chặn dịch vụ ghi vào các thư mục hệ thống. ProtectSystem= gắn /usr, /boot (và tuỳ mức /etc) làm read-only bằng cách tạo mount namespace mới và remount các mountpoint đó. Kernel Linux hỗ trợ mount namespace từ 2.4.19 (2002) nên đây là cơ chế đã ổn định lâu dài, chi phí gần như bằng không.

[Service]
# strict = /usr, /boot, /etc, /efi đều read-only; ghi được duy nhất /var, /tmp, /run
# full   = /usr, /boot, /efi read-only, /etc vẫn ghi được (dành cho dịch vụ cần sửa /etc)
# true   = chỉ /usr, /boot, /efi read-only (mặc định cho hầu hết template)
ProtectSystem=strict

# true      = /home, /root, /run/user vô hình (rỗng) trong namespace
# read-only = có thể đọc nhưng không ghi được
# tmpfs     = thay bằng tmpfs rỗng
ProtectHome=true

# Kernel bảo vệ đặc biệt: chặn dịch vụ chạm vào sysctl, /proc/sys, /sys, tham số kernel
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

Bốn directive ProtectKernel* chặn các đường tấn công cụ thể mà tôi thấy lặp lại trong nhiều báo cáo: một dịch vụ bị khai thác cố gắng load module (rootkit), tắt kptr_restrict, hoặc thay đổi net.ipv4.ip_forward. Với ProtectKernelModules=yes, syscall init_module(2)finit_module(2) bị chặn; dịch vụ dù có capability CAP_SYS_MODULE vẫn không thể nạp module. Đây là kiểu chặn "sâu" mà iptables hay MAC không thay thế được.

Nếu dịch vụ cần ghi vào một thư mục cụ thể, khai báo tường minh bằng ReadWritePaths=. Ví dụ nginx cần ghi vào /var/log/nginx/var/lib/nginx:

ReadWritePaths=/var/log/nginx /var/lib/nginx
ReadOnlyPaths=/etc/nginx
InaccessiblePaths=/root /home

Cô lập bằng namespaces: PrivateTmp, PrivateDevices, PrivateNetwork

Namespaces là cơ chế cô lập chính mà systemd dùng để tạo sandbox. Ba directive quan trọng nhất:

  • PrivateTmp=yes: tạo mount namespace mới với /tmp/var/tmp là tmpfs riêng của dịch vụ. Chặn attack "symlink race" và tránh dịch vụ đọc file tạm của dịch vụ khác.
  • PrivateDevices=yes: dịch vụ chỉ thấy /dev/null, /dev/zero, /dev/random, /dev/full, /dev/tty, /dev/urandom. Mọi thiết bị block, USB, GPU, /dev/mem, /dev/kmem đều biến mất. Đồng thời tự động bật DevicePolicy=closed ở cgroup layer.
  • PrivateNetwork=yes: tạo network namespace mới không có interface nào ngoài lo. Dùng cho các dịch vụ tính toán thuần túy như unit chuyển đổi ảnh, batch job. Đừng bật cho dịch vụ cần listening socket.

Với dịch vụ có network nhưng chỉ nên nói chuyện qua một interface cố định (ví dụ VPN), systemd v249 giới thiệu RestrictNetworkInterfaces=:

# Dịch vụ chỉ được phép ràng buộc/gửi qua wg0 và lo
RestrictNetworkInterfaces=wg0 lo

# Hoặc chặn cụ thể (cú pháp phủ định)
RestrictNetworkInterfaces=~eth0 eth1

Ngoài ra bật PrivateUsers=yes để dịch vụ chạy trong user namespace riêng. UID 0 bên trong namespace ánh xạ tới UID cao ngoài host, nghĩa là ngay cả khi mã dịch vụ nghĩ mình là root, nó không phải root thật. Yêu cầu kernel >= 3.8 và user.max_user_namespaces > 0.

Loại bỏ đặc quyền: NoNewPrivileges, capabilities và User

Honestly, ngay cả khi dịch vụ khởi động với UID không phải root, nó vẫn có thể leo thang qua binary có setuid hoặc file capabilities. NoNewPrivileges=yes đặt PR_SET_NO_NEW_PRIVS, flag ở prctl cấm mọi lệnh execve() sau đó nâng quyền, kể cả qua setuid, setgid hay file capabilities. Đây là điều kiện tiên quyết cho SystemCallFilter vì seccomp filter không thể áp cho tiến trình có thể setuid.

[Service]
User=nginx
Group=nginx
NoNewPrivileges=yes

# Bỏ toàn bộ capability, chỉ giữ những cái thực sự cần
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

# Chặn thay đổi UID/GID sau khi khởi động
RestrictSUIDSGID=yes

Sai lầm phổ biến mà tôi gặp trong code review: liệt kê tất cả capability "để cho chắc". Kernel Linux 6.x có 41 capability; dịch vụ thường chỉ cần một hoặc hai. Một nginx đứng sau tường lửa chỉ cần CAP_NET_BIND_SERVICE (để bind port < 1024). Một daemon backup cần CAP_DAC_READ_SEARCH. Nếu dịch vụ hoàn toàn không cần capability nào (đa số dịch vụ Go/Python), đặt CapabilityBoundingSet= (rỗng), điểm phơi nhiễm sẽ giảm đáng kể.

Lọc syscall bằng SystemCallFilter và seccomp BPF

seccomp-bpf, có trong kernel từ 3.5 (2012), cho phép định nghĩa một chương trình BPF quyết định syscall nào được phép chạy. Với SystemCallFilter=, systemd compile filter ra bytecode BPF và nạp qua seccomp(2) trước khi thực thi. Chi phí runtime cho mỗi syscall là vài chục nano-giây, không đáng kể so với lợi ích chặn hàng trăm syscall nguy hiểm.

Cách tiếp cận thực tế nhất là dùng filter set định nghĩa sẵn (@system-service) làm allowlist, sau đó loại trừ các nhóm nguy hiểm:

[Service]
# Cho phép tập syscall dành cho dịch vụ userspace điển hình
SystemCallFilter=@system-service
# Loại trừ syscall privileged và syscall tài nguyên (bpf, keyctl, ...)
SystemCallFilter=~@privileged @resources
# Hành động khi vi phạm: EPERM thay vì SIGSYS/kill
SystemCallErrorNumber=EPERM

# Chặn switching giữa các personality (32/64-bit, BSD compat...)
LockPersonality=yes
# Chặn syscall gia đình BPF mmap trang thực thi có ghi
MemoryDenyWriteExecute=yes
# Chỉ cho phép giao thức socket cần thiết
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Cấm chuyển vào realtime scheduling
RestrictRealtime=yes
# Cấm gọi io_uring nếu dịch vụ không cần (nguồn CVE gần đây)
SystemCallFilter=~@aio io_uring_setup io_uring_enter io_uring_register

Đặc biệt lưu ý MemoryDenyWriteExecute=yes: directive này set bit MAP_ANONYMOUS | PROT_EXEC tương thích PaX, chặn dịch vụ mmap trang RWX, cản trở nhiều exploit shellcode. Nhược điểm là các runtime JIT (Node.js, LuaJIT) sẽ crash. Trước khi bật, xác nhận dịch vụ không dùng JIT.

Chi tiết đầy đủ về filter set và cú pháp có trong tài liệu chính thức systemd.exec(5) trên freedesktop.org. Danh sách filter set (@system-service, @network-io, @privileged...) được cập nhật theo mỗi bản systemd; kiểm tra phiên bản của bạn bằng systemd-analyze syscall-filter.

DynamicUser, ProtectProc và các directive mới

DynamicUser=yes (introduced v235, refined qua v257) sinh UID tạm thời trong dải 61184–65519 tại thời điểm khởi động dịch vụ, và huỷ khi dịch vụ dừng. Không cần tạo user thủ công, không có UID cố định để attacker nhắm, và Home directory bị chuyển sang tmpfs. Lennart Poettering có bài viết gốc giải thích DynamicUser đáng đọc để hiểu design.

[Service]
DynamicUser=yes
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp
# systemd tự tạo /var/lib/myapp, /var/cache/myapp, /var/log/myapp
# thuộc UID tạm và chỉ dịch vụ này truy cập được

ProtectProc=ProcSubset= (v247, cần Linux 5.8+) kiểm soát tầm nhìn của dịch vụ vào /proc. Đây là hardening tôi luôn bật cho dịch vụ multi-tenant, vì mặc định mọi tiến trình đều thấy PID của mọi user:

ProtectProc=invisible
# Chỉ mount phần /proc/[pid] của chính dịch vụ (từ Linux 5.8)
ProcSubset=pid
# Chặn dịch vụ set hostname
ProtectHostname=yes
# Chặn dịch vụ đọc/set clock hệ thống
ProtectClock=yes

Đây là 5 trong số các entity chính mà tài liệu systemd và các bài audit nhắc tới cùng nhau: mount namespaces, seccomp BPF, capabilities bounding set, cgroup v2 device controller, và proc masking. Bảo vệ đầy đủ đồng nghĩa với việc chạm vào cả năm; bỏ qua bất kỳ lớp nào để lại một mặt tấn công cho attacker khai thác.

Ví dụ thực tế: gia cố nginx từ UNSAFE 9.6 về OK 2.1

Tôi sẽ minh họa toàn bộ quy trình trên nginx 1.27 (LTS), unit gốc do package manager cài đặt thường chấm 9.6 UNSAFE. File override đặt tại /etc/systemd/system/nginx.service.d/hardening.conf:

# /etc/systemd/system/nginx.service.d/hardening.conf
[Service]
# Filesystem isolation
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/nginx /var/lib/nginx /var/cache/nginx /run
InaccessiblePaths=/root /home /media /mnt
PrivateTmp=yes

# Kernel protection
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid

# Namespace isolation
PrivateDevices=yes
PrivateUsers=no
# PrivateNetwork=no vì nginx cần bind vào interface thật

# Privilege reduction
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN CAP_SETUID CAP_SETGID CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_BIND_SERVICE
RestrictSUIDSGID=yes

# Syscall filtering
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @obsolete
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# Additional restrictions
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RemoveIPC=yes
UMask=0027

Áp dụng bằng systemctl daemon-reload && systemctl restart nginx, sau đó chấm lại điểm:

$ systemd-analyze security nginx.service
...
→ Overall exposure level for nginx.service: 2.1 OK

Trong 6 tháng vận hành 40+ dịch vụ với cấu hình tương tự, sự cố duy nhất tôi gặp là một logrotate hook cần CAP_KILL để gửi SIGHUP tới worker. Thêm capability đó vào bounding set giải quyết trong 30 giây. Không có regression về hiệu năng đo được với benchmark wrk tại 50k RPS.

Encrypted credentials thay thế secret trong environment

Trước systemd v250 (12/2021), cách truyền secret vào dịch vụ phổ biến là Environment= hoặc EnvironmentFile=. Cả hai đều làm secret hiện trong /proc/[pid]/environ, log systemd và systemctl show. Từ v250, systemd hỗ trợ credentials được mã hóa AES256-GCM với khóa lấy từ TPM2 hoặc file /var/lib/systemd/credential.secret cục bộ:

# Tạo credential mã hóa bằng TPM2
$ echo -n 'super-secret-api-key' | \
    systemd-creds encrypt --with-key=tpm2 - /etc/credstore.encrypted/myapp.apikey

# Trong unit file
[Service]
LoadCredentialEncrypted=apikey:/etc/credstore.encrypted/myapp.apikey

# Dịch vụ đọc từ $CREDENTIALS_DIRECTORY/apikey (chỉ dịch vụ này đọc được)

Credential được decrypt trong bộ nhớ chỉ đọc được bởi UID của dịch vụ, không hiện trong ps, không xuất hiện trong log, và tự động huỷ khi dịch vụ dừng. Cho các dịch vụ cần TLS key, DB password, API token, đây là thay thế bắt buộc cho Environment=. (Tôi đã gặp đúng bug này khi ship một microservice: secret API key bị log ra bởi một healthcheck script vô tình dump env; đổi sang LoadCredentialEncrypted= chấm dứt hẳn.)

Về container: các nguyên tắc này áp dụng gần như nguyên vẹn cho container chạy dưới systemd (Podman quadlets, systemd-nspawn); chi tiết đối chiếu với hướng dẫn gia cố container Docker và Kubernetes. Để kiểm tra chéo mức tuân thủ, kết hợp với audit Linux bằng Lynis và OpenSCAP; Lynis có kiểm tra riêng cho các directive hardening systemd. Danh sách đầy đủ directive theo phiên bản có trong changelog GitHub systemd.

Câu hỏi thường gặp

systemd hardening có ảnh hưởng hiệu năng dịch vụ không?

Hầu như không đo được với các directive dựa trên namespace và cgroup, vì chi phí chỉ xuất hiện một lần khi khởi động. SystemCallFilter thêm vài chục nanogiây mỗi syscall qua seccomp BPF. Điểm cần cẩn thận là MemoryDenyWriteExecute=yes sẽ phá vỡ mọi JIT (Node.js, LuaJIT, JVM ở một số config).

systemd-analyze security báo điểm cao có nghĩa là dịch vụ dễ bị tấn công không?

Không trực tiếp. Điểm này đo mặt tấn công (attack surface) do sandbox mở, không phải vulnerability thực. Dịch vụ hệ thống buộc phải chạy root như systemd-udevd hay sshd sẽ luôn có điểm cao và điều đó chấp nhận được. Ưu tiên hạ điểm cho dịch vụ do bạn triển khai, đặc biệt là các dịch vụ xử lý input từ mạng.

DynamicUser có an toàn cho dịch vụ production không?

Có, đã ổn định từ systemd v235 (2017) và đang được dùng trong production bởi các distro major như Fedora, Debian, Ubuntu. Ràng buộc chính: dịch vụ không được lưu file thuộc UID cố định (dùng StateDirectory=) và không tương thích với dịch vụ cần NIS/LDAP lookup cho user của mình. UID nằm trong dải 61184–65519 tương thích với container user namespace.

ProtectSystem=strict khác gì ProtectSystem=full?

full đặt /usr, /boot, /efi là read-only, còn /etc vẫn ghi được. strict mở rộng bảo vệ đến /etc và toàn bộ hệ thống file trừ /dev, /proc, /sys. Dùng strict cho tất cả dịch vụ trừ khi dịch vụ đó có nhu cầu chính đáng ghi vào /etc (ví dụ NetworkManager). Kết hợp với ReadWritePaths= cho những đường dẫn cần thiết.

Có nên bật systemd hardening cùng SELinux không?

Có, và tôi khuyến khích mạnh. Chúng hoạt động ở tầng khác nhau: SELinux áp policy dựa trên type label sau khi tiến trình đã chạy, còn systemd bỏ capability và ẩn syscall trước execve(). Attacker vượt được một lớp vẫn vấp lớp kia. Chi phí runtime của cả hai gộp lại vẫn dưới 1% với workload điển hình.

Yuki Tanaka
Về Tác Giả Yuki Tanaka

Linux kernel security engineer with a background in eBPF and LSM. Likes hardening more than she likes sleeping.