Một tình huống không hiếm trong các đội vận hành Kubernetes: file kubeconfig quản trị của cluster đang chạy production lọt vào repo Helm, nằm đó qua nhiều commit, rồi một ngày có người phát hiện và gỡ nó bằng một commit ngắn. Xoá file là việc dễ nhất và cũng là việc ít tác dụng nhất: file vẫn còn trong lịch sử Git, trong mọi bản clone, và chiếc chìa khoá bên trong vẫn mở được cluster cho tới khi bị thu hồi. Bài này trình bày quy trình xử lý chuẩn: khoanh vùng sự việc mà không chép lại nội dung bí mật, rotate trước, dọn lịch sử sau, và dựng các lớp chặn để chuyện này không lặp lại.
Triệu chứng: không có gì báo động
Một kubeconfig lọt vào repo riêng tư không gây lỗi, không làm chậm site nào. Không pipeline nào đỏ, không cảnh báo nào kêu. Repo ở chế độ private, nên ai đọc được repo cũng là người trong đội hoặc runner CI. Chính vì "người trong nhà" mà sự việc dễ bị xem nhẹ. Nhưng kubeconfig mặc định mà LKE cấp mang quyền quản trị cao nhất, như bài Dựng cluster LKE đầu tiên: node pool, kubeconfig và lựa chọn ban đầu đã nói: ai cầm file là có toàn quyền với cluster, từ đọc Secret của mọi site tới xoá namespace. Nếu không có nhật ký truy cập API đủ dài để khẳng định chìa khoá chưa từng bị dùng sai, phải xử lý như một chìa khoá đã lộ.
Điều tra: khoanh vùng mà không chép bí mật
Bước 1: tìm mọi file kubeconfig từng tồn tại trong lịch sử
Nguyên tắc khi điều tra rò rỉ: không cat, không git show nội dung file ra màn hình hay vào ticket. Chỉ làm việc với tên file, commit và mã băm:
# mọi file có tên chứa kubeconfig từng được thêm vào, trên mọi nhánh
git log --all --diff-filter=A --name-only --format='@@ %h %ad' --date=short \
| grep -iE 'kubeconfig|kube.?config'
# lịch sử thêm và xoá của từng file
git log --all --format='%h %ad %s' --date=iso -- '*kubeconfig*'Kết quả thường có nhiều hơn một file: một bản đầy đủ có token, một bản sao bị xoá lẫn trong commit khác, có khi một file chỉ chứa địa chỉ API server và CA (không có phần users nên không chứa token). Ghi lại cho từng file: đường dẫn, commit thêm, commit xoá, loại nội dung. Cũng nên xem commit thêm file là commit gì. Rất hay gặp trường hợp file nằm sẵn trong thư mục làm việc rồi bị gom vào khi git add cả thư mục, cùng một thay đổi không liên quan.
Bước 2: xác định độ nghiêm trọng mà không đọc giá trị
# các bản sao có giống nhau không: so mã blob, không in nội dung
git rev-parse <commit>:<đường-dẫn-1> <commit>:<đường-dẫn-2>
# file có những khoá nào: thay mọi giá trị bằng rỗng trước khi in
git show <commit>:<đường-dẫn> | sed -E 's/^( *-? *[A-Za-z_-]+:).*/\1/' | sort | uniq -cDanh sách khoá có đủ clusters, contexts, users và một dòng token, với user mang hậu tố -admin, thì đó là kubeconfig quản trị. Muốn biết token trong lịch sử có còn là token đang dùng hay không, so mã băm của hai bên, vẫn không in giá trị nào ra:
old=$(git show <commit>:<đường-dẫn> | awk '/ token:/{print $2}' | sha256sum | cut -c1-12)
cur=$(awk '/ token:/{print $2}' ~/.kube/prod.yaml | sha256sum | cut -c1-12)
[ "$old" = "$cur" ] && echo "TRUNG: token trong lich su van con hieu luc"Nếu hai mã trùng nhau, ai từng clone repo trong khoảng thời gian file tồn tại đều đang giữ một chìa khoá còn mở được cluster. Kể cả khi không trùng, vẫn nên rotate nếu không chắc token cũ đã bị thu hồi.
Bước 3: ai đã có bản sao
Mọi nhánh tách ra sau commit làm lộ file đều chứa nó. Bản sao của repo nằm ở máy các thành viên, trong mỗi job CI clone repo, và trong artifact của pipeline nếu thư mục repo được lưu làm artifact. Không có cách nào thu hồi những bản sao này. Đó là lý do dọn lịch sử không thay thế được việc thu hồi chìa khoá.
Nguyên nhân gốc
- Một kubeconfig quản trị dùng chung nằm trên máy cá nhân và đi theo người làm việc, thay vì mỗi người, mỗi pipeline một danh tính có quyền tối thiểu.
- Không có lớp chặn nào trước khi file vào Git:
.gitignorekhông có mẫu kubeconfig, không có pre-commit hook quét bí mật, và commit thủ công gom cả thư mục. - Xoá file được coi là xử lý xong: một bản sao bị xoá lẫn trong commit khác mà không ai rotate.
Khắc phục: thứ tự quan trọng hơn công cụ
1. Rotate kubeconfig trước tiên
Kubeconfig của LKE được tạo lại bằng thao tác reset trong Cloud Manager, hoặc qua Linode API (endpoint xoá và tạo lại kubeconfig của cluster). Token cũ mất hiệu lực ngay, nên phải chuẩn bị trước danh sách mọi nơi đang dùng kubeconfig để cập nhật trong cùng một khung giờ bảo trì:
- Biến CI chứa kubeconfig của các project vận hành, build và deploy.
- Hệ thống gọi pipeline từ bên ngoài, nếu nó truyền kubeconfig sang (cách làm nên bỏ).
- Máy cá nhân của những người có quyền vận hành cluster.
Sau khi rotate, chạy lại một pipeline đọc cluster, ví dụ kubectl get ns, để chắc chắn từng nơi đã nhận kubeconfig mới, và thử kubeconfig cũ để xác nhận nó đã bị từ chối. Cấu trúc file và cách tổ chức nhiều kubeconfig có trong tài liệu Organizing Cluster Access Using kubeconfig Files.
2. Dọn lịch sử Git
Chỉ làm sau khi đã rotate, vì dọn lịch sử chỉ làm sạch repo trên remote, không làm sạch các bản clone. Với git filter-repo, quy trình trên một bản mirror:
git clone --mirror git@git.example.com:team/helm-values.git
cd helm-values.git
git filter-repo --invert-paths --path-glob '*kubeconfig*'
# kiểm tra lại: không còn file nào khớp mẫu
git log --all --name-only --format= | grep -ci kubeconfig
git push --force --mirrorBFG Repo-Cleaner làm được việc tương tự với bfg --delete-files '*kubeconfig*'. Cái giá đi kèm: mọi commit kể từ commit làm lộ file đổi mã. Phải tạm dừng mọi pipeline đang ghi repo, nhất là pipeline tự commit values, vì một job còn giữ bản clone cũ sẽ push lịch sử cũ ngược trở lại. Sau đó mọi người clone lại từ đầu. Dịch vụ Git có thể giữ object không còn tham chiếu thêm một thời gian, nên cần làm việc với nhà cung cấp để dọn phía server.
3. Ghi nhận và đóng sự việc
Hậu kiểm ghi rõ: file nào, tồn tại trong khoảng commit nào, token đã rotate, lịch sử đã dọn, những nơi đã cập nhật kubeconfig mới. Sự việc chỉ được đóng khi cả ba bước đã xong, không phải khi commit xoá file được merge.
Phòng ngừa
Lớp thứ nhất là .gitignore cho mọi repo hạ tầng:
*kubeconfig*
*.kubeconfig
.kube/Lớp thứ hai là quét bí mật trước khi commit bằng gitleaks qua pre-commit, thêm một luật riêng cho dòng token của kubeconfig:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
# .gitleaks.toml
[extend]
useDefault = true
[[rules]]
id = "kubeconfig-token"
description = "token trong file kubeconfig"
regex = '''(?m)^\s*token:\s*['"]?[A-Za-z0-9._\-]{20,}'''
keywords = ["token"]Lớp thứ ba chạy ở CI, vì hook trên máy cá nhân có thể bị bỏ qua: một job quét toàn bộ lịch sử bằng gitleaks detect --source . --log-opts="--all" trên mỗi lần push, chặn merge nếu có phát hiện.
Lớp thứ tư mới là lớp gốc: không ai cần kubeconfig quản trị cho việc hằng ngày. Mỗi người và mỗi pipeline nên có kubeconfig sinh từ ServiceAccount riêng, gắn Role trong đúng namespace. Khi đó, một file lọt ra ngoài chỉ mở được một phần nhỏ của cluster và thu hồi được mà không ảnh hưởng ai khác. Kubeconfig quản trị nên cất ở kho bí mật, chỉ lấy ra khi cần và rotate sau mỗi lần dùng cho việc đặc biệt.
Cùng tinh thần đó, values của từng site không nên chứa mật khẩu database hay khoá ứng dụng ở dạng rõ. Dùng Sealed Secrets, SOPS hoặc External Secrets để đưa bí mật ra khỏi Git hoặc mã hoá chúng.
Checklist khi phát hiện kubeconfig trong repo
- Không in nội dung file; khoanh vùng bằng tên file, commit, mã blob.
- Xác định quyền của kubeconfig (admin hay ServiceAccount) và token còn hiệu lực không.
- Lập danh sách mọi nơi đang dùng kubeconfig.
- Rotate, cập nhật mọi nơi, xác nhận kubeconfig cũ bị từ chối.
- Dừng pipeline ghi repo, dọn lịch sử bằng git filter-repo, yêu cầu clone lại.
- Thêm
.gitignore, gitleaks pre-commit và job quét ở CI. - Chuyển dần sang kubeconfig quyền hẹp theo ServiceAccount.
Bài học
Xoá một file bí mật khỏi repo chỉ là bước nhìn thấy được. Phần quan trọng là thu hồi chiếc chìa khoá, và phần tốn công nhất là tìm hết những nơi đang dùng nó. Điều tra rò rỉ không cần chạm vào bí mật: tên file, mã blob và mã băm đủ để trả lời mọi câu hỏi cần thiết. Cuối cùng, kubeconfig quản trị dùng chung là một điểm lỗi đơn kiểu khác: không làm sập site, nhưng một lần sơ suất là đủ để cả cluster phải đổi khoá. Về việc nhiều pipeline cùng ghi vào repo Helm, xem bài Conflict khi nhiều pipeline cùng push: push_with_retry và bài học GitOps; các bài khác nằm trong chuyên mục blog kỹ thuật của Sudo.
Bài tiếp theo
Deadlock MySQL: đọc gap lock và insert intention trong InnoDB status. Cách đọc phần LATEST DETECTED DEADLOCK của InnoDB, từng dòng một, để biết hai transaction thực sự tranh nhau khoá nào.
