Keep secrets out of code and logs
API keys, passwords and signing secrets give whoever holds them your program's power. They come from the environment, never from source code; they never appear in a log or an error message; and they are compared in a way that leaks nothing.
- Read a required secret from
os.environand stop at startup when it is missing - Mask secrets in
repr, logs and error messages; redact them from data before it is written anywhere - Sign and verify a message with
hmac, comparing withcompare_digest, and generate tokens withsecrets
A secret is a value that grants access: the payment provider's API key, the database password, the key that signs webhooks. Three rules keep them safe.
Not in the source code. Code is copied, committed, shared in screenshots and pasted into chats; a key in it is a key published. The deployment platform puts secrets in environment variables (see CLI & Automation) and the program reads them at startup. If a required one is missing, the program stops right away with a clear message, rather than running half-configured.
Not in output. Logs are kept for months and read by many people. A secret printed once in a debug line is leaked. Objects that hold secrets show a mask in their repr.
Compared carefully. To check a signature or a token, use hmac.compare_digest. Plain == stops at the first different character, so how long it takes reveals how much of a guess was right; compare_digest takes the same time either way.
Run each example to see what it prints. Running examples doesn't count toward progress.
import os def require_env(name): value = os.environ.get(name, "") if not value: raise RuntimeError(f"{name} is not set") return value def mask(secret): return "****" + secret[-4:] # On a server the platform sets this; it is set here only because this page has no deployment environment. os.environ["PAYMENT_API_KEY"] = "sk_live_51HxQe2Kd9f2a" key = require_env("PAYMENT_API_KEY") print("payments configured with key", mask(key)) try: require_env("SHIPPING_API_KEY") except RuntimeError as err: print("startup stopped:", err)
Reading with os.environ[name] also fails for a missing variable, but with a bare KeyError and no explanation, and only when that line runs:
import os def ship(order_id): key = os.environ["SHIPPING_API_KEY"] return f"label for {order_id}" print(ship("O100282"))
Secrets also *verify* things. When the payment provider calls your webhook, how do you know the call is really from them? They sign the body with a shared secret using HMAC (a hash keyed by the secret) and send the signature in a header. You compute the same HMAC over the body you received and compare. Change one byte of the body and the signature no longer matches. For tokens you generate yourself (password-reset links, API keys for your users), use the secrets module, not random, which is predictable.
import hashlib import hmac import secrets class Settings: def __init__(self, shop, webhook_secret): self.shop = shop self.webhook_secret = webhook_secret def __repr__(self): return f"Settings(shop={self.shop!r}, webhook_secret='****')" def sign(body, secret): return hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() def verify(body, signature, secret): return hmac.compare_digest(sign(body, secret), signature) settings = Settings("runtime-market", "whsec_7f3a9c2e") print(settings) body = b'{"payment_id": "PAY0000313", "status": "paid"}' signature = sign(body, settings.webhook_secret) print(verify(body, signature, settings.webhook_secret)) print(verify(b'{"payment_id": "PAY0000313", "status": "refunded"}', signature, settings.webhook_secret)) token = secrets.token_hex(16) print(len(token), token == secrets.token_hex(16))
When to use it: for every credential and every signature check. Mistakes to avoid: default values for secrets in code (os.environ.get("KEY", "sk_test_...")); printing a settings dictionary while debugging; putting a secret in an exception message; == to compare signatures; random for tokens; and committing a .env file.
- Secrets come from the environment; a missing one stops the program at startup with the variable's name, never a value.
- Mask secrets in
repr, logs and error messages; redact them from data before it is logged. - Verify signatures with HMAC and
hmac.compare_digest; generate tokens withsecrets. - Next: data that reaches a database query can attack it. SQL injection, and why placeholders stop it.