- Khóa API giống mật khẩu kèm thẻ thanh toán: ai có khóa là dùng được tài khoản của bạn và bạn trả tiền.
- Ba lớp phòng: khóa nằm trong tệp
.envvà đọc qua biến môi trường;.envnằm trong.gitignore; quét dự án trước khi commit, và để push protection của GitHub chặn thêm một lần khi push. - Theo GitHub Docs (11/11/2026), push protection cho tài khoản cá nhân bật sẵn và chặn bí mật khi push lên kho công khai; khóa API của Anthropic, OpenAI và token Hugging Face đều nằm trong danh sách mẫu được quét.
- Lộ khóa thì thu hồi và tạo khóa mới trước, rồi mới tính chuyện dọn lịch sử git.
Bài này dựa trên GitHub Docs về push protection, các mẫu bí mật được quét, gỡ dữ liệu nhạy cảm và tệp bỏ qua, đọc ngày 11/11/2026. Ví dụ dùng khóa API của Claude, nhưng cách làm giống hệt với khóa của các hãng khác.
Vì sao khóa API dễ bị lộ
Khi học AI, bạn sẽ gọi API của mô hình bằng một khóa bí mật. Những đường lộ khóa hay gặp:
- Viết thẳng khóa vào code "cho nhanh", rồi quên và commit.
- Commit cả tệp
.envvì chạygit add .khi chưa có.gitignore. - Notebook in ra biến chứa khóa, và đầu ra của ô đó được lưu cùng tệp
.ipynb. - Chụp màn hình hay dán log có khóa vào nhóm chat hỏi lỗi.
- Đưa khóa vào tệp cấu hình của agent AI, và agent đọc hay ghi tệp đó ra chỗ khác.
Kho công khai trên GitHub có thể bị người khác đọc ngay sau khi bạn push. Vì vậy cách an toàn là để khóa không bao giờ đi vào git.
Lớp 1: khóa nằm trong .env, đọc qua biến môi trường
Tạo tệp .env ở thư mục gốc dự án, mỗi dòng một biến:
ANTHROPIC_API_KEY=khóa-của-bạn
Code chỉ đọc tên biến, không bao giờ chứa giá trị. Thư viện anthropic tự đọc biến ANTHROPIC_API_KEY, nên khi biến đã được đặt, bạn chỉ cần tạo client mà không truyền khóa. Đoạn Python dưới đây nạp .env vào biến môi trường mà không cần thư viện ngoài (bạn cũng có thể dùng thư viện python-dotenv), không ghi đè biến đã có, báo lỗi rõ ràng khi thiếu khóa, và chỉ in độ dài khóa chứ không in khóa.
Kèm theo, tạo tệp .env.example chỉ có tên biến, để người khác clone dự án biết cần đặt biến gì:
ANTHROPIC_API_KEY=
Tệp .env.example commit được, tệp .env thì không.
Lớp 2: .gitignore chặn .env
Thêm vào .gitignore ở gốc kho:
.env
.venv/
__pycache__/
Theo GitHub Docs về bỏ qua tệp, .gitignore chỉ có tác dụng với tệp chưa được git theo dõi. Nếu .env đã từng được commit, thêm vào .gitignore không làm nó biến mất khỏi kho. Khi đó phải bỏ theo dõi tệp bằng git rm --cached .env, commit thay đổi, và coi như khóa trong đó đã lộ (xem phần cuối bài).
Lớp 3: quét trước khi commit
Đoạn Python thứ hai quét một thư mục dự án, tìm chuỗi trông giống khóa của Anthropic (bắt đầu bằng sk-ant-), OpenAI, Hugging Face (hf_) và token GitHub, và nhắc khi có tệp .env mà .gitignore chưa chặn. Kết quả luôn che bớt khóa, chỉ hiện vài ký tự đầu và độ dài. Nó bỏ qua thư mục .git, môi trường ảo và tệp nhị phân. Chạy với đường dẫn dự án thì trả mã thoát 1 khi thấy điều đáng ngờ, nên có thể gắn vào hook trước commit.
"""Quét thư mục dự án tìm chuỗi trông giống khóa API trước khi commit, và nhắc nếu .env chưa nằm trong .gitignore.
Chạy: python quet_khoa.py duong/dan/du_an (chỉ đọc, không sửa gì, không gửi đi đâu)
Chạy không tham số: tạo một dự án mẫu trong thư mục tạm rồi quét.
Kết quả luôn che bớt khóa, chỉ hiện vài ký tự đầu. Đây là lưới lọc thô; GitHub còn có push protection quét khi push.
"""
import os
import re
import sys
import tempfile
MAU_KHOA = [
("Anthropic", re.compile(r"sk-ant-[A-Za-z0-9_-]{20,}")),
("OpenAI", re.compile(r"sk-(?:proj-)?[A-Za-z0-9_-]{20,}")),
("Hugging Face", re.compile(r"hf_[A-Za-z0-9]{30,}")),
("GitHub", re.compile(r"(?:ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{50,})")),
]
BO_QUA_THU_MUC = {".git", ".venv", "venv", "__pycache__", "node_modules"}
def che(khoa):
return khoa[:7] + "…(" + str(len(khoa)) + " ký tự)"
def quet_van_ban(van_ban):
thay = []
for so_dong, dong in enumerate(van_ban.splitlines(), 1):
da_thay = set()
for hang, mau in MAU_KHOA:
for m in mau.finditer(dong):
if any(m.start() >= a and m.end() <= b for a, b in da_thay):
continue # khóa Anthropic cũng khớp mẫu sk- của OpenAI: chỉ báo một lần
da_thay.add((m.start(), m.end()))
thay.append((so_dong, hang, che(m.group())))
return thay
def env_bi_bo_qua(goc):
duong = os.path.join(goc, ".gitignore")
if not os.path.exists(duong):
return False
with open(duong, encoding="utf-8") as f:
dong = [d.strip() for d in f]
return any(d in (".env", ".env*", "*.env", "/.env") for d in dong)
def quet_thu_muc(goc):
ket_qua = []
for thu_muc, con, tep in os.walk(goc):
con[:] = sorted(c for c in con if c not in BO_QUA_THU_MUC)
for ten in sorted(tep):
duong = os.path.join(thu_muc, ten)
try:
with open(duong, encoding="utf-8") as f:
van_ban = f.read()
except (UnicodeDecodeError, OSError):
continue # bỏ qua tệp nhị phân
for so_dong, hang, khoa in quet_van_ban(van_ban):
ket_qua.append(f"{os.path.relpath(duong, goc)}:{so_dong} {hang:<12} {khoa}")
if os.path.exists(os.path.join(goc, ".env")) and not env_bi_bo_qua(goc):
ket_qua.append(".env có trong thư mục nhưng CHƯA nằm trong .gitignore")
return ket_qua
def tao_du_an_mau(goc):
tep = {
"app.py": 'import os\nkhoa = os.environ["ANTHROPIC_API_KEY"] # đúng: đọc từ biến môi trường\n',
"thu_nghiem.py": 'client = Client(api_key="sk-ant-api03-' + "x" * 40 + '") # sai: khóa viết thẳng\n',
"notebook_cu.py": 'HF_TOKEN = "hf_' + "A" * 34 + '"\n',
".env": "ANTHROPIC_API_KEY=sk-ant-api03-" + "y" * 40 + "\n",
".gitignore": "__pycache__/\n.venv/\n",
}
for ten, noi_dung in tep.items():
with open(os.path.join(goc, ten), "w", encoding="utf-8") as f:
f.write(noi_dung)
def chay_thu():
with tempfile.TemporaryDirectory() as goc:
tao_du_an_mau(goc)
kq = quet_thu_muc(goc)
print("\n".join(kq) if kq else "không thấy gì đáng ngờ")
return kq
if __name__ == "__main__":
if len(sys.argv) > 1:
kq = quet_thu_muc(sys.argv[1])
print("\n".join(kq) if kq else "không thấy gì đáng ngờ")
sys.exit(1 if kq else 0)
chay_thu()
"""Đọc khóa API từ biến môi trường; nếu chưa có thì nạp từ tệp .env cạnh chương trình (không cần thư viện ngoài).
Tệp .env (KHÔNG commit, đã ghi trong .gitignore):
ANTHROPIC_API_KEY=...khóa của bạn...
Tệp .env.example (commit được, chỉ có tên biến):
ANTHROPIC_API_KEY=
Thư viện anthropic tự đọc biến ANTHROPIC_API_KEY, nên sau bước này chỉ cần anthropic.Anthropic().
"""
import os
def nap_env(duong=".env"):
"""Nạp các dòng TEN=giatri vào os.environ, không ghi đè biến đã có sẵn."""
if not os.path.exists(duong):
return 0
so = 0
with open(duong, encoding="utf-8") as f:
for dong in f:
dong = dong.strip()
if not dong or dong.startswith("#") or "=" not in dong:
continue
ten, gia_tri = dong.split("=", 1)
ten, gia_tri = ten.strip(), gia_tri.strip().strip('"').strip("'")
if ten and ten not in os.environ:
os.environ[ten] = gia_tri
so += 1
return so
def lay_khoa(ten="ANTHROPIC_API_KEY"):
nap_env()
khoa = os.environ.get(ten, "")
if not khoa:
raise SystemExit(f"Chưa có {ten}. Tạo tệp .env theo mẫu .env.example, hoặc đặt biến môi trường.")
return khoa
if __name__ == "__main__":
khoa = lay_khoa()
print(f"Đã có khóa ({len(khoa)} ký tự).") # không bao giờ in chính khóa ra màn hình hay log
Với dự án mẫu trong code, kết quả bắt được ba chỗ: khóa trong .env (đúng chỗ, nhưng .gitignore chưa chặn), khóa viết thẳng trong thu_nghiem.py, token Hugging Face còn sót trong notebook_cu.py. Tệp app.py đọc khóa qua os.environ thì không bị báo. Đây là lưới lọc thô bằng biểu thức chính quy; các công cụ chuyên dụng quét được nhiều loại bí mật hơn và quét cả lịch sử commit.
Notebook: chỗ khóa hay lọt nhất
Notebook Jupyter lưu cả đầu ra của từng ô vào tệp .ipynb. Nếu có lúc bạn chạy print(khoa) hay hiển thị biến chứa khóa để kiểm tra, khóa nằm luôn trong tệp dù đã xóa dòng code đó. Vài thói quen cho notebook:
- Đọc khóa bằng
os.environở ô đầu, không gán chuỗi khóa vào biến trong ô. - Trước khi commit, xóa toàn bộ đầu ra: trong Jupyter chọn xóa đầu ra mọi ô, hoặc chạy
jupyter nbconvert --clear-output --inplace ten_notebook.ipynb. - Chạy công cụ quét ở trên cho cả thư mục, vì nó đọc tệp
.ipynbnhư văn bản và bắt được khóa nằm trong đầu ra. - Với Google Colab, dùng mục bí mật (Secrets) có sẵn của Colab thay vì dán khóa vào ô code.
Push protection của GitHub
GitHub có thêm một lớp ở phía máy chủ. Theo trang About push protection (11/11/2026):
- Push protection cho người dùng bật sẵn với tài khoản cá nhân trên GitHub.com, chặn bạn push bí mật lên kho công khai. Khi bị chặn, GitHub từ chối lần push và giải thích lý do; bạn gỡ bí mật khỏi commit rồi push lại.
- Push protection cho kho thì mặc định tắt và cần quản trị viên bật (yêu cầu GitHub Secret Protection).
- Có thể vượt qua chặn bằng cách nêu lý do, nhưng đừng làm vậy với khóa thật.
Trang Supported secret scanning patterns liệt kê các loại bí mật GitHub nhận ra. Ngày 11/11/2026, danh sách có anthropic_api_key, openai_api_key và hf_user_access_token, cả ba đều được đánh dấu là mẫu của đối tác và có push protection. Mẫu đối tác nghĩa là khi khóa xuất hiện trong kho công khai, GitHub báo cho chính hãng phát hành khóa.
Push protection là lớp cuối, không thay cho hai lớp đầu: nó chỉ nhận ra những dạng bí mật nó biết.
Khóa đã lộ thì làm gì
Theo trang Removing sensitive data from a repository, bước đầu tiên là thu hồi hoặc đổi khóa. Khóa đã đổi thì không dùng được nữa, và nhiều khi việc này đã giải quyết xong vấn đề mà không cần viết lại lịch sử.
Trình tự nên làm:
- Vào trang quản lý khóa của hãng (với Claude là phần API keys trong Claude Console), thu hồi khóa bị lộ, tạo khóa mới.
- Cập nhật khóa mới vào
.envtrên máy và vào Secrets của GitHub Actions nếu có dùng. - Xem lịch sử sử dụng và hóa đơn trên trang của hãng để phát hiện lượt gọi lạ.
- Nếu vẫn muốn gỡ khóa khỏi lịch sử git, GitHub Docs khuyên dùng
git-filter-repo. Docs cũng cảnh báo nhiều tác dụng phụ: mã băm commit thay đổi, chữ ký commit bị mất, người khác còn bản clone cũ có thể push dữ liệu trở lại, bản fork và bộ nhớ đệm vẫn còn giữ dữ liệu.
Vì bước 4 phức tạp và không chắc chắn, hãy luôn coi khóa từng nằm trong một commit đã push là khóa đã lộ.
Khóa trong GitHub Actions và agent AI
Workflow cần gọi API thì đặt khóa trong Settings, Secrets and variables, Actions của kho, rồi tham chiếu bằng secrets.TEN_KHOA trong tệp YAML; xem bài GitHub Actions. Với agent AI chạy trên máy (như Claude Code), không để agent đọc tệp .env khi không cần, và cấu hình quyền để chặn việc đó; bài giới hạn quyền của agent có ví dụ luật chặn tệp bí mật.
Một vài thói quen nữa:
- Mỗi dự án một khóa riêng, đặt hạn mức chi tiêu nếu trang của hãng cho phép, để khi lộ thì thiệt hại có giới hạn.
- Không dán khóa vào notebook, không in khóa ra log, không gửi khóa qua tin nhắn.
- Khi nhờ người khác xem lỗi, xóa khóa khỏi đoạn log trước khi gửi.
- Viết khóa thẳng vào code "tạm thời" rồi quên.
- Thêm
.envvào.gitignoresau khi đã commit nó, và tưởng thế là xong. - Chỉ xóa commit chứa khóa mà không thu hồi khóa.
- Vượt qua push protection bằng lý do "sẽ sửa sau" với một khóa thật.
- Để khóa trong ô đầu ra của notebook rồi đẩy notebook lên GitHub.
Câu hỏi thường gặp
Kho riêng tư thì có cần cẩn thận không?
Có. Kho riêng tư vẫn có thể bị chuyển thành công khai, được chia sẻ cho người khác, hay bị lộ qua bản clone trên máy. Khóa vẫn chỉ nên nằm trong .env.
Tệp .env có cần commit cho người cùng nhóm không?
Không. Commit .env.example chỉ có tên biến; mỗi người tự tạo .env với khóa của mình, hoặc nhóm chia sẻ khóa qua kênh riêng có quản lý.
Đã đổi khóa rồi có cần xóa khỏi lịch sử git không?
Khóa cũ đã thu hồi thì không còn dùng được. GitHub Docs nói việc đổi khóa nhiều khi đã đủ; viết lại lịch sử chỉ nên làm khi thật cần và hiểu các tác dụng phụ.
Push protection có bắt được mọi loại khóa không?
Không. Nó chỉ nhận ra các mẫu trong danh sách của GitHub. Khóa của dịch vụ nhỏ hay mật khẩu tự đặt có thể lọt qua, nên hai lớp đầu vẫn quan trọng nhất.
Học tiếp
- Dùng khóa trong workflow tự động: GitHub Actions.
- Ôn lại commit và
.gitignorekhi đưa dự án lên: đưa code lên GitHub. - Chặn agent AI đọc tệp bí mật: giới hạn quyền của agent.
- Xem lại toàn bộ chủ đề: GitHub cho người học AI.
Nếu bạn muốn học Python và AI bài bản cùng giảng viên, tham khảo khóa học AI của Big-O Coding.
Khóa AI – Introduction to Machine Learning: nền tảng Máy học từ thuật toán cơ bản đến Neural Networks và LLM, học bằng Python. Học thử buổi 1 miễn phí.


