Phân tích sau sự cố lúc 2:14 sáng
Thiết bị báo động của tôi vang lên chính xác vào lúc 2:14 sáng. Sau khi chặn hơn 1.200 nỗ lực tấn công brute-force SSH vào đầu đêm đó, tôi cứ ngỡ điều tồi tệ nhất đã qua. Nhưng tôi đã lầm. Một công cụ thu thập dữ liệu web bằng Python, một công cụ nhỏ mà tôi đã bỏ qua, đã bị khai thác thông qua lỗ hổng thực thi mã từ xa (RCE). Mặc dù nó chạy dưới quyền người dùng không phải root, kẻ tấn công vẫn thực hiện thành công việc tải một tệp thực thi độc hại dung lượng 1,2MB vào /tmp và bắt đầu quét /etc để tìm thông tin đăng nhập cơ sở dữ liệu.
Sự cố đó đã chứng minh rằng quyền hạn người dùng tiêu chuẩn là không đủ. Nếu một dịch vụ không cần chạm vào phần còn lại của hệ thống, nó thậm chí không nên nhìn thấy chúng. Cơ chế sandboxing của Systemd giải quyết vấn đề này bằng cách sử dụng các Linux namespaces để bao bọc ứng dụng trong một môi trường bị hạn chế. Nó biến một vụ xâm nhập tiềm ẩn trên toàn hệ thống thành một sự cố nhỏ và được kiểm soát.
Bắt đầu nhanh: Gia cố bảo mật dịch vụ trong 5 phút
Bạn có thể giảm đáng kể bề mặt tấn công bằng cách thêm chỉ sáu dòng vào khối [Service] của mình. Hãy xem một quá trình chuyển đổi điển hình cho một tệp dịch vụ đặt tại /etc/systemd/system/my-app.service.
Cấu hình dễ bị tấn công
[Unit]
Description=Ứng dụng dễ bị tấn công của tôi
[Service]
ExecStart=/usr/bin/python3 /opt/my-app/app.py
User=myappuser
Restart=always
[Install]
WantedBy=multi-user.target
Cấu hình đã được gia cố
[Unit]
Description=Ứng dụng bảo mật của tôi
[Service]
ExecStart=/usr/bin/python3 /opt/my-app/app.py
User=myappuser
Restart=always
# Gia cố bảo mật
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed
[Install]
WantedBy=multi-user.target
Áp dụng các thay đổi này bằng cách chạy lệnh systemctl daemon-reload sau đó là systemctl restart my-app. Bạn vừa mới khóa chặt các cánh cửa rồi đấy.
Cơ chế hoạt động của Sandbox
Systemd sử dụng các namespace ở cấp độ kernel—cụ thể là mount, UTS, IPC và PID—để thực thi các hạn chế này. Dưới đây là những gì các chỉ thị cụ thể đó thực sự thực hiện bên dưới hệ thống.
1. PrivateTmp=true
Các thư mục /tmp và /var/tmp là những sân chơi khét tiếng cho những kẻ tấn công vì chúng có quyền ghi cho tất cả mọi người (world-writable). Bằng cách đặt PrivateTmp=true, dịch vụ sẽ có namespace hệ thống tệp riêng. Nó sẽ thấy một thư mục /tmp riêng tư hoàn toàn tách biệt với phần còn lại của máy chủ. Khi dịch vụ dừng lại, Systemd sẽ xóa sạch thư mục riêng tư này, ngay lập tức loại bỏ bất kỳ mã độc nào còn sót lại.
2. ProtectSystem=full
Chỉ thị này kiểm soát những phần nào của hệ điều hành mà dịch vụ có thể sửa đổi. Nó có ba cấp độ chính:
- true: Gắn kết (mount)
/usr,/boot, và/efidưới dạng chỉ đọc. - full: Bao gồm các thư mục trên và cũng đặt
/etcở chế độ chỉ đọc. - strict: Đặt toàn bộ hệ thống tệp ở chế độ chỉ đọc. Sau đó, bạn phải cấp quyền ghi thủ công cho các thư mục cụ thể bằng
ReadWritePaths=.
Đối với hầu hết các API, full là sự cân bằng tốt nhất. Nó ngăn chặn kẻ tấn công thay đổi trái phép các tệp cấu hình hoặc tệp thực thi của bạn.
3. NoNewPrivileges=true
Điều này ngăn dịch vụ và bất kỳ tiến trình con nào của nó nhận được các đặc quyền mới. Nó thiết lập cờ PR_SET_NO_NEW_PRIVS, vô hiệu hóa các bit SUID/SGID. Ngay cả khi kẻ tấn công tìm thấy lỗi trong một tệp thực thi SUID như sudo hoặc pkexec khi đang ở bên trong ứng dụng của bạn, chúng cũng không thể sử dụng nó để leo thang lên quyền root.
4. ProtectHome=true
Ứng dụng của bạn có thực sự cần truy cập /home/admin không? Chắc là không. Việc đặt giá trị này thành true sẽ khiến /home, /root, và /run/user hiển thị hoàn toàn trống rỗng. Điều này bảo vệ các SSH key và dữ liệu cá nhân của bạn khỏi bị trích xuất trái phép nếu ứng dụng bị xâm nhập.
Gia cố bảo mật nâng cao
Nếu dịch vụ của bạn công khai ra internet, bạn nên áp dụng các ràng buộc chặt chẽ hơn nữa để hạn chế rủi cho kernel.
Lọc các lời gọi hệ thống (System Calls)
Một ứng dụng web thông thường không có lý do gì để gọi lệnh reboot() hoặc kexec_load(). Bạn có thể giới hạn các syscall nào được phép bằng một danh sách trắng (whitelist). Systemd cung cấp một nhóm @system-service tiện lợi đáp ứng hầu hết các nhu cầu phổ biến:
# Chặn các lời gọi kernel nguy hiểm hoặc không cần thiết
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
Loại bỏ các Kernel Capabilities
Trong Linux, đặc quyền root được chia nhỏ thành các ‘capabilities’. Ví dụ, CAP_NET_BIND_SERVICE cho phép một tiến trình lắng nghe trên cổng 443. Nếu ứng dụng của bạn chạy trên cổng 8080, nó không cần bất kỳ capability nào. Bạn có thể loại bỏ tất cả chúng chỉ bằng một dòng:
CapabilityBoundingSet=
Cô lập mạng
Nếu bạn đang chạy một worker cục bộ chỉ xử lý các tệp tin, hãy tắt hoàn toàn mạng. PrivateNetwork=true sẽ ngắt kết nối dịch vụ khỏi mọi thứ ngoại trừ giao diện loopback cục bộ (lo).
Mẹo triển khai trong môi trường Production
Việc gia cố bảo mật có thể gây ra lỗi nếu bạn không cẩn thận. Dưới đây là cách triển khai các thay đổi này một cách an toàn.
Kiểm tra với systemd-analyze
Đừng đoán mò—hãy kiểm tra kết quả của bạn. Systemd tích hợp sẵn một công cụ kiểm tra bảo mật. Chạy lệnh này để xem mức độ rủi ro hiện tại của bạn:
systemd-analyze security my-app.service
Hầu hết các dịch vụ mặc định đều đạt số điểm nguy hiểm là 9.6/10. Mục tiêu của bạn cho một dịch vụ cấp độ production nên là điểm dưới 2.5.
Xử lý quyền truy cập hệ thống tệp
Nếu bạn sử dụng ProtectSystem=strict, ứng dụng của bạn sẽ gặp lỗi khi cố gắng ghi log. Hãy sử dụng các chỉ thị hiện đại này để quản lý các đường dẫn một cách tự động:
RuntimeDirectory=my-app # Tạo thư mục /run/my-app
StateDirectory=my-app # Tạo thư mục /var/lib/my-app
LogsDirectory=my-app # Tạo thư mục /var/log/my-app
Systemd xử lý việc tạo thư mục và đặt chủ sở hữu cho User= được định nghĩa trong tệp dịch vụ của bạn. Cách này sạch sẽ và an toàn hơn so với việc sử dụng các lệnh chown thủ công.
Tối ưu hóa ghi log
Tránh ghi vào các tệp văn bản thuần bên trong sandbox. Hãy cấu hình ứng dụng của bạn để ghi log ra stdout hoặc stderr. journald của Systemd sẽ tự động thu thập các luồng dữ liệu này, loại bỏ nhu cầu dịch vụ phải có quyền ghi vào /var/log.
Kết luận
Bảo mật là một hệ thống gồm nhiều lớp. Sandboxing trong Systemd đảm bảo rằng một lỗ hổng đơn lẻ không dẫn đến việc chiếm quyền kiểm soát toàn bộ hệ thống. Bằng cách nhốt những kẻ tấn công vào một chiếc hộp chỉ đọc, không có mạng và không có cách nào để leo thang đặc quyền, bạn sẽ có thêm thời gian để phản ứng. Hãy bắt đầu với PrivateTmp và ProtectSystem, sau đó sử dụng systemd-analyze để lấp đầy các lỗ hổng còn lại.

