skip to content

A web application sits behind a reverse proxy that terminates TLS and forwards to the app over plain HTTP. Every HTTPS request now ends in an endless redirect loop: the app believes the scheme is http, redirects to https, and the next request reaches it as http again. What did the proxy hide from the app, and what has to change on each hop?

level: seniorimportance: should knowfreq 44%

answer

  1. TLS ended one hop earlier
  2. the app's socket is genuinely plain http
  3. the edge must state the original scheme
  4. overwrite at the boundary, never append
  5. trust the claim only from your own proxy

basics

~20 s

TLS ended at the proxy, so the app's own connection is plain HTTP and its force-https rule fires forever. Fix both hops: the proxy must send X-Forwarded-Proto, and the app must honour that header only on connections coming from the proxy.

solid answer

~50 s

The proxy terminated the client's TLS and opened a *separate*, plain-HTTP connection to the app, so from the app's point of view the request genuinely arrived over `http`. Its "redirect http to https" rule therefore fires on every request, the browser retries over HTTPS, the proxy terminates again, and the loop never ends. Confirm it in two calls: request through the edge and look at `Location`, then request the backend directly and see the same redirect. The fix needs both hops. The proxy must state the original scheme — conventionally `X-Forwarded-Proto` — and it must **overwrite** rather than append whatever the client sent. The application must be configured to derive its notion of scheme, host and client address from those forwarded values instead of from the socket, and to do so only for connections arriving from the proxy's addresses. Trusting the header from anywhere lets any client that can reach the app directly claim it is already on HTTPS.

code

bash · 8 lines
bash
# 1. Reproduce through the edge
curl -sSI https://app.example.com/dashboard | grep -i '^location'

# 2. Ask the backend directly - does it redirect on its own?
curl -sSI http://10.0.0.12:8080/dashboard | grep -i '^location'

# 3. Assert the original scheme and see whether the loop stops
curl -sSI -H 'X-Forwarded-Proto: https' http://10.0.0.12:8080/dashboard | grep -i '^location'

go deeper

for a junior

Know that a proxy which terminates TLS talks to the app over a separate, often plain-HTTP connection, so the app cannot tell the user arrived over HTTPS unless it is told.

for a middle

Explain the loop step by step and name the mechanism that carries the original scheme across the hop, plus the matching values for host and client address. Say that the app must be configured to use them.

for a senior

Isolate the failure at each hop with direct requests rather than guessing, fix both sides, and state the trust boundary unprompted: the edge overwrites the forwarded values and the app honours them only from the proxy.

for a principal

Own it as policy: which hop is the trust boundary, that backends must not be reachable around it, how forwarded-header handling is made uniform across services, and how you keep a new service from shipping with blanket trust.

## What actually happened A reverse proxy does not hand your application the client's connection. It terminates that connection — TLS and all — and opens its own connection to the backend. If that second connection is plain HTTP, then plain HTTP is the truth of the request as far as the application's socket is concerned. There is no hidden channel that tells it otherwise. So the app's usual rule — *if the request is not secure, redirect to the https URL* — is behaving exactly as written. The browser obeys the redirect, connects over HTTPS, the proxy terminates TLS again, forwards plain HTTP again, and the app redirects again. Browsers cut this off after a fixed number of hops and show a "too many redirects" error, which is the symptom users report. ## Confirming it rather than guessing Two requests separate the layers. Ask the edge, then ask the backend directly, and compare the `Location` each returns: ```bash curl -sSI https://app.example.com/dashboard | grep -i '^location' curl -sSI http://10.0.0.12:8080/dashboard | grep -i '^location' ``` If the backend redirects to `https://...` when called over plain HTTP, you have reproduced the loop in isolation and know the app's rule is the actor. Then repeat the direct call while asserting the forwarded scheme: ```bash curl -sSI -H 'X-Forwarded-Proto: https' http://10.0.0.12:8080/dashboard | grep -i '^location' ``` If the redirect disappears, the app already understands the header and the proxy simply is not sending it. If the redirect persists, the app is not configured to honour forwarded headers at all. That one test tells you which hop to change, and it takes seconds. ## The fix on the proxy The edge is the only place that knows the original scheme, so it has to say so explicitly, conventionally with `X-Forwarded-Proto` (alongside `X-Forwarded-For` for the address and `X-Forwarded-Host` where the app builds absolute URLs). The critical detail is that the edge must **set** these values, not append to or pass through whatever arrived from the client. Everything the client sent for those fields is a claim by an untrusted party; at the trust boundary it must be replaced. ## The fix on the application The app must stop asking the socket and start asking the forwarded values — for the scheme, and usually for the host and client address too. Most web frameworks and servers have an explicit forwarded-headers mode for exactly this, and it is off or restricted by default *on purpose*. It must also be told **which peers are allowed to make that claim**: the proxy's addresses, or the internal network the proxy sits on. This is the half that candidates skip, and it is the half that matters. ## Why blanket trust is dangerous If the app honours `X-Forwarded-Proto: https` from any source, then any party who can open a connection to the backend directly — another workload in the same network, something that found the pod or instance port, an SSRF chain inside your own estate — can assert that its request already arrived securely. That defeats force-https redirects, any "secure transport only" gate, and any audit that records how a request arrived. The same reasoning applies with more force to a forwarded client address, where a spoofed value bypasses address allowlists and per-client rate limits and poisons abuse attribution. The rule that survives every product: **overwrite at the trust boundary, trust only from the hop you operate, and make the backend unreachable except through that hop.** ## Everything else that breaks the same way Once you see the loop as *the proxy hid a property of connection one*, the rest of the family is predictable: - Absolute URLs generated by the app — password-reset links, canonical tags, pagination links — come out as `http://` or carry the backend's internal port. - Cookies marked `Secure` are set on what the app thinks is a plain connection, and logic gated on "is this request secure" takes the wrong branch. - Pages load over HTTPS but pull sub-resources over `http://`, so the browser blocks them as mixed content. - Application logs and any per-client limiting show the proxy's address for every request. - Anything keyed on the protocol version or on a client certificate is likewise invisible unless deliberately forwarded. ## What a strong answer sounds like Name the two-connection model as the cause, not "a misconfiguration". Show the two-curl isolation. Give the fix on both hops. Then, unprompted, state the trust boundary — that the edge overwrites and the app trusts only its own proxy — because the interviewer's next question is otherwise "and what if a client sends that header itself?"

  • What else breaks for exactly the same reason once TLS ends at the proxy?
    Anything derived from the connection rather than forwarded explicitly: absolute URLs built with the wrong scheme or the backend's internal port, cookies marked Secure set on a connection the app thinks is plain, sub-resources referenced over http and blocked as mixed content, per-client limits and logs showing the proxy's address, and client-certificate identity vanishing entirely.
  • Why is "just always trust X-Forwarded-Proto" the wrong fix?
    Because it is a claim, not a fact. Anyone who can reach the backend directly — a neighbouring workload, an exposed instance port, an SSRF chain — can assert that their request already arrived over HTTPS, defeating force-https behaviour, secure-transport gates and audit records. Honour it only from your proxy's addresses, and have the edge overwrite whatever the client sent.
  • The team's proposed fix is to switch off the application's redirect to HTTPS. What is wrong with that?
    It removes the symptom and the protection together. Requests that genuinely arrive over plain HTTP at the edge would then be served rather than upgraded, and any later direct exposure of the backend loses its last guard. Keep the redirect and make the app able to see the real scheme, which is the actual defect.

saying these in an interview costs you the question

  • Just turn off the application's redirect to HTTPS
  • The app can read the real scheme from its own socket anyway
  • Trust X-Forwarded-Proto from every source, it is standard
  • This is a browser or DNS caching problem
  • Appending to whatever header the client sent is fine

context