Danh sách bài viết

Bài 53: API Key management — không hard-code, dùng env / secret manager

Quản lý API key và secret cho AI app: tại sao hard-code là rủi ro lớn nhất với LLM key, cách dùng env variable và .env file, Pydantic Settings 2.x cho FastAPI, pre-commit hook chống commit secret, secret manager cho production (AWS, GCP, Vault), inject secret vào container, tạo và quản lý API key của riêng app, và các lỗi hay gặp cần tránh.

27/05/2026
1 lượt xem
1

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, .env file, 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
2

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

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
4

.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ải KEY hay TOKEN.
  • Không gộp nhiều secret vào 1 biến — mỗi secret riêng, tiện rotate độc lập.
5

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.

6

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
7

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
8

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
9

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.

10

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"))):
    ...
11

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:

  1. Tạo key mới song song với key cũ.
  2. Deploy app với logic chấp nhận cả 2 key (trong thời gian chuyển tiếp).
  3. Cập nhật tất cả service dùng key mới.
  4. 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.

12

Khi Key Bị Leak — Incident Response

Quy Trình Xử Lý

  1. 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.
  2. 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).
  3. 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.
  4. Xóa khỏi Git history — dùng git filter-repo (thay thế cho git 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.

  5. Tạo key mới — thay thế key đã revoke, cập nhật trong secret manager.
  6. 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.

13

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"
14

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 .env vào .gitignore ngay từ đầu
  • ✅ FastAPI: dùng pydantic-settings với BaseSettingsSecretStr để load + validate + tránh leak qua log
  • ✅ Cài gitleaks hoặc detect-secrets qua pre-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