Đừng quá tin vào Tag: Bảo mật chuỗi cung ứng Container với Cosign

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

Cuộc gọi đánh thức lúc 2:14 sáng: Tại sao tính toàn vẹn của Image là điều kiện tiên quyết

Thông báo trên điện thoại của tôi rất ngắn gọn: một pod production trong cụm Kubernetes của chúng tôi đang bị crash. Kiểm tra log, tôi thấy các kết nối đi ra (outbound) tới một hồ đào tiền ảo Monero đã biết. Sau 45 phút tìm hiểu, sự thật dần lộ diện.

Registry riêng tư của chúng tôi không hề bị xâm nhập, nhưng một kẻ tấn công đã thực hiện hành vi ghi đè tag (tag-overwrite). Chúng thay thế image node:20-alpine của chúng tôi bằng một phiên bản độc hại. Nó trông hoàn toàn giống hệt, nhưng bên trong chứa một mã độc đào tiền ảo ẩn và một reverse shell. Pipeline CI/CD của chúng tôi đã kéo nó về mà không mảy may nghi ngờ vì tag hoàn toàn khớp.

Đây chính là thực trạng của các cuộc tấn công chuỗi cung ứng phần mềm. Tin tưởng vào một registry hay một tag đơn thuần là một trò đánh cược mà cuối cùng bạn sẽ thua. Bạn cần một cách để chứng minh rằng tệp thực thi đang chạy trong cụm của bạn chính xác là tệp mà hệ thống build của bạn đã tạo ra. Đây là lúc Cosign và dự án Sigstore thay đổi cuộc chơi.

Sự tiến hóa: GPG vs. Notary vs. Cosign

Trước khi chuyển sang Cosign, chúng tôi đã đánh giá một vài phương pháp ký số. Nếu bạn đã làm DevOps vài năm, có lẽ bạn còn nhớ Docker Content Trust (Notary v1). Đó là một nỗ lực đáng ghi nhận, nhưng lại là một cơn ác mộng khi cần mở rộng quy mô. Dưới đây là bức tranh toàn cảnh hiện nay:

  • Ký bằng GPG: Quen thuộc với việc ký gói phần mềm, nhưng cực kỳ tệ cho container. Việc quản lý một “mạng lưới tin cậy” (web of trust) và phân phối khóa công khai (public key) tới hàng hundreds node là một thảm họa vận hành trực chờ xảy ra.
  • Docker Content Trust (Notary v1): Phương pháp này yêu cầu một máy chủ Notary riêng biệt và một cơ sở dữ liệu MySQL chuyên dụng chỉ để lưu trữ chữ ký. Nó rất dễ hỏng và chữ ký thường bị lỗi khi chúng tôi di chuyển image giữa các registry khác nhau.
  • Cosign (Sigstore): Đây là tiêu chuẩn vàng mới. Cosign coi chữ ký như các artifact OCI. Chúng nằm ngay trong registry của bạn cùng với image. Nếu registry của bạn có thể lưu trữ container, nó có thể lưu trữ chữ ký. Không cần thêm cơ sở dữ liệu bên ngoài.

Ưu điểm, Nhược điểm và Ký không dùng khóa (Keyless)

Chuyển sang Cosign không chỉ là chạy theo xu hướng. Chúng tôi đã phải xem xét những sự đánh đổi thực tế cho đội ngũ kỹ thuật của mình.

Ưu điểm

  • Khả năng di động (Portability): Chữ ký được lưu trữ dưới dạng tag (ví dụ: sha256-abc.sig). Khi bạn di chuyển một image từ ghcr.io sang docker.io, chữ ký sẽ đi theo một cách dễ dàng.
  • Ký không dùng khóa (Keyless Signing): Đây chính là khoảnh khắc “Aha!”. Sử dụng Fulcio (một CA) và Rekor (một nhật ký minh bạch), bạn có thể ký image bằng danh tính OIDC như tài khoản GitHub hoặc Google. Không còn lo rò rỉ khóa riêng tư (private key).
  • Tích hợp gốc (Native Integration): Nó khớp hoàn hảo với GitHub Actions và hoạt động tốt với các admission controller như Kyverno để chặn các image chưa được ký ngay tại cửa ngõ của cụm máy chủ.

Thách thức

  • Tính công khai: Nếu bạn sử dụng instance Sigstore công cộng, địa chỉ email và dấu thời gian ký của bạn sẽ được ghi lại trong một sổ cái công khai, bất biến. Đối với các dự án doanh nghiệp yêu cầu bảo mật cao, bạn sẽ cần tự triển khai (host) một stack Sigstore riêng.
  • Tốc độ phát triển hệ sinh thái: Dự án tiến triển rất nhanh. Chúng tôi đã thấy các thay đổi gây lỗi (breaking changes) trong các flag của CLI giữa các phiên bản phụ, vì vậy việc cố định (pin) phiên bản Cosign là bắt buộc.

Quy trình làm việc khuyến nghị

Tôi đề xuất một chiến lược kết hợp. Sử dụng Ký không dùng khóa (Keyless signing) trong các pipeline CI/CD để loại bỏ việc quản lý secret. Đối với các môi trường air-gapped (cách ly mạng) nơi OIDC không khả dụng, hãy sử dụng Ký dựa trên khóa (Key-based signing) với khóa riêng tư được lưu trữ trong module bảo mật phần cứng (HSM) hoặc một dịch vụ quản lý như AWS KMS.

Khi tôi thiết lập các tài khoản dịch vụ registry ban đầu, tôi đã sử dụng trình tạo mật khẩu tại toolcraft.app/vi/tools/security/password-generator. Nó chạy hoàn toàn trong trình duyệt. Vì không có dữ liệu nào rời khỏi máy tính của bạn, đây là một lựa chọn tin cậy để tạo các chuỗi có độ hỗn loạn cao (high-entropy) cần thiết cho thông tin đăng nhập registry.

Từng bước: Ký Image đầu tiên của bạn

Hãy cùng thực hiện quy trình ký thủ công. Điều này giúp làm rõ cơ chế hoạt động trước khi bạn tự động hóa nó.

1. Cài đặt Cosign

Trên Linux, tải về bản binary mới nhất. Hiện tại, v2.4.0 là bản ổn định:

curl -L -o cosign https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64
chmod +x cosign
sudo mv cosign /usr/local/bin/cosign

2. Tạo cặp khóa (Key Pair)

Nếu bạn chưa sẵn sàng cho việc ký không dùng khóa, hãy bắt đầu với một cặp khóa cục bộ. Bạn sẽ được yêu cầu nhập mật khẩu để mã hóa tệp khóa riêng tư.

cosign generate-key-pair

Lệnh này tạo ra cosign.keycosign.pub. Hãy giữ khóa riêng tư đó thật an toàn—nếu mất nó, bạn sẽ không thể ký các bản cập nhật.

3. Ký Image

Sau khi image của bạn đã được đẩy lên (ví dụ: my-registry.com/app:v1.0), hãy ký nó:

cosign sign --key cosign.key my-registry.com/app:v1.0

Cosign tính toán digest của image và đẩy một đối tượng .sig lên registry của bạn. Quá trình này mất khoảng 2 giây.

4. Xác minh (Verification)

Xác minh là nơi tính bảo mật được thực thi. Bất kỳ ai có khóa công khai của bạn đều có thể chạy:

cosign verify --key cosign.pub my-registry.com/app:v1.0

Nếu image bị thay đổi dù chỉ một byte, quá trình xác minh sẽ thất bại và thoát với mã lỗi khác không.

Tự động hóa với GitHub Actions (Keyless)

Keyless là con đường đúng đắn cho CI hiện đại. Nó sử dụng một chứng chỉ ngắn hạn (chỉ có hiệu lực trong 10 phút) do Fulcio cấp, gắn liền với danh tính workflow GitHub của bạn.

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Cần thiết cho OIDC
      packages: write

    steps:
      - uses: actions/checkout@v4
      - uses: sigstore/[email protected]

      - name: Build và Push
        run: |
          docker build -t ghcr.io/${{ github.repository }}:latest .
          docker push ghcr.io/${{ github.repository }}:latest

      - name: Ký Image
        run: cosign sign --yes ghcr.io/${{ github.repository }}:latest

Không cần secret, không cần khóa GPG và không phiền toái. Để xác minh image này, bạn kiểm tra danh tính thay vì kiểm tra khóa:

cosign verify ghcr.io/my-org/my-repo:latest \
  --certificate-identity-regexp https://github.com/my-org/my-repo/.github/workflows/ \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Lời kết: Cưỡng chế thực thi (Enforcement)

Ký image mới chỉ là một nửa chặng đường. Bạn phải cưỡng chế thực thi nó. Hãy sử dụng một Admission Controller như Kyverno trong cụm Kubernetes của bạn. Bạn có thể thiết lập chính sách để từ chối bất kỳ pod nào nếu image của nó không được ký bởi danh tính GitHub cụ thể của bạn.

Nếu chúng tôi áp dụng điều này trong sự cố lúc 2 giờ sáng đó, image độc hại sẽ bị chặn ngay lập tức. Kubernetes sẽ từ chối kéo mã nguồn chưa được ký về, và tôi đã có thể tiếp tục giấc ngủ của mình. Bảo mật không phải là về một công cụ đơn lẻ; đó là về việc xây dựng một chuỗi tin cậy bắt đầu từ commit của bạn và kết thúc trong cụm máy chủ của bạn.

Share: