Mục lục
- Mục Tiêu Bài Học
- Vì Sao Quan Trọng Với AI Engineer
- 3 Cấp Độ Quản Lý Secret
- .env File Pattern
- Pydantic Settings Cho FastAPI
- Ngăn Commit .env — .gitignore và Pre-commit Hook
- Secret Manager Cho Production
- Inject Secret Vào Container
- API Key Bên Phía Server — Không Bao Giờ Xuống Browser
- Tạo và Quản Lý API Key Của Riêng App
- Secret Rotation
- Khi Key Bị Leak — Incident Response
- Common Pitfalls
- Tóm Tắt
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Hiểu rủi ro cụ thể khi hard-code API key cho LLM
- ✅ Dùng được env variable,
.envfile, vàpython-dotenv - ✅ Cấu hình Pydantic Settings 2.x cho FastAPI app
- ✅ Cài pre-commit hook chặn commit secret (gitleaks, detect-secrets)
- ✅ Gọi AWS Secrets Manager và GCP Secret Manager từ Python
- ✅ Inject secret vào Docker container và Kubernetes pod đúng cách
- ✅ Tạo và lưu API key của riêng app (hash, không plaintext)
- ✅ Biết quy trình xử lý khi key bị leak
Vì Sao Quan Trọng Với AI Engineer
API key của LLM không giống API key của dịch vụ bình thường — chi phí tính theo token, và bị lạm dụng có thể tạo ra bill rất lớn trong thời gian ngắn.
Chi Phí Thực Tế
Giá tham khảo (2025):
- GPT-4o: $2.50 input / $10.00 output per million token
- Claude Sonnet: $3.00 input / $15.00 output per million token
- GPT-4o mini: $0.15 input / $0.60 output per million token
Nếu attacker dùng key bị leak để chạy fine-tuning hoặc batch inference với model lớn, bill có thể lên $1,000–$10,000 trong vài tiếng. Các provider có thể hoàn tiền theo case, nhưng không đảm bảo.
Quy Mô Của Vấn Đề
GitGuardian (công ty chuyên scan secret) báo cáo:
- Khoảng 10 triệu secret bị commit lên GitHub công khai trong năm 2022 (tăng 67% so với năm trước).
- Sau khi commit, bot scanner có thể phát hiện key trong vài giây — trước cả khi developer kịp xóa.
- Xóa khỏi history Git không xóa được khỏi các mirror, fork, cache của GitHub — chỉ rotate key mới thực sự an toàn.
Các Loại Secret Trong AI App
- LLM API key: OpenAI, Anthropic, Google Gemini, Cohere
- Vector DB key: Pinecone, Weaviate Cloud
- Database URL: kèm username/password
- Redis URL: nếu có auth
- Cloud credentials: AWS access key, GCP service account JSON
- Webhook secret: Slack, GitHub, Stripe
- JWT secret key: ký token
3 Cấp Độ Quản Lý Secret
Level 0 — Hard-code (KHÔNG bao giờ làm)
# KHÔNG BAO GIỜ làm thế này
client = OpenAI(api_key="sk-proj-abc123...")
# Hoặc trong config file
OPENAI_API_KEY = "sk-proj-abc123..."
Vấn đề:
- Commit lên Git → mọi người có access repo đều thấy.
- Nếu repo public → bot scanner thu thập trong giây.
- Rotate key phải sửa code, redeploy.
Level 1 — Environment Variable
import os
api_key = os.environ["OPENAI_API_KEY"]
# KeyError nếu biến chưa set — fail nhanh, rõ ràng
# Hoặc dùng .get() với fallback
api_key = os.environ.get("OPENAI_API_KEY", "")
if not api_key:
raise RuntimeError("OPENAI_API_KEY is not set")
Chạy với env variable:
OPENAI_API_KEY=sk-proj-... python app.py
# Hoặc export trước
export OPENAI_API_KEY=sk-proj-...
python app.py
Dùng được cho: dev trên máy local, CI/CD pipeline, server không có file system tiện.
Level 2 — .env File với python-dotenv
Phù hợp cho dev/local, tránh phải export lại mỗi lần mở terminal mới.
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # Đọc file .env trong thư mục hiện tại
api_key = os.environ["OPENAI_API_KEY"]
Level 3 — Secret Manager
Dùng cho production: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault. Chi tiết ở phần 7.
So sánh nhanh theo môi trường:
| Môi trường | Cách khuyên dùng |
|---|---|
| Dev local | .env + python-dotenv |
| CI/CD (GitHub Actions) | Repository Secrets → env variable |
| Staging / Production | Secret Manager (AWS / GCP / Vault) |
| Kubernetes | Kubernetes Secrets hoặc External Secrets Operator |
.env File Pattern
Cấu Trúc File .env
# .env (LOCAL ONLY — KHÔNG commit lên Git)
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
PINECONE_API_KEY=pcsk_...
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379/0
# JWT secret — dùng giá trị random đủ dài
JWT_SECRET_KEY=change-me-use-openssl-rand-hex-32
APP_ENV=development
DEBUG=true
File .env.example — Cho Team Mới
Commit file .env.example (có key name, không có value) vào repo để người mới biết cần set những gì:
# .env.example — COMMIT file này, KHÔNG commit .env
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
PINECONE_API_KEY=
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379/0
JWT_SECRET_KEY=
APP_ENV=development
DEBUG=true
Load Trong Python
from dotenv import load_dotenv
import os
# load_dotenv() không ghi đè env variable đã tồn tại (behavior mặc định)
# Nếu muốn ghi đè: load_dotenv(override=True)
load_dotenv()
openai_key = os.environ["OPENAI_API_KEY"]
db_url = os.environ["DATABASE_URL"]
debug = os.environ.get("DEBUG", "false").lower() == "true"
Đặt Tên Biến Theo Convention
- UPPER_SNAKE_CASE cho tất cả.
- Tên rõ ràng:
OPENAI_API_KEY, không phảiKEYhayTOKEN. - Không gộp nhiều secret vào 1 biến — mỗi secret riêng, tiện rotate độc lập.
Pydantic Settings Cho FastAPI
pydantic-settings (package riêng từ Pydantic v2) tự động load env variable + validate type.
pip install pydantic-settings
Cơ Bản
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr
class Settings(BaseSettings):
openai_api_key: SecretStr # SecretStr: không in ra khi log
anthropic_api_key: SecretStr
database_url: str
redis_url: str = "redis://localhost:6379/0"
jwt_secret_key: SecretStr
app_env: str = "development"
debug: bool = False
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False, # OPENAI_API_KEY = openai_api_key
)
# Singleton — load 1 lần
settings = Settings()
Dùng Trong FastAPI
# main.py
from fastapi import FastAPI, Depends
from openai import AsyncOpenAI
from config import settings
app = FastAPI()
# Dùng .get_secret_value() để lấy string từ SecretStr
_openai_client: AsyncOpenAI | None = None
def get_openai_client() -> AsyncOpenAI:
global _openai_client
if _openai_client is None:
_openai_client = AsyncOpenAI(
api_key=settings.openai_api_key.get_secret_value()
)
return _openai_client
@app.post("/chat")
async def chat(prompt: str, client: AsyncOpenAI = Depends(get_openai_client)):
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return {"reply": response.choices[0].message.content}
SecretStr Tránh Leak Qua Log
from pydantic import SecretStr
key = SecretStr("sk-proj-abc123")
print(key) # Output: **********
print(repr(key)) # Output: SecretStr('**********')
print(key.get_secret_value()) # Output: sk-proj-abc123 (chỉ khi cần dùng thực sự)
Validate Khi Startup
Nếu thiếu biến bắt buộc, Pydantic raise ValidationError ngay khi import settings, không chờ đến lúc endpoint được gọi:
# Khi chạy app mà chưa set OPENAI_API_KEY:
# pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
# openai_api_key
# Field required [type=missing, input_url=...]
Fail sớm và rõ ràng tốt hơn fail ở runtime khi xử lý request thật.
Ngăn Commit .env — .gitignore và Pre-commit Hook
.gitignore Cho Secret
# .gitignore
.env
.env.local
.env.production
.env.staging
*.env
# Nhưng commit .env.example
!.env.example
# Cloud credentials
*.pem
*.key
service-account.json
credentials.json
Lưu ý: .gitignore chỉ ngăn file chưa được track. Nếu lỡ commit .env rồi, thêm vào .gitignore sau không giúp ích — phải dùng git rm --cached .env và rotate key.
Pre-commit Hook Với gitleaks
gitleaks quét pattern secret (regex) trong diff trước khi commit.
pip install pre-commit
Tạo file .pre-commit-config.yaml ở root repo:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
# Cài hook vào repo
pre-commit install
# Test thủ công (scan toàn bộ history)
pre-commit run gitleaks --all-files
Sau đó mỗi lần git commit, gitleaks tự chạy. Nếu tìm thấy secret, commit bị chặn.
Kết Hợp detect-secrets (Yelp)
detect-secrets dùng thêm entropy analysis, bắt được secret chưa có pattern cụ thể:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]
# Tạo baseline (scan lần đầu để đánh dấu false positive đã biết)
detect-secrets scan > .secrets.baseline
# Commit baseline vào repo
git add .secrets.baseline
Secret Manager Cho Production
Secret manager giải quyết vấn đề mà .env file không giải quyết được: audit log, auto rotation, access control per-service, và không cần copy secret sang máy/server.
a) AWS Secrets Manager
pip install boto3
import boto3
import json
def get_secret(secret_id: str, region: str = "us-east-1") -> dict:
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_id)
return json.loads(response["SecretString"])
# Sử dụng
secrets = get_secret("prod/myapp/llm-keys")
openai_key = secrets["openai_api_key"]
Tạo secret trên AWS:
aws secretsmanager create-secret \
--name "prod/myapp/llm-keys" \
--secret-string '{"openai_api_key":"sk-proj-...","anthropic_api_key":"sk-ant-..."}'
Pricing: $0.40/secret/tháng + $0.05/10,000 API call. Auto rotation có sẵn cho RDS, Redshift, và custom Lambda.
Access control qua IAM: mỗi service/role chỉ được đọc secret nó cần, không phải tất cả.
b) GCP Secret Manager
pip install google-cloud-secret-manager
from google.cloud import secretmanager
def get_secret(project_id: str, secret_id: str, version: str = "latest") -> str:
client = secretmanager.SecretManagerServiceClient()
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("utf-8")
# Sử dụng
openai_key = get_secret("my-gcp-project", "openai-api-key")
Pricing: $0.06/active secret version/tháng + $0.03/10,000 access operation.
Auth: chạy trên GCE/Cloud Run tự dùng service account của VM, không cần credential file.
c) HashiCorp Vault
Self-hosted, phù hợp on-prem hoặc khi policy không cho phép lưu secret ở cloud provider.
pip install hvac
import hvac
client = hvac.Client(url="https://vault.internal:8200", token=vault_token)
secret = client.secrets.kv.v2.read_secret_version(path="myapp/llm-keys")
openai_key = secret["data"]["data"]["openai_api_key"]
Vault hỗ trợ dynamic secret — tạo credential on-demand với TTL, tự revoke sau khi hết hạn. Phù hợp database credential hơn là LLM API key (LLM provider không hỗ trợ dynamic credential).
d) Azure Key Vault
pip install azure-keyvault-secrets azure-identity
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
vault_url = "https://my-vault.vault.azure.net/"
client = SecretClient(vault_url=vault_url, credential=DefaultAzureCredential())
openai_key = client.get_secret("openai-api-key").value
DefaultAzureCredential tự chọn auth method phù hợp (managed identity khi chạy trên Azure, VS Code credential khi dev local).
Cache Secret Trong Memory
Mỗi request không nên gọi secret manager — latency cao, tốn tiền. Cache trong memory với TTL:
import time
from functools import lru_cache
# Đơn giản: cache với TTL thủ công
_secret_cache: dict = {}
def get_secret_cached(secret_id: str, ttl_seconds: int = 300) -> dict:
now = time.monotonic()
if secret_id in _secret_cache:
value, fetched_at = _secret_cache[secret_id]
if now - fetched_at < ttl_seconds:
return value
value = get_secret(secret_id) # gọi AWS/GCP/Vault
_secret_cache[secret_id] = (value, now)
return value
Inject Secret Vào Container
Docker Compose — Inject Từ Shell Env
Không đặt giá trị secret trực tiếp trong docker-compose.yml. Thay vào đó dùng substitution từ shell env:
# docker-compose.yml
services:
api:
image: myapp:latest
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
DATABASE_URL: ${DATABASE_URL}
# Trên host, export trước rồi mới compose up
export OPENAI_API_KEY=sk-proj-...
docker compose up
Docker Compose cũng đọc file .env ở cùng thư mục tự động (không cần env_file explicit). Đây là .env của Compose, không phải của Python app.
Dockerfile — Không Dùng ENV Cho Secret
# SAI: secret bị bake vào image layer
ENV OPENAI_API_KEY=sk-proj-...
# ĐÚNG: không đặt secret trong Dockerfile
# Runtime inject qua -e hoặc --env-file
# Runtime inject
docker run -e OPENAI_API_KEY=sk-proj-... myapp:latest
# Hoặc từ file (file này KHÔNG commit vào repo)
docker run --env-file .env myapp:latest
Kubernetes Secrets
# k8s-secret.yaml — tạo object Secret
apiVersion: v1
kind: Secret
metadata:
name: llm-api-keys
namespace: production
type: Opaque
stringData:
openai-api-key: sk-proj-...
anthropic-api-key: sk-ant-...
# Apply (không commit file này nếu chứa giá trị thật)
kubectl apply -f k8s-secret.yaml
# Tốt hơn: tạo bằng kubectl thay vì file YAML
kubectl create secret generic llm-api-keys \
--from-literal=openai-api-key=sk-proj-... \
--namespace=production
# Pod spec — mount secret vào env
spec:
containers:
- name: api
image: myapp:latest
env:
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: llm-api-keys
key: openai-api-key
External Secrets Operator (Khuyên Dùng Cho Production)
Kubernetes Secrets mặc định chỉ base64-encode, không encrypt. External Secrets Operator đồng bộ từ AWS/GCP/Vault → Kubernetes Secret, tự refresh khi secret thay đổi:
# ExternalSecret resource
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: llm-keys
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: llm-api-keys
data:
- secretKey: openai-api-key
remoteRef:
key: prod/myapp/llm-keys
property: openai_api_key
API Key Bên Phía Server — Không Bao Giờ Xuống Browser
Lỗi hay gặp với developer mới build AI app: gọi trực tiếp từ JavaScript/React trên browser.
Sai — Gọi Trực Tiếp Từ Browser
// SAI: key lộ trong source code client-side
const response = await fetch("https://api.openai.com/v1/chat/completions", {
headers: {
"Authorization": "Bearer sk-proj-abc123", // bất kỳ ai inspect source đều thấy
"Content-Type": "application/json",
},
// ...
});
Dù minify hay obfuscate code, key vẫn tồn tại dưới dạng string và có thể extract được.
Đúng — Backend Làm Proxy
# FastAPI backend — giữ key, làm proxy
from fastapi import FastAPI, Depends
from openai import AsyncOpenAI
from config import settings
app = FastAPI()
@app.post("/api/chat")
async def chat_proxy(prompt: str, user=Depends(get_current_user)):
# Auth user trước
# Apply rate limit per user
client = AsyncOpenAI(api_key=settings.openai_api_key.get_secret_value())
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=500, # giới hạn token để tránh bill cao
)
return {"reply": response.choices[0].message.content}
// Frontend — gọi backend của mình, không gọi OpenAI trực tiếp
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${userToken}`, // token của user, không phải LLM key
},
body: JSON.stringify({ prompt }),
});
Pattern này còn giúp thêm auth, rate limit, logging, và cost tracking per-user — không thể làm được khi gọi trực tiếp từ browser.
Tạo và Quản Lý API Key Của Riêng App
Nếu app của bạn cũng cấp API cho người khác (B2B SaaS, developer API), cần tạo và quản lý key cho user.
Generate và Hash Key
Lưu hash trong DB, không lưu plaintext — giống cách lưu password:
import secrets
import hashlib
def generate_api_key(prefix: str = "sk-app") -> tuple[str, str]:
"""
Trả về (raw_key, hashed_key).
raw_key: trả cho user 1 lần duy nhất.
hashed_key: lưu vào DB.
"""
token = secrets.token_urlsafe(32) # 32 bytes = 256-bit entropy
raw_key = f"{prefix}-{token}"
hashed_key = hashlib.sha256(raw_key.encode()).hexdigest()
return raw_key, hashed_key
def verify_api_key(provided_key: str, stored_hash: str) -> bool:
computed = hashlib.sha256(provided_key.encode()).hexdigest()
# secrets.compare_digest tránh timing attack
return secrets.compare_digest(computed, stored_hash)
# Tạo key
raw, hashed = generate_api_key()
# Trả raw cho user qua HTTPS 1 lần, lưu hashed vào DB
# Nếu user mất key → tạo mới, không recover được
Schema DB
from sqlalchemy import Column, String, DateTime, Boolean, JSON
from datetime import datetime, timezone
class ApiKey(Base):
__tablename__ = "api_keys"
id = Column(String, primary_key=True)
key_hash = Column(String(64), unique=True, index=True, nullable=False)
key_prefix = Column(String(16)) # 8 ký tự đầu để user identify ("sk-app-xxxx...")
user_id = Column(String, nullable=False)
name = Column(String) # label do user đặt, vd "Production key"
scopes = Column(JSON, default=[]) # ["read", "predict"]
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
last_used_at = Column(DateTime, nullable=True)
expires_at = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True)
Middleware Xác Thực Key
from fastapi import Request, HTTPException, status
from fastapi.security import APIKeyHeader
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
async def verify_key_from_header(
request: Request,
api_key: str | None = Depends(API_KEY_HEADER),
) -> ApiKey:
if not api_key:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
# Hash key nhận được rồi tra DB
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
db_key = await db.query(ApiKey).filter_by(key_hash=key_hash).first()
if not db_key or not db_key.is_active:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
if db_key.expires_at and db_key.expires_at < datetime.now(timezone.utc):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Key expired")
# Cập nhật last_used_at async (không blocking)
await db.execute(
"UPDATE api_keys SET last_used_at = NOW() WHERE id = :id",
{"id": db_key.id}
)
return db_key
Scope/Permission Check
def require_scope(scope: str):
async def checker(key: ApiKey = Depends(verify_key_from_header)):
if scope not in (key.scopes or []):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Key does not have scope: {scope}"
)
return key
return checker
# Sử dụng
@app.post("/predict")
async def predict(key: ApiKey = Depends(require_scope("predict"))):
...
Secret Rotation
Tần Suất Rotation
- LLM API key: 3–6 tháng/lần, hoặc ngay khi nghi ngờ leak.
- Database password: 90 ngày nếu theo compliance (SOC 2, ISO 27001).
- JWT secret: có thể giữ lâu hơn nếu dùng rotation với 2 key song song.
- Internal API key: 1 năm hoặc theo policy công ty.
Zero-downtime Rotation
Quy trình để không gián đoạn service:
- Tạo key mới song song với key cũ.
- Deploy app với logic chấp nhận cả 2 key (trong thời gian chuyển tiếp).
- Cập nhật tất cả service dùng key mới.
- Revoke key cũ sau khi xác nhận tất cả đã chuyển sang key mới.
AWS Secrets Manager Auto Rotation
# Bật auto rotation mỗi 30 ngày
aws secretsmanager rotate-secret \
--secret-id prod/myapp/db-password \
--rotation-rules AutomaticallyAfterDays=30 \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsManagerRDSRotation
Với LLM key (OpenAI, Anthropic), không có Lambda rotation sẵn — cần script tự tạo hoặc thực hiện thủ công.
Khi Key Bị Leak — Incident Response
Quy Trình Xử Lý
- Revoke key ngay lập tức — đây là bước quan trọng nhất. Vào dashboard OpenAI/Anthropic/GCP và revoke. Không chờ xác nhận thiệt hại rồi mới revoke.
- Kiểm tra usage log — provider cung cấp log API call. Xem có call bất thường không (model lạ, prompt lạ, volume đột biến, IP/region không quen).
- Kiểm tra bill — nếu có charge bất thường, liên hệ support ngay và cung cấp log làm bằng chứng. Một số provider có policy hoàn tiền cho leak.
-
Xóa khỏi Git history — dùng
git filter-repo(thay thế chogit filter-branchđã deprecated):pip install git-filter-repo git filter-repo --path-glob "*.env" --invert-paths # Hoặc xóa chuỗi cụ thể git filter-repo --replace-text <(echo "sk-proj-abc123==>REMOVED")Sau đó force push lên remote và notify collaborators pull lại từ đầu.
- Tạo key mới — thay thế key đã revoke, cập nhật trong secret manager.
- Post-mortem — ghi lại timeline, nguyên nhân, biện pháp phòng ngừa. Không cần blame, cần hiểu để không lặp lại.
GitHub Secret Scanning
GitHub có tính năng Secret Scanning tự động quét commit và gửi alert khi phát hiện pattern key của các provider lớn. Nếu nhận được email từ GitHub về secret, coi như đã bị public — revoke ngay không chờ.
Bật Push Protection trong repo settings để GitHub chặn push có chứa secret trước khi vào repo.
Common Pitfalls
| Lỗi | Hậu quả | Phòng tránh |
|---|---|---|
| Hard-code key trong code | Leak khi repo bị public hoặc bị clone | Dùng env variable, pre-commit hook |
| Print/log key ra stdout | Leak qua log aggregator (Datadog, CloudWatch) | Dùng SecretStr, không log secret |
| Pass key qua URL query string | Lưu trong proxy log, browser history | Dùng header Authorization hoặc request body |
Quên .env trong .gitignore |
Commit .env vào repo |
Thêm vào .gitignore ngay từ đầu |
| 1 key cho mọi env (dev/staging/prod) | Leak dev = leak prod | Tạo key riêng per env, set budget limit per key |
| Secret trong CI variable không mask | In ra log build, ai xem log đều thấy | Bật mask cho CI secret (GitHub Actions: dùng ${{ secrets.NAME }}) |
Container image có ENV với secret |
Push image lên registry = leak | Không đặt secret trong ENV Dockerfile |
| YAML manifest có secret plaintext | Commit YAML vào repo = leak | Dùng kubectl create secret hoặc External Secrets Operator |
Debug Mode Leak
Một số framework in ra environment variable khi debug mode bật. Đảm bảo DEBUG=false trong production:
# FastAPI với debug=True — in ra request body và env trong một số middleware
app = FastAPI(debug=False) # Production luôn False
# Django debug=True — có thể expose env trong error page
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
Tóm Tắt
- ✅ Hard-code API key vào code là lỗi không thể chấp nhận — LLM key có thể gây bill $1,000+ nếu bị lạm dụng
- ✅ Dev local: dùng
.env+python-dotenv, thêm.envvào.gitignorengay từ đầu - ✅ FastAPI: dùng
pydantic-settingsvớiBaseSettingsvàSecretStrđể load + validate + tránh leak qua log - ✅ Cài
gitleakshoặcdetect-secretsquapre-commitđể chặn commit có chứa secret - ✅ Production: dùng AWS Secrets Manager, GCP Secret Manager, hoặc HashiCorp Vault — không copy key vào server
- ✅ Container: inject env tại runtime, không bake secret vào image
- ✅ API key của riêng app: lưu hash (SHA-256), không plaintext; dùng
secrets.compare_digestđể verify - ✅ LLM key chỉ tồn tại ở backend — không bao giờ ship xuống browser
- ✅ Khi bị leak: revoke ngay → kiểm tra log → kiểm tra bill → clean git history → post-mortem
Bài Tiếp Theo
Tài liệu tham khảo
- Python docs — os.environ
- python-dotenv — PyPI
- Pydantic Settings — pydantic-settings docs
- gitleaks — GitHub
- detect-secrets — Yelp GitHub
- AWS Secrets Manager — User Guide
- GCP Secret Manager — Overview
- HashiCorp Vault — Documentation
- External Secrets Operator — Docs
- GitHub Secret Scanning — Docs
- git-filter-repo — GitHub
- GitGuardian — State of Secrets Sprawl 2023
