Build cần token để tải gói từ registry riêng, cần khoá SSH để git clone kho nội bộ, cần mật khẩu để gọi API cấp phép. Có bốn cách người ta hay dùng để đưa chúng vào, và tôi tìm chuỗi bí mật ở bốn chỗ khác nhau sau khi build xong.

Bảng rò rỉ

Cách docker history Config.Env Filesystem ảnh cuối Layer thô
--build-arg CÓ LỘ sạch sạch CÓ LỘ
COPY rồi rm sạch sạch sạch CÓ LỘ
ENV CÓ LỘ CÓ LỘ sạch CÓ LỘ
--mount=type=secret sạch sạch sạch sạch

Ba cột đầu là những chỗ người ta thường kiểm. Cột cuối mới là chỗ bí mật thật sự nằm.

COPY rồi rm là cách nguy hiểm nhất

Nó nguy hiểm chính vì trông sạch:

COPY token.txt /tmp/token.txt
RUN cat /tmp/token.txt > /dev/null && echo ok > /log
RUN rm /tmp/token.txt

docker history không có nó. docker inspect không có nó. Chạy container lên và grep khắp filesystem cũng không thấy. Ba phép kiểm mà đa số người dừng lại ở đó.

Nhưng nhìn vào lịch sử layer:

8.19kB  RUN /bin/sh -c rm /tmp/token.txt
8.19kB  RUN /bin/sh -c cat /tmp/token.txt > /dev/null
12.3kB  COPY token.txt /tmp/token.txt          <-- tep bi mat nam o day

Đây chính là luật đã đo ở phần 2: layer chỉ cộng thêm, không bao giờ trừ đi. Lệnh rm ghi thêm một dấu "đã xoá" ở layer trên; tệp gốc vẫn nằm nguyên trong layer dưới, và bất kỳ ai kéo được image đều giải nén ra được.

Kiểm chứng bằng cách xuất image ra rồi tìm trong từng layer:

docker save -o anh.tar <ten-anh>
# roi tim chuoi bi mat trong tung tep bên trong

Một ghi chú về cách tôi đo: bộ dò đầu tiên của tôi báo COPY rồi xoásạch. Nó có một điều kiện lọc bỏ qua một số tệp trong tar lồng nhau, nên không mở tới layer chứa bí mật. Tôi chỉ phát hiện ra khi dựng một ca đối chứng — COPYkhông xoá, thứ chắc chắn phải lộ. Ca đó cũng báo "sạch", và đó là lúc rõ ràng bộ dò hỏng chứ không phải image an toàn.

Nếu bạn tự viết công cụ quét bí mật, hãy luôn có một ca đối chứng mà bạn biết phải dương tính.

--build-argENV lộ ở đâu ai cũng thấy

$ docker history --no-trunc <anh> | grep -o 'TOKEN=[^ ]*'
TOKEN=MAT-KHAU-SIEU-BI-MAT-abc123

Phần 13 đã đo chi tiết. ENV còn tệ hơn vì nó nằm cả trong Config.Env và có mặt trong môi trường của mọi tiến trình bên trong container — nghĩa là bất kỳ công cụ báo lỗi nào gửi biến môi trường về máy chủ cũng mang bí mật đi.

Cách đúng: --mount=type=secret

FROM alpine:3.20
RUN --mount=type=secret,id=tk \
    test -s /run/secrets/tk && echo "da doc bi mat" > /log
docker build --secret id=tk,src=token.txt -t app .

BuildKit gắn bí mật vào một tmpfs chỉ tồn tại trong đúng lệnh RUN đó. Không có layer nào chứa nó, không có metadata nào ghi nó. Cả bốn cột trong bảng đều sạch.

Bốn biến thể cú pháp, tất cả đều chạy:

# 1. duong dan mac dinh /run/secrets/<id>
RUN --mount=type=secret,id=tk cat /run/secrets/tk

# 2. dat duong dan rieng
RUN --mount=type=secret,id=tk,target=/tmp/bimat cat /tmp/bimat

# 3. dua thang vao bien moi truong cua lenh RUN
RUN --mount=type=secret,id=tk,env=TOKEN echo "${#TOKEN} ky tu"

# 4. bat buoc phai co, khong thi build hong ngay
RUN --mount=type=secret,id=tk,required=true ...

Biến thể 3 rất tiện cho những công cụ chỉ đọc biến môi trường:

RUN --mount=type=secret,id=npm,env=NPM_TOKEN npm ci

Nguồn bí mật cũng có hai dạng:

docker build --secret id=tk,src=token.txt ...        # tu tep
TOKEN_MT=abc docker build --secret id=tk,env=TOKEN_MT ...   # tu bien moi truong

Dạng thứ hai hợp với CI, nơi bí mật đã nằm sẵn trong biến môi trường của tác vụ.

Thiếu bí mật thì build hỏng theo hai kiểu

khong khai --secret        : RUN ... exit code 1
voi required=true          : bao loi ngay o buoc kiem cu phap

Không khai --secret thì /run/secrets/tk tồn tại nhưng rỗng, và lệnh của bạn hỏng ở đâu đó bên trong với thông báo của chính công cụ đó. Thêm required=true để BuildKit báo lỗi rõ ràng trước khi chạy — nên có, vì nó biến một lỗi khó hiểu thành một lỗi nói thẳng.

Bí mật không tham gia vào khoá cache

Đây là hành vi cần biết trước khi nó cắn bạn:

doi tep bi mat roi build lai : 1 buoc CACHED
noi dung trong image moi     : (van la bi mat CU)

Đổi token rồi build lại, BuildKit dùng lại layer cũ vì nội dung Dockerfile không đổi — và lệnh RUN đó không chạy lại, nên bí mật mới không được dùng.

Điều này hợp lý về mặt thiết kế (bí mật không nên ảnh hưởng tới cache, nếu không thì cache sẽ vô dụng mỗi lần xoay khoá). Nhưng nó có hệ quả thật: xoay token xong build lại không có tác dụng gì cho tới khi có thứ khác làm mất cache. Nếu bạn cần chắc chắn, dùng --no-cache cho lần build đó — và nhớ rằng phần 18 đã đo được --no-cache cũng vô hiệu hoá cache mount, nên lần build ấy sẽ chậm.

Còn SSH

Cho trường hợp git clone kho riêng, BuildKit có một kiểu mount riêng:

RUN --mount=type=ssh git clone git@github.com:to-chuc/kho-rieng.git
docker build --ssh default -t app .

Nó chuyển tiếp agent SSH của bạn vào build chứ không chép khoá riêng vào đâu cả. Khoá không rời khỏi máy bạn.

Kiểm image của bạn

Ba lệnh, theo thứ tự từ rẻ tới chắc chắn:

# 1. re nhat, bat duoc --build-arg va ENV
docker history --no-trunc <anh> | grep -iE 'password|token|secret|key|api'

# 2. bat duoc ENV
docker image inspect <anh> --format '{{json .Config.Env}}' | tr ',' '\n' | grep -iE 'password|token|secret'

# 3. chac chan nhat, bat duoc ca COPY roi xoa
docker save -o /tmp/anh.tar <anh>
tar -xf /tmp/anh.tar -C /tmp/giai-nen && grep -rl 'chuoi-can-tim' /tmp/giai-nen

Và nếu tìm thấy: coi như bí mật đó đã lộ. Xoá dòng trong Dockerfile không lấy lại được gì — image đã đẩy lên registry thì ai kéo về cũng có. Việc cần làm là xoay khoá, không phải sửa Dockerfile rồi build lại.

Thử ba mươi giây

docker history --no-trunc <anh-cua-ban> 2>/dev/null | grep -icE 'password|token|secret|apikey|_key='

Con số lớn hơn 0 là đủ để bắt đầu xoay khoá. Và nếu Dockerfile của bạn có bất kỳ dòng COPY nào chép tệp cấu hình, .env, hay khoá vào image — kể cả có rm ở dòng sau — hãy chạy phép kiểm số 3 ở trên, vì hai phép kiểm đầu sẽ báo sạch.

Phần sau bàn về SBOM và provenance: ghi lại image chứa những gì và được dựng từ đâu.