FastAPI Backend Development Bootcamp · 강의

비밀 관리와 키 순환

볼트에서 비밀을 불러오고 키를 안전하게 순환하며 로그나 이미지에 자격 증명이 노출되지 않도록 합니다.

레슨 3/413개 단계

비밀 관리와 키 순환은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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 /debug endpoints

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 needed

SecretStr 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 .env or pass secrets via ARG/ENV at 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 .env and key files to .dockerignore and .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 BaseSettings and wrap values in SecretStr
  • 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 kid so old tokens stay valid and any key is independently revocable
  • Redact secrets in logs with a logging filter and SecretStr masking
  • 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을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
21
레슨
84

자주 묻는 질문

“비밀 관리와 키 순환” 강의는 무료인가요?

네 — “비밀 관리와 키 순환” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“비밀 관리와 키 순환”에서 뭘 배우나요?

볼트에서 비밀을 불러오고 키를 안전하게 순환하며 로그나 이미지에 자격 증명이 노출되지 않도록 합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“비밀 관리와 키 순환” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. OWASP API 보안 상위 10개 위협 완화
  2. 요청률 제한과 봇 악용 방지
  3. 비밀 관리와 키 순환
  4. CORS, CSP 및 보안 헤더 정책
← FastAPI Backend Development Bootcamp(으)로 돌아가기