Systemd Hardening: Bảo mật dịch vụ với ProtectSystem, ProtectHome và PrivateTmp

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Chuyện xảy ra lúc 2 giờ sáng

Mình từng bị brute-force SSH và phải xử lý gấp lúc nửa đêm. Cảm giác nhìn log thấy hàng trăm lần thử đăng nhập từ IP lạ — không phải vui. Từ hôm đó, mình thay đổi hoàn toàn cách setup server: bảo mật ngay từ đầu, không chờ đến khi có sự cố mới vá.

Một trong những thứ mình áp dụng sau vụ đó là systemd hardening — cụ thể là các directive ProtectSystem, ProtectHome, và PrivateTmp. Phần lớn sysadmin không dùng vì không biết hoặc thấy phức tạp. Thực ra setup chỉ mất 10 phút, và lợi ích thì rõ ràng ngay.

Vấn đề cốt lõi là: khi một service bị compromise (bị khai thác lỗ hổng), attacker có thể đọc /etc/passwd, ghi vào thư mục home, hoặc leak data qua /tmp sang service khác. systemd hardening chặn điều đó ở tầng kernel — không cần cài thêm phần mềm nào.

Ba directive cần nắm trước khi cấu hình

ProtectSystem — Khóa filesystem hệ thống

Giới hạn khả năng ghi vào filesystem. Có 3 mức tăng dần:

  • true — Mount /usr/boot ở chế độ read-only
  • full — Thêm /etc vào danh sách read-only
  • strict — Toàn bộ filesystem read-only, trừ những path được whitelist qua ReadWritePaths

Mình thường dùng strict cho các service không cần ghi ra ngoài thư mục data riêng của chúng.

ProtectHome — Ẩn thư mục home

Ẩn hoặc làm trống thư mục home của tất cả user. Có 3 mức:

  • true/home, /root, /run/user trở thành inaccessible
  • read-only — Mount ở chế độ chỉ đọc
  • tmpfs — Mount tmpfs trống lên trên, service thấy thư mục home rỗng hoàn toàn

PrivateTmp — Namespace /tmp riêng biệt

Cung cấp namespace /tmp/var/tmp riêng biệt cho mỗi service. Quan trọng hơn bạn nghĩ: nhiều ứng dụng dùng /tmp để trao đổi dữ liệu tạm. Nếu một service bị compromise và ghi file vào /tmp, không có PrivateTmp thì service khác trên cùng server có thể đọc được.

Cài đặt và cấu hình

Không cần cài thêm gì — systemd có sẵn trên Ubuntu, Debian, RHEL. Chỉ cần chỉnh service file.

Chỉnh service hiện có bằng override file

Không nên sửa trực tiếp service file gốc vì package update sẽ ghi đè. Dùng systemctl edit thay thế:

sudo systemctl edit nginx

Lệnh này mở editor và tạo file tại /etc/systemd/system/nginx.service.d/override.conf. Thêm nội dung:

[Service]
# Filesystem protection
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

# Nginx cần ghi vào các path này — phải whitelist
ReadWritePaths=/var/log/nginx /var/lib/nginx /run/nginx

Sau khi lưu, reload và restart:

sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl status nginx

Viết service file mới với hardening từ đầu

Nếu bạn đang tạo service file cho ứng dụng tự phát triển (Python, Node.js…), thêm trực tiếp vào section [Service]:

[Unit]
Description=My Web App
After=network.target

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

# --- Hardening ---
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true

# Chỉ cho phép ghi vào thư mục data của app
ReadWritePaths=/opt/myapp/data /var/log/myapp

[Install]
WantedBy=multi-user.target

Các directive bổ sung đáng dùng

Ngoài 3 directive chính, mình thường thêm những cái này vào mọi service:

  • NoNewPrivileges=true — Service không thể escalate privilege qua setuid/setgid
  • ProtectKernelTunables=true — Chặn ghi vào /proc/sys/sys
  • ProtectKernelModules=true — Chặn load kernel module tùy ý
  • ProtectControlGroups=true/sys/fs/cgroup trở thành read-only
  • RestrictSUIDSGID=true — Chặn tạo file SUID/SGID mới
  • RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 — Giới hạn loại socket được dùng

Kiểm tra và Monitoring

Verify cấu hình đang áp dụng

Sau khi restart, xác nhận directive đã có hiệu lực:

systemctl show nginx | grep -E "ProtectSystem|ProtectHome|PrivateTmp"

Output mong đợi:

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

Dùng systemd-analyze để đánh giá security score

Đây là công cụ built-in mà ít người biết đến. Nó chấm điểm bảo mật của từng service (0 = tốt nhất, 10 = tệ nhất):

systemd-analyze security nginx

Kết quả trả về bảng chi tiết từng directive và tổng điểm:

  NAME                              DESCRIPTION                          EXPOSURE
✓ PrivateTmp=yes                   Service has a private /tmp           0.0
✓ ProtectSystem=strict             Protected system files               0.0
✓ NoNewPrivileges=yes              Cannot gain new privileges           0.0
✗ User=/DynamicUser=               Service runs as root user            0.4
...
→ Overall exposure level for nginx.service: 3.8 OK 🙂

Mục tiêu là đưa score xuống dưới 4.0. Chỉ với 3 directive chính, score thường giảm từ 7–8 xuống còn 4–5.

Test thực tế — xác nhận protection hoạt động

Cách nhanh nhất để kiểm tra ProtectHome hoạt động: nhảy vào namespace của process và xem thư mục home:

# Lấy PID của nginx
PID=$(systemctl show nginx --property MainPID --value)

# Kiểm tra /home trong namespace của nginx
sudo nsenter -t $PID --mount -- ls -la /home/
# Nếu ProtectHome=true → thư mục trống hoàn toàn

# Kiểm tra /tmp riêng biệt
sudo nsenter -t $PID --mount -- ls /tmp/
# Nếu PrivateTmp=true → /tmp sạch, không thấy file của process khác

Theo dõi lỗi sau khi bật hardening

Bước này quan trọng nhất — thiếu ReadWritePaths là nguyên nhân phổ biến nhất khiến service crash sau khi bật ProtectSystem=strict:

sudo journalctl -u nginx -f

Lỗi điển hình:

nginx: [emerg] open() "/var/log/nginx/error.log" failed (30: Read-only file system)

Fix bằng cách thêm path vào whitelist trong override file:

sudo systemctl edit nginx
# Thêm dòng: ReadWritePaths=/var/log/nginx
sudo systemctl daemon-reload && sudo systemctl restart nginx

Script audit nhanh toàn bộ service

Muốn biết service nào trên server chưa có hardening:

#!/bin/bash
echo "Services có security score > 6.0 (cần hardening):"
for svc in $(systemctl list-units --type=service --state=running --no-legend | awk '{print $1}'); do
  score=$(systemd-analyze security "$svc" 2>/dev/null | grep "Overall exposure" | grep -oP '[0-9]+\.[0-9]+')
  if [[ -n "$score" ]] && (( $(echo "$score > 6.0" | bc -l) )); then
    echo "  $svc → score: $score"
  fi
done

Thứ tự ưu tiên khi áp dụng

Không phải service nào cũng cần cùng mức độ. Mình phân theo thứ tự:

  1. Service exposed ra internet (nginx, apache, Node.js web) — áp dụng ngay, mức strict
  2. Service xử lý dữ liệu nhạy cảm (database, auth service) — strict + thêm PrivateNetwork nếu chỉ dùng localhost
  3. Background job, cron task — ít nhất ProtectSystem=full + PrivateTmp=true
  4. System service cốt lõi (sshd, systemd-resolved) — thận trọng, test kỹ trên môi trường staging trước

Điểm mình học được từ sau vụ brute-force SSH: attacker không cần root ngay từ đầu. Họ chỉ cần vào được một service, đọc được file cấu hình có chứa database password, rồi pivot sang target khác. ProtectSystem=strict chặn đúng bước đó — dù service bị compromise cũng không đọc được /etc/otherapp/db.conf của service hàng xóm.

Setup mất 10 phút, audit bằng systemd-analyze security thêm 5 phút. Đổi lại là ngủ ngon hơn lúc 2 giờ sáng.

Share: