Xây dựng Docker Image an toàn trong Kubernetes: Chuyển đổi từ DinD sang Kaniko

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

“Nợ” bảo mật trong Pipeline CI/CD của bạn

Kubernetes là lựa chọn hàng đầu cho CI/CD, nhưng việc xây dựng Docker image bên trong nó thường gây ra những vấn đề bảo mật đau đầu. Hầu hết các đội ngũ đều gặp khó khăn ngay từ đầu: làm thế nào để xây dựng một container image bên trong một container vốn đã đang chạy trên một node của cluster?

Nhiều người trong chúng ta bắt đầu với Docker-in-Docker (DinD) vì nó quen thuộc. Bạn đã sử dụng Docker cục bộ trong nhiều năm, vì vậy việc khởi chạy một daemon bên trong một pod có vẻ là con đường ít trở ngại nhất. Tuy nhiên, trong một đợt kiểm tra hạ tầng năm 2023 tại công ty cũ của tôi, chúng tôi nhận thấy thiết lập này đã kích hoạt nhiều vấn đề bảo mật mức độ nghiêm trọng cao. Bằng cách cấp quyền truy cập đặc quyền (privileged) cho các build container, về cơ bản chúng tôi đã giao “chìa khóa vạn năng” cho toàn bộ cluster production của mình.

Nguyên nhân gốc rễ: Tại sao Container đặc quyền lại là một gánh nặng rủi ro

Vấn đề nằm ở cách Docker hoạt động. Nó yêu cầu một daemon chạy ngầm (dockerd). Để chạy daemon này bên trong một container, container đó cần quyền truy cập sâu vào kernel của máy host để mount hệ thống tập tin và quản lý các giao diện mạng.

Điều này thường dẫn đến một trong hai cấu hình đầy rủi ro:

  1. Chế độ Đặc quyền (Privileged Mode): Thiết lập --privileged=true. Điều này loại bỏ gần như tất cả các ranh giới bảo mật giữa container và node host. Nếu một script build bị xâm nhập, kẻ tấn công sẽ chiếm được quyền root của máy vật lý hoặc máy ảo bên dưới.
  2. Gắn Docker Socket (Docker Socket Binding): Mount /var/run/docker.sock từ host. Cách này thường còn tệ hơn. Bất kỳ tiến trình nào có quyền truy cập vào socket đó đều có thể điều khiển Docker daemon của host để tạo container mới, xóa volume hoặc thâm nhập vào các namespace nhạy cảm khác.

Trong một cluster đa người dùng (multi-tenant), những thiết lập này giống như một thảm họa trực chờ. Chúng vi phạm nguyên tắc đặc quyền tối thiểu (principle of least privilege) và tạo ra một bề mặt tấn công gần như không thể giám sát hiệu quả.

So sánh các giải pháp thay thế

Khi quyết định loại bỏ DinD, chúng tôi đã đánh giá một số công cụ được thiết kế để giải quyết “vấn đề daemon”. Mỗi công cụ đều có những sự đánh đổi nhất định.

Docker-in-Docker (DinD)

  • Ưu điểm: Đầy đủ tính năng của Docker; lập trình viên không cần học cách dùng mới.
  • Nhược điểm: Yêu cầu quyền truy cập privileged; chậm do chi phí xử lý hệ thống tập tin lồng nhau.

Docker Socket Binding

  • Ưu điểm: Cực kỳ nhanh vì sử dụng bộ nhớ đệm (cache) image hiện có của host.
  • Nhược điểm: Lỗ hổng bảo mật khổng lồ; một container bị chiếm quyền có thể đánh sập toàn bộ node.

Buildah

  • Ưu điểm: Không cần daemon; tạo ra các image tuân thủ chuẩn OCI.
  • Nhược điểm: Chủ yếu được xây dựng cho Podman (rootless); có thể hoạt động không ổn định trong môi trường Kubernetes tiêu chuẩn.

Kaniko

  • Ưu điểm: Không cần daemon; chạy hoàn toàn trong userspace; hoạt động tự nhiên với Kubernetes Pod mà không cần quyền host nâng cao.
  • Nhược điểm: Mặc định không có cache cục bộ (nó cần một registry từ xa để lưu trữ các layer).

Giải pháp: Tại sao Kaniko chứng minh được sự bền bỉ nhất

Kaniko đã chiến thắng trong các đợt thử nghiệm nội bộ của chúng tôi cho các quy trình làm việc trên Kubernetes. Không giống như Docker, nó không cần daemon. Thay vào đó, Kaniko giải nén hệ thống tập tin của image cơ sở vào thư mục gốc của container, thực thi các lệnh Dockerfile và chụp ảnh (snapshot) sau mỗi bước. Sau đó, nó đẩy các snapshot này dưới dạng các layer trực tiếp lên registry của bạn.

Sau khi triển khai giải pháp này trên một cluster 50 node, độ tin cậy khi build của chúng tôi đã cải thiện đáng kể. Chúng tôi không còn phải xử lý các lỗi vụn vặt của sidecar daemon khi chúng không khởi động được. Quan trọng hơn, đội ngũ bảo mật của chúng tôi đã phê duyệt pipeline này để đưa vào sản xuất chỉ trong vòng 24 giờ.

Triển khai Kaniko trong Kubernetes

Thiết lập Kaniko yêu cầu hai thành phần chính: mã nguồn của bạn và thông tin đăng nhập registry.

1. Cấu hình Thông tin đăng nhập Registry

Kaniko cần quyền để đẩy image đã hoàn thành của bạn lên Docker Hub, GCR hoặc ECR. Chúng tôi xử lý việc này thông qua một Kubernetes Secret.

# Chuẩn bị thông tin đăng nhập của bạn
AUTH=$(echo -n "ten_dang_nhap:mat_khau" | base64)
cat <<EOF > config.json
{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "$AUTH"
    }
  }
}
EOF

# Lưu trữ cấu hình an toàn trong Kubernetes
kubectl create secret generic regcred \
    --from-file=.dockerconfigjson=config.json \
    --type=kubernetes.io/dockerconfigjson

2. Mẫu Pod Kaniko (Pod Template)

Định nghĩa Pod này sẽ thực hiện việc build. Mặc dù ví dụ này sử dụng ConfigMap cho Dockerfile, nhưng trong thiết lập CI/CD thực tế, bạn có thể sẽ sử dụng Git volume hoặc PersistentVolume.

apiVersion: v1
kind: Pod
metadata:
  name: kaniko-build
spec:
  containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:latest
    args:
    - "--dockerfile=Dockerfile"
    - "--context=dir:///workspace"
    - "--destination=your-repo/app-name:v1.2.0"
    volumeMounts:
    - name: kaniko-secret
      mountPath: /kaniko/.docker
    - name: source-code
      mountPath: /workspace
  restartPolicy: Never
  volumes:
  - name: kaniko-secret
    secret:
      secretName: regcred
      items:
        - key: .dockerconfigjson
          path: config.json
  - name: source-code
    configMap:
      name: app-source-code

3. Cắt giảm thời gian build với Caching

Mặc định, Kaniko chậm hơn Docker vì nó thiếu bộ nhớ đệm daemon cục bộ. Bạn có thể khắc phục điều này bằng cách bật flag --cache=true. Trong các thử nghiệm của chúng tôi, việc sử dụng một repository cache chuyên dụng đã giảm thời gian build từ 6 phút xuống còn 90 giây đối với những thay đổi mã nguồn nhỏ.

# Thêm các tham số này vào phần container spec của bạn
args:
- "--dockerfile=Dockerfile"
- "--context=dir:///workspace"
- "--destination=your-repo/app-name:v1.2.0"
- "--cache=true"
- "--cache-repo=your-repo/kaniko-cache"

Những bài học thực tế

Có hai điều đáng lưu ý khi bạn chuyển đổi. Thứ nhất, Kaniko chạy dưới quyền root bên trong container của chính nó để thao tác với hệ thống tập tin, nhưng nó không yêu cầu quyền root ở cấp độ host. Đây là điểm khác biệt quan trọng làm hài lòng các kiểm toán viên bảo mật.

Thứ hai, nếu bạn sử dụng GitLab Runner hoặc GitHub Actions với các self-hosted runner, việc thay thế docker build bằng một lời gọi container Kaniko là rất đơn giản. Hầu hết các công cụ hiện đại đều hỗ trợ việc này một cách tự nhiên. Việc chuyển đổi thường chỉ mất một buổi chiều nhưng loại bỏ được nỗi lo thường trực về thoát container (container escape) và xâm nhập node. Bằng cách áp dụng kiến trúc không daemon, bạn sẽ điều chỉnh CI/CD của mình theo các nguyên tắc ưu tiên bảo mật mà Kubernetes đã hướng tới ngay từ đầu.

Share: