Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Make an application work behind an entry point

By updated

Improve this page

An application placed behind an entry point no longer sees the real scheme, host name or identity of the client: it has to learn them from the proxy, or it guesses them wrong. The entry point is the service that receives requests in place of the application, most often a reverse proxy. This lesson is for anyone publishing an application they did not write, or one they wrote without thinking about the proxy. It covers the common failures: a login that loops, a cookie the browser does not send back, a session lost from one copy of the application to the next, an origin policy that is too broad, and an identity header copied from the client.

What you will learn

  • Tell the application the scheme, the host name and the path the client sees.
  • Choose the attributes of a session cookie and recognize a cookie the browser rejects.
  • Place session state without hiding it behind session affinity.
  • Explain what CORS allows, and what it does not forbid.
  • Tell authentication, authorization, CORS and CSRF protection apart.
  • Decide identity on the server side and refuse the identity a client injects.

Prerequisites

This lesson assumes you can read an HTTP exchange and have already published a service behind a proxy. It does not re-explain TLS termination, the X-Forwarded-* headers, or the trust granted to a header:

Why does a correct application break behind a proxy?

Because the application trusts what it receives, and the proxy changes three things: the scheme, the host name and the origin of the connection. On its own, the application receives HTTPS directly, with the name the client typed. Behind a proxy that terminates TLS, it receives plain HTTP, coming from an internal address, sometimes with a replaced Host. Everything it infers from the request ("am I on HTTPS?", "what is my public name?") becomes wrong without anything crashing.

Four questions come out of this, and the lesson takes them in this order. Which URL does the application believe in and generate? What happens to the session cookie? Where does the session state live when several copies of the application answer? Who decides to allow an origin and to know the identity?

What the application infersWhat is true behind the proxyConsequence
The client speaks HTTPThe client speaks HTTPS, the proxy speaks HTTP to the applicationRedirect loop to HTTPS
My name is the one in the requestThe public name is the one the client typedLinks and redirections to an internal name
I am at the root of the siteThe proxy publishes the application under a prefixLinks that leave the prefix
The caller is whoever reaches meThe caller is the proxyIdentity and address must be read from headers

Which URL should the application believe and generate?

The application must know the public scheme, name and prefix, through the proxy's headers or an explicit setting, and trust those headers only from the proxy. The page Publishing a web service already set the trust rule for the X-Forwarded-* headers (X-Forwarded-Proto for the scheme, X-Forwarded-Host for the name), along with the list of trusted proxies. It is not repeated here. What it lacks is the consequence on the application side: the URL the application generates.

An application produces URLs in several places: the Location header of a redirection (the address to which the response sends the browser), the links of a page, the callback URLs of a login flow, the links in an email. For each one it chooses a scheme, a name and a path. If it believes it is on HTTP, it writes http://. If it redirects to HTTPS when it thinks it is on HTTP, and the proxy still talks HTTP to it, the loop is certain: the browser receives a redirection, follows it, and lands on the same response again until it gives up. The details of redirection codes are in Reading an HTTP exchange.

The base path is the prefix under which the proxy publishes the application, for example /compte/ for an application that, on its own, lives at the root. If the proxy strips the prefix before forwarding, the application never sees it and generates links to /login instead of /compte/login. The browser then follows a link that leaves the prefix and lands on another route, or on a 404.

Adding headers at the proxy therefore does not fix every generated URL. A header only helps if the application reads it. Many frameworks require a setting (trusted proxies, public URL, prefix) before they use it, and some URLs go through no request at all: a link in an email, a callback URL registered with an identity provider, a value hard-coded in the configuration. The exact name of these settings depends on the framework: read its documentation, which this lesson cannot do for you.

To see what the application generates, look at the redirection without following it:

Terminal window
curl -ski https://app.exemple.fr/compte | grep -i '^location'

The options -s (silent), -k (accept a self-signed certificate, reserve this for a trial) and -i (print the headers) reveal the raw Location. If it starts with http:// or with an internal name while you are on HTTPS on the public name, the application has learned its URL wrongly, and it is the application you must configure.

A cookie is a small value the server asks the browser to remember and send back with every request to it; a session cookie carries the identifier of the user's login. The server sets it with the Set-Cookie response header, and the browser sends it back in the Cookie request header. A session is the state the application keeps for a logged-in user (who they are, what they did) between two requests: since HTTP has no memory, the cookie is the thread that ties together the requests of one person.

Five attributes control what the browser does with it. The definitions come from the Set-Cookie page on MDN.

AttributeWhat it decidesPitfall
SecureThe cookie is only sent over https: (except on localhost)Set by a response received over http:, it is rejected: a possible cause of "login succeeded, then anonymous"
HttpOnlyThe page's JavaScript cannot read it, for example through document.cookieThe cookie is still sent with requests made by fetch()
SameSiteWhether the cookie is sent when the request comes from another site: Strict, Lax or NoneNone requires Secure
DomainWhich host names the cookie is sent back toA value that is neither the sender's name nor a parent of it is ignored
PathWhich paths receive the cookieIt is not a security measure

Two attributes need a few more words, Domain then SameSite. Without Domain, the cookie is limited to the exact host that set it (MDN calls it a host-only cookie): this is stricter than stating the name, because subdomains do not receive it. With Domain=exemple.fr, all subdomains receive it.

SameSite decides on a cross-site send (a site is a registrable domain: app.exemple.fr and api.exemple.fr belong to the same site exemple.fr). With Strict, the cookie is only sent for requests from the same site; with Lax, it is also sent for a top-level navigation using a method that changes nothing (GET), such as a click on a link; with None, it is always sent, provided it is Secure. Chromium browsers (Chrome, Edge, Opera) apply Lax when the attribute is absent, with one exception: the cookie is also sent with a cross-site navigation POST during the two minutes after it is set. According to MDN's compatibility data, Firefox and Safari do not apply this default. So write the attribute rather than count on it.

Here is a reasonable session cookie for an application served on a single name:

Set-Cookie: session=a1b2c3; Path=/; Secure; HttpOnly; SameSite=Lax

It has no Domain, so it stays bound to the host that set it. Unless the proxy is explicitly configured to do so, it does not modify these attributes: it is the application that issues the cookie.

Because the browser silently ignores a cookie whose Domain is neither the name of the responder nor one of its parents. MDN puts it this way: a response from api.exemple.fr can set Domain=api.exemple.fr or Domain=exemple.fr, but not Domain=autre.exemple.fr nor Domain=fr (a public suffix), and cookies that break these rules are ignored.

The symptom is confusing: the login succeeds (the server answers 302 with its Set-Cookie), then the next page treats you as anonymous. Nothing in the response signals the error, and curl does not show it: without a cookie file, it does not evaluate the cookie and simply prints the header. It is the browser that rejects it, and its developer tools are where to look. A login that "works" in curl and fails in the browser should therefore make you inspect the attributes of the Set-Cookie, starting with Domain. The safest fix is usually to not set Domain: the cookie goes back to the host that issued it.

Where does the session live when several copies of the application answer?

Session state must live somewhere other than in the memory of one instance, otherwise the user is logged out as soon as another instance answers. Behind a proxy that spreads the load, two requests from the same person can reach two instances. If the session is kept in the memory of the first, the second does not know it: the user is logged out "at random".

There are three places, and the choice is an architecture choice, not a proxy choice:

Where the state livesWhat it takesTrade-off
Shared storage (a common database or cache)All instances read the same storeOne more component to keep available
In the cookie itself, signed by the serverA signing key shared by the instancesThe cookie grows; the key becomes a secret to protect
Instance memoryThe proxy must always send the client back to the same instanceThis is session affinity, see below

Session affinity (a sticky session) glues a client to an instance, often through a cookie that the proxy sets itself. It works, and the lesson Bound time, queues and long-lived flows with HAProxy shows how far it holds. But it hides misplaced state instead of fixing it: when the instance restarts or fails, the sessions it carried disappear, and load distribution becomes uneven. Choose affinity knowingly, never to mask a session that was forgotten in memory.

What does CORS do, and what does it not do?

CORS (Cross-Origin Resource Sharing) is a browser mechanism by which a server states which other origins may send it certain requests and read its responses; it prevents no client from reaching the server. An origin is the triplet scheme, host name and port: https://app.exemple.fr and http://app.exemple.fr:8080 are two different origins. By default, a browser forbids a page's JavaScript from reading the response of another origin (the same-origin policy). CORS is the controlled exception: the API server answers with the Access-Control-Allow-Origin header to say which origin is admitted.

The preflight is the preliminary request the browser sends, with the OPTIONS method, before certain requests. It skips it for a simple request: a GET, HEAD or POST method, a limited set of headers, and for the body a type of application/x-www-form-urlencoded, multipart/form-data or text/plain. A POST with an application/json body is not simple: the browser first asks the API with OPTIONS, announcing the planned method and headers, then sends the real request only if the response allows it.

Read the diagram from left to right and top to bottom. The first four exchanges show an allowed origin A: preflight, POST, readable response. The last two show an origin C for which the response does not carry Access-Control-Allow-Origin: the API did answer, but it is the browser that gives up on what follows. It does not send the POST, and it hands nothing to page C.

A page from origin A sends an OPTIONS preflight to the API at origin B. The API answers with Access-Control-Allow-Origin: A, the browser sends the real POST and the page can read the response. From origin C, the same preflight receives a response without that header: the browser does not send the POST and the page reads nothing.APIorigin BBrowserpage from origin AA is in the response:the browser goes onthe script ofpage A canread theresponsethe API answered, the browser refuses to go on:no POST, page C reads nothing1 · OPTIONS (preflight)Origin: A, planned method and headers2 · 204 + Access-Control-Allow-Origin: A3 · actual POST (Origin: A)4 · response + Access-Control-Allow-Origin: A5 · same preflight from origin C6 · response withoutAccess-Control-Allow-Origin
The browser decides: the API answers in both cases, and it is the browser that, based on that answer, opens or closes what follows for the page. Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Two points avoid misunderstandings. First, a server that answers Access-Control-Allow-Origin: * allows any origin for requests without credentials. With credentials (the cookie), the page must ask for them (the credentials: "include" option of fetch()), the response must carry Access-Control-Allow-Credentials: true, and MDN specifies that the wildcard is forbidden: the server must name one explicit origin, otherwise the browser rejects the response for the page and reports a CORS error. Second, when the server returns a specific origin, which may vary with the request, rather than *, MDN states that the response should carry Vary: Origin, so that a cache does not serve to one origin the response meant for another. The syntax of the settings on the entry point side is in Protecting the Traefik entry point.

Does CORS stop a non-browser client from reaching the API?

No: CORS is not an access control, it only instructs a browser. A client that is not a browser, such as curl, a script or a server application, applies no same-origin policy. It sends its request, reads the response, and ignores Access-Control-Allow-Origin. The Origin header that the browser adds is, moreover, written by the client: another client puts in whatever it wants.

The difference can be measured with a command. It plays the role of a hostile script, announces an invented origin, and reaches the API whatever the origin policy:

Terminal window
curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Origin: https://site-hostile.test' \
-H 'Content-Type: application/json' -d '{}' \
https://app.exemple.fr/api/question

The command prints the code the API chose to return (200, or 401 if it requires authentication): the request arrived, and no CORS error can appear, since only a browser produces one. The same call, made by the JavaScript of a page at https://site-hostile.test, is stopped by the browser if the API does not allow that origin: the body being JSON, the preflight fails and the POST is not sent. Even a browser sometimes sends the request before knowing: MDN notes that a simple request goes out without a preflight, because an HTML form can already target any origin. CORS therefore decides what a third-party page can send and read from a browser, it does not protect the API. What protects the API is authentication (who is calling?) and authorization (are they allowed?), as set out by the entry point policy.

What does SameSite protect, and what is CSRF?

CSRF (Cross-Site Request Forgery) is an attack in which a third-party site makes a victim's browser send a request that carries their session cookie; SameSite reduces the risk by holding the cookie back. If you are logged in to a bank and another page triggers a transfer to it, the browser attaches your cookie, and the bank believes the request is yours. With SameSite=Lax or Strict, the cookie is not sent for a form request coming from another site with a method that modifies data, such as a POST.

Do not confuse these four notions, which answer different questions:

NotionQuestion it answersWho decides
AuthenticationWho is this caller?The server
AuthorizationIs it allowed to do this?The server
CORSWhich origin may, from a browser, send this request and read the response?The browser, on the server's indication
CSRF protectionDoes a request carrying my cookie really come from me?The server, helped by SameSite

Which identity header can the server trust?

An identity header is only worth something if the entry point decides it itself, from proof it has verified, and never by copying it from the client. A common architecture has the proxy recognize the user, then pass their name to the application in a header (call it X-Utilisateur). It is convenient, and dangerous as soon as the proxy copies an X-Utilisateur already present in the request: a header is text, and any client can write it.

The remedy has two steps. The entry point removes the identity header received from the client, whatever its value. Then it sets a new one only if it has validated a proof, for example a session cookie that only it knows how to verify, because it is signed with a server secret or because it points to an entry in a session store. Without a valid proof, the application receives no header, and treats the caller as anonymous.

This rule is the same as in two other lessons of the course: the entry point policy sets that an identity header is only believed when set by a trusted proxy, and client and backend identities tell an identity from an authorization. The two defenses complement each other: the policy says from which addresses the application believes the header, and the remedy above guarantees that the trusted proxy never relays one written by the client. Keep the distinction: the entry point establishes who is calling; what that caller is allowed to do remains an authorization decision, made afterwards.

To check it, send a request without a session, with an identity header you wrote yourself:

Terminal window
curl -sk -H 'X-Utilisateur: etudiant' https://app.exemple.fr/api/identite

The response must present the caller as anonymous. If it presents them as etudiant, the entry point believes a header the client wrote.

Which errors will you see, and what do they tell you?

Each failure in this lesson has a precise symptom, which points to the faulty attribute or setting.

SymptomProbable causeWhere to look
The browser gives up after too many redirectionsThe application thinks it is on HTTP and redirects to HTTPSRaw Location with curl -i, the application's scheme setting
Login succeeded, then an "anonymous" pageCookie rejected: invalid Domain, or a Secure cookie on an http: connectionBrowser developer tools, Set-Cookie header
Links leading to a 404Prefix stripped by the proxy and unknown to the applicationURLs generated in the page, base path setting
CORS error in the console, while curl answersThe origin is not allowed, or * is used with credentialsPreflight response, Access-Control-Allow-Origin header
Random logouts with several instancesSession kept in the memory of one instanceWhere the session is stored

How do you prove the repair in a real browser?

Two of these failures can only be seen in a browser: the rejection of a cookie and the CORS refusal are decisions of the browser, not of the server. A test made with curl sees the Set-Cookie and a 200 code, and concludes everything is fine, while the browser has rejected the cookie and kept the page from calling the API. The lab for this lesson therefore drives a real browser under automation (Chromium, driven by a supplied script), which plays the journey in place of a human. This changes the nature of the proof: you observe what the user would experience, not what a header says.

Practice

The lab brings together four of the lesson's failures on a small application behind a TLS entry point: a login that loops, a session cookie restricted to a domain no browser will accept, an origin policy that says yes to everything, and an identity header that the entry point repeats from the client. You fix a single policy file and observe each effect with the supplied browser. The validation measures, in that browser, that the login succeeds over HTTPS without a loop and that the account page shows the session; that the partner origin reads the API's response while the intruder origin cannot; and that a forged identity header, without a session, is not recognized as an identity by the API. The Secure and SameSite attributes are taught above but are not measured by this lab.

Key points

  • The application learns its public URL from the proxy or from a setting: a header does not fix the URLs it generates outside a request, nor those of a prefix it does not know.
  • A redirect loop comes from an application that thinks it is on HTTP while the client is on HTTPS.
  • A cookie whose Domain is invalid is silently ignored by the browser: you see it in its developer tools.
  • Secure, HttpOnly and SameSite are set explicitly, and SameSite reduces the risk of CSRF; without Domain, the cookie stays bound to the issuing host.
  • Session state goes in shared storage or in a signed cookie; affinity only hides misplaced state.
  • CORS is not an access control: it governs what a page from another origin can send and read, and curl reaches the API without regard for it.
  • Identity is decided on the server side: the entry point removes the header received from the client and sets one only after validating a proof.

Knowledge check

These eight questions cover the skills of the lesson: reading a generated URL, choosing a cookie attribute, placing a session, interpreting CORS and refusing a forged identity.

Knowledge check

Check what you have learned with this interactive quiz

8 questions
8 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn