Our application logic is already secure (Parts 3–5). Now we close off the last threat that most often slips through: Security Misconfiguration — a mistake not in the code, but in how the application is configured and deployed.
Secrets Management — Never Trust a Default
Look back at our config.py:
class Settings(BaseSettings):
...
secret_key: str = "CHANGE-ME-INSECURE-DEFAULT-DO-NOT-USE-IN-PRODUCTION"
The default is DELIBERATELY made ugly and obvious. If someone forgets to set SECRET_KEY via an environment variable, and the app still runs on this default, the consequences are severe: ANYONE who knows our code (which might be open source, or leaked) can forge a FAKE JWT token that the server will accept as valid — because the signature is computed from a secret key everyone already knows.
We take this one step further in docker-compose.yml, with syntax that FORCES SECRET_KEY to be set:
api:
environment:
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set, don't use the default}
The ${VAR:?error message} syntax in Docker Compose means: if SECRET_KEY isn't present in the host environment, FAIL startup entirely, don't silently continue with an empty value. Prove it directly:
docker compose up -d
error while interpolating services.api.environment.SECRET_KEY:
required variable SECRET_KEY is missing a value:
SECRET_KEY must be set, don't use the default
The application refuses to start at all — instead of starting silently insecure. This is the fail secure principle: if there's a configuration mistake, the system should fail in a SAFE way (stop entirely, make it obvious something's wrong), not fail in a SILENTLY DANGEROUS way (keep running with a weak configuration).
export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
docker compose up -d
# now it runs normally
Another equally important rule: secret_key, a DATABASE_URL containing a password, and other credentials are NEVER written directly in code or committed to git — not even in a .env file (our .gitignore already excludes .env). If you've already followed the CI/CD for Beginners series, this is the exact same principle — secrets live in GitHub Secrets or a dedicated vault, not in the repository.
HTTPS/TLS — Protecting Data While It's In Transit
So far we've protected data AT REST (hashing) and WHILE BEING PROCESSED (validation, authorization). There's one more gap: data WHILE BEING SENT over the network. Without HTTPS, requests (including the password at login, and the JWT token) are sent as plain text that can be intercepted by anyone "in the middle" of the network path (say, on the same public WiFi).
In our app, TLS/HTTPS is typically NOT implemented in the Python code itself — that's the job of the layer in front of it: a reverse proxy (Nginx, like we used in the Scalability series), a cloud load balancer, or a hosting platform (Vercel, as in the CI/CD series) that handles TLS termination, then forwards traffic to the app over a trusted internal network.
What we CAN do at the application level: tell the browser to ALWAYS use HTTPS via the Strict-Transport-Security header (covered in the next section) — once a browser has received this header once, it automatically refuses to access that same domain over plain HTTP for a set period, even if the user explicitly types http://.
CORS — Controlling Who Can Call the API From a Browser
CORS (Cross-Origin Resource Sharing) is a browser mechanism that by default BLOCKS JavaScript on one domain from calling an API on another domain — unless that API explicitly allows it. Without CORS configured, modern browsers automatically reject API requests from a frontend on a different origin.
app.add_middleware(
CORSMiddleware,
allow_origins=[origin.strip() for origin in settings.cors_allowed_origins.split(",")],
allow_credentials=True,
allow_methods=["GET", "POST", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
The most common mistake: setting allow_origins=["*"] (allow every origin) to make things "easy, no need to think about it." This is especially dangerous combined with allow_credentials=True — that combination means ANY site on the internet can make a request to our API with the user's credentials (cookies, auth headers) attached. Always list SPECIFIC, trusted origins (cors_allowed_origins in .env), never a wildcard.
Security Headers — Extra Instructions for the Browser
Finally, a handful of HTTP headers that give the browser extra instructions on how to treat our response:
@app.middleware("http")
async def security_headers_middleware(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains"
response.headers["Referrer-Policy"] = "no-referrer"
return response
Each one:
X-Content-Type-Options: nosniff — prevents the browser from "guessing" a response's content type (MIME sniffing), which can sometimes be exploited to execute content as a script when it was only meant to be data.X-Frame-Options: DENY — prevents our page/response from being displayed inside an <iframe> on another site, basic protection against clickjacking (tricking a user into clicking something hidden in an iframe).Strict-Transport-Security — the "always use HTTPS for this domain" instruction, as covered in the previous section.Referrer-Policy: no-referrer — prevents the full URL (which might contain a token or sensitive ID in the query string) from being sent to another site via the Referer header when a user clicks an outbound link.Prove the headers are actually attached:
curl -s -D - -o /dev/null localhost:8000/health | grep -iE "X-Content-Type-Options|X-Frame-Options|Strict-Transport-Security|Referrer-Policy"
x-content-type-options: nosniff
x-frame-options: DENY
strict-transport-security: max-age=63072000; includeSubDomains
referrer-policy: no-referrer
These headers are free to apply — no added logic complexity, just a handful of lines of middleware — but they close off several classes of attack at once.
Summary
${VAR:?message} in Docker Compose).Strict-Transport-Security header.* wildcard combined with allow_credentials=True.X-Content-Type-Options, X-Frame-Options, etc.) are cheap to apply, closing off several classes of browser-side attacks at once.