New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
reverseproxy: Support performing pre-check requests #4739
Conversation
Hey I'm glad we managed to get this far. I didn't get time to test it for you tonight, however I will over the weekend. I must say it's been a pleasure working with you thus far. I can't wait to add this to Authelia's list of supported proxies. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM overall. I like the possibilities here. Interesting implicit "pass_thru" mode, like you mentioned in Slack.
a98f758
to
f834992
Compare
Alright I did a bit more thinking and I pivoted a bit on how this will be configured. Instead of two new fields So the proposed config for ForwardAuth looks like this (long way with # the entrypoint
:8881 {
log
route {
# pre-check request
reverse_proxy :8882 {
# rewrite to the auth endpoint, prevent body from being sent
method GET
rewrite "/api/verify?rd={scheme}://{hostport}{uri}"
# send additional information about the original request
header_up X-Forwarded-Method {method}
header_up X-Forwarded-Uri {uri}
# handle the response; copy a response header onto the
# incoming request to let is pass through to the app
@good status 200
handle_response @good {
request_header Remote-User {http.reverse_proxy.header.Remote-User}
# Add more headers to copy here
}
# not authenticated; triggers a redirect to the auth gateway
@needs-auth status 4xx
handle_response @needs-auth {
redir * {http.reverse_proxy.header.Location}
}
# oops, we got an error; terminate
@bad status 5xx
handle_response @bad {
error {http.reverse_proxy.status_text} {http.reverse_proxy.status_code}
}
}
# the "actual" handling, e.g. your app
reverse_proxy :8883
}
}
# pre-check, just responding with a 200 response
:8882 {
log
header Remote-User bob
respond "Bob is logged in!"
}
# your app
:8883 {
log
respond "User: {header.Remote-User} | Body: {http.request.body}"
} So the new thing is this:
This applies a rewrite to the cloned request (not the original request), changing both the method and the URI (path + query). Also, if the method is set to |
f834992
to
58cfffd
Compare
While testing, we realized there's a usecase where using the |
This is now confirmed as working with Authelia. We've not done thorough integration tests yet, but we will probably do this in the coming days. Here is my Caddyfile I used for testing: Caddyfile
A much simpler one is provided below that more closely represents other implementations which makes migration so much easier! |
Just a small update to above, the following Caddyfile I believe more closely represents other implementations of this flow, and is much simpler: Caddyfile
|
I've implemented the auth.example.com {
reverse_proxy authelia:9091
}
app1.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
respond "Application: app1. User: {header.Remote-User}. Groups: {header.Remote-Groups}. Body: {http.request.body}."
} This results in essentially the same config as @james-d-elliott posted in the previous comment. Aside from these two subdirectives, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great. v2.5.1 will be the most exciting patch release we've ever done 馃構
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Well done! This is a neat feature and I'm impressed with how quickly and easily it came together. Thanks for your help, @james-d-elliott, and for your quality work as usual, @francislavoie.
I really like this approach, but am wondering if there's any possible bottleneck here when there's a large amount of requests - every 1 (authenticated) request would turn into 2 requests, wouldn't it? Even if it is routed internally on localhost, it probably has some overhead. Has there been any testing into how performant this is? |
Hey @codecat! 馃憢 This is the same approach as Traefik and Nginx have used; ideally the auth gateway should be on the same machine or close to, so it should only have like 1-2ms latency ideally, especially if it just checks a But @james-d-elliott has more experience with this. What kind of round-trip time do you typically see with Authelia? |
So I actually have not benchmarked the difference, however that's because I have never personally felt like there was a drop in my personal experience. I had in mind doing some benchmarking in general because I believe we're one of the most efficient solutions available at present (15-20mb RAM, 0.00%-0.01% CPU consistently). I recall checking our policy engine and with 100 policies getting the last policy took about 4ms if I recall correctly. I'll add this idea to my list of things to benchmark. Authelia is actually unofficially used by some fairly large companies too which I can't name the names of, some of which have in the 100's of thousands of employees; I don't really have any data on how many active users they have or how they've deployed it, but I plan to ask if they'd be willing to do anonymous write up to put on our site. |
I'd love to see any data on that! |
Thank you for this great feature! But one question. When I want to configure the same in Caddy like this, it doesn't work: My configuration also catch Authelia use the FQDN:
Snippet of my Caddyfile:
|
It shoud just be |
Makes no difference. header_up was just for testing and was my idea to force it. I have tried a lot yesterday with whoami image. Problem is, that the Remote-* headers are empty or not there at all when I request something else than |
The only way that the headers can get filled is if All that said, I suggest we move this discussion to the forums; this isn't the right place for support at this point. https://caddy.community |
Yes, of course. |
Edit: Sorry just saw the comment about moving the discussion. As per this comment whoami is showing the expected headers, i.e. it's 100% working: authelia/authelia#3319 (comment) |
@francislavoie i can't find |
This is super exciting - absolute well done to @francislavoie and @james-d-elliott . This is not ideal (as KeyCloak) is the tool we use for our authentication, and I know Authelia does not currently support OIDC providers. Push comes to shove, I'll put Authelia in place and use this new support to secure the internal apps. |
It has not been specifically tested with anything other than Authelia. But the intention during implementation was not to reinvent the wheel as creating something for the sake of one product makes no sense since there are multiple similar implementations of what traefik call forward auth we made it operate theoretically nearly identical to that. So in short, yes, it should work. If you find an issue after making an earnest attempt I'm sure the caddy maintainers would be happy for you to ask on the forums (or potentially in an issue here if you find a legitimate bug).
It acts as a provider, but yes, it does not support acting as a relying party as of yet. |
Superb - making an earnest effort to make it work with oauth2 and Authelia as a fallback. |
@mikesutton my understanding is that keycloak doesn't support the forward auth flow, but that there is a workaround by running another piece of software like https://github.com/mesosphere/traefik-forward-auth which despite the name should work fine with Caddy as well (because the flow is the same). Also, it has been tested by some Tailscale users and it works with their recent "nginx auth" https://tailscale.com/blog/tailscale-auth-nginx/ see #4763 |
Would this work for usage with goauthentik? |
@NakaTheShifu yes, I think it should! See authentik's forward auth docs: https://goauthentik.io/docs/providers/proxy/forward_auth. If you have success with it, be sure to propose that Caddy is added to their docs! |
Hi! I have a question regarding the behavior of subsequent requests after successful authentication (access is granted). Do these subsequent requests still need to pass through the authentication node, or is authentication bypassed for authenticated users, such as using a sessionToken? |
Authentication is checked independently for every request. There's no state kept in Caddy between requests for this. So the auth upstream should write a cookie to avoid repeating auth on subsequent requests. |
So, this is super cool. We were just missing two small pieces plus a bug fix to make this pattern possible.
Recently, we found out that there's demand for integrating Caddy with Authelia authelia/authelia#1241, so that Authelia can be used for acting as an auth gateway for apps served by Caddy. The way it's typically done with other proxies is with a built-in feature called ForwardAuth in Traefik and auth_request in Nginx. TL;DR, an HTTP request is made to Authelia, and Authelia either responds with a
200
if the request authorized 馃憤 or with a401
or redirect if auth is required 馃憥So that got me thinking,
reverse_proxy
can make requests (obviously) and we've recently implemented a quite flexiblehandle_response
feature that makes is quite simple to interact with the proxy response and even ignore the response body if we need. So what if we just usereverse_proxy
to perform ForwardAuth-like functionality? That means we don't need a plugin, and it would work basically out-of-the-box with Caddy.The key bits that were missing though, is that we need a way to tell the proxy "don't use the request body" and "always make GET requests" so that the request body isn't consumed so it can be actually used by a later HTTP handler. That's pretty easy, so I added
no_body
andoverride_method
subdirectives to do this. We'd also need a way letreverse_proxy
not be a terminal HTTP handler... but... turns out,handle_response
was already implemented to work that way!Then I started testing it a bit. Turns out that
handle_response
had a small bug, it would pass the cloned request to subsequent routes; that's bad, because thenheader_up
manipulations would become permanent and apply to subsequent routes, and if we usedno_body
, we wouldn't have access to the body, etc. So I rewired things so that the original request is passed through subsequent routes. Fixed!So here's how I tested this:
Edit: Note that this is no longer how the config looks, read further comments below for the changes
And then making a request like this; a
POST
to prove that:8882
indeed sees aGET
, and:8883
sees the originalPOST
, and the body:And the logs from this:
So, the logs in order:
:8882
as the upstream for the pre-check:8882
serverlog
s the request, notice it's aGET
here and there's noContent-Length
header:8882
proxy logs its roundtriphandle_response
is selected and runs, then continues the handling chain:8883
as the upstream for the "actual" handling of the request:8883
serverlog
s the request, notice it's aPOST
here andContent-Length
is38
(my dumb little JSON payload):8883
proxy logs its roundtrip:8881
server finallylog
s the request and the50
byte response (theFoo
header value and echoed request body)I just noticed as I write this though that the last log line has
Server: Caddy
three times 馃 there might be a bug with the response writer, I'll need to look into this more closely to see what's going on, but interestingly the response incurl
only has two (which is correct, i.e.:8881
and:8883
ultimately should be the only ones manipulating the response).So with all this out of the way, this means that ForwardAuth can be done purely with
reverse_proxy
. But obviously this is pretty verbose and has a lot of boilerplate, so we'll probably provide aforward_auth
Caddyfile directive, similarly tophp_fastcgi
which is a shortcut/sugar overreverse_proxy
to make it nicer to use, with good defaults.