Tăng cường bảo mật Docker với gVisor: Ngăn chặn triệt để tấn công Container Escape

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

Cảnh báo lúc 2 giờ sáng đã thay đổi cách tiếp cận của tôi

Tôi vẫn nhớ cái cảm giác hụt hẫng khi nhận cảnh báo lúc 2 giờ sáng khi máy chủ của mình lần đầu tiên bị tấn công brute-force SSH. Sự cố đó đã dạy cho tôi một bài học đắt giá: phòng thủ vòng ngoài chỉ mang tính chất tham khảo. Nếu kẻ tấn công khai thác được lỗ hổng trong ứng dụng web, chúng sẽ xâm nhập được vào bên trong container. Trong một thiết lập Docker tiêu chuẩn, container đó chia sẻ chung kernel với máy chủ. Nếu chúng thoát khỏi container (escape), chúng không chỉ chiếm quyền điều khiển ứng dụng mà còn chiếm luôn toàn bộ hạ tầng của bạn.

Hầu hết các đội ngũ kỹ thuật đều dựa vào runc, runtime mặc định của Docker. Nó cực kỳ nhanh, nhưng không được thiết kế để cô lập đa người dùng (multi-tenant isolation). Mặc dù nó sử dụng namespaces và cgroups để tạo ra các ranh giới, bề mặt tấn công vẫn còn rất lớn. Container vẫn có thể thực hiện các lời gọi hệ thống (syscalls) trực tiếp đến kernel của máy chủ, để lại kẽ hở cho các lỗi khai thác như CVE-2019-5736.

Vấn đề cốt lõi: Kernel dùng chung

Rủi ro với runc bắt nguồn từ kiến trúc của nó. Khi một tiến trình bên trong container cần mở một tệp hoặc gửi một gói tin mạng, nó sẽ giao tiếp trực tiếp với kernel Linux của máy chủ. Linux có hơn 300 syscalls. Nhiều syscall trong số đó rất phức tạp và có lịch sử tồn tại nhiều lỗi. Nếu kẻ tấn công kích hoạt được một lỗ hổng trong một trong các lời gọi này, chúng có thể leo thang đặc quyền và thoát khỏi container.

Hãy tưởng tượng một tòa chung cư nơi mọi căn hộ đều dùng chung hệ thống ống nước và dây điện. Nếu một đường ống bị vỡ ở căn 4B, nước cuối cùng sẽ làm hỏng trần nhà ở căn 3B. Trong môi trường production, chúng ta cần một cách để cung cấp cho mỗi người thuê một nền tảng riêng biệt, cô lập hoàn toàn.

So sánh các đối thủ: runc vs. Kata vs. gVisor

Để khắc phục khoảng cách về sự cô lập này, ba công nghệ chính thường được nhắc đến:

  • runc (Mặc định): Cung cấp hiệu năng gần như tối đa nhưng dùng chung kernel máy chủ. Khả năng cô lập bảo mật ở mức tối thiểu.
  • Kata Containers: Khởi tạo một máy ảo (VM) siêu nhẹ cho mỗi container. Nó cung cấp khả năng cô lập tuyệt vời nhưng tiêu tốn nhiều bộ nhớ hơn đáng kể—thường là 25MB đến 50MB tài nguyên bổ sung cho mỗi container.
  • gVisor: Một “user-space kernel” được viết bằng Go. Nó chặn các syscall trước khi chúng đến được kernel máy chủ. Nó mang lại một giải pháp trung hòa: bảo mật tốt hơn runc với mức tiêu thụ tài nguyên ít hơn nhiều so với VM đầy đủ.

Đối với hầu hết các khối lượng công việc production, gVisor là lựa chọn tối ưu. Nó cung cấp sự cô lập mạnh mẽ trong khi vẫn giữ mức sử dụng tài nguyên ở mức có thể quản lý được cho các cụm máy chủ dày đặc.

Cơ chế hoạt động bên dưới của gVisor

gVisor đóng vai trò như một kernel khách (guest kernel). Nó triển khai API syscall của Linux trong không gian người dùng (user-space), nghĩa là nó không chạy với quyền root trên máy chủ. Khi một ứng dụng cố gắng thực hiện một syscall, thành phần Sentry của gVisor sẽ chặn nó lại. Nếu yêu cầu đó an toàn, gVisor sẽ tự xử lý nội bộ hoặc thực hiện một lời gọi hạn chế, đã qua lọc tới kernel máy chủ. Một thành phần riêng biệt gọi là Gofer sẽ xử lý các thao tác với hệ thống tệp. Điều này đảm bảo container không bao giờ chạm trực tiếp vào các tệp trên máy chủ.

Từng bước: Cài đặt gVisor trên Ubuntu

Để bắt đầu, chúng ta cần cài đặt binary runsc (run Sandboxed Container) và đăng ký nó với Docker. Tôi sẽ sử dụng thiết lập Ubuntu 22.04 tiêu chuẩn cho ví dụ này.

1. Cài đặt các tệp thực thi (Binaries)

Chúng ta sẽ tải các bản gVisor mới nhất trực tiếp từ kho lưu trữ của Google. Cách này đáng tin cậy hơn việc chờ đợi các repo tiêu chuẩn cập nhật.

( 
  set -e
  ARCH=$(uname -m)
  URL="https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}"
  wget ${URL}/runsc ${URL}/runsc.sha256
  sha256sum -c runsc.sha256
  chmod a+rx runsc
  sudo mv runsc /usr/local/bin
)

2. Đăng ký Runtime với Docker

Cần thông báo cho Docker rằng runsc đang tồn tại. Mở tệp daemon.json của bạn (hoặc tạo mới nếu chưa có).

sudo nano /etc/docker/daemon.json

Dán cấu hình sau vào:

{
    "runtimes": {
        "runsc": {
            "path": "/usr/local/bin/runsc"
        }
    }
}

Khởi động lại Docker để áp dụng cấu hình mới:

sudo systemctl restart docker

3. Xác minh thiết lập

Xác nhận rằng Docker đã nhận diện runtime mới:

docker info | grep -i runtime

Bạn sẽ thấy runsc xuất hiện trong danh sách đầu ra.

Hãy đưa vào thực tế

Chạy một container trong sandbox rất đơn giản. Chỉ cần thêm cờ --runtime=runsc. Hãy so sánh kết quả kiểm tra kernel.

Container tiêu chuẩn (runc)

docker run --rm alpine uname -a

Lệnh này trả về phiên bản kernel thực tế của máy chủ, ví dụ: Linux 5.15.0-generic.

Container trong Sandbox (gVisor)

docker run --rm --runtime=runsc alpine uname -a

Lệnh này thường sẽ trả về Linux 4.4.0. Đó không phải là kernel máy chủ của bạn. Đó là Sentry của gVisor đang giả dạng một kernel Linux cũ hơn để đáp ứng ứng dụng. Ứng dụng của bạn hiện đã thực sự bị nhốt trong một sandbox.

Tăng cường bảo mật ứng dụng Node.js với Docker Compose

Bạn có thể dễ dàng tích hợp điều này vào quy trình làm việc hiện tại. Đây là một đoạn mã docker-compose.yml được cấu hình cho môi trường bảo mật cao:

version: "3.9"
services:
  web-app:
    image: node:18-slim
    runtime: runsc
    ports:
      - "3000:3000"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    networks:
      - isolated-tier

networks:
  # Tầng mạng cô lập
  isolated-tier:
    driver: bridge

Đánh giá thực tế về hiệu năng

Sự minh bạch là quan trọng: gVisor không miễn phí. Vì nó chặn mọi syscall, các ứng dụng có nhiều lời gọi hệ thống liên tục sẽ cảm thấy sự chậm trễ. Các khối lượng công việc nặng về I/O hoặc các cơ sở dữ liệu tần suất cao có thể thấy hiệu năng giảm đáng kể, đôi khi chậm hơn 2 đến 3 lần trong các trường hợp cực đoan.

Tuy nhiên, đối với các web server tiêu chuẩn như Node.js hoặc Go, độ trễ thường không đáng kể (thường dưới 10%). Quy tắc của tôi là gì? Hãy sử dụng gVisor cho các thành phần tiếp xúc với bên ngoài (public-facing) xử lý dữ liệu người dùng không đáng tin cậy. Giữ các cơ sở dữ liệu nội bộ hiệu năng cao trên runc phía sau một mạng nội bộ nghiêm ngặt.

Lời kết

Tăng cường bảo mật Docker không chỉ là quét các lớp image xấu. Đó là việc giả định rằng một cuộc tấn công chắc chắn sẽ xảy ra. Bằng cách triển khai gVisor, bạn đảm bảo rằng ngay cả khi kẻ tấn công giành được quyền thực thi, chúng sẽ va phải một bức tường mã Go thay vì kernel máy chủ của bạn. Nó biến một vụ thoát container thảm khốc thành một sự kiện không gây thiệt hại.

Hãy bắt đầu với những container dễ bị tổn thương nhất—những container xử lý tải tệp lên hoặc lưu lượng web không xác định. Hãy chuyển chúng sang runsc trước. Đó là một trong những cách hiệu quả nhất để bạn có thể ngủ ngon hơn khi máy chủ của mình đang đối mặt với internet công cộng.

Share: