机密管理与密钥轮换
从保险库加载机密,安全轮换密钥,并避免在日志或镜像中泄露凭据。
机密管理与密钥轮换 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Secrets Need Special Handling
A secret is any value that grants access: database passwords, API keys, signing keys, OAuth client secrets. In a FastAPI backend these leak far more often than people expect.
- Hardcoded in source and pushed to Git history forever
- Printed into logs during debugging
- Baked into Docker image layers
- Echoed back in error responses or
/debugendpoints
The discipline in this lesson: load secrets from a trusted source at runtime, never persist them where humans or images can read them, and rotate them on a schedule so a leak has a short blast radius.
Step 1 — Pull Config From the Environment
The baseline for any production FastAPI app is loading secrets from the environment, not from code. Pydantic's BaseSettings reads env vars (and optionally a local .env for dev) and validates them at startup.
If a required secret is missing the app fails fast on boot instead of crashing on the first request. Notice we type SecretStr so the value is masked if the object is ever printed.
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file='.env', extra='ignore')
database_url: SecretStr
jwt_signing_key: SecretStr
stripe_api_key: SecretStr
settings = Settings()
# Printing the model never reveals the raw value:
print(settings.jwt_signing_key) # secret='**********'
print(settings.jwt_signing_key.get_secret_value()[:0]) # access only when neededSecretStr Stops Accidental Logging
SecretStr is a small but powerful guard. Its __repr__ and __str__ return '**********', so the raw value never appears in logs, tracebacks, or a serialized settings dump. You must call .get_secret_value() to read the real string — an explicit, greppable action.
This standalone example shows the masking behavior without any framework.
from pydantic import SecretStr
token = SecretStr('super-secret-token-123')
# Safe: these never reveal the value
print(f'token = {token}') # token = **********
print(repr(token)) # SecretStr('**********')
# Explicit unwrap when you truly need the value
real = token.get_secret_value()
print('length of real secret:', len(real))Step 2 — Load From a Real Vault
Environment variables are fine, but a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) adds auditing, access control, and built-in rotation.
The pattern is the same everywhere: authenticate with a short-lived identity, fetch the secret by name at startup, and cache it in memory. Below is AWS Secrets Manager via boto3.
import json
import boto3
from functools import lru_cache
@lru_cache(maxsize=None)
def load_secret(secret_name: str) -> dict:
client = boto3.client('secretsmanager', region_name='eu-central-1')
resp = client.get_secret_value(SecretId=secret_name)
return json.loads(resp['SecretString'])
# At app startup:
# secrets = load_secret('prod/fastapi/app')
# db_url = secrets['database_url']
# Credentials come from the instance/task IAM role, NOT from env files.Never Hardcode the Vault Credentials Themselves
A common mistake is putting the vault's own access key in the code or .env — you have just moved the problem, not solved it. Use workload identity instead:
- AWS: IAM role attached to the ECS task / EC2 instance / Lambda
- GCP: service account bound to the workload (Workload Identity)
- Kubernetes: projected service-account token + IRSA / Workload Identity Federation
- Vault: AppRole or Kubernetes auth, exchanged for a short-lived token
The golden rule: the only thing your container needs is an identity, and the platform supplies that — no long-lived key ships with the app.
Step 3 — Inject Secrets Into FastAPI Cleanly
Inside FastAPI, expose settings through a cached dependency. The @lru_cache makes get_settings() a singleton, so the vault is hit once and the object is reused. Routes depend on settings rather than reaching for globals, which also makes them easy to override in tests.
This is framework code, so it is not runnable on a plain judge.
from functools import lru_cache
from fastapi import Depends, FastAPI
app = FastAPI()
@lru_cache
def get_settings() -> Settings:
return Settings() # loads/validates secrets once
@app.get('/health')
def health(settings: Settings = Depends(get_settings)):
# Use settings.database_url.get_secret_value() internally;
# never return the secret in the response body.
return {'status': 'ok'}Step 4 — Rotate Keys Without Downtime
Rotation means replacing a secret with a new value on a schedule (or after a suspected leak). The hard part is doing it without dropping requests. The trick is to support two valid keys at once during the overlap window:
- Sign new tokens with the current key
- Verify against current OR previous key
- After all old tokens expire, retire the previous key
This dual-key window applies to JWT signing keys, HMAC webhook secrets, and API keys alike.
Verifying JWTs Against Multiple Keys
Here is the verify-old-or-new pattern using a simple HMAC signature to stay framework-free and runnable. New tokens are signed with the current key; verification accepts either the current or the previous key during the overlap window. The same idea maps directly onto python-jose JWT keys with a kid header.
import hashlib
import hmac
CURRENT_KEY = b'key-v2-new'
PREVIOUS_KEY = b'key-v1-old'
def sign(payload: str, key: bytes) -> str:
return hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
def verify(payload: str, sig: str) -> bool:
for key in (CURRENT_KEY, PREVIOUS_KEY):
if hmac.compare_digest(sign(payload, key), sig):
return True
return False
old_token_sig = sign('user=42', PREVIOUS_KEY)
new_token_sig = sign('user=42', CURRENT_KEY)
print('old still valid:', verify('user=42', old_token_sig))
print('new valid:', verify('user=42', new_token_sig))
print('tampered:', verify('user=99', new_token_sig))Key IDs Make Rotation Auditable
Tag each key with a key id (kid) so a token announces which key signed it. Verification looks up the matching key instead of trying all of them, and you can revoke a single kid the moment it leaks. JWTs carry the kid in the header; this standalone example shows the lookup logic.
import hashlib
import hmac
KEYS = {
'k2': b'current-secret',
'k1': b'previous-secret',
}
ACTIVE_KID = 'k2'
def issue(payload: str) -> dict:
key = KEYS[ACTIVE_KID]
sig = hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
return {'kid': ACTIVE_KID, 'payload': payload, 'sig': sig}
def check(token: dict) -> bool:
key = KEYS.get(token['kid'])
if key is None:
return False # revoked / unknown kid
expected = hmac.new(key, token['payload'].encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, token['sig'])
t = issue('order=7')
print('issued with kid:', t['kid'])
print('valid:', check(t))
t['kid'] = 'k0' # pretend signed by a revoked key
print('after revoke:', check(t))Step 5 — Keep Secrets Out of Logs
Even with SecretStr, you can still leak via custom log lines, request dumps, or exception messages. Defend in depth with a logging filter that redacts known patterns (tokens, bearer headers, connection strings) before records are emitted.
This filter is standalone and runnable.
import logging
import re
SECRET_RE = re.compile(r'(Bearer\s+)[A-Za-z0-9._-]+|(password=)[^\s&]+')
class RedactFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
msg = record.getMessage()
record.msg = SECRET_RE.sub(r'\1\2[REDACTED]', msg)
record.args = ()
return True
logger = logging.getLogger('app')
logger.addHandler(logging.StreamHandler())
logger.addFilter(RedactFilter())
logger.setLevel(logging.INFO)
logger.info('calling api with Authorization: Bearer abc123tok')
logger.info('db dsn password=hunter2 host=db')Step 6 — Don't Bake Secrets Into Images
Docker images are layered and shippable; anything in a layer is recoverable with docker history even if a later layer deletes it. So secrets must never enter the build.
- Do not
COPY .envor pass secrets viaARG/ENVat build time - Inject at runtime via the orchestrator (env from a secret store, mounted file, or sidecar)
- For build-time needs (private package install) use BuildKit
--mount=type=secret, which never persists in a layer - Add
.envand key files to.dockerignoreand.gitignore
Verify with docker history --no-trunc <image> — no secret should appear in any layer.
Quick Check
You must rotate the JWT signing key of a live FastAPI API without invalidating tokens that users are still carrying. Which approach achieves zero-downtime rotation?
Recap — Secrets Management and Key Rotation
You now have a full defense chain for backend secrets:
- Load secrets from the environment or a vault at runtime; validate on startup with
BaseSettingsand wrap values inSecretStr - Authenticate to the vault with workload identity (IAM role / service account), never a hardcoded key
- Inject via a cached FastAPI dependency, and never echo secrets in responses
- Rotate with a dual-key overlap window and a
kidso old tokens stay valid and any key is independently revocable - Redact secrets in logs with a logging filter and
SecretStrmasking - Exclude secrets from Docker layers; inject at runtime and verify with
docker history
Minimize where secrets live, keep them short-lived, and make every access explicit and auditable.
常见问题解答
「机密管理与密钥轮换」课时是免费的吗?
是的 — 「机密管理与密钥轮换」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。
「机密管理与密钥轮换」这节课中我会学到什么?
从保险库加载机密,安全轮换密钥,并避免在日志或镜像中泄露凭据。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 FastAPI Backend Development Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「机密管理与密钥轮换」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?
能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。