Bí mật nằm trong code: khóa API lộ ở đâu và cách xoay khóa trong 15 phút
Xóa khóa API khỏi code chưa phải là xong: nó vẫn nằm trong lịch sử git. Bài chạy thật một thí nghiệm với khóa giả, nói thứ tự đúng khi lộ khóa (xoay trước, dọn sau), cách chặn từ đầu bằng pre-commit hook và .gitignore, và thói quen khi làm việc với AI agent.
Dành cho bất kỳ ai viết code hoặc nhờ AI viết code, kể cả người mới. Không cần biết bảo mật. Thuật ngữ nào cũng đi kèm một ví dụ đời thường, và phần chạy lệnh được đánh dấu để bạn bỏ qua nếu chưa cần.
Mọi khóa trong bài đều là khóa giả, gồm cặp khóa ví dụ có sẵn trong tài liệu của AWS. Số liệu lấy từ nguồn công khai có liệt kê cuối bài. Ví dụ ghi là "minh họa" được dựng lại, không nhắc đến người hay công ty cụ thể. Bài này là hướng dẫn thực hành, không phải tư vấn bảo mật cho hệ thống của bạn.
Tóm tắt nhanh#
- Khóa API, mật khẩu và mã truy cập là bí mật: ai cầm được là dùng được, như chìa khóa nhà.
- Xóa khóa khỏi tệp chưa phải là xong. Git nhớ mọi phiên bản cũ. Trong thí nghiệm của bài, sau khi xóa khóa ở commit thứ hai,
git log -Svẫn tìm ra cả hai commit vàgit showin lại nguyên khóa. - Thứ tự đúng khi lộ khóa: xoay hoặc thu hồi khóa trước, dọn lịch sử sau, rồi đọc nhật ký sử dụng. Tài liệu của GitHub cũng nói như vậy.
- Viết lại lịch sử không thu hồi các bản sao. Trong thí nghiệm, bản sao chép trước khi viết lại vẫn còn khóa.
- Có thể chặn từ đầu bằng biến môi trường,
.gitignore, một pre-commit hook 30 dòng và tính năng quét của nền tảng. Mỗi lớp đều có chỗ sót, nên cần nhiều lớp. - Khi làm việc với AI agent, thêm ba thói quen: không dán khóa thật vào cuộc trò chuyện, giữ tệp
.envngoài tầm với của agent, và đọc lại phần thay đổi trước khi commit.
Từ khó, nói dễ#
| Thuật ngữ | Nói dễ hiểu |
|---|---|
| Secret (bí mật) | Thứ ai cầm được là dùng được: khóa API, mật khẩu, mã truy cập, chuỗi kết nối cơ sở dữ liệu, khóa riêng. Như chìa khóa nhà. |
| Commit | Một lần lưu trong git, kèm tên và lời nhắn. Mỗi commit giữ nguyên các tệp tại lúc đó. |
| History (lịch sử) | Toàn bộ các commit từ trước đến nay. Xóa một dòng ở commit mới không xóa nó ở các commit cũ. |
| Rotate (xoay khóa) | Tạo khóa mới, chuyển nơi dùng sang khóa mới, rồi vô hiệu hóa khóa cũ. Như đổi ổ khóa và phát chìa mới. |
| Revoke (thu hồi) | Vô hiệu hóa ngay một khóa để nó không còn mở được gì. |
| Environment variable (biến môi trường) | Giá trị đặt bên ngoài code, chương trình đọc lúc chạy. Như để chìa khóa ở hộp riêng, không khắc lên cửa. |
| Pre-commit hook | Đoạn lệnh git tự chạy ngay trước khi lưu commit, có thể từ chối lần lưu đó. |
| Secret scanning / push protection | Tính năng của nền tảng (như GitHub) quét code đã đẩy lên, hoặc chặn việc đẩy khi thấy dạng bí mật. |
Sau bài này bạn giải thích được#
- Vì sao xóa khóa khỏi tệp không làm nó biến mất, và cách tự kiểm bằng ba lệnh git.
- Thứ tự đúng khi lộ khóa (xoay trước, dọn sau) và vì sao viết lại lịch sử không đủ.
- Ba lớp phòng thủ (giữ ngoài code, chặn lúc commit, quét sau khi đẩy), và mỗi lớp sót ở đâu.
1. Hình dung trước: chìa khóa dưới thảm chùi chân#
Hình dung bạn để chìa khóa nhà dưới thảm chùi chân, rồi hôm sau nhận ra như thế không ổn nên chuyển chìa vào ngăn kéo. Nhưng trong một ngày đó, hàng xóm đi ngang đã kịp đúc một bản sao. Cất chìa khóa đi không làm bản sao biến mất. Cách duy nhất chắc chắn là đổi ổ khóa.
Khóa API trong code hoạt động như vậy. Khi bạn commit nó, git đã lưu một bản. Người khác có thể đã sao chép kho. Một công cụ tự động quét kho công khai có thể đã đọc nó. Xóa dòng đó ở commit sau giống như cất chìa vào ngăn kéo: tốt, nhưng chưa giải quyết bản sao.
Điều rút ra: khi một bí mật đã đi ra khỏi chỗ cất, hãy coi nó đã bị lộ và đổi khóa, chứ đừng chỉ cất lại.
2. Bí mật là gì, và lộ ở đâu#
| Loại bí mật | Ví dụ | Hay lộ ở đâu |
|---|---|---|
| Khóa API | Khóa dịch vụ đám mây, khóa thanh toán, khóa dịch vụ AI | Gán thẳng trong code, trong tệp cấu hình đã commit |
| Mật khẩu | Mật khẩu cơ sở dữ liệu, tài khoản dịch vụ | Chuỗi kết nối trong code hoặc ví dụ README |
| Mã truy cập (token) | Mã đăng nhập tạm hoặc lâu dài | Nhật ký chương trình, ảnh chụp màn hình |
| Khóa riêng | Khóa SSH, khóa ký | Tệp cho vào kho nhầm |
| Chuỗi kết nối | Địa chỉ cơ sở dữ liệu kèm mật khẩu | Tệp cấu hình, biến môi trường bị in ra |
Những chỗ lộ thường gặp:
- Commit thẳng vào git, rồi xóa ở commit sau (phần 3 chạy thật).
- Tệp
.envbị commit vì quên ghi vào.gitignore, hoặc đã commit từ trước khi ghi. - Nhật ký và ảnh chụp màn hình có chứa khóa, rồi gửi cho đồng nghiệp hoặc đăng lên diễn đàn.
- Dán vào cuộc trò chuyện với AI để "cho nó thấy lỗi". Nội dung đó có thể được lưu và hiển thị lại trong lịch sử cuộc trò chuyện.
- Mã chạy ở phía trình duyệt (client-side bundle): mọi thứ gửi xuống trình duyệt người dùng đều đọc được, nên không để khóa bí mật ở đó.
Điều rút ra: bí mật rò ra không chỉ qua một cửa. Cách phòng phải nhiều lớp.
3. Thí nghiệm thật: xóa rồi vẫn còn#
Tất cả lệnh và đầu ra trong phần này chạy thật trong một kho thử, với Git 2.56.0 và Python 3.14.8, chỉ dùng khóa giả. Mã hash khác nhau mỗi lần chạy. Thời gian ghi ở dưới là của máy tôi, cho kho 2 commit, nên chỉ để bạn hình dung cỡ.
3.1 Commit khóa giả, rồi xóa ở commit sau#
git init -q -b main repo
cd repo
# config.py chứa cặp khóa ví dụ trong tài liệu AWS (khóa giả)
# AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
# AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
git add .
git commit -m "add config" # khoảng 0,03 giây
# commit sau đổi sang đọc từ biến môi trường
git add .
git commit -m "remove secret from config"
git log --oneline
2bfea15 remove secret from config
a0d01a2 add config
3.2 Nhìn thì sạch#
grep -n AKIA config.py # không có kết quả
git grep AKIA # không có kết quả, chỉ tìm trong các tệp hiện tại
Cả hai lệnh không in ra gì. Đúng là tệp hiện tại đã sạch.
3.3 Nhưng lịch sử vẫn còn#
git log --oneline -S AKIAIOSFODNN7EXAMPLE
2bfea15 remove secret from config
a0d01a2 add config
Lệnh -S tìm các commit làm thay đổi số lần xuất hiện của chuỗi đó, nên nó chỉ ra cả commit thêm và commit xóa. Tiếp theo, tìm trong mọi commit và in lại tệp cũ:
git grep -n AKIA $(git rev-list --all) # khoảng 0,02 giây
git show a0d01a2:config.py
a0d01a2473ddcb923dbfd69164c1df8a11de379b:config.py:3:AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
import os
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
REGION = "ap-southeast-1"
Chỉ cần ba lệnh và chưa đến một giây, ai có kho này cũng đọc lại được cả hai khóa.
Điều rút ra: tệp sạch không có nghĩa kho sạch. Phải tìm trong lịch sử.
4. Thứ tự đúng khi lộ khóa#
Hình dung có người báo "chìa khóa nhà bạn bị đúc bản sao". Bạn sẽ đổi ổ khóa trước, rồi mới đi tìm ai đã vào nhà.
- Thu hồi hoặc xoay khóa ngay. Tài liệu secret scanning của GitHub khuyên xoay "ngay lập tức" và nói rằng xóa bí mật khỏi lịch sử git thì chậm và thường không cần thiết khi khóa đã bị thu hồi.
- Đọc nhật ký sử dụng của khóa trên dịch vụ: ai dùng, từ đâu, lúc nào, trong khoảng thời gian lộ.
- Dọn lịch sử (phần 6), nếu cần, để bản mới đẩy lên không còn khóa.
- Ghi lại điều đã xảy ra và đặt rào chắn để lần sau không lặp lại (phần 5).
4.1 Xoay khóa AWS: quy trình tài liệu mô tả#
Tài liệu AWS về cập nhật access key mô tả quy trình xoay không làm gián đoạn ứng dụng:
- Khi khóa thứ nhất còn hoạt động, tạo khóa thứ hai (lúc này có hai khóa cùng hoạt động).
- Cập nhật mọi ứng dụng và công cụ sang khóa mới.
- Xem thông tin "last used" của khóa cũ để biết còn ai dùng không. Tài liệu gợi ý có thể đợi vài ngày.
- Không xóa ngay khóa cũ. Đổi nó sang trạng thái vô hiệu (
Inactive) trước. - Dùng riêng khóa mới để kiểm ứng dụng còn chạy. Nơi nào vẫn dùng khóa cũ sẽ hỏng, và bạn có thể bật lại khóa cũ tạm thời.
- Sau một thời gian đợi, mới xóa khóa cũ.
Quy trình trên viết cho việc xoay bình thường, có thời gian chờ. Với khóa đã lộ, việc vô hiệu hóa khóa cũ ngay là suy luận của tôi theo nguyên tắc "thu hồi trước" của GitHub, không phải lời của tài liệu AWS. Cái giá là ứng dụng nào còn dùng khóa cũ sẽ hỏng ngay, nên hãy chuẩn bị sẵn khóa mới.
4.2 Trong 15 phút làm được gì, chưa làm được gì#
Đây là mốc mục tiêu tôi đề xuất, không phải số đo. Thời gian thật phụ thuộc bạn có sẵn quyền và có biết khóa đang dùng ở đâu không.
| Phút | Việc | Làm được trong 15 phút không |
|---|---|---|
| 0 đến 2 | Vô hiệu hóa khóa bị lộ trên dịch vụ | Được, nếu bạn có quyền |
| 2 đến 5 | Tạo khóa mới, lưu vào nơi cất bí mật | Được |
| 5 đến 10 | Cập nhật các nơi dùng (biến môi trường, máy chủ, CI) | Được nếu ít nơi, khó nếu không nhớ hết |
| 10 đến 13 | Kiểm ứng dụng vẫn chạy với khóa mới | Được |
| 13 đến 15 | Báo người liên quan, ghi lại giờ xử lý | Được |
| Sau đó | Đọc nhật ký sử dụng, đánh giá thiệt hại | Không, thường mất hàng giờ |
| Sau đó | Dọn lịch sử, nhắc mọi người kéo lại | Không, và không thay cho việc xoay khóa |
| Sau đó | Xoay các khóa khác mà khóa này có thể chạm tới | Không, cần liệt kê trước |
Điều rút ra: 15 phút đủ để đóng cửa. Việc kiểm tra thiệt hại là việc của vài giờ sau.
5. Chặn từ đầu: nhiều lớp, mỗi lớp có chỗ sót#
Hình dung nhà có ba lớp: không mang chìa ra khỏi nhà, có người nhắc ở cửa, và có camera ngoài đường.
5.1 Lớp một: giữ bí mật ngoài code#
Đặt bí mật vào biến môi trường hoặc một nơi cất bí mật của dịch vụ, và để tệp cấu hình chỉ nói "đọc từ biến tên này". Trong thí nghiệm, commit thứ hai đã làm đúng như vậy:
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
Nếu dùng tệp .env cho máy cục bộ, hãy ghi nó vào .gitignore trước khi tạo tệp. Thử thật (tôi dùng tên dev.env thay cho .env, vì môi trường chạy của tôi không cho tạo tệp tên .env; cách hoạt động giống nhau):
git status --short
?? dev.env
?? tools/
echo "dev.env" > .gitignore
git status --short
git check-ignore -v dev.env
?? .gitignore
?? tools/
.gitignore:1:dev.env dev.env
Tệp dev.env biến mất khỏi danh sách, và check-ignore cho biết dòng nào của .gitignore đang chặn nó. Nhưng .gitignore chỉ có tác dụng với tệp chưa được theo dõi. Tôi thử với một tệp đã commit từ trước:
git add -f old.env
git commit -m "oops: commit old.env"
echo "old.env" >> .gitignore
git ls-files old.env
old.env
Dù đã ghi vào .gitignore, git vẫn theo dõi tệp này. Muốn ngừng theo dõi phải dùng git rm --cached old.env, và khóa trong lịch sử vẫn còn như phần 3.
5.2 Lớp hai: chặn lúc commit bằng pre-commit hook#
Dưới đây là một bộ quét rất nhỏ (khoảng 30 dòng Python) đọc các dòng sắp được thêm và dò ba dạng: khóa kiểu AWS, đầu khóa riêng, và dòng gán chuỗi cho tên như api_key, secret, token, password:
PATTERNS = {
"aws access key id": re.compile(r"AKIA[0-9A-Z]{16}"),
"private key header": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"key or password assigned to a string": re.compile(
r"""(?i)(api[_-]?key|secret|token|passwd|password)\w*\s*[=:]\s*['"][^'"\s]{8,}['"]"""
),
}
# đọc: git diff --cached -U0, xét các dòng bắt đầu bằng "+", thấy khớp thì thoát với mã 1
Gắn vào .git/hooks/pre-commit:
#!/bin/sh
exec python3 tools/scan_secrets.py
Thử commit một tệp có khóa giả:
Blocked: this commit adds something that looks like a secret.
deploy.py: key or password assigned to a string
Use an environment variable, then rotate the key if it was ever real.
exit code: 1
Commit bị từ chối (cả lần thử mất khoảng 0,14 giây). Nhưng bộ quét này có hai chỗ yếu, và tôi thử luôn:
- Bỏ sót: nếu khóa bị tách làm hai chuỗi (
part1 = "AKIA",part2 = "IOSFODNN7EXAMPLE", rồi nối lại), commit được cho qua với mã 0. - Báo nhầm: một dòng
password = "change-me-please"trong tài liệu mẫu bị chặn, dù đó chỉ là chữ giữ chỗ.
Điều rút ra: công cụ quét dựa trên mẫu chữ bắt được lỗi hay gặp, không bắt được lỗi cố ý hay khéo léo, và đôi khi la nhầm. Đừng coi nó là bảo chứng.
Hook nằm trong thư mục .git/hooks, không đi theo kho. Muốn cả nhóm dùng chung phải có cách chia sẻ (ví dụ một công cụ quản lý hook trong dự án), và mỗi người vẫn có thể bỏ qua nó bằng --no-verify.
5.3 Lớp ba: quét sau khi đẩy, và chặn lúc đẩy#
Các nền tảng có tính năng cho việc này. Theo trang tài liệu GitHub tôi đọc (qua công cụ tóm tắt trang):
- Secret scanning tự dò thông tin xác thực bị lộ như khóa API và mật khẩu, và quét "toàn bộ lịch sử git trên mọi nhánh", cùng với nội dung issue, pull request, thảo luận và wiki.
- Push protection chặn lệnh đẩy có chứa bí mật, trước khi nó vào kho. Nó nhận mẫu của nhà cung cấp, mẫu chung (như khóa riêng, chuỗi kết nối cơ sở dữ liệu) và mẫu tự định nghĩa. Với kho, tính năng này mặc định tắt; ai có quyền ghi có thể bỏ qua bằng cách nêu lý do. Ở cấp người dùng, tài liệu ghi là bật sẵn để chặn việc đẩy bí mật lên kho công khai trên GitHub.com.
Tôi đọc tài liệu này qua công cụ tóm tắt trang, chưa kiểm từng chữ, nên hãy mở trang gốc (có ở phần nguồn) và xem cài đặt của chính tài khoản bạn. Các công cụ độc lập như gitleaks, trufflehog hay git-filter-repo cũng có, nhưng chưa chạy ở đây: tôi không cài chúng trong môi trường chạy thử.
6. Dọn lịch sử: làm được gì và không làm được gì#
Sau khi đã xoay khóa, bạn có thể muốn kho không còn khóa nữa. Công cụ khuyên dùng hiện nay là git filter-repo, nhưng tôi chưa chạy được vì máy thử không có nó. Vì vậy tôi demo bằng git filter-branch có sẵn trong Git (trang tài liệu của Git ghi rằng việc dùng lệnh này "không được khuyến nghị" và gợi ý dùng git filter-repo, nhưng nó chạy được và đủ để minh họa):
git clone -q repo clone-before # chụp một bản sao TRƯỚC khi viết lại
cd repo
git filter-branch -f --tree-filter "python3 redact.py" -- --all # khoảng 0,8 giây, 5 commit
Trong đó redact.py thay hai chuỗi khóa giả bằng REDACTED ở mọi commit. Kiểm tra:
git log --oneline -S AKIAIOSFODNN7EXAMPLE main
Không in ra gì: nhánh main đã sạch. Nhưng ba chỗ khác vẫn còn khóa:
git for-each-ref --format='%(refname)'
refs/heads/main
refs/original/refs/heads/main
Công cụ để lại nhánh sao lưu refs/original/refs/heads/main, và git log -S AKIAIOSFODNN7EXAMPLE --all vẫn thấy hai commit cũ. Muốn dọn phải xóa nhánh sao lưu, xóa reflog và dọn các đối tượng không còn ai trỏ tới:
git for-each-ref --format='%(refname)' refs/original | xargs -n1 git update-ref -d
git reflog expire --expire=now --all
git gc -q --prune=now # khoảng 0,1 giây
git log --oneline -S AKIAIOSFODNN7EXAMPLE --all --reflog # không in gì
git cat-file -t a0d01a2 # fatal: Not a valid object name
Kho đã sạch hẳn. Còn bản sao chụp trước khi viết lại thì sao?
cd ../clone-before
git log --oneline -S AKIAIOSFODNN7EXAMPLE
git cat-file -t a0d01a2
2bfea15 remove secret from config
a0d01a2 add config
commit
Bản sao đó vẫn còn khóa. Đó là lý do dọn lịch sử không thay được việc xoay khóa: mọi người đã kéo (pull) hay fork kho trước đó vẫn giữ bản cũ, và một kho công khai có thể đã được người khác sao chép hay lưu lại.
Một cảnh báo thêm: viết lại lịch sử làm đổi mã hash của các commit, nên đồng đội phải kéo lại và có thể gặp xung đột. Phải báo trước. Bài Git cho người dùng agent giải thích vì sao đẩy ép (force push) là thao tác cần thận trọng.
7. Khi làm việc với AI agent: thêm sáu thói quen#
Hình dung bạn thuê thợ vào nhà: bạn đưa chìa khóa có hạn dùng, chỉ mở một phòng, chứ không đưa chìa chủ.
- Đừng dán khóa thật vào cuộc trò chuyện. Nếu cần cho AI xem lỗi, thay khóa bằng chữ giữ chỗ như
<KHOA_API>. - Giữ tệp
.envngoài đường dẫn agent đọc được hoặc dùng cơ chế chặn đọc của công cụ. Một agent có quyền đọc cả thư mục dự án có thể đọc luôn bí mật trong đó. - Dùng khóa quyền hẹp, hạn ngắn cho mọi việc agent làm: chỉ đủ phục vụ tác vụ, hết hạn sớm.
- Đọc diff trước khi commit. Agent có thể ghi chuỗi kết nối cố định vào code cho "chạy được ngay". Một lệnh
git addlà đủ để đưa nó vào lịch sử. Xem bài Duyệt code do AI viết. - Không cho agent tự đẩy lên nhánh chính và không để nó tự chạy lệnh cài hoặc lệnh có quyền cao khi chưa ai xem. Xem thêm bài OWASP Top 10 cho ứng dụng dùng AI.
- Đưa quy tắc vào chính sách nhóm. Bài Chính sách dùng AI một trang có cột "cấm" cho loại dữ liệu này.
7.1 Minh họa: một buổi sáng nhờ agent kết nối cơ sở dữ liệu#
Đây là tình huống minh họa, không nhắc đến người hay sản phẩm thật. Tên và giá trị là bịa.
Không có thói quen trên:
- 10:00. Dev nhờ agent: "Kết nối cơ sở dữ liệu giúp tôi, chuỗi kết nối đây" và dán chuỗi thật vào cuộc trò chuyện.
- 10:10. Agent viết
config.py, gán thẳng chuỗi kết nối vào code cho "chạy được ngay", rồi commit. - 10:20. Dev xem lướt, thấy chạy ổn, đẩy lên.
- 10:45. Nền tảng gửi cảnh báo phát hiện bí mật trong kho. Dev xóa dòng đó, đẩy lại. Nghĩ là xong.
Có thói quen:
- 10:00. Dev đặt chuỗi kết nối vào biến môi trường trên máy, nhờ agent: "đọc từ biến
DATABASE_URL, đừng ghi giá trị vào code". - 10:10. Agent viết
config.pyđọc biến môi trường. Hook pre-commit kiểm không thấy gì lạ. - 10:20. Dev đọc diff, chỉ thấy tên biến, đẩy lên.
Nếu rơi vào phiên bản đầu, việc nên làm lúc 10:45 là: xoay mật khẩu cơ sở dữ liệu ngay, rồi đọc nhật ký truy cập, rồi mới tính chuyện dọn lịch sử. Xóa dòng ở commit sau không giải quyết gì vì lịch sử đã giữ chuỗi cũ và cuộc trò chuyện với AI cũng đã chứa nó.
8. Hay sai ở đâu#
| Chỗ sai | Vì sao sai | Cách làm đúng |
|---|---|---|
| Xóa khóa ở commit mới rồi coi như xong | Lịch sử và bản sao vẫn còn | Xoay hoặc thu hồi khóa trước |
| Commit khóa, đẩy ép (force push) đè lên và coi như sạch | Người đã kéo hoặc fork vẫn có bản cũ, nền tảng có thể giữ bản đã đẩy | Coi khóa đã lộ, xoay khóa |
| Xoay một khóa nhưng quên khóa khác cùng chỗ lộ | Tệp có hai khóa, kẻ xấu dùng khóa còn lại | Liệt kê mọi bí mật trong tệp và lịch sử bị lộ, xoay hết |
Thêm tệp vào .gitignore sau khi đã commit |
Git vẫn theo dõi tệp đã commit | git rm --cached, và xoay khóa nếu từng đẩy lên |
| Tin hoàn toàn vào công cụ quét | Mẫu chữ bỏ sót khóa tách chuỗi hay mã hóa, báo nhầm chữ giữ chỗ | Giữ bí mật ngoài code, nhiều lớp |
| Xoay khóa nhưng không cập nhật nơi dùng | Ứng dụng đột nhiên hỏng | Tạo khóa mới trước, cập nhật rồi mới vô hiệu khóa cũ |
| Không đọc nhật ký sử dụng sau khi lộ | Không biết có bị lạm dụng không | Xem ai dùng khóa trong khoảng thời gian lộ |
| Dán khóa thật vào cuộc trò chuyện với AI | Nội dung có thể nằm trong lịch sử cuộc trò chuyện | Dùng chữ giữ chỗ |
9. Số liệu và tài liệu đã dùng#
| Nguồn | Nói gì | Cách đọc | Lưu ý |
|---|---|---|---|
| Tài liệu GitHub, About secret scanning | Quét toàn bộ lịch sử git trên mọi nhánh; khuyên xoay khóa ngay, xóa khỏi lịch sử là chậm và thường không cần khi đã thu hồi | Căn cứ cho thứ tự "xoay trước, dọn sau" | Đọc qua công cụ tóm tắt trang |
| Tài liệu GitHub, About push protection | Chặn lệnh đẩy chứa bí mật; mặc định tắt ở cấp kho; bỏ qua được bằng lý do | Là lớp chắn, không phải bảo chứng | Đọc qua công cụ tóm tắt trang |
| Tài liệu AWS, Update access keys | Tạo khóa thứ hai, cập nhật ứng dụng, vô hiệu hóa khóa cũ rồi mới xóa | Quy trình xoay không gián đoạn | Viết cho việc xoay thường, không riêng cho khóa đã lộ |
| Escape, The State of Security of Vibe Coded Apps | Hơn 2.000 lỗ hổng, hơn 400 bí mật bị lộ, 175 mẫu thông tin cá nhân trong ứng dụng làm bằng nền tảng "vibe coding" | Ứng dụng làm nhanh dễ lộ bí mật | Chỉ do một hãng bảo mật tự quét. Số ứng dụng được nêu khác nhau giữa các trang tôi thấy (khoảng 1,4 nghìn và 5,6 nghìn) nên tôi không nêu số ứng dụng, và trang không ghi ngày |
| Thí nghiệm của bài này | Khóa vẫn đọc được từ lịch sử sau khi xóa; bộ quét 30 dòng bỏ sót và báo nhầm; bản sao trước khi viết lại còn khóa | Cho thấy cơ chế, không cho thấy tần suất ngoài đời | Một máy, kho 2 đến 5 commit, khóa giả |
Về một con số tôi không dùng: tôi đã thử kiểm một số liệu "41% trong 100 ứng dụng có lộ bí mật hoặc khóa API" gắn với một nghiên cứu của VibeWrench (tháng 3/2026). Tôi không tìm được nguồn xác nhận. Trang sản phẩm VibeWrench chỉ nói họ quét 100 ứng dụng và tìm ra 318 lỗ hổng, không nêu tỷ lệ lộ khóa. Vì vậy bài này không dùng con số đó.
Đọc cho đúng: có bằng chứng tốt cho cơ chế (git nhớ lịch sử, bản sao còn) vì bạn tự chạy lại được. Chưa có số liệu đáng tin cho tần suất lộ khóa nói chung, và tôi không tìm thấy nghiên cứu đo hiệu quả riêng của pre-commit hook.
10. Bài tập có đáp án (30 phút)#
Đề. Tạo một kho thử, dùng khóa giả (như cặp khóa ví dụ của AWS ở trên, tuyệt đối không dùng khóa thật), rồi:
- Commit một tệp có khóa giả, rồi xóa khóa ở commit sau.
- Dùng
git log -Svàgit showđể chứng minh khóa vẫn còn. - Viết một thứ tự năm bước nếu đó là khóa thật.
- Gắn một pre-commit hook quét, rồi thử một commit có khóa gán trực tiếp và một commit có khóa tách làm hai chuỗi.
- Chụp một bản sao kho, viết lại lịch sử bằng
git filter-branch, rồi kiểm cả hai kho.
Đáp án.
- Sau hai commit,
git log --onelinehiện hai dòng. git log --oneline -S <khóa>in ra cả hai commit.git show <commit thứ nhất>:<tệp>in lại khóa.- Một thứ tự hợp lý: (1) vô hiệu hóa khóa cũ trên dịch vụ, (2) tạo khóa mới và cập nhật nơi dùng, (3) kiểm ứng dụng, (4) đọc nhật ký sử dụng, (5) dọn lịch sử nếu cần và báo đồng đội. Điểm chấm: xoay khóa đứng trước dọn lịch sử.
- Commit khóa gán trực tiếp bị chặn (mã thoát 1). Commit khóa tách chuỗi không bị chặn, vì bộ quét chỉ tìm mẫu nguyên.
- Kho đã viết lại sạch sau khi xóa nhánh sao lưu, reflog và chạy
gc; bản sao chụp trước vẫn còn khóa. Gợi ý cải thiện bộ quét: quét cả tệp thay vì chỉ dòng thêm, dò theo độ ngẫu nhiên của chuỗi, hoặc dùng công cụ chuyên dụng.
11. Danh sách kiểm trước khi đẩy code#
- Có bí mật thật nào trong phần thay đổi không (khóa, mật khẩu, chuỗi kết nối, khóa riêng)?
- Bí mật nằm ở biến môi trường hoặc nơi cất bí mật, không nằm trong code?
- Tệp
.envvà tệp tương tự đã ghi vào.gitignoretrước khi tạo? - Tôi đã đọc diff và tìm các chuỗi dài lạ trước khi commit?
- Có hook hoặc công cụ quét chạy cục bộ, và có quét phía nền tảng?
- Khóa tôi dùng có quyền hẹp và hạn ngắn không?
- Tôi đã dán khóa thật vào cuộc trò chuyện với AI chưa? Nếu có, xoay khóa đó.
- Nếu khóa lộ lúc này, tôi biết xoay ở đâu, trong bao lâu, và ai cần được báo?
12. Nhìn cân bằng: biết gì, chưa biết gì#
Khá chắc: git giữ lịch sử và có thể đọc lại khóa đã xóa; viết lại lịch sử không thu hồi các bản sao có sẵn; công cụ quét mẫu chữ có thể bỏ sót và báo nhầm; xoay khóa là bước gốc. Cả bốn điều đều đã chạy thật trong bài.
Chưa rõ hoặc có giới hạn:
- Tần suất khóa bị lộ ngoài đời và mức thiệt hại. Số liệu hiện có chủ yếu từ hãng bảo mật tự quét, và một con số tôi không xác nhận được nên đã bỏ.
- Hiệu quả thực tế của từng lớp phòng thủ. Tôi không tìm thấy nghiên cứu đo riêng pre-commit hook hay push protection.
- Các công cụ chuyên dụng (git-filter-repo, gitleaks, trufflehog) tôi chưa chạy. Hãy đọc tài liệu của chúng trước khi dùng.
- Nền tảng khác nhau về tính năng và cài đặt mặc định. Trang tài liệu GitHub tôi đọc qua công cụ tóm tắt và có thể đã đổi.
Đừng thổi phồng: không có lớp nào đủ một mình. Đừng coi nhẹ: chi phí phòng ngừa là vài phút mỗi lần thiết lập, còn một khóa lộ có thể khiến người khác chạm vào dữ liệu hay tài nguyên của bạn.
13. Kết luận#
Một khóa API đã vào git thì coi như đã ra khỏi tay bạn. Xóa dòng ở commit sau là cất chìa vào ngăn kéo, còn bản sao thì vẫn ở ngoài. Hãy xoay khóa trước, đọc nhật ký, rồi mới dọn lịch sử. Để lần sau không lặp lại, giữ bí mật ngoài code, chặn lúc commit, quét sau khi đẩy, và với AI agent thì đưa chìa khóa hạn ngắn, quyền hẹp, và luôn đọc diff trước khi commit.
Nguồn tham khảo#
Số liệu lấy từ nguồn công khai tính đến tháng 10/2026. Các trang GitHub và AWS tôi đọc qua công cụ tóm tắt trang, nên hãy mở bản gốc để kiểm tra trước khi trích dẫn.
- GitHub Docs, About secret scanning
- GitHub Docs, About push protection
- AWS Documentation, Update access keys (IAM User Guide)
- Escape, The State of Security of Vibe Coded Apps (không ghi ngày, do hãng bảo mật tự thực hiện)
- Git, git-filter-branch và git-log (tùy chọn -S)
Câu hỏi thường gặp
- Tôi lỡ commit khóa API rồi xóa ở commit sau thì có an toàn không?
- Không. Git lưu cả lịch sử, nên khóa vẫn đọc được bằng git log, git show hoặc git grep trên các commit cũ. Trong thí nghiệm của bài, sau khi xóa khóa khỏi tệp, lệnh git log -S vẫn tìm ra cả hai commit và git show in lại nguyên khóa. Việc đầu tiên cần làm là thu hồi hoặc xoay khóa, vì khi khóa còn sống thì mọi bản sao đều còn nguy hiểm.
- Khi lộ khóa nên làm gì trước, xóa khỏi lịch sử hay xoay khóa?
- Xoay hoặc thu hồi khóa trước. Tài liệu secret scanning của GitHub cũng khuyên xoay ngay lập tức và nói việc xóa bí mật khỏi lịch sử git là chậm và thường không cần thiết khi khóa đã bị thu hồi. Sau đó mới dọn lịch sử và đọc nhật ký sử dụng để xem có ai dùng khóa trong lúc nó lộ không.
- Viết lại lịch sử git (filter-branch, git-filter-repo) có xóa hẳn khóa không?
- Nó xóa khóa khỏi kho bạn viết lại, nhưng không xóa các bản sao đã có. Trong thí nghiệm, bản sao chép trước khi viết lại vẫn tìm ra khóa. Công cụ còn để lại nhánh sao lưu và reflog, cần dọn thêm. Vì vậy viết lại lịch sử chỉ là bước phụ sau khi đã xoay khóa.
- Pre-commit hook và công cụ quét bí mật có bắt hết không?
- Không. Công cụ quét dựa trên mẫu chữ nên có thể bỏ sót và báo nhầm. Trong thí nghiệm, bộ quét 30 dòng chặn được dòng gán khóa trực tiếp nhưng cho qua khóa bị tách làm hai chuỗi, và chặn nhầm một mật khẩu mẫu. Hãy dùng nó như một lớp chắn thêm, không thay cho việc xoay khóa và giữ bí mật ngoài code.
- Làm việc với AI agent cần chú ý gì về khóa API?
- Đừng dán khóa thật vào cuộc trò chuyện, giữ tệp .env ngoài đường dẫn agent đọc được, dùng khóa có quyền hẹp và hạn ngắn, và đọc lại phần thay đổi trước khi commit để chắc agent không ghi khóa cố định vào code. Một lệnh git add đã đủ để đưa nó vào lịch sử.