SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

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= và 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 với tài khoản nào

Nếu không có User=, dịch vụ sẽ chạy với quyền root, và mọi chỉ thị khác trong phần này chỉ nhằm giảm thiểu thiệt hại. Có hai cách để tránh điều đó.

Tài khoản system cố định. Tạo tài khoản bằng sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes, sau đó đặt User=notes trong unit. UID giữ nguyên qua các lần khởi động lại và reboot. Điều này quan trọng khi dịch vụ sở hữu các file bên ngoài thư mục state riêng, hoặc khi một backup job cần đọc các file đó. Lý do cấp cho mỗi dịch vụ một tài khoản riêng không có đặc quyền vẫn áp dụng nguyên vẹn ở đây.

DynamicUser=yes. systemd cấp một UID từ dải dành riêng khi dịch vụ khởi động và thu hồi UID đó khi dịch vụ dừng. Không có tài khoản nào tồn tại trên disk, nên không còn gì sót lại khi bạn gỡ dịch vụ. Trong thời gian dịch vụ chạy, getent passwd notes 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 đó biến mất.

DynamicUser=yes cũng tự bật thêm một số thiết lập khác cho bạn: RemoveIPC=yes, NoNewPrivileges=yes, RestrictSUIDSGID=yes, ProtectSystem=strict, ProtectHome=read-only và một /tmp riêng. Trên systemd hiện tại, /tmp riêng đó là PrivateTmp=disconnected, một tmpfs mới, trừ khi bạn tự đặt PrivateTmp=yes như unit mẫu. Chỉ một dòng đã tạo được phần lớn sandbox. Vì vậy unit mẫu vẫn ngắn.

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

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 và /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= và 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 và /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-only và tmpfs 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 và /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 và /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.

Dịch vụ có thể làm gì: NoNewPrivileges và capabilities

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

Những gì bị ảnh hưởng. Bất kỳ thành phần nào gọi sudo từ bên trong service. sudo hiện tại kiểm tra cờ này trước mọi việc khác rồi thoát với The "no new privileges" flag is set, which prevents sudo from running as root. Lỗi effective uid is not 0 ... 'nosuid' option cũ hơn mà bạn có thể thấy được trích dẫn ở nơi khác lại liên quan đến lỗi mount hoặc cài đặt, không phải lỗi do directive này tạo ra. Các bước kiểm tra mật khẩu của PAM (pluggable authentication modules) gọi shell đến unix_chkpwd cũng thất bại vì cùng lý do. Các công cụ container rootless cần newuidmap cũng vậy. Nếu service của bạn phụ thuộc vào một trong các thành phần đó, hãy bỏ dependency thay vì bỏ directive.

CapabilityBoundingSet= giới hạn các capability mà mọi tiến trình trong unit có thể giữ, còn việc gán giá trị rỗng sẽ loại bỏ toàn bộ capability. Với service vốn đã chạy bằng user không phải root, đây là lớp khóa thứ hai chứ không phải lớp đầu tiên, vì NoNewPrivileges=yes chặn cách thông thường để tiến trình lấy thêm 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 bị chặn hai lần.

Capability duy nhất mà web service thường cần là CAP_NET_BIND_SERVICE để dùng cổng nhỏ hơn 1024. Tiến trình không phải root cần được cấp capability này, không chỉ được phép sử dụng, nên cần có cả hai dòng.

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

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

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ể kết nối đến đâu

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

Nó làm hỏng những gì. AF_NETLINK, thường xuyên hơn nhiều so với dự đoán. getifaddrs() của glibc mở một socket netlink và không có phương án dự phòng, net.Interfaces() của Go đọc trực tiếp cùng một bản dump netlink, còn .NET liệt kê các interface thông qua getifaddrs(). Vì vậy, một dịch vụ chỉ muốn biết địa chỉ IP của chính nó cũng dừng với OSError: [Errno 97] Address family not supported by protocol hoặc lỗi tương đương trong ngôn ngữ đó. Java là ngoại lệ: java.net.NetworkInterface của OpenJDK trên Linux dùng một ioctl cho IPv4 và đọc /proc/net/if_inet6 cho IPv6, nên vẫn hoạt động mà không cần AF_NETLINK. Khi lỗi xảy ra, danh sách trở thành AF_UNIX AF_INET AF_INET6 AF_NETLINK, vẫn hẹp hơn nhiều so với mặc định. AF_PACKET là quyền cần cho việc capture packet thô, và hầu như không dịch vụ nào khác nên có quyền này.

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

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 system và ls /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

C?ProtectSystem=strict thực sự làm những gì thành chỉ đọc?

Toàn bộ cây filesystem, ngoại trừ /dev, /proc và /sys. Các đường dẫn này được xử lý thay bằng PrivateDevices=, ProtectKernelTunables= và ProtectControlGroups=. Điều đó bao gồm /etc, /var, /srv, /opt và /tmp. Các ngoại lệ mà systemd tự thêm lại là những thư mục do systemd quản lý: StateDirectory=, CacheDirectory=, LogsDirectory= và RuntimeDirectory=. Mọi vị trí khác mà service cần ghi đều phải có một mục ReadWritePaths= rõ ràng. Lưu ý rằng chỉ đọc không có nghĩa là không thể đọc. Vì vậy, service vẫn có thể mở một file chứa secret ở vị trí khác trên máy, trừ khi bạn liệt kê file đó trong InaccessiblePaths=.

Vì sao service của tôi fail với status=226/NAMESPACE?

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

Service có PrivateTmp có thể vẫn chia sẻ file qua /tmp không?

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

Service được sandbox có thể listen trên port 443 mà không cần root bằng cách nào?

Chỉ cấp một capability thay vì toàn bộ tài khoản root. Đặt AmbientCapabilities=CAP_NET_BIND_SERVICE cùng với CapabilityBoundingSet=CAP_NET_BIND_SERVICE, giữ User= hoặc DynamicUser=yes, khi đó process có thể bind các port thấp và không có thêm quyền nào khác. Chỉ đặt bounding set là lỗi thường gặp. Khi đó capability được phép sử dụng nhưng chưa được cấp, và service thoát với lỗi permission có nêu port. Nếu VPS đã chạy reverse proxy, cách gọn hơn là bind 127.0.0.1:8080 trong service và để nginx giữ port 443. Khi đó unit không cần capability nào.

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

Hãy dùng DynamicUser khi service lưu toàn bộ dữ liệu trong StateDirectory=, CacheDirectory= hoặc LogsDirectory=. Cách này phù hợp với phần lớn web application self-host nhỏ. Bạn nhận được một UID chỉ tồn tại trong thời gian service chạy, cùng với PrivateTmp=, ProtectSystem=strict, ProtectHome=read-only, NoNewPrivileges=, RestrictSUIDSGID= và RemoveIPC= được bật sẵn. Hãy dùng static system user khi UID phải ổn định, chẳng hạn với các file được sở hữu bên ngoài những thư mục đó, một SSH key, một NFS mount hoặc process thứ hai cần đọc cùng dữ liệu. Lưu ý rằng với DynamicUser=yes, dữ liệu thực sự nằm trong /var/lib/private/notes. Thư mục đó có mode 0700 và thuộc sở hữu của root, nên các backup job chạy bằng non-root sẽ fail.