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/usrvà/bootở chế độ read-onlyfull— Thêm/etcvào danh sách read-onlystrict— Toàn bộ filesystem read-only, trừ những path được whitelist quaReadWritePaths
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/usertrở thành inaccessibleread-only— Mount ở chế độ chỉ đọctmpfs— 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 và /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/setgidProtectKernelTunables=true— Chặn ghi vào/proc/sysvà/sysProtectKernelModules=true— Chặn load kernel module tùy ýProtectControlGroups=true—/sys/fs/cgrouptrở thành read-onlyRestrictSUIDSGID=true— Chặn tạo file SUID/SGID mớiRestrictAddressFamilies=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ự:
- Service exposed ra internet (nginx, apache, Node.js web) — áp dụng ngay, mức
strict - Service xử lý dữ liệu nhạy cảm (database, auth service) —
strict+ thêmPrivateNetworknếu chỉ dùng localhost - Background job, cron task — ít nhất
ProtectSystem=full+PrivateTmp=true - 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.

