Skip to content

ServiceAccount Token & Projection Mechanics — Danh Tính Được Ký

Why this matters in production

Toàn bộ chuỗi xác thực Workload Identity bắt đầu từ một artifact duy nhất: một JWT do cluster ký, đại diện cho Kubernetes ServiceAccount của Pod. Nếu bạn không hiểu chính xác token này — ai sinh nó, nó chứa gì, nó hết hạn khi nào, ai verify nó — thì mọi lỗi ở tầng trên (metadata server, STS, IAM) sẽ trông như nhiễu loạn ngẫu nhiên. Ngược lại, hiểu token gốc này biến phần lớn việc debug Workload Identity thành công việc cơ học: decode một JWT, đọc vài claim, kiểm một timestamp.

Trong production, sự hiểu lầm về token này gây ra những sự cố rất cụ thể:

  • Đội ngũ vẫn tư duy theo "legacy token vĩnh viễn". Trước Kubernetes 1.21–1.24, ServiceAccount token là một Secret tự động tạo, không hết hạn, không có audience, không bound vào Pod. Nhiều mental model, tutorial, và công cụ vẫn dựa trên mô hình cũ đó. Khi áp vào cluster GKE hiện đại — nơi token là bound, có audience, short-lived — họ ngạc nhiên vì token "biến mất" hoặc bị từ chối ở một audience khác. Hiểu sự dịch chuyển từ legacy token sang bound token (qua TokenRequest API) là điều kiện để không viết code phụ thuộc vào hành vi đã bị loại bỏ.

  • Workload đọc token sai audience và bị STS từ chối. Projected token cho Workload Identity có aud=sts.googleapis.com. Một workload tự ý đọc token đó và gửi tới một service kỳ vọng audience khác sẽ bị từ chối — đúng theo thiết kế OIDC. Không hiểu vai trò của claim aud khiến lỗi này trông bí ẩn.

  • Sự cố clock skew và token hết hạn. Vì token short-lived (≤ 1 giờ) và verify dựa trên exp/iat, lệch đồng hồ giữa node và backend Google có thể gây từ chối token "hợp lệ". Hiểu rằng token này có vòng đời thời gian chặt chẽ là điều kiện để chẩn đoán đúng các lỗi 401 thoáng qua.

Token này là nền móng. Mọi thứ trong file 03 (metadata server, STS exchange) chỉ là vận chuyển và biến đổi cái token mà file này mổ xẻ.

Internal model: từ legacy secret token đến bound token

Legacy ServiceAccount token (mô hình cũ — cần hiểu để loại bỏ)

Trong Kubernetes cổ điển, mỗi ServiceAccount tự động được cấp một Secret kiểu kubernetes.io/service-account-token, chứa một JWT. Đặc tính của token này — và lý do nó bị loại bỏ:

  • Không hết hạn (exp vô hạn). Token sống mãi cho đến khi Secret bị xóa. Một token lộ là một credential vĩnh viễn — đúng vấn đề mà Workload Identity sinh ra để giải quyết, nhưng ở tầng Kubernetes.
  • Không có audience cụ thể. Token dùng được với bất kỳ ai chấp nhận nó — không có ràng buộc "token này chỉ dành cho service X".
  • Không bound vào Pod. Token đại diện ServiceAccount, không gắn với một Pod cụ thể. Pod bị xóa, token vẫn hợp lệ. Không có cách thu hồi token khi workload kết thúc.
  • Lưu trong etcd như Secret. Mọi ai đọc được Secret (qua RBAC lỏng, qua node compromise, qua etcd) đều lấy được token vĩnh viễn.

Bốn đặc tính này khiến legacy token là một vector tấn công. Kubernetes hiện đại (1.24+) không còn tự tạo Secret token cho ServiceAccount mới, và GKE đi theo mặc định này.

TokenRequest API và bound token (mô hình hiện đại)

Thay thế là TokenRequest API — một API Kubernetes cấp token theo yêu cầu (on-demand) với ba thuộc tính then chốt mà legacy token thiếu:

  • Có thời hạn (expirationSeconds). Token hết hạn sau một khoảng cấu hình được, mặc định và tối đa thường là 1 giờ trong ngữ cảnh projected volume. Hết hạn = phải xin token mới.
  • Có audience (aud). Token được phát hành cho một hoặc nhiều audience cụ thể. Audience nào không khớp thì backend từ chối token. Với Workload Identity, audience là sts.googleapis.com.
  • Bound vào đối tượng (boundObjectRef). Token gắn với một Pod (và optionally Secret). Khi Pod bị xóa, token bound trở nên không hợp lệ — token có vòng đời gắn với workload.

Đây gọi là bound service account token. Tính năng BoundServiceAccountTokenVolume (GA từ Kubernetes 1.22) biến đây thành cơ chế mặc định cấp token cho Pod. Workload Identity for GKE xây trực tiếp trên cơ chế này.

Projection mechanics: kubelet mount token thế nào

Projected volume với serviceAccountToken

Token bound không nằm trong một Secret tĩnh — nó được kubelet sinh và mount trực tiếp vào Pod qua một loại volume gọi là projected volume với source serviceAccountToken. Cấu hình điển hình (thường được tự động tiêm, nhưng có thể khai báo tường minh):

yaml
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: payment        # KSA mà token đại diện
  volumes:
  - name: token-vol
    projected:
      sources:
      - serviceAccountToken:
          path: token                # tên file token trong volume
          audience: sts.googleapis.com   # audience của token (WI dùng STS)
          expirationSeconds: 3600    # thời hạn token (tối đa ~1 giờ)
  containers:
  - name: app
    volumeMounts:
    - name: token-vol
      mountPath: /var/run/secrets/tokens
      readOnly: true

Ba trường quyết định ngữ nghĩa token:

  • audience: đặt claim aud của JWT. Với Workload Identity, đây là sts.googleapis.com — token chỉ STS chấp nhận. Một workload có thể yêu cầu token với audience khác cho mục đích khác (ví dụ gọi một service nội bộ verify OIDC), và đó là token khác với token Workload Identity.
  • expirationSeconds: đặt exp. Kubelet sẽ tự refresh token trước khi hết hạn (thường khi token đi được ~80% vòng đời hoặc còn dưới một ngưỡng), ghi đè file token tại path. Workload đọc file mỗi lần cần để luôn có token tươi.
  • path: vị trí file token trong volume. Application đọc file này (hoặc client library tự đọc) để lấy JWT hiện tại.

Điểm vận hành cốt lõi: token là một file được kubelet liên tục làm mới, không phải một giá trị tĩnh đọc-một-lần. Code đọc token một lần lúc khởi động rồi cache vĩnh viễn sẽ dùng token hết hạn sau một giờ — một lỗi kinh điển. Client library Google xử lý việc đọc-lại này tự động; code tự viết thì phải đọc lại file mỗi lần dùng.

Vì sao projection an toàn hơn Secret

So với legacy Secret token, projected token cải thiện bảo mật ở nhiều mặt: token không bao giờ nằm trong etcd (kubelet sinh trực tiếp), không persistent (chỉ trong memory-backed volume của Pod), short-lived (lộ thì hết hạn nhanh), bound vào Pod (Pod chết thì token vô dụng), và có audience (không dùng được ngoài phạm vi). Đây là defense-in-depth ở chính tầng token gốc, trước khi Workload Identity thêm bất kỳ lớp nào.

Cấu trúc JWT: token chứa gì

Khi decode projected token Workload Identity (ví dụ bằng jwt.io cho debug, hoặc jq sau khi base64-decode payload), bạn thấy ba phần JWT chuẩn: header, payload, signature.

Header chứa thuật toán ký và kid (key ID) trỏ tới public key trong JWKS:

json
{
  "alg": "RS256",
  "kid": "a1b2c3..."
}

Payload chứa các claim danh tính. Các claim quan trọng với Workload Identity:

json
{
  "iss": "https://container.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/clusters/CLUSTER_NAME",
  "sub": "system:serviceaccount:NAMESPACE:KSA_NAME",
  "aud": ["sts.googleapis.com"],
  "exp": 1735689600,
  "iat": 1735686000,
  "kubernetes.io": {
    "namespace": "NAMESPACE",
    "serviceaccount": { "name": "KSA_NAME", "uid": "..." },
    "pod": { "name": "...", "uid": "..." }
  }
}

Ý nghĩa từng claim trong ngữ cảnh xác thực:

  • iss (issuer): OIDC issuer URL của cluster — chính endpoint mà file 01 mô tả. STS đọc claim này để biết IdP nào ký token, từ đó tải JWKS đúng để verify.
  • sub (subject): danh tính KSA dạng system:serviceaccount:NAMESPACE:KSA_NAME. Đây là claim mà phần subject trong principal identifier (subject/ns/NAMESPACE/sa/KSA_NAME) ánh xạ tới. Sự khớp giữa sub của token và principal trong IAM binding là điều kiện để cấp quyền đúng.
  • aud (audience): sts.googleapis.com — token này dành cho Security Token Service. STS verify rằng nó nằm trong danh sách audience trước khi chấp nhận.
  • exp / iat: timestamp hết hạn và phát hành. Khoảng cách thường ≤ 1 giờ. Backend từ chối token quá exp hoặc trước iat (chống token "từ tương lai" do clock skew).
  • kubernetes.io: metadata mở rộng — namespace, serviceaccount (name + uid), pod (name + uid). Đây là nguồn cho định danh dạng UID (file 01, dạng 2) và là thông tin forensic quý: token gắn với đúng Pod nào.

Signature: chữ ký RS256 do private key của cluster tạo. Đây là phần chứng minh token thật — chỉ cluster (control plane Google quản lý) có private key, nên không ai giả mạo được token mà qua được verify.

OIDC issuer và JWKS: verify offline thế nào

Điểm kiến trúc tinh tế nhất của file này: Google STS verify token mà không cần gọi ngược về cluster. Cơ chế:

  1. Token chứa iss = issuer URL của cluster.
  2. STS (đã được cấu hình tin tưởng pool, vốn liên kết các issuer trong project) tải tài liệu cấu hình OIDC tại ISSUER_URL/.well-known/openid-configuration. Tài liệu này khai báo URL của JWKS.
  3. STS tải JWKS — tập public key (định dạng RFC 7517 JSON Web Key Set) — từ endpoint mà cấu hình OIDC chỉ tới, và cache nó.
  4. Với mỗi token, STS đọc kid trong header, chọn public key tương ứng trong JWKS, verify chữ ký RS256.
  5. Nếu chữ ký hợp lệ và các claim (aud, exp, iat) đạt, STS chấp nhận token là bằng chứng danh tính.

Hệ quả vận hành và bảo mật quan trọng:

  • Verify là stateless và offline với cluster. STS không gọi vào API server của cluster mỗi lần. Điều này cho phép verify ở quy mô lớn (hàng nghìn token/giây) mà cluster không thành bottleneck — phù hợp với quota STS 6.000 request/phút và giới hạn connection ở tầng metadata server (file 03).
  • JWKS public, private key bí mật. Việc công bố public key qua JWKS là an toàn theo thiết kế mật mã bất đối xứng — public key chỉ verify được, không ký được. Kẻ tấn công có JWKS không tạo được token giả.
  • Rotation key trong suốt. Khi cluster xoay private key, nó công bố public key mới trong JWKS (với kid mới); STS tải lại JWKS và verify token mới bằng key mới. Toàn bộ trong suốt với workload — không có "rotation credential" mà workload phải biết.

Một chi tiết tinh tế về rotation đáng hiểu cho vận hành ở quy mô lớn: vì STS cache JWKS một khoảng thời gian, có một cửa sổ truyền bá giữa lúc cluster bắt đầu ký bằng kid mới và lúc mọi STS edge đã tải JWKS chứa key đó. Trong cửa sổ này, một token ký bằng key rất mới có thể bị từ chối thoáng qua nếu rơi vào một STS instance chưa refresh cache. Để cửa sổ này vô hại, key rotation đúng cách luôn chồng lấn (overlap): public key cũ vẫn nằm trong JWKS một thời gian sau khi key mới xuất hiện, nên token đã phát hành bằng key cũ tiếp tục verify được tới khi hết hạn. Đây là lý do bạn thường thấy nhiều kid trong JWKS cùng lúc (kubectl get --raw /openid/v1/jwks | jq '.keys[].kid' trả về nhiều key) — đó là trạng thái khỏe mạnh, không phải lỗi. Hệ quả thực hành: không thiết kế hệ thống dựa trên giả định "chỉ có đúng một key hợp lệ tại một thời điểm".

Token cho nhiều audience: một volume, nhiều source

Projected volume cho phép khai nhiều serviceAccountToken source trong cùng một volume, mỗi source một audience và một path. Đây là cách đúng khi một workload vừa cần token Workload Identity (aud=sts.googleapis.com) vừa cần token cho một service nội bộ verify OIDC (aud=https://service-b.internal): khai hai source, hai file token riêng, mỗi cái có vòng đời refresh độc lập do kubelet quản. Tránh cám dỗ "lấy một token rồi dùng cho cả hai" — vi phạm ràng buộc aud (xem anti-pattern bên dưới) và phá vỡ nguyên tắc token gắn chặt với đúng người nhận.

Bạn có thể tự kiểm tra cơ chế này từ bên trong cluster — đọc cấu hình OIDC và lần ra JWKS:

bash
# Cấu hình OIDC của cluster (chứa issuer và jwks_uri)
kubectl get --raw /.well-known/openid-configuration | jq

# JWKS — tập public key cluster dùng để ký token KSA
kubectl get --raw /openid/v1/jwks | jq

Production architecture patterns

Pattern: để client library tự quản token, đừng tự đọc

Mặc dù bạn có thể đọc projected token thủ công, pattern đúng cho hầu hết workload là để Google Cloud client library xử lý. Library phát hiện môi trường GKE, đọc token từ đúng path, gửi tới metadata server, và refresh tự động. Tự đọc token chỉ cần khi: bạn gọi một service không phải Google API nhưng verify OIDC token của cluster (audience tùy chỉnh), hoặc bạn xây một credential flow đặc thù. Trong các trường hợp đó, nhớ rằng token là file được refresh — đọc lại mỗi lần.

Pattern: audience tùy chỉnh cho service-to-service OIDC nội bộ

Ngoài Workload Identity (audience sts.googleapis.com), cùng cơ chế projected token cho phép cấp token với audience tùy chỉnh để xác thực service-to-service nội bộ: service A yêu cầu một projected token với audience=https://service-b.internal, gửi nó cho service B; service B verify token bằng JWKS của cluster (cùng cơ chế STS dùng) và biết chắc đó là KSA A. Đây là cách dùng cluster làm IdP cho xác thực nội bộ mà không cần hệ thống token riêng — một pattern mạnh nhưng ít người biết, dựng trên đúng nền tảng file này mô tả.

Common mistakes / anti-patterns

1. Đọc token một lần rồi cache vĩnh viễn. Code đọc file token lúc khởi động, giữ trong biến, dùng mãi. Vì sao xảy ra: tư duy "token là hằng số" từ thời legacy token. Hệ quả: sau ~1 giờ token hết hạn, mọi lời gọi 401, workload chết hàng loạt cùng lúc khi token đồng loạt hết hạn. Phòng tránh: dùng client library (tự refresh), hoặc nếu tự xử lý thì đọc lại file token mỗi lần dùng.

2. Tái dùng token sai audience. Lấy token Workload Identity (aud=sts.googleapis.com) đem gửi cho một service kỳ vọng audience khác. Vì sao xảy ra: nghĩ "token là token". Hệ quả: service từ chối vì audience không khớp. Phòng tránh: yêu cầu một projected token riêng với đúng audience cho mỗi mục đích.

3. Tạo lại legacy Secret token thủ công. Tạo một Secret kubernetes.io/service-account-token để có token "không hết hạn" cho tiện. Vì sao xảy ra: tutorial cũ, muốn token tĩnh cho script. Hệ quả: tái tạo đúng vector mà bound token loại bỏ — credential vĩnh viễn trong etcd. Phòng tránh: dùng TokenRequest (kubectl create token KSA --duration=...) cho token short-lived khi cần thủ công; không tạo Secret token.

4. Bỏ qua clock skew khi debug 401 thoáng qua. Token bị từ chối lẻ tẻ, nghi ngờ IAM. Vì sao xảy ra: không nghĩ tới thời gian. Hệ quả: điều tra sai hướng. Hệ quả thực: node lệch đồng hồ làm iat/exp rơi ngoài cửa sổ backend chấp nhận. Phòng tránh: đảm bảo NTP đồng bộ trên node; xét clock skew khi lỗi token mang tính thời gian/ngắt quãng.

5. Mount token với expirationSeconds quá dài để "đỡ refresh". Đặt expirationSeconds lớn hòng giảm refresh. Vì sao xảy ra: hiểu nhầm refresh là tốn kém. Hệ quả: token sống lâu hơn = cửa sổ lộ lớn hơn, đi ngược nguyên tắc short-lived; ngoài ra GKE giới hạn trần ~1 giờ nên thường vô hiệu. Phòng tránh: để mặc định; refresh do kubelet lo, không phải gánh nặng đáng tối ưu.

GCP-native implementation guidance

bash
# Tạo một bound token thủ công cho KSA (TokenRequest API) — short-lived, có audience
kubectl create token KSA_NAME -n NAMESPACE \
  --audience=sts.googleapis.com \
  --duration=3600s

# Decode payload của token để xem claim (debug)
kubectl create token KSA_NAME -n NAMESPACE | cut -d. -f2 | base64 -d 2>/dev/null | jq

# Xem cấu hình OIDC và JWKS của cluster (cơ sở để STS verify)
kubectl get --raw /.well-known/openid-configuration | jq
kubectl get --raw /openid/v1/jwks | jq '.keys[].kid'

Khi kiểm chứng một sự cố Workload Identity, ba điều cần xác nhận ở tầng token này, theo thứ tự: iss có đúng là issuer của cluster đang chạy Pod không; aud có chứa sts.googleapis.com không; exp còn hiệu lực không. Ba kiểm tra này tách bạch lỗi tầng token khỏi lỗi tầng metadata server hay IAM — phần lớn sự cố thực ra nằm ở tầng cao hơn, và loại trừ tầng token sớm giúp điều tra đúng hướng (chi tiết quy trình ở file 07).

Operational implications

Sự dịch chuyển từ legacy token sang bound token là một thay đổi mô hình vận hành sâu sắc mà nhiều đội ngũ chưa nội hóa hết. Trước đây, "token của ServiceAccount" là một Secret bạn có thể đọc, copy, lưu — một artifact tĩnh. Bây giờ nó là một stream token được kubelet liên tục làm mới, không có "giá trị token" cố định để nắm giữ. Điều này đúng đắn về bảo mật nhưng đòi hỏi code và công cụ phải tư duy theo "luôn đọc token mới nhất" thay vì "lấy token rồi giữ".

Hệ quả cho khả năng quan sát: vì token short-lived và bound vào Pod, mỗi token mang dấu vết về đúng Pod nào (kubernetes.io.pod) tại đúng thời điểm nào (iat). Khi điều tra một truy cập API đáng ngờ, claim kubernetes.io trong token (nếu bắt được) cho bạn định vị chính xác Pod nguồn — một mức forensic mà legacy token không cung cấp. Đây là một ví dụ nữa cho luận điểm xuyên suốt chương: thiết kế bảo mật tốt không chỉ ngăn tấn công mà còn để lại dấu vết audit phong phú hơn.

Token mà file này mổ xẻ giờ sẵn sàng cho hành trình của nó. File 03 theo dấu token này từ lúc client library yêu cầu nó, qua gke-metadata-server, tới Security Token Service, và trở về dưới dạng một access token Google mà workload thực sự dùng để gọi API.

References