Application vs server
Most hardening advice mixes two different jobs. Separating them is the fastest way to work out why a fix “did nothing”.
The dividing line
Section titled “The dividing line”| The fix is… | Whose job |
|---|---|
| a directive in a daemon’s config file | the server |
| a line in the application you deploy | the application |
TLS ciphers are the server’s. A cookie’s Secure flag is the application’s.
X-Forwarded-For handling inside nginx or HAProxy is the server’s. trust proxy in
Express and SECURE_PROXY_SSL_HEADER in Django are the application’s.
This site covers the second column. That is not a claim the first matters less — it is that the two need different people, different access and often different deploys, and conflating them is how a control ends up set in neither place.
Where they overlap, and why it bites
Section titled “Where they overlap, and why it bites”Three controls genuinely span both halves, and each produces a bug that looks like the fix failed.
Scheme awareness behind a proxy
Section titled “Scheme awareness behind a proxy”TLS terminates at the proxy; the application receives plain HTTP. Anything the app
decides from “was this request secure” now decides wrongly — HTTPS redirects, Secure
cookies, CSRF origin checks.
The application must be told to trust a forwarded header, and the proxy must
actually set it and strip any client-supplied copy. Configure one side only and you
have either a broken app or a spoofable one. That is
Django’s SECURE_PROXY_SSL_HEADER and
Express’s trust proxy.
Security response headers
Section titled “Security response headers”Both layers can set them, and when both do you need to know which wins — the answer
differs by header and by proxy. Setting the same header in two places is how a site
ends up serving two conflicting Content Security Policies, or an X-Frame-Options
that contradicts a frame-ancestors directive.
Pick one layer to own them. The application is usually the better choice, because the policy then travels with the code that needs it.
Client IP
Section titled “Client IP”Rate limiting, allow-lists, audit logs and abuse heuristics all depend on it. Behind a proxy the application sees the proxy’s address unless told otherwise — which turns a rate limiter into one shared bucket for every visitor.
Told otherwise too permissively, the value becomes whatever the client claims. Both failure directions are real, and they are the subject of client IP spoofing via trust proxy.
The practical rule
Section titled “The practical rule”For each of those three, write down which layer owns it before changing either. The common failure is not that someone chose wrong — it is that both sides assumed the other had it.