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á là 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 — COPY mà khô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-arg và ENV 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.