Có hai kiểu "giấy tờ tuỳ thân" rất khác nhau. Một là chiếc thẻ khách in tạm ở quầy lễ tân: có ảnh bạn, ghi rõ bạn được vào tầng nào, hết hạn lúc 5 giờ chiều, và bảo vệ thu lại khi bạn ra về — mất nó ngoài đường cũng chẳng ai dùng được. Hai là một chiếc chìa khóa vạn năng cắt sẵn: ai cầm cũng mở được mọi cửa, dùng được mãi mãi, và nếu rơi vào tay kẻ xấu thì bạn chỉ còn cách thay hết ổ khóa. Token của pod trong Kubernetes hiện đại là chiếc thẻ khách, còn token kiểu cũ là chiếc chìa vạn năng — và biết rõ mình đang cầm loại nào là cả sự khác biệt về an toàn. Phần trước đo RBAC — ai được làm gì; bài này đo nửa còn lại: pod tự xưng danh với API server bằng cách nào.

Ba tệp gắn vào mọi pod

/var/run/secrets/kubernetes.io/serviceaccount/
   namespace   r
   ca.crt      1.107 byte
   token       1.121 byte

namespace để pod biết nó đang ở đâu. ca.crt để tin chứng chỉ của API server. token để tự xưng danh. Và cấu trúc thư mục vẫn là liên kết mềm qua ..data — giống hệt ConfigMap (phần 61). Nghĩa là token được làm mới tại chỗ, không cần khởi động lại pod. Đó là thay đổi lớn từ Kubernetes 1.24: token không còn là một Secret sống mãi mà là token ngắn hạn do kubelet xin và tự gia hạn.

Bên trong token

sub:          system:serviceaccount:r:ung
aud:          https://kubernetes.default.svc.cluster.local
hạn dùng:     8.760 giờ  (một năm)
gắn với pod:  sa1
gắn với node: sc-worker2

Hai trường cuối là điểm quan trọng nhất. Token gắn với pod cụ thể. Pod bị xoá là token hết hiệu lực ngay, không đợi hết hạn. Kẻ lấy được token của một pod đã chết không dùng được gì. Với token kiểu Secret cũ (trước 1.24), token sống mãi và dùng được ở bất cứ đâu — lấy được một lần là dùng được vĩnh viễn.

aud giới hạn token chỉ dùng được với API server, không dùng để xưng danh với dịch vụ khác. Bạn xin token cho aud khác nếu cần liên thông với hệ thống ngoài:

kubectl create token ung --audience=https://vault.cong-ty.vn

Đó là nền của việc xác thực không cần mật khẩu giữa Kubernetes và Vault, AWS IAM, hay bất kỳ hệ thống nào tin OIDC của cụm.

Dùng token gọi thẳng API server

Từ trong pod:

T=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
C=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
curl --cacert $C -H "Authorization: Bearer $T" \
  https://kubernetes.default.svc/api/v1/namespaces/r/pods
Đường dẫn Mã
/api/v1/namespaces/r/pods 200
/api/v1/namespaces/r/secrets 200
/api/v1/nodes 403
/api/v1/namespaces/default/pods 403
không có token 403

Kết quả khớp chính xác RBAC đã cấp ở phần 72: Role cho pods và secrets trong namespace r, không gì khác. Điều này làm rõ ranh giới: token chỉ là cách tự xưng danh; quyền vẫn do RBAC quyết định. Có token không có nghĩa có quyền. Và kubernetes.default.svc là tên DNS luôn có trong mọi cụm, trỏ tới API server — thứ mà mọi thư viện client Kubernetes dùng khi chạy trong pod.

automountServiceAccountToken: false

ls /var/run/secrets/kubernetes.io/serviceaccount/
-> No such file or directory

Không có token thì không có gì để đánh cắp. Với ứng dụng không gọi API Kubernetes — tức phần lớn ứng dụng — dòng này nên có. Đặt được ở hai chỗ:

# trên pod
spec:
  automountServiceAccountToken: false

# hoặc trên ServiceAccount, áp cho mọi pod dùng nó
apiVersion: v1
kind: ServiceAccount
metadata: {name: ung}
automountServiceAccountToken: false

Cách thứ hai tốt hơn vì nó là mặc định an toàn: pod nào cần token thì tự khai true.

Ba lỗi hay gặp

Dùng ServiceAccount default. Pod không khai serviceAccountName sẽ dùng default của namespace. Nếu ai đó từng gắn quyền cho default, mọi pod trong namespace có quyền đó — kể cả pod chạy mã của bên thứ ba. Mỗi ứng dụng nên có ServiceAccount riêng, kể cả khi nó không cần quyền gì.

Tạo token dài hạn cho CI/CD.

kubectl create token ci --duration=8760h

Token đó không gắn với pod nào — đo được gắn với pod: None — nên dùng được ở bất cứ đâu, và không thu hồi được trừ khi xoá ServiceAccount. Với CI/CD hiện đại, dùng OIDC federation thay vì token tĩnh: GitHub Actions và GitLab đều xin được token ngắn hạn từ chính cụm.

Đọc token từ Secret theo thói cũ. Từ 1.24, tạo ServiceAccount không tự sinh Secret nữa — đo được namespace chỉ có đúng Secret tôi tự tạo. Mã cũ tìm Secret theo serviceAccountName sẽ không tìm thấy gì.

TokenRequest cho trường hợp đặc biệt

Cần token với aud riêng hoặc hạn riêng thì khai trong pod:

volumes:
  - name: vault-token
    projected:
      sources:
        - serviceAccountToken:
            path: token
            expirationSeconds: 3600
            audience: vault

kubelet tự xin và tự gia hạn khi còn 20% thời gian. Ứng dụng chỉ cần đọc lại tệp định kỳ — đọc một lần lúc khởi động rồi giữ mãi là lỗi phổ biến, và nó chỉ lộ ra sau khi token đầu tiên hết hạn.

Mẫu số chung: giới hạn thông tin xác thực theo ba chiều, và "anh là ai" khác "anh được làm gì"

Bài này gói hai nguyên tắc bảo mật đúng ở mọi hệ. Thứ nhất: một thông tin xác thực tốt được giới hạn theo ba chiều — thời gian (hết hạn), đối tượng (aud: chỉ dùng được với ai), và người giữ (gắn với pod, chết theo pod) — để thu nhỏ bán kính sát thương khi nó rò rỉ. Chiếc thẻ khách hết hạn 5 giờ chiều, chỉ mở được tầng của bạn, và bị vô hiệu khi bạn ra về; chiếc token của pod cũng vậy. Đối lập là bearer secret vĩnh viễn dùng-được-ở-mọi-nơi — chiếc chìa vạn năng — mà token dài hạn cho CI chính là ví dụ, và nó "không thu hồi được trừ khi xoá ServiceAccount". Đây là lằn ranh khắp nơi giữa OAuth access token / AWS STS (ngắn hạn, có scope/aud, tự hết hạn) và API key tĩnh (sống mãi, ai cầm cũng dùng); giữa JWT có exp+aud và một token trần không hạn. Chọn loại có giới hạn ba chiều, và nếu buộc phải phát chìa vạn năng thì phải có đường thu hồi.

Thứ hai: "anh là ai" (xác thực) và "anh được làm gì" (phân quyền) là hai tầng tách biệt — cầm một danh tính hợp lệ không có nghĩa có quyền. Cùng một token cho 200 ở pods và 403 ở nodes, khớp chính xác RBAC: token chỉ chứng minh danh tính, còn quyền nằm ở chỗ khác. Lẫn hai tầng này là nguồn của vô số lỗ hổng: một người dùng đã đăng nhập không đương nhiên là người được phép; một JWT hợp lệ về chữ ký vẫn phải qua bước kiểm vai; một kết nối CSDL đã xác thực vẫn bị GRANT chặn từng bảng. Luôn hỏi cả hai câu, và đừng để câu đầu trả lời hộ câu sau.

Mẫu số chung: phát thông tin xác thực có giới hạn thời gian + đối tượng + người giữ để rò rỉ cũng ít hại, và luôn tách bạch chứng-minh-danh-tính khỏi quyết-định-quyền — cộng thêm một lưu ý thực dụng: thứ gì tự xoay vòng (token, chứng chỉ) thì phải đọc lại định kỳ, đọc một lần lúc khởi động rồi giữ mãi là hẹn giờ cho một sự cố đúng lúc nó hết hạn.

Muốn tự soi, tìm pod đang gắn token mà không cần:

kubectl get pods -A -o json | python3 -c '
import sys,json
for p in json.load(sys.stdin)["items"]:
    s=p["spec"]
    if s.get("automountServiceAccountToken") is False: continue
    if s.get("serviceAccountName","default")=="default":
        print(p["metadata"]["namespace"], p["metadata"]["name"])
'

Mỗi dòng là một pod dùng ServiceAccount default và có token gắn vào. Với ứng dụng không gọi API Kubernetes, đó là một khoá không dùng tới nằm trong hệ tệp của container.

Phần sau đo Pod Security Standards: ba mức, và mức chặt nhất từ chối cả một pod nginx bình thường.