Behind a proxy (forwardAuth)
The application needs no code, no library and no knowledge that Cardinal exists, and one assumption that is not code.
For an application with no login of its own, and for one whose login you would rather not use. Traefik asks Cardinal about every request; Cardinal answers with a verdict and, when it allows, a set of identity headers.
Configuring the middleware
http:
middlewares:
cardinal-auth:
forwardAuth:
address: "http://cardinal:8080/api/auth/verify"
# false means Traefik sets X-Forwarded-* itself and discards what the
# client sent. This is the setting that makes trusted_proxies safe, and
# `true` is the credulous option despite sounding like the careful one.
trustForwardHeader: false
authResponseHeaders:
- X-Auth-Request-User
- X-Auth-Request-Preferred-Username
- X-Auth-Request-Name
- X-Auth-Request-Groups
- X-Auth-Request-Group-Ids
- X-Auth-Request-Auth-Method
- X-Auth-Request-Device-Bound
- X-Cardinal-Policy
# The request's cookies without Cardinal's own. Since 0.6.2; see
# "Cardinal's cookies" below before adding it.
- Cookie
Only the headers listed there are copied onto the original request. Anything
Cardinal sets and Traefik does not list is dropped silently: the single most
common cause of "it worked in curl and not through the proxy". Cardinal shipped
that bug itself: X-Auth-Request-Group-Ids was set on every response and
listed nowhere, so no application ever saw it.
Two things worth doing:
- Strip inbound
X-Auth-Request-*headers before the auth middleware runs. forwardAuth overwrites the ones it sets, but one it does not set would pass straight through, and an application reading a header Cardinal never emits is trusting the client. - Key on
X-Auth-Request-Group-Ids, not on group names. Names are mutable attributes; the ids are not.
The application must be unreachable except through the proxy. It trusts those headers unconditionally, and that is only safe if nothing else can set them.
Register the hostname so Cardinal knows which application it is:
cardinal application create grafana
cardinal app hostname add grafana grafana.internal
cardinal group create grafana-users
cardinal grant grafana-users alice
cardinal policy rule add web-access -group grafana-users -app grafana
A hostname nothing claims is refused before policy is even consulted. The default ships no blanket web-access rule, so an application is reachable only once a rule names the group that may reach it; the rule builder writes that rule.
Signing in, and being refused
A browser that is not signed in is sent to Cardinal, with the URL it asked
for in return_to. Once signed in, it goes back to exactly that URL, path and
query kept, after Cardinal has set the proxy cookie. The URL must be https,
on a registered application hostname or Cardinal's own host; anything else
lands on the console's home, so a Cardinal link cannot be made into a redirect
to somebody else's site. The scheme is what the proxy reports in
X-Forwarded-Proto: behind a load balancer that terminates TLS in front of
the proxy, the proxy must still report https.
A browser that is refused is sent to a page on Cardinal rather than shown the proxy's error:
- refused by policy, signed in: a page showing that one decision, what decided and what would change it, which only that person can read;
- an address no application claims, signed in or not: a page saying it is not registered, with nothing personal on it.
A program keeps the status: a request asking for JSON, or presenting a token,
gets the 403 with X-Cardinal-Policy, or the 401 when it is not signed in.
Cardinal's cookies
A browser is recognised by the proxy cookie, __Secure-cardinal_proxy,
which Cardinal sets on cookie_domain so that every protected host receives
it. Its token is accepted by forwardAuth and by nothing else: replayed against
the console, the API or the command line's endpoints, it is refused. The
console's own session cookie stays on Cardinal's host, and forwardAuth never
reads it.
forwardAuth also answers with the request's Cookie header minus Cardinal's
cookies. With Cookie in authResponseHeaders, Traefik puts that on the
request it forwards, so the application receives its own cookies and none of
Cardinal's. Without it, the application receives the proxy cookie: not a way
into the console, but a way into the other protected applications as that
person.
Add Cookie only once Cardinal is 0.6.2 or later, and remove it before
rolling back. Earlier versions send no such header, and Traefik then
forwards no cookies at all, the application's own included. That was measured
against Traefik v3.6, not assumed.
Up to 0.6.1, the console's session cookie itself was scoped to cookie_domain,
so every application under it received the session of whoever was signed in.
The 0.6.2 changelog says what to do about that: sign everyone out with
cardinal-server sessions end-all after upgrading.
What the application receives
| Header | Meaning |
|---|---|
X-Auth-Request-User |
The subject's UUID, stable forever |
X-Auth-Request-Preferred-Username |
Login, for display |
X-Auth-Request-Name |
Display name |
X-Auth-Request-Groups |
Group names, for display |
X-Auth-Request-Group-Ids |
Group identifiers, branch on these |
X-Auth-Request-Auth-Method |
passkey or access_token |
X-Auth-Request-Device-Bound |
Whether the credential is hardware-bound |
Which groups appear is a setting, and by default it is all of them.
The trust boundary, stated plainly
The application believes those headers because only the proxy can reach it.
That is an assumption about the network, not something Cardinal can enforce,
and it is the same assumption X-Forwarded-For rests on.
If the application is reachable directly (a container port published for debugging, a second ingress, a service mesh sidecar), anybody who can reach it is whoever they say they are. Cardinal cannot detect this and will not warn about it.
Registering one
cardinal application create grafana
cardinal app hostname add grafana grafana.internal
cardinal app hostname list
cardinal group create grafana-users
cardinal grant grafana-users alice
cardinal policy rule add web-access -group grafana-users -app grafana
One application per hostname, and a hostname nothing claims is refused before policy is consulted. Registering makes an application findable, not reachable: what makes it reachable is a rule naming the group that may reach it, because the default ships none (see groups and policy).