Image càng gọn thì càng ít thứ để kẻ tấn công dùng — và cũng càng ít thứ cho bạn dùng lúc gỡ rối. Bài này đo lối vào khi kubectl exec không còn tác dụng.

Kết quả đo có và không có --target, và cách đọc hệ tệp của container mục tiêu

Khi exec bó tay

Pod chạy image chỉ có đúng một tệp nhị phân:

kubectl exec ... -- sh
   exec: "sh": executable file not found in $PATH

kubectl exec ... -- /bin/ls
   stat /bin/ls: no such file or directory

Không có shell, không có ls, không có cat. kubectl exec chỉ chạy được thứ đã có sẵn trong image, nên với image distroless nó gần như vô dụng.

Đây không phải trường hợp hiếm: image distroless, image scratch của Go, image nonroot của Google đều như vậy. Chúng đang phổ biến dần, đúng vì lý do đó.

kubectl debug: thêm container vào pod đang chạy

kubectl debug <pod> --image=busybox:1.36 --target=app -it -- sh

Lệnh này thêm một ephemeral container vào pod. Không tạo pod mới, không khởi động lại gì cả — pod vẫn chạy nguyên trạng, chỉ có thêm một container nữa bên trong.

Đó là điểm cốt lõi: bạn gỡ rối đúng cái pod đang hỏng, chứ không phải một bản sao có thể không tái hiện lỗi.

--target là bắt buộc, dù nó không bắt buộc

Tôi đo hai lần, chỉ khác cờ đó.

không --target --target=app
PID 1 sh của chính nó /pause của mục tiêu
Số tiến trình thấy được 4 5

Không có --target, container gỡ rối nằm cùng pod nhưng không gian PID riêng: nó không nhìn thấy tiến trình nào của ứng dụng. Nó chỉ dùng chung mạng và volume.

--target, nó dùng chung không gian PID với container kia. Từ đó mới xem được ứng dụng đang chạy gì, mở cổng nào, đọc /proc của nó.

Cờ đó không bắt buộc về cú pháp, nhưng thiếu nó thì gần như mọi mục đích gỡ rối đều hỏng. Đây là chỗ đáng nhớ nhất của lệnh này.

Hệ tệp thì vẫn riêng — và lối vòng

Ngay cả với --target, container gỡ rối vẫn thấy hệ tệp của chính nó (busybox). Nó không thấy tệp của ứng dụng.

Lối vòng nằm ở không gian PID vừa chia sẻ:

ls /proc/1/root
-> dev etc pause proc product_uuid sys var

ls -la /proc/1/root/pause
-> -rwxr-xr-x 1 root root 514000 Oct 13 2022

/proc/<pid>/root là hệ tệp gốc theo cách nhìn của tiến trình đó. Chia sẻ không gian PID rồi thì đọc được ngay.

Đây là cách đọc tệp cấu hình, chứng chỉ, tệp tạm của một container distroless — thứ mà kubectl exec không bao giờ với tới.

Vài lệnh dùng được ngay sau khi đã vào:

cat /proc/1/root/etc/config.yaml     # cấu hình thật đang chạy
cat /proc/1/environ | tr '\0' '\n'   # biến môi trường thật
ls -l /proc/1/fd                     # tệp và socket đang mở
cat /proc/1/limits                   # ulimit thật của tiến trình

/proc/1/environ đặc biệt hữu ích: nó là môi trường thật sự mà tiến trình nhận được, không phải cái bạn nghĩ mình đã khai. Sai một tên biến trong ConfigMap thì chỗ này lộ ra ngay.

Ephemeral container không gỡ ra được

kubectl patch ... --subresource=ephemeralcontainers ... op: remove
-> Forbidden: existing ephemeral containers "debugger-t2w79"
   may not be removed

Thêm vào là ở lại tới khi pod chết. API cố ý không cho xoá.

Nó ít phiền hơn nghe thấy, vì ephemeral container không tính vào trạng thái sẵn sàng — pod vẫn hiện 1/1 Running sau khi tôi thêm ba cái. Nó cũng không có resources, probes hay lifecycle.

Nhưng có hai hệ quả thật:

  • Pod đã gỡ rối thì mang theo vết đó tới cuối đời. Muốn sạch thì xoá pod, để bộ điều khiển dựng lại.
  • Mỗi lần chạy kubectl debug là thêm một container nữa. Gõ năm lần là năm cái nằm trong spec.

Ba dạng của kubectl debug

Lệnh này thực ra là ba lệnh khác nhau dùng chung một tên:

# 1. Thêm container vào pod đang chạy — dùng nhiều nhất
kubectl debug <pod> --image=busybox --target=<container> -it -- sh

# 2. Sao chép pod ra bản mới, đổi lệnh khởi động
kubectl debug <pod> --copy-to=go-thu --set-image=app=busybox -it -- sh

# 3. Gỡ rối chính node
kubectl debug node/<node> -it --image=busybox

Dạng 2 dành cho pod đang crashloop: pod gốc không sống đủ lâu để gắn container vào, nên phải chép ra một bản với lệnh khởi động khác rồi mổ xẻ. Pod gốc giữ nguyên.

Dạng 3 đo được như sau:

hostPID=true  hostNetwork=true  nodeName=sc-worker3
/host chứa:  bin boot dev etc home kind lib ... usr var
/host/etc/hostname -> sc-worker3
số tiến trình thấy được: 19

Nó tạo một pod đặc quyền, gắn toàn bộ hệ tệp của node vào /host, dùng chung không gian PID và mạng của node. Từ đó đọc được /host/var/log/pods/... — đúng chỗ chứa nhật ký đã xoay vòng ở phần trước.

Lưu ý: pod đó nằm ở namespace default với tên node-debugger-<node>-xxxxxkhông tự xoá. Xoá tay sau khi xong, vì nó là một pod đặc quyền còn sống.

Chọn image gỡ rối

busybox đủ cho phần lớn việc. Cần hơn thì:

Image Có gì
busybox shell, ls, cat, wget — nhẹ nhất
nicolaka/netshoot tcpdump, dig, curl, ss, iperf
alpine apk để cài thêm lúc chạy

netshoot là lựa chọn mặc định hợp lý khi gỡ lỗi mạng, vì nó có sẵn mọi thứ ở các phần về DNS và NetworkPolicy trước đây.

Một cảnh báo: ở namespace có enforce=restricted (phần 34), kubectl debug sẽ bị từ chối vì image gỡ rối thường chạy root. Đó là lúc dùng --profile=restricted, hoặc chấp nhận rằng namespace siết chặt thì gỡ rối cũng khó hơn.

Thử ba mươi giây

Tìm pod nào trong cụm không có shell — tức là pod mà kubectl exec sẽ bó tay lúc bạn cần nó nhất:

for p in $(kubectl get pods -A -o jsonpath='{range .items[?(@.status.phase=="Running")]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}'); do
  ns=${p%%/*}; n=${p##*/}
  if ! kubectl exec -n "$ns" "$n" -- sh -c 'exit 0' >/dev/null 2>&1; then
    echo "khong co shell: $p"
  fi
done

Với mỗi dòng in ra, thử ngay kubectl debug một lần bây giờ, lúc không có sự cố. Lần đầu dùng lệnh này giữa một sự cố thật, giữa đêm, là lần tệ nhất để phát hiện ra mình quên --target.

Phần sau: kubectl port-forward và các cách chọc thủng ranh giới cụm — cái nào đi qua API server, cái nào không.