1. Tạo file Unit Systemd để quản lý Docker Container
Chúng ta sẽ tạo một file unit tùy chỉnh thay vì dùng systemd-run để có toàn quyền kiểm soát cấu hình, đặc biệt là phần [Service] và [Install].
Việc tạo file unit riêng giúp Docker daemon có thể quản lý container như một service độc lập, tách biệt khỏi các container khác, đồng thời cho phép cấu hình các tham số khởi động phức tạp.
Tạo file cấu hình unit
Đường dẫn file: /etc/systemd/system/db-container.service
Nội dung file hoàn chỉnh:
[Unit]
Description=Database Container Service (Hardened)
Documentation=man:docker(1)
After=docker.service
Requires=docker.service
Wants=docker.socket
[Service]
Type=notify
ExecStart=/usr/bin/docker start -a db-mysql-secure
ExecStop=/usr/bin/docker stop -t 10 db-mysql-secure
Restart=on-failure
RestartSec=5s
TimeoutStartSec=300
TimeoutStopSec=60
LimitNOFILE=65536
LimitNPROC=65536
ProtectHome=true
ProtectSystem=strict
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Kết quả mong đợi: File được tạo thành công tại /etc/systemd/system/ với nội dung chính xác, sẵn sàng để systemd nhận diện.
Cấu hình phần [Service] chi tiết
Tham số Restart=on-failure đảm bảo container tự động khởi động lại nếu bị sập do lỗi (exit code khác 0), nhưng không khởi động lại nếu dừng thủ công.
Tham số TimeoutStartSec=300 cho phép container 5 phút để khởi động xong (cần thiết cho Database cần thời gian init schema).
Tham số LimitNOFILE và LimitNPROC giới hạn số file descriptor và tiến trình con mà container có thể tạo, ngăn chặn tấn công DoS từ trong container.
Tham số NoNewPrivileges=true ngăn chặn container từ chối bất kỳ yêu cầu nâng cao đặc quyền nào (setuid/setgid) sau khi khởi động.
Verify kết quả tạo file
Thực hiện lệnh kiểm tra syntax của file unit:
systemd-analyze verify /etc/systemd/system/db-container.service
Kết quả mong đợi: Không có dòng lỗi (error) hoặc cảnh báo (warning) nào xuất hiện. Nếu có, hãy sửa lại file unit trước khi tiếp tục.
2. Sử dụng Systemd Scope để cô lập quyền hạn
Thay vì chạy container trực tiếp như một tiến trình bình thường, chúng ta sẽ sử dụng systemd-run để tạo một Scope (nhóm tiến trình) hoặc chạy container trong một scope cô lập.
Việc này giúp systemd quản lý tài nguyên (CPU, RAM) và mạng lưới cho container như một đơn vị độc lập, đồng thời áp dụng các chính sách bảo mật của systemd (cgroup) lên toàn bộ container.
Tạo Scope với hệ thống cgroups
Chúng ta sẽ cấu hình lại file unit để sử dụng Delegate=yes trong phần [Service], cho phép container tự quản lý cgroup của mình (thường là do Docker daemon tạo ra), nhưng vẫn nằm trong scope của systemd.
Tuy nhiên, để đơn giản và an toàn hơn cho việc quản lý container đã tồn tại, chúng ta sẽ sử dụng lệnh systemd-run để khởi động container trong một scope riêng biệt với các ràng buộc tài nguyên.
Lệnh khởi động container trong Scope
Thay vì chạy docker start trực tiếp, chúng ta dùng systemd-run để bọc lệnh Docker:
systemd-run --unit=db-mysql-scope --scope --property=LimitNOFILE=65536 --property=LimitNPROC=65536 --property=NoNewPrivileges=true /usr/bin/docker start -a db-mysql-secure
Kết quả mong đợi: Lệnh chạy thành công, tạo ra một unit mới tên db-mysql-scope.scope chứa tiến trình của container.
Update file Unit để sử dụng Scope
Chúng ta sẽ cập nhật file db-container.service để sử dụng cấu hình Scope thay vì chạy trực tiếp Docker, đảm bảo tính nhất quán khi khởi động lại hệ thống.
Chỉnh sửa dòng ExecStart trong file unit để trỏ vào việc khởi động scope hoặc giữ nguyên cấu hình [Service] với Type=oneshot nếu dùng để wrap lệnh khởi động một lần.
Tuy nhiên, cách chuẩn nhất cho container dài hạn là giữ nguyên file unit như phần 1, nhưng thêm thuộc tính Delegate=yes nếu container tự quản lý cgroup, hoặc dùng SystemdScope=yes trong Docker (nếu Docker version hỗ trợ) để Docker tự động tạo scope.
Để đơn giản và tương thích, chúng ta sẽ thêm dòng Delegate=yes vào file unit cũ để systemd không can thiệp quá sâu vào cgroup của Docker, nhưng vẫn giám sát tiến trình master.
[Service]
# ... các dòng cũ giữ nguyên ...
Delegate=yes
KillMode=mixed
Kết quả mong đợi: File unit đã được cập nhật, cho phép Docker quản lý cgroup con của container trong khi systemd quản lý tiến trình Docker client.
Verify kết quả Scope
Kiểm tra xem container đã nằm trong scope hay chưa:
systemctl list-units --type=scope | grep db-mysql
Kiểm tra các thuộc tính của scope:
systemctl show db-mysql-scope.scope | grep -E "LimitNOFILE|LimitNPROC|NoNewPrivileges"
Kết quả mong đợi: Xuất hiện unit db-mysql-scope.scope và các giá trị giới hạn (Limit) khớp với cấu hình đã đặt.
3. Kích hoạt và kiểm tra trạng thái Service
Sau khi đã có file unit và cấu hình scope, bước cuối là thông báo cho systemd biết về service mới và kích hoạt nó.
Việc này sẽ đăng ký service vào hệ thống, cho phép khởi động tự động khi máy lên và kiểm soát vòng đời (start/stop/restart) qua lệnh systemctl.
Reload Daemon và Enable Service
Reload cấu hình của systemd để nhận diện file unit mới được tạo:
systemctl daemon-reload
Enable service để tự động khởi động khi hệ thống boot:
systemctl enable db-container.service
Khởi động service ngay lập tức:
systemctl start db-container.service
Kết quả mong đợi: Lệnh chạy không lỗi, service được thêm vào symlink tại /etc/systemd/system/multi-user.target.wants/.
Kiểm tra trạng thái (Status)
Xem trạng thái chi tiết của service:
systemctl status db-container.service
Kết quả mong đợi: Dòng Active: active (running) xuất hiện, cùng với thông tin Main PID trỏ vào tiến trình Docker client hoặc container.
Xem log của Service
Xem log realtime của service để kiểm tra quá trình khởi động và hoạt động:
journalctl -u db-container.service -f
Kiểm tra log khi service bị sập (giả lập lỗi để test RestartPolicy):
docker stop db-mysql-secure
Chờ vài giây và kiểm tra lại status:
systemctl status db-container.service
Kết quả mong đợi: Sau khi dừng container thủ công, service tự động chuyển sang trạng thái active (running) trở lại nhờ Restart=on-failure.
Verify bảo mật và tài nguyên
Kiểm tra xem các giới hạn tài nguyên (LimitNOFILE) đã được áp dụng chưa trong cgroup của container:
systemctl show db-container.service | grep LimitNOFILE
Kiểm tra xem container có bị giới hạn đặc quyền không:
systemctl show db-container.service | grep NoNewPrivileges
Kết quả mong đợi: Giá trị LimitNOFILE=65536 và NoNewPrivileges=true được trả về.
Điều hướng series:
Mục lục: Series: Xây dựng Database an toàn với Linux Seccomp và Systemd Service Hardening
« Phần 5: Hardening Docker Container với Linux Capabilities và Namespaces
Phần 7: Tích hợp Seccomp và Capabilities vào Systemd Service »