Cấu hình Linux Capabilities: Nguyên tắc Tối thiểu (Least Privilege)
Linux Capabilities là cơ chế phân chia quyền root thành các đơn vị quyền lực nhỏ hơn. Mặc định, container Docker chạy với quyền root và mang theo nhiều capabilities nguy hiểm không cần thiết cho việc chạy Database.
Bước 1: Tắt toàn bộ Capabilities không cần thiết
Chúng ta bắt đầu bằng việc loại bỏ toàn bộ quyền mặc định của container để đảm bảo không có lỗ hổng nào từ các quyền thừa. Sau đó, chỉ thêm vào những quyền tối thiểu mà Database cần để hoạt động.
Database (ví dụ PostgreSQL/MySQL) thường cần quyền tạo socket mạng (NET_BIND_SERVICE) và quyền đọc ghi file (CHOWN, FOWNER). Các quyền như SYS_ADMIN hay SYS_PTRACE là cực kỳ nguy hiểm và phải bị loại bỏ.
Thực thi lệnh sau để khởi động container với danh sách quyền trắng:
docker run -d --name db-hardened --cap-drop ALL --cap-add NET_BIND_SERVICE --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE postgres:15
Kết quả mong đợi: Container khởi động thành công. Nếu Database cần thêm quyền nào khác (ví dụ: cần truy cập file device đặc biệt), bạn sẽ thấy lỗi "permission denied" trong log, lúc đó mới bổ sung capability cụ thể đó.
Bước 2: Xác minh danh sách Capabilities hiện hành
Sau khi container chạy, bạn cần xác minh rằng các quyền không mong muốn đã bị loại bỏ hoàn toàn. Không dùng lệnh `docker inspect` vì nó hiển thị cấu hình, hãy kiểm tra trực tiếp trên file system của container.
Truy cập vào container và kiểm tra file capabilities của tiến trình chính:
docker exec -it db-hardened cat /proc/1/status | grep Cap
Kết quả mong đợi: Dòng `CapEff` (Effective Capabilities) sẽ chỉ hiển thị các hex value tương ứng với các quyền bạn đã `cap-add`. Các quyền như `SYS_ADMIN` (0x2000) sẽ không xuất hiện trong danh sách này.
Cô lập tiến trình với Linux Namespaces
Docker mặc định đã cô lập container với các namespace cơ bản (PID, UTS, Network, Mount, IPC). Tuy nhiên, để tăng cường bảo mật cho Database, chúng ta cần kiểm soát chặt chẽ hơn việc container nhìn thấy các tiến trình bên ngoài hoặc các tài nguyên IPC.
Bước 3: Cô lập PID Namespace để ẩn tiến trình Host
Trong môi trường production, database không cần biết về các tiến trình đang chạy trên host. Mặc định Docker đã sử dụng PID namespace, nhưng chúng ta cần cấu hình lại để đảm bảo container chỉ nhìn thấy tiến trình của chính nó (PID 1) và các tiến trình con.
Điều này ngăn chặn kẻ tấn công nếu xâm nhập được vào container thực hiện quét các tiến trình nhạy cảm trên host thông qua namespace PID.
Chạy lại container với flag `--pid=private` (mặc định nhưng nên khai báo rõ ràng để hardening):
docker run -d --name db-hardened-pid --pid=private --cap-drop ALL --cap-add NET_BIND_SERVICE postgres:15
Kết quả mong đợi: Khi `docker exec` vào container và chạy lệnh `ps aux`, bạn sẽ chỉ thấy các tiến trình của Database, không thấy bất kỳ tiến trình nào của host (như systemd, docker daemon, sshd).
Bước 4: Cô lập IPC Namespace để chặn Shared Memory
Inter-Process Communication (IPC) là cơ chế trao đổi dữ liệu giữa các tiến trình. Nếu container chia sẻ IPC namespace với host, tiến trình bên trong container có thể truy cập vào Shared Memory của các ứng dụng nhạy cảm trên host.
Chúng ta yêu cầu Docker tạo một IPC namespace riêng biệt cho container.
Tham số `--ipc=private` đảm bảo container có không gian IPC độc lập, không thể tương tác với host:
docker run -d --name db-hardened-ipc --ipc=private --cap-drop ALL --cap-add NET_BIND_SERVICE postgres:15
Kết quả mong đợi: Trong container, lệnh `ipcs -m` sẽ chỉ hiển thị shared memory do chính Database tạo ra, không hiển thị bất kỳ shared memory nào của host.
Chế độ Read-Only Filesystem
Ngăn chặn việc ghi dữ liệu vào filesystem của container là biện pháp bảo mật mạnh mẽ. Nếu hacker xâm nhập được, họ không thể thay đổi cấu hình hệ thống, cài đặt backdoor hoặc ghi log giả mạo vào các thư mục hệ thống.
Bước 5: Áp dụng chế độ Read-Only cho toàn bộ container
Chúng ta sẽ sử dụng flag `--read-only` để khóa toàn bộ filesystem của image gốc. Lưu ý: Database cần ghi dữ liệu vào thư mục data và log, do đó chúng ta phải mount volume riêng cho các thư mục này để ghi dữ liệu vào host, trong khi phần còn lại của container vẫn ở chế độ chỉ đọc.
Đối với các thư mục tạm thời như `/tmp` hoặc `/run`, chúng ta cần sử dụng tmpfs (bộ nhớ RAM) để container có thể ghi tạm thời mà không làm thay đổi filesystem gốc.
Cấu hình container với filesystem read-only, volume cho data, và tmpfs cho thư mục tạm:
docker run -d --name db-readonly --read-only --cap-drop ALL --cap-add NET_BIND_SERVICE --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --tmpfs /tmp:rw,noexec,nosuid,size=100M --tmpfs /run:rw,noexec,nosuid,size=50M -v /var/lib/postgresql/data:/var/lib/postgresql/data:rw,z postgres:15
Kết quả mong đợi: Container khởi động thành công. Nếu bạn cố gắng tạo file mới vào thư mục root của container (ví dụ: `touch /test`), bạn sẽ nhận được lỗi `Read-only file system`. Dữ liệu database vẫn được ghi vào volume mount `/var/lib/postgresql/data`.
Kiểm thử và Xác minh kết quả Hardening
Sau khi áp dụng tất cả các bước trên, chúng ta cần thực hiện kiểm thử để đảm bảo các biện pháp bảo mật đã hoạt động đúng như mong đợi và không làm gián đoạn hoạt động của Database.
Bước 6: Kiểm tra khả năng ghi dữ liệu bất hợp pháp
Thử nghiệm việc ghi file vào filesystem gốc của container. Mục đích là xác nhận flag `--read-only` đang hoạt động.
Thực thi lệnh sau bên trong container:
docker exec -it db-readonly sh -c "touch /etc/malicious_file"
Kết quả mong đợi: Lệnh bị từ chối với thông báo lỗi `touch: cannot touch '/etc/malicious_file': Read-only file system`. Điều này chứng tỏ filesystem gốc đã được khóa.
Bước 7: Kiểm tra quyền truy cập vào Host
Xác minh container không thể truy cập vào các tiến trình hoặc IPC của host, đồng thời kiểm tra lại danh sách capabilities.
Thực hiện các lệnh kiểm tra sau trong container:
docker exec -it db-readonly cat /proc/1/status | grep Cap
docker exec -it db-readonly ps aux
docker exec -it db-readonly ipcs -m
Kết quả mong đợi:
- Danh sách capabilities chỉ bao gồm các quyền đã cấp (`NET_BIND_SERVICE`, `CHOWN`, v.v.), không có `SYS_ADMIN`.
- Lệnh `ps` chỉ hiển thị tiến trình của Database.
- Lệnh `ipcs` không hiển thị shared memory của host.
Bước 8: Kiểm tra chức năng Database
Đảm bảo các hạn chế bảo mật không làm Database mất chức năng cơ bản. Database vẫn phải có thể khởi động, lắng nghe kết nối và ghi dữ liệu vào volume.
Thực hiện kết nối và thử ghi dữ liệu:
docker exec -it db-readonly psql -U postgres -c "CREATE TABLE test (id INT); INSERT INTO test VALUES (1); SELECT * FROM test;"
Kết quả mong đợi: Lệnh SQL thực thi thành công, trả về kết quả `1` dòng dữ liệu. Điều này chứng tỏ quyền `CHOWN`, `FOWNER` và volume mount hoạt động đúng, cho phép Database ghi dữ liệu vào thư mục dữ liệu dù filesystem gốc là read-only.
Đ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 4: Tạo profile Seccomp tùy chỉnh cho Docker Container
Phần 6: Xây dựng Systemd Service để quản lý Database container »