SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-09-01

Hướng dẫn cấu hình ProtectSystem trong systemd service

Tìm hiểu cách dùng ProtectSystem, PrivateTmp và DynamicUser để cô lập service. Bài viết giải thích chi tiết các chỉ thị này chặn gì, gây lỗi ra sao và cách debug unit.

Sandboxing trong systemd là gì

Sandboxing trong systemd là phần thứ hai khi viết một unit file. Type= quyết định cách một service khởi động, còn các chỉ thị như ProtectSystem=PrivateTmp= quyết định những gì nó có thể truy cập sau khi đã chạy. Đây là các tính năng của kernel, namespace mount và bộ lọc seccomp, được systemd áp dụng trước khi tiến trình của bạn giành quyền kiểm soát. Ứng dụng không bao giờ thấy chúng và không cần thay đổi code.

Mặc định là không có gì cả. Một unit không có sandboxing sẽ chạy dưới quyền root, ghi vào bất cứ đâu, đọc mọi file trên máy và có thể load kernel module. Nếu unit đó là một ứng dụng web có thể truy cập từ Internet, một lỗi upload file sẽ khiến toàn bộ server bị compromised. Với unit bên dưới, cùng ứng dụng đó sẽ nhận được một filesystem chỉ đọc, một /home trống, một /tmp mà không tiến trình nào khác có thể thấy, và không có đường dẫn đến root ngay cả khi nó tìm thấy một binary setuid.

Tất cả những điều này áp dụng cho từng unit một. Việc hardening một service không có tác dụng với các service lân cận, vì vậy hãy bắt đầu với bất kỳ thứ gì đang lắng nghe trên một cổng công khai.

File unit, toàn bộ nội dung

notes là một web service nhỏ. Nó lắng nghe trên localhost, lưu database SQLite tại /var/lib/notes, và nằm sau nginx. Type=exec phù hợp vì binary luôn chạy ở foreground, và sự khác biệt giữa Type=simple, exec, forking và notify quyết định cách systemd theo dõi quá trình khởi động. Mọi chỉ thị bên dưới đều được giải thích chi tiết, cùng với những lỗi nó ngăn chặn và những vấn đề thường gặp.

[Unit]
Description=Notes web service
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
ExecStart=/opt/notes/bin/notes --listen 127.0.0.1:8080
DynamicUser=yes
StateDirectory=notes
Environment=NOTES_DB=/var/lib/notes/notes.db

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectProc=invisible

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target

Ghi nội dung vào /etc/systemd/system/notes.service, chạy sudo systemctl daemon-reload, sau đó là sudo systemctl restart notes.service. Đối với unit cài từ package, không được sửa file của nhà cung cấp. sudo systemctl edit notes.service mở một file drop-in tại /etc/systemd/system/notes.service.d/override.conf, nơi chỉ chứa các bổ sung [Service] của bạn và sẽ không bị mất khi nâng cấp package. systemctl cat notes.service in ra kết quả đã gộp, với file của nhà cung cấp đứng trước.

Dịch vụ chạy dưới quyền người dùng nào

Nếu không có User=, dịch vụ sẽ chạy dưới quyền root, và mọi chỉ thị khác ở đây chỉ là biện pháp hạn chế thiệt hại. Có hai cách để giải quyết vấn đề này.

Một system user tĩnh. Tạo user bằng sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes, sau đó thiết lập User=notes trong unit. UID sẽ giữ nguyên qua các lần khởi động lại, điều này quan trọng khi dịch vụ sở hữu các file nằm ngoài thư mục trạng thái của chính nó, hoặc khi một tác vụ backup cần đọc các file đó. Lý do đằng sau việc cấp cho mỗi dịch vụ một tài khoản không đặc quyền riêng cũng áp dụng tương tự ở đây.

DynamicUser=yes. systemd sẽ cấp phát một UID từ dải dự phòng khi dịch vụ khởi động và giải phóng nó khi dịch vụ dừng. Không có tài khoản nào tồn tại trên đĩa, vì vậy không có dữ liệu thừa lại khi bạn xóa dịch vụ. Trong khi dịch vụ chạy, getent passwd notes sẽ phân giải tên thông qua module NSS (name service switch) của systemd. Sau khi dịch vụ dừng, tên đó sẽ biến mất.

DynamicUser=yes cũng tự động bật bốn thiết lập khác cho bạn: RemoveIPC=yes, PrivateTmp=yes, ProtectSystem=strictProtectHome=read-only. Đó là phần lớn cấu hình sandbox chỉ với một dòng lệnh, đó là lý do tại sao unit mẫu vẫn rất ngắn gọn.

Những gì bị ảnh hưởng. Một dynamic UID không thể sở hữu các file ở những vị trí tùy ý, vì con số này được tái sử dụng giữa các dịch vụ. Dữ liệu bền vững phải đi qua StateDirectory=, CacheDirectory= hoặc LogsDirectory=, những thư mục mà systemd sẽ tạo và gán quyền sở hữu (chown) cho UID hiện tại mỗi khi khởi động. Với DynamicUser=yes, thư mục thực tế là /var/lib/private/notes, và /var/lib/notes là một symlink trỏ đến đó. /var/lib/private có mode 0700 và thuộc sở hữu của root, vì vậy một tiến trình backup chạy dưới quyền người dùng bình thường sẽ gặp lỗi Permission denied trên một đường dẫn mà root nhìn thấy là hoàn toàn có thể đọc được. Bất kỳ thứ gì khác cần một chủ sở hữu cố định, một SSH key, một NFS export, hoặc một file mà dịch vụ thứ hai cần đọc, đều cần một user tĩnh thay thế.

Cấu trúc hệ thống file: ProtectSystem, ProtectHome, PrivateTmp

ProtectSystem= nhận ba giá trị. yes mount /usr và các thư mục boot ở chế độ chỉ đọc (read-only). full thêm /etc. strict mount toàn bộ hệ thống phân cấp ở chế độ chỉ đọc, ngoại trừ các thư mục API kernel /dev, /proc/sys, vốn được quản lý bởi các chỉ thị khác. Hãy bắt đầu từ strict và mở dần các quyền truy cập, vì việc bắt đầu với quyền lỏng lẻo rồi mới thắt chặt sau thường không bao giờ xảy ra.

Các lỗ hổng được mở bằng ReadWritePaths=/srv/notes/uploads. Bạn cần ít quyền hơn bạn nghĩ: StateDirectory=, LogsDirectory=, CacheDirectory=RuntimeDirectory= tự động giữ quyền ghi dưới ProtectSystem=strict, đó là lý do tại sao unit mẫu không có dòng ReadWritePaths= nào cả. Dưới strict, /tmp cũng ở chế độ chỉ đọc, trừ khi PrivateTmp=yes cấp cho unit một thư mục riêng có quyền ghi.

Những gì bị ảnh hưởng. Mọi thao tác ghi bên ngoài các đường dẫn đó giờ đây sẽ thất bại với lỗi Read-only file system. Các ứng dụng tự lưu cấu hình vào /etc, tạo file PID trực tiếp vào /run, hoặc giải nén plugin vào /opt đều sẽ gặp lỗi này. Hãy đọc đường dẫn từ thông báo lỗi và chỉ thêm đúng đường dẫn đó, không thêm thư mục cha. Một đường dẫn được liệt kê trong ReadWritePaths= mà không tồn tại sẽ khiến service không khởi động được thay vì chỉ cảnh báo, vì vậy hãy thêm dấu gạch ngang vào trước các đường dẫn tùy chọn: ReadWritePaths=-/srv/notes/uploads.

Chỉ đọc không có nghĩa là ẩn. Dưới ProtectSystem=strict, service vẫn có thể đọc /etc/passwd và bất kỳ secret nào mà mọi người dùng đều có quyền đọc thuộc về ứng dụng khác. InaccessiblePaths=/etc/ssh /srv/otherapp loại bỏ hoàn toàn một cây thư mục khỏi tầm nhìn của unit. Đối với các secret của riêng service, LoadCredential=dbpass:/etc/notes/dbpass sao chép file vào một thư mục riêng cho từng unit mà chỉ service này mới có thể đọc, và ứng dụng sẽ tìm thấy nó dưới $CREDENTIALS_DIRECTORY.

ProtectHome=yes làm cho /home, /root/run/user trông như trống rỗng. Một web service không có lý do gì để truy cập thư mục home, và điều này ngăn chặn lỗi path traversal tiếp cận /root/.ssh. read-onlytmpfs là các giá trị nhẹ nhàng hơn. Nó làm hỏng bất kỳ thứ gì mà dữ liệu thực sự nằm trong thư mục home, bao gồm nhiều ứng dụng cài đặt thủ công dưới /home/app. Hãy di chuyển dữ liệu sang /var/lib, hoặc đặt ProtectHome=read-only và chấp nhận mức độ bảo mật thấp hơn.

PrivateTmp=yes cung cấp cho service /tmp/var/tmp riêng, được tạo khi service khởi động và xóa khi service dừng. Nó chấm dứt hoàn toàn các lỗi tranh chấp file tạm giữa các service, và một sự cố crash không còn để lại các secret trong một thư mục mà mọi người dùng trên máy đều có thể liệt kê.

Những gì bị ảnh hưởng. Bất cứ thứ gì coi /tmp là điểm kết nối chung. Một service được cấu hình để kết nối với MySQL thông qua /tmp/mysql.sock giờ đây sẽ báo lỗi Can't connect to local MySQL server through socket '/tmp/mysql.sock', vì database tạo socket của nó trong /tmp của host trong khi service lại tìm kiếm bên trong thư mục của chính nó. Hãy trỏ nó đến 127.0.0.1 hoặc đường dẫn socket thực dưới /run. Sự ngạc nhiên tương tự cũng xảy ra khi debug: các file mà service ghi vào /tmp không hiển thị trong /tmp của shell bạn đang dùng. Để xem bên trong, hãy truy cập vào mount namespace của service đó.

pid=$(systemctl show --property=MainPID --value notes.service)
sudo nsenter --target "$pid" --mount ls -l /tmp

PrivateDevices=yes thay thế /dev bằng một tập hợp nhỏ các thiết bị giả lập như /dev/null, /dev/zero/dev/urandom. Các thiết bị vật lý hoàn toàn không tồn tại. Các node đĩa, /dev/kvm, /dev/net/tun, card âm thanh và GPU đều biến mất, vì vậy một media server thực hiện transcoding phần cứng sẽ không thể mở /dev/dri/renderD128 và sẽ chuyển sang dùng phần mềm hoặc thoát. Khi một service thực sự cần một node thiết bị, hãy tắt PrivateDevices= cho unit đó và chỉ định node bằng DeviceAllow=/dev/dri/renderD128 rw, cách này vẫn an toàn hơn nhiều so với mặc định là cho phép truy cập mọi thiết bị trên hệ thống.

Những gì service có thể trở thành: NoNewPrivileges và capabilities

NoNewPrivileges=yes thiết lập một cờ tiến trình mà kernel không bao giờ xóa. Kể từ thời điểm đó, tiến trình và mọi tiến trình con mà nó khởi chạy đều không thể giành được đặc quyền thông qua binary setuid hoặc file capability. Đây là dòng lệnh giá trị nhất trong unit, vì nó khiến hầu hết các chuỗi leo thang đặc quyền cục bộ bị chặn ngay tại bước đầu tiên.

Những gì nó làm hỏng. Bất cứ thứ gì gọi sudo từ bên trong service, vốn bây giờ sẽ in ra sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?. Các kiểm tra mật khẩu PAM (pluggable authentication modules) vốn gọi ra unix_chkpwd cũng sẽ thất bại tương tự, và các công cụ container không cần root vốn cần newuidmap cũng vậy. Nếu service của bạn phụ thuộc vào một trong số đó, hãy loại bỏ sự phụ thuộc thay vì loại bỏ chỉ thị này.

CapabilityBoundingSet= giới hạn những capability mà bất kỳ tiến trình nào trong unit có thể nắm giữ, và một phép gán trống sẽ loại bỏ tất cả chúng. Đối với một service đã chạy dưới quyền người dùng không phải root, đây là lớp khóa thứ hai thay vì lớp đầu tiên, vì NoNewPrivileges=yes đã chặn cách thông thường để lấy một capability. Hãy giữ cả hai, vì chúng thất bại theo những cách khác nhau và bạn muốn lỗi được phát hiện hai lần.

Capability duy nhất mà một web service thường muốn là CAP_NET_BIND_SERVICE, để sử dụng cổng dưới 1024. Một tiến trình không phải root cần được cấp quyền này, chứ không chỉ là cho phép, vì vậy cả hai dòng đều là bắt buộc.

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

systemd áp dụng ambient capabilities trước khi nó hạ đặc quyền, vì vậy điều này vẫn hoạt động cùng với NoNewPrivileges=yes. Nếu không có các dòng đó, service sẽ khởi động và sau đó thoát với một lỗi chỉ định cổng, ví dụ như listen tcp :443: bind: permission denied. Trên hầu hết các thiết lập VPS, giải pháp tốt hơn là bind 127.0.0.1:8080 và để Nginx hoặc Caddy đảm nhận cổng 443, điều này giúp loại bỏ hoàn toàn capability đó khỏi unit. Một dấu ~ đứng đầu sẽ đảo ngược danh sách, vì vậy CapabilityBoundingSet=~CAP_SYS_ADMIN sẽ chặn capability đó và cho phép các cái còn lại. Hãy ưu tiên dạng cho phép (allow list): danh sách chặn (deny list) sẽ nhanh chóng trở nên lỗi thời, vì các capability mới liên tục được thêm vào.

Những gì kernel cung cấp

ProtectKernelTunables=yes đặt /proc/sys, /sys và các file có thể ghi bên trong chúng thành chế độ chỉ đọc (read-only) cho unit này. Nó sẽ làm hỏng bất kỳ service nào thiết lập sysctl trong quá trình khởi động: script khởi động sẽ in ra sysctl: setting key "vm.max_map_count": Read-only file system và dừng lại. Hãy đưa giá trị đó vào /etc/sysctl.d/, nơi nó thuộc về và vẫn tồn tại sau khi reboot, sau đó hãy giữ nguyên directive này.

ProtectKernelModules=yes chặn việc load module. Một unit chạy modprobe sẽ nhận modprobe: ERROR: could not insert 'nf_conntrack': Operation not permitted. Hãy load module khi boot thông qua /etc/modules-load.d/ thay vì từ service.

ProtectKernelLogs=yes loại bỏ dmesg. ProtectControlGroups=yes đặt /sys/fs/cgroup thành chế độ chỉ đọc, điều mà các container runtime và bất kỳ thứ gì tự quản lý cgroup của nó sẽ nhận ra ngay lập tức. ProtectProc=invisible ẩn các tiến trình của người dùng khác trong /proc, vì vậy một service bị chiếm quyền không thể đọc command line của daemon khác và mật khẩu mà ai đó đã truyền vào đó. Các agent giám sát quét /proc là những thứ cần tắt tùy chọn này.

RestrictNamespaces=yes ngăn service tạo các namespace mới, đây là thứ mà container runtime cần và là thứ kẻ tấn công sử dụng để thực hiện thoát khỏi container (escape). LockPersonality=yes chặn việc thay đổi kernel execution domain, và RestrictSUIDSGID=yes ngăn service tạo các file setuid. Cả hai đều tốn ít tài nguyên và hiếm khi làm hỏng một ứng dụng thông thường.

Dịch vụ có thể giao tiếp với những gì

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 cho phép các socket cục bộ cùng với IPv4 và IPv6, đồng thời khiến socket() thất bại với lỗi EAFNOSUPPORT đối với mọi kết nối khác. Đây là một bộ lọc seccomp, vì vậy nó hoạt động trên x86-64 và arm64, bao phủ mọi VPS hiện nay.

Những gì bị ảnh hưởng. AF_NETLINK, thường xuyên hơn mức mọi người dự đoán. Hàm getifaddrs() của glibc sử dụng một netlink socket, tương tự như các đường dẫn liệt kê giao diện trong runtime của Go, Java và .NET. Do đó, một dịch vụ chỉ muốn biết địa chỉ IP của chính nó sẽ bị dừng với lỗi OSError: [Errno 97] Address family not supported by protocol hoặc lỗi tương đương trong ngôn ngữ đó. Khi điều đó xảy ra, danh sách trở thành AF_UNIX AF_INET AF_INET6 AF_NETLINK, vốn vẫn hẹp hơn nhiều so với mặc định. AF_PACKET là quyền cần thiết cho việc bắt gói tin thô (raw packet capture), và hầu như không có dịch vụ nào khác nên có quyền này.

IPAddressDeny=any với IPAddressAllow=localhost là một cơ chế khác: một bộ lọc BPF được gắn vào cgroup của unit. Nó hoạt động theo từng unit và không hiển thị với nft list ruleset, điều này làm cho nó phù hợp với một dịch vụ chỉ nên truy cập database trên cùng máy chủ, nhưng lại gây khó hiểu cho người debug hệ thống sau này. Trên kernel không hỗ trợ cgroup BPF, systemd sẽ ghi log rằng unit cấu hình một IP firewall trong khi hệ thống cục bộ không hỗ trợ firewalling bằng BPF/cgroup, và các quy tắc này sẽ lặng lẽ không có tác dụng. Vì vậy, hãy đọc journal thay vì tự suy đoán.

Tại sao một unit đã được hardened lại không khởi động?

Mọi chỉ thị ở trên đều có thể khiến một service đang chạy bình thường trở nên lỗi, và lỗi đó thường không liên quan trực tiếp đến chỉ thị gây ra nó. Quy trình xử lý luôn giống nhau: Đọc journal, nới lỏng đúng một chỉ thị, kiểm tra lại.

sudo systemctl restart notes.service
systemctl status notes.service
sudo journalctl -u notes.service -n 50 --no-pager

Các dòng định nghĩa sandbox trông như sau:

notes.service: Failed to set up mount namespacing: No such file or directory
notes.service: Failed at step NAMESPACE spawning /opt/notes/bin/notes: No such file or directory
notes.service: Main process exited, code=exited, status=226/NAMESPACE

226/NAMESPACE nghĩa là systemd không thể tạo view cho hệ thống file, nên binary của bạn chưa bao giờ được chạy. Nguyên nhân thường gặp là một đường dẫn trong ReadWritePaths=, BindPaths= hoặc InaccessiblePaths= không tồn tại. 228/SECCOMP chỉ ra rằng SystemCallFilter= hoặc SystemCallArchitectures= không thể áp dụng. code=killed, status=31/SYS lại là một trường hợp khác: tiến trình đã khởi động, sau đó thực hiện một system call bị filter chặn lại, và kernel đã kill nó. Hãy luôn mở tài liệu tham khảo mã thoát systemd cho các unit không khởi động trong khi làm việc, vì con số này là cách nhanh nhất để phân biệt lỗi namespace với lỗi ứng dụng.

Hãy nới lỏng từng chỉ thị một thông qua file drop-in để kết quả trả về có ý nghĩa. Chạy sudo systemctl edit notes.service và thêm một dòng duy nhất vào đó:

[Service]
ProtectSystem=full

Khởi động lại. Nếu service chạy được, bạn sẽ biết cần thu hẹp chỉ thị nào thay vì xóa bỏ hoàn toàn. Hãy đặt lại thành strict, thêm dòng ReadWritePaths= cho đúng đường dẫn mà ứng dụng thực sự cần, rồi khởi động lại lần nữa. Việc xóa toàn bộ block chỉ vì service không khởi động sẽ khiến unit mất đi sự bảo vệ và để lại một comment mà không ai nhớ lý do tại sao.

Để kiểm tra sandbox mà không cần thông qua service, hãy chạy một shell bên trong nó:

sudo systemd-run --pty -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /bin/bash

Bên trong shell đó, touch /etc/test trả về Read-only file systemls /home không hiển thị gì cả. Đây là cách nhanh nhất để biết ứng dụng sẽ thấy những gì, vì bạn có thể chạy thủ công các lệnh của nó và theo dõi lệnh nào bị lỗi.

Có một chế độ lỗi không tạo ra bất kỳ thông báo nào. Một chỉ thị viết sai chính tả chỉ là một cảnh báo, và service vẫn khởi động mà không có chỉ thị đó:

/etc/systemd/system/notes.service:14: Unknown key name 'ProtectSytem' in section 'Service', ignoring.

Unit vẫn chạy, sandbox không tồn tại, và không có thêm thông báo lỗi nào. Hai lệnh sau đây sẽ phát hiện điều này. sudo systemd-analyze verify /etc/systemd/system/notes.service in ra cảnh báo tương tự theo yêu cầu, và systemctl show in ra những gì service đang chạy thực sự nhận được:

systemctl show notes.service -p User -p ProtectSystem -p PrivateTmp -p NoNewPrivileges

Nếu lệnh đó trả về ProtectSystem=no trong khi bạn đã viết strict, nghĩa là unit đã không load cấu hình như bạn nghĩ.

Sử dụng systemd-analyze security làm danh sách kiểm tra

sudo systemd-analyze security notes.service in ra mọi thiết lập sandboxing mà một unit có thể sử dụng, trạng thái hiện tại của unit đó với từng thiết lập, và đánh giá cho mỗi dòng. Chạy lệnh này mà không có đối số để liệt kê mọi service đang được tải trên máy, và thêm --offline=true kèm đường dẫn để kiểm tra file unit trước khi bạn cài đặt nó.

Hãy đọc kết quả như một danh sách việc cần làm. Đi qua các dòng được gắn cờ và trả lời một câu hỏi cho mỗi dòng: service này có cần quyền truy cập đó không? Phần lớn thời gian câu trả lời là không, và bạn chỉ cần thêm dòng đó vào. Đôi khi câu trả lời là có. Một media server cần một device node. Một backup agent cần đọc /home. Những dòng đó sẽ luôn bị gắn cờ, và đó là kết quả đúng, không phải là lỗi.

Con số tổng kết ở cuối là kết quả tổng hợp của các dòng phía trên. Nó không biết service của bạn làm gì, nó giữ dữ liệu gì, hoặc liệu ứng dụng đó có lỗi ngay từ đầu hay không. Một unit có thể thỏa mãn mọi dòng mà công cụ kiểm tra nhưng vẫn là điểm yếu nhất trên server, vì thứ nó đo lường là mức độ phơi nhiễm của file unit chứ không phải mức độ phơi nhiễm của phần mềm. Một trình quản lý mật khẩu tự host làm cho khoảng cách này trở nên rõ ràng: một unit Vaultwarden có thể vượt qua mọi dòng trong khi token quản trị và file backup của nó vẫn là yếu tố quyết định sự an toàn của vault. Việc chạy theo con số này khiến người dùng dán vào các chỉ thị mà họ không hiểu, và đó chính là những thứ gây lỗi sau khi nâng cấp gói, mà không còn ai giải thích được tại sao dòng đó lại tồn tại ở đó.

Giới hạn của sandboxing

Các chỉ thị này kiểm soát những gì một service có thể truy cập. Chúng không quy định mức tiêu thụ tài nguyên, vì vậy một unit được sandbox hoàn toàn vẫn có thể chiếm dụng toàn bộ core CPU và bộ nhớ trên máy chủ. Đó là một nhóm thiết lập riêng biệt, được đề cập trong hướng dẫn về giới hạn CPU và bộ nhớ cho systemd service.

Chúng cũng không thay thế cho mandatory access control. Namespace được áp dụng cho từng unit và do người viết file unit thiết lập, trong khi SELinux thực thi một chính sách duy nhất trên toàn hệ thống. Hai cơ chế này hoạt động cùng nhau và không cái nào loại bỏ nhu cầu sử dụng cái còn lại.

Cuối cùng, chúng chỉ bao phủ các tiến trình mà systemd khởi chạy bên trong unit đó. Một service chuyển công việc cho một helper daemon thông qua socket thì chưa sandbox helper đó. Hãy viết cùng một khối cấu hình vào unit của helper và kiểm tra kết quả bằng systemctl show thay vì chỉ tin tưởng vào file cấu hình.

FAQ

ProtectSystem=strict thực sự đặt những gì ở chế độ chỉ đọc (read-only)?

Toàn bộ hệ thống file, ngoại trừ /dev, /proc/sys, vốn được quản lý bởi PrivateDevices=, ProtectKernelTunables=ProtectControlGroups=. Điều này bao gồm cả /etc, /var, /srv, /opt/tmp. Các ngoại lệ mà systemd tự động thêm vào cho bạn là những thư mục mà nó quản lý: StateDirectory=, CacheDirectory=, LogsDirectory=RuntimeDirectory=. Bất kỳ vị trí nào khác mà service cần ghi dữ liệu đều phải được khai báo rõ ràng trong ReadWritePaths=. Lưu ý rằng chỉ đọc không có nghĩa là không thể đọc được, vì vậy một file bí mật ở nơi khác trên máy chủ vẫn có thể bị service truy cập trừ khi bạn liệt kê nó trong InaccessiblePaths=.

Tại sao service của tôi bị lỗi với status=226/NAMESPACE?

systemd không thể tạo mount namespace, nên file thực thi không bao giờ khởi chạy. Dòng log trong journal ngay phía trên lỗi đó thường là Failed to set up mount namespacing: No such file or directory. Trong hầu hết các trường hợp, một đường dẫn trong ReadWritePaths=, BindPaths= hoặc InaccessiblePaths= không tồn tại trên ổ đĩa. Hãy tạo thư mục đó, hoặc thêm dấu gạch ngang (ReadWritePaths=-/srv/notes/uploads) vào trước đường dẫn để systemd bỏ qua nếu nó không tồn tại. Nếu đường dẫn đã tồn tại, hãy kiểm tra lỗi chính tả trong unit và xác nhận lại bằng systemctl cat notes.service, vì một file drop-in có thể đang thêm một dòng cấu hình mà bạn không để ý.

Service dùng PrivateTmp có thể chia sẻ file qua /tmp được không?

Không, và đó chính là mục đích của nó. Service nhận được một /tmp/var/tmp mới, chỉ tồn tại trong thời gian service chạy, vì vậy socket hoặc file mà tiến trình khác tạo trong /tmp của host sẽ không hiển thị với nó. Socket database tại /tmp/mysql.sock thường là nạn nhân phổ biến, cách khắc phục là kết nối qua 127.0.0.1 hoặc trỏ client đến socket thực tại /run. Để kiểm tra các file tạm của chính service, hãy lấy PID chính từ systemctl show --property=MainPID --value và truy cập vào mount namespace của nó bằng sudo nsenter --target <pid> --mount.

Làm thế nào để service bị sandbox nghe trên cổng 443 mà không cần quyền root?

Hãy cấp một capability thay vì toàn bộ quyền root. Thiết lập AmbientCapabilities=CAP_NET_BIND_SERVICE cùng với CapabilityBoundingSet=CAP_NET_BIND_SERVICE, giữ lại User= hoặc DynamicUser=yes, khi đó tiến trình có thể bind vào các cổng thấp mà không cần quyền gì khác. Sai lầm thường gặp là chỉ thiết lập bounding set: khi đó capability được cho phép nhưng không bao giờ được cấp, và service sẽ thoát với lỗi phân quyền khi bind cổng. Trên một VPS đã chạy reverse proxy, giải pháp sạch hơn là để service bind 127.0.0.1:8080 và để nginx giữ cổng 443, nhờ đó unit không cần bất kỳ capability nào cả.

Tôi có nên dùng DynamicUser thay vì tạo một system user không?

Hãy dùng nó khi service lưu trữ toàn bộ dữ liệu bên trong StateDirectory=, CacheDirectory= hoặc LogsDirectory=, điều này áp dụng cho hầu hết các ứng dụng web tự host nhỏ. Bạn sẽ có một UID chỉ tồn tại khi service chạy, cộng với các tính năng PrivateTmp=, ProtectSystem=strict, ProtectHome=read-onlyRemoveIPC= được bật mặc định. Hãy dùng static system user khi UID cần phải cố định: ví dụ như khi có file sở hữu bên ngoài các thư mục đó, SSH key, NFS mount, hoặc một tiến trình thứ hai cần đọc cùng dữ liệu. Hãy nhớ rằng với DynamicUser=yes, dữ liệu thực sự nằm trong /var/lib/private/notes, và thư mục đó có mode 0700 và thuộc sở hữu của root, đây chính là lý do khiến các tác vụ backup không chạy bằng quyền root bị thất bại.