In this blog post How to Secure API Keys with Python for Apps and Infrastructure we will walk through practical ways to keep secrets safe from laptop to production.
API keys are bearer secrets. Anyone who has the string can act as your application. That makes key security a lifecycle problem: how you store, retrieve, use, log, rotate, and revoke keys. We will start with a high-level view, then move into Python patterns you can deploy today.
What is happening under the hood
Two ideas drive secret security:
- Control access to the plaintext key. Store encrypted at rest; decrypt only where needed.
- Minimize exposure. Keep keys out of code, VCS history, logs, and crash reports.
Technologies involved:
- Operating system environment variables and file permissions.
- Secret managers (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) backed by KMS/HSM for encryption and audit.
- Python libraries: os for env access, python-dotenv for local dev, keyring for OS vaults, cryptography for encryption, boto3/azure/gsdk clients for cloud secret retrieval, pydantic for typed config.
Non-negotiables for API key safety
- Never hardcode secrets in source.
- Prefer a managed secret store in production.
- Load secrets at runtime via environment or files with strict permissions.
- Rotate regularly and automate revocation.
- Prevent secrets from entering logs, error traces, and analytics.
Baseline pattern with environment variables
Environment variables are the simplest cross-platform channel for injecting secrets at runtime. They work well with containers, serverless, and CI/CD.
import os
stripe_key = os.environ["STRIPE_API_KEY"] # Will raise KeyError if missing
# Use with requests without printing it
import requests
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {stripe_key}"})
Pros: trivial, portable. Cons: discoverable by any process with access to the environment. Combine with least-privilege OS users and masking in logs.
Typed config with Pydantic
Model your config and fail fast when secrets are missing. This avoids scattered os.environ calls.
from pydantic import BaseSettings, SecretStr
from dotenv import load_dotenv
load_dotenv() # For local dev only; not needed in prod
class Settings(BaseSettings):
stripe_api_key: SecretStr
database_url: SecretStr
class Config:
env_file = ".env" # local override, ignored in prod
settings = Settings()
stripe_key = settings.stripe_api_key.get_secret_value()
Local development without leaks
- Use a .env file; never commit it.
- Add .env* to .gitignore.
- Use a pre-commit scanner to catch accidental commits.
# .gitignore
.env
.env.*
# .pre-commit-config.yaml (example)
repos:
- repo: https://github.com/zricethezav/gitleaks
rev: v8.18.3
hooks:
- id: gitleaks
For laptops, you can also use the OS keychain.
import keyring
# Set once (outside your code path)
keyring.set_password("my-app", "stripe", "sk_live_...")
# Retrieve in code
stripe_key = keyring.get_password("my-app", "stripe")
Production-grade secret management
In production, rely on a managed secret store. You get encryption-at-rest, IAM, versioning, rotation hooks, and audit trails.
Example AWS Secrets Manager
import os, json, base64
import boto3
from botocore.exceptions import ClientError
from functools import lru_cache
@lru_cache(maxsize=64)
def get_secret(name: str, region: str | None = None) -> dict:
region = region or os.getenv("AWS_REGION", "ap-southeast-2")
client = boto3.client("secretsmanager", region_name=region)
try:
resp = client.get_secret_value(SecretId=name)
except ClientError as e:
raise RuntimeError(f"Secret fetch failed for {name}") from e
payload = resp.get("SecretString") or base64.b64decode(resp["SecretBinary"]).decode()
return json.loads(payload)
secrets = get_secret("prod/app/stripe")
stripe_key = secrets["api_key"]
Grant your app role only the specific secret ARN and read-only access. Rotate using AWS tooling, and deploy with versions. Equivalent SDKs exist for Google and Azure.
GCP and Azure shapes
# GCP
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
name = client.secret_version_path("project", "stripe", "latest")
payload = client.access_secret_version(request={"name": name}).payload.data.decode()
# Azure
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
client = SecretClient(vault_url="https://<vault>.vault.azure.net", credential=DefaultAzureCredential())
stripe_key = client.get_secret("stripe-api-key").value
Containers, Docker, and Kubernetes
With Docker, use built-in secrets to mount files readable only by the container.
# docker-compose.yml snippet
services:
app:
secrets:
- stripe_api_key
secrets:
stripe_api_key:
file: ./secrets/stripe_api_key
# Python
with open("/run/secrets/stripe_api_key") as f:
stripe_key = f.read().strip()
In Kubernetes, mount Secrets as volumes or inject as env vars. Prefer volumes for large or structured secrets and to avoid env exposure.
# Mounted file path defined in your Pod spec
with open("/var/run/secrets/stripe/api_key") as f:
stripe_key = f.read().strip()
Prevent secrets from leaking into logs
Never log full tokens. Add a redacting formatter for defense in depth.
import logging, re
TOKEN_PATTERN = re.compile(r"(sk_live_[A-Za-z0-9]{16,}|Bearer\s+[A-Za-z0-9._-]+)")
class RedactingFormatter(logging.Formatter):
def format(self, record):
s = super().format(record)
return TOKEN_PATTERN.sub("[REDACTED]", s)
logger = logging.getLogger("app")
handler = logging.StreamHandler()
handler.setFormatter(RedactingFormatter("%(levelname)s %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
Key rotation without drama
Rotate on a schedule, not only after incidents. Support overlapping keys so you can phase out old ones.
import os, requests
PRIMARY = os.getenv("STRIPE_API_KEY_PRIMARY")
SECONDARY = os.getenv("STRIPE_API_KEY_SECONDARY")
def call_api(key: str) -> bool:
r = requests.get("https://api.example.com/ping", headers={"Authorization": f"Bearer {key}"})
return r.status_code < 500 and r.status_code != 401
for key in filter(None, [PRIMARY, SECONDARY]):
if call_api(key):
active_key = key
break
else:
raise RuntimeError("No valid key available")
During rotation, set SECONDARY to the new key, deploy, confirm traffic works, then swap PRIMARY and remove the old value.
Encrypting your own secret files
Prefer managed stores. If you must keep an encrypted blob in Git, use envelope encryption: keep the data encrypted with a data key, and protect that key with a KMS or OS keystore.
from cryptography.fernet import Fernet
import os
# FERNET_KEY must come from a secure store (not from the repo!)
fernet = Fernet(os.environ["FERNET_KEY"])
ciphertext = fernet.encrypt(b"sk_live_...")
plaintext = fernet.decrypt(ciphertext)
Do not store the encryption key next to the ciphertext. That defeats the purpose.
CI/CD hygiene
- Store secrets in your CI provider’s vault and inject as masked environment variables.
- Never echo secrets. Mask command output by default.
- Use separate secrets per environment and per service account.
- Run secret scanners on pull requests and block on findings.
Testing without real keys
Mock environment variables and secret clients.
import os
from unittest import mock
with mock.patch.dict(os.environ, {"STRIPE_API_KEY": "test_123"}):
# run unit tests that depend on the key
pass
# AWS example with Stubber
from botocore.stub import Stubber
client = boto3.client("secretsmanager", region_name="ap-southeast-2")
stub = Stubber(client)
stub.add_response("get_secret_value", {"SecretString": '{"api_key":"test_123"}'}, {"SecretId": "prod/app/stripe"})
stub.activate()
Common pitfalls to avoid
- Hardcoding or committing secrets. Even once in history is risky.
- Printing keys when debugging. Use redaction or structured logging filters.
- Using hashing to “store” API keys you need to call services. Hashing is one-way; you need the plaintext. Use encryption or a managed secret store.
- Sharing one key across multiple services or environments. Use per-service keys with least privilege.
- Long-lived keys with no rotation plan.
Wrap-up
Securing API keys with Python is about discipline and good defaults. Keep secrets out of code, fetch them from a proper store, limit who can read them, and design for rotation. With the patterns above, you can raise your security bar without slowing down delivery.
Discover more from CPI Consulting
Subscribe to get the latest posts sent to your email.