Closed mholt closed 3 years ago
excited to see what's in store for Caddy 2!
The Vouch Proxy community would like to integrate with other web servers and ingress controllers.
To do so we'd need something similar to the Nginx auth_request
module...
Would really love to see a PAM authentication mechanism. We had a need for this recently and wrote focal to accomodate it.
It supports a typical POST to a /login endpoint (Could just as well be http auth) where validation for the user details are done in PAM and a JWT is returned which is used to authenticate anything downstream.
We used a simple library called tekmor to handle the actual PAM authentication component.
Some points you might want to consider:
What am I missing? Caddy has an awesome extension mechanism for adding plugins / middleware.
This is all excellent feedback so far, thank you everyone! (Feel free to keep it coming.)
I'll be working on authentication features after beta 13 is released (that's the next beta release).
@sarge Yes, you're right. :) I'm going to build an official authentication module. This is for discussion about specific features we want in that module.
(Others? What do you need? Please comment below!)
I would like to suggest Kerberos / NTLMv2
I know, it sounds a bit like from a different age but krb5 is still the default logon and SSO method for traditional Windows / Active Directory networks when the AD isn't connected to Azure AD or self-hosted AD Federation services. All browsers and OSes support it. It's still often used for Intranet webapps and I think it will still be around for years to come. AD also supports LDAP (which you already have on the list) but Kerberos has multiple advantages over it. Examples: It's passwordless after the OS login, no auth traffic to the domain controller/ldap server for every single http request to check the password, it also works with smartcard login (at least on windows), etc.
So, as mholt knows, I've been working on a port of caddy-jwt
(and loginsrv
) to caddy2. While doing this, I've come across a couple of things that I find are missing from the Authentication module that I'm going to document here:
As previously noted, User is a bit lacking. I'm preparing a PR that will expand it to have a generic metadata map store, and maybe add some common fields (at least username). Those fields will be exposed to the replacers.
Caddy's old JWT module had some amount of configuration on what happens on authentication failure (it could either return forbidden, redirect the user to the login page, or "passthrough" the request without setting the user authenticated headers/vars).
Currently, when the caddy2 authentication fails, the Authentication module simply raises an error from ServeHTTP, with caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
. Ideally, users need to be able to configure what happens when authorization fails. I guess this is currently possible by wrapping the Authentication module in a subroute and providing an error route (like this) but it's frankly not great (I feel like it's very, very verbose)
In my caddy-jwt fork, I just do the redirect directly within the func Authenticate
, but I feel like that's not such a good idea.
While working on the above config, I came to the realization that replacer and vars are different concepts. IMO the vars
matcher should have access to everything available in the replacer, so I can write { "matcher": "vars", "http.error.status_code": 401 }
without the extra vars
middleware. I'll make a separate issue for that.
EDIT: Make an issue for the matcher stuff: https://github.com/caddyserver/caddy/issues/3051
Great discussion so far.
@roblabla
Currently, when the caddy2 authentication fails, the Authentication module simply raises an error from ServeHTTP, with caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")). Ideally, users need to be able to configure what happens when authorization fails. I guess this is currently possible by wrapping the Authentication module in a subroute and providing an error route (like this) but it's frankly not great (I feel like it's very, very verbose)
The verbosity here is intentional: it allows the user to configure literally any behavior that the primary handler chain is also capable of, but in response to an error. So you can handle errors by showing an error page, sure, or you can reverse proxy to a backup server, or issue a redirect, or you can write a static response. It's very powerful! (But it can be a little cumbersome sometimes.)
Caddy's HTTP error values are (supposed to be) structured, so you can get not only an error string, but an ID, the actual error
value (which can be asserted to a concrete type), the recommended status code, and the trace.
In the case of a failed authentication, it may be useful to access that actual error value to make certain decisions based on the kind of error. This should be doable already, a handler just needs to be programmed to do it.
Another option is to make the Authentication handler accept an embedded route configuration to invoke if authentication fails. This could help simplify some more complex decision logic, like how to respond to certain kinds of authentication-specific errors, and keep it all in one place. The existing subroute
handler does this, but in a more general sense. I'm not sure if you'd want to use an *HTTPErrorsConfig
or embed a RouteList
directly, just kind of depends. I can help give guidance here.
In my caddy-jwt fork, I just do the redirect directly within the func Authenticate, but I feel like that's not such a good idea.
I think I agree, we should expose a little more surface for configuring how to handle certain kinds of errors, with more flexibility where possible.
I think the thing that bugs me with using the errors
routes is that if an error chain exists, it will override the default behavior of logging the error and setting the status code to whatever was provided (or 500 if it wasn't a HandlerError). E.G. if those middlewares are removed, then the server will return a blank page with code 200 (instead of 401) and no error will be logged. This means that to handle one specific error, you need to add a bunch of boilerplate to recover the original behavior in other error cases.
And in our case, we'd certainly want any errors unrelated to auth to be logged! So I find the status quo rather unpleasant.
I'm unsure how to deal with it though. I couldn't even figure out what makes the server return 200 - I guess it's either go's http server doing this silently or I'm missing something (wouldn't be surprising, I'm by no means a Go expert).
Maybe the boilerplate I provided should be automatically appended to the error handling chain if it exists?
@roblabla Well, remember we're still in beta, so now's the time for us to iron out these inconveniences.
There's no particular reason that an error chain prevents logging the error and responding with the status code. That actually sounds like a bug rather than intended behavior. Can you open a new issue to describe a minimal config that reproduces it, and I can look at it?
(I got a small detail wrong: the error is logged regardless, I couldn't see it in the logs because they only happen when we return 5XX error codes, and I only tested with 4XX haha. Status code is still unset though).
I made an issue about it, here it is #3053
So with #3053 on its way to being fixed and #3051 currently being discussed, the amount of boilerplate necessary for a minimal JSON doing (for the sake of the example) a redirection in case of auth failure got much smaller:
As far as the Caddyfile goes, I see there's a new handle_errors directive available since this commit. I think it should make it possible to write something equivalent to the above like so (again, assuming we get the compare directive from #3051):
@auth_error {
compare {
{http.error.status_code} = 401
}
}
http://localhost:4444 {
basic_auth {}
handle_errors @auth_error {
redir /login
}
}
This more or less resolves all my complaints about the error handling story of the Authentication module, I think. Only thing I'd change, maybe, is some way of knowing for sure the error came from the authentication module (and not some other plugin), e.g. by introducing a new replacer item like http.middleware.authentication.error
or something.
EDIT: Though in jwt
, I think I'll keep doing things like jwt { redirect /login }
and the other various convenience functions currently in jwt. The only difference being that it would expand to something similar to what is done in the above JSON, instead of being handled in the Authenticate type.
Hi All, I propose separating auth* plugins into the following categories:
Principles:
Let's take an example of JWT token issuance/validation. We have multiple routes that require protection. So, we add "authentication" handler as the very first handle
for the route. Then, we go to a different route. and do the same ... wait, do we need to redefine the configuration because it is a different instance of a plugin?
Yes, we do:
{"level":"info","ts":1587593245.7803967,"logger":"http.authentication.providers.jwt","msg":"provisioning plugin instance"}
{"level":"info","ts":1587593245.780405,"logger":"http.authentication.providers.jwt","msg":"found JWT token name","token_name":"JWT_TOKEN"}
{"level":"info","ts":1587593245.7804825,"logger":"http.authentication.providers.jwt","msg":"provisioning plugin instance"}
{"level":"info","ts":1587593245.7804863,"logger":"http.authentication.providers.jwt","msg":"found JWT token name","token_name":"JWT_TOKEN"}
bottom line ... access to shared configuration pool.
access to shared configuration pool at the time of the initialization of the plugin
Also, if there is more than one instance of a plugin, which instance will take care of "Validation"? What about Provisioner? Who is responsible for provisioning? Are we "locking" when Provisioner runs?
@greenpau There is a facility in the Caddy core which can help with pooling global state for all instances of the modules: https://pkg.go.dev/github.com/caddyserver/caddy/v2?tab=doc#UsagePool - the UsagePool is currently used by Caddy's reverse proxy to keep track of the state of upstreams across multiple instances of the proxy handler, and for keeping track of log writers. You could use something similar for JWT configuration, for example.
As far as "who gets to do Validation/provisioning/etc", the UsagePool helps with that, since it's "first-come-first-serve, and everyone clean up after yourselves" kinda thing. There's no guarantee as to what order modules are loaded, so it's best to use the UsagePool for things like this where the pool knows whether something needs to be provisioned or not. (We can chat on Slack if you have technical questions about that.)
As for chaining handlers: authorization -> authentication -> authorization
-- sure, but maybe a "supermodule" should do that, i.e. a simple wrapper module in which you configure the authz/authn once and then it does the chaining for you.
so it's best to use the UsagePool for things like
@mholt , agreed! :+1:
Proxying protected endpoints into something like Authelia would be nice. It has support for OTP and Duo Mobile Push for 2FA, can be configured to use backends like LDAP already. I've looked over the docs since switching fully to caddy2 this last week and don't currently see a way to do this.
Example nginx configuration: https://docs.authelia.com/deployment/supported-proxies/nginx.html
@roblabla said elsewhere:
Thinking about this more, the caddy authentication module kind of merges several concept that could be kept distinct: the authentication flow, authentication backend and session management.
For instance, caddy-jwt is clearly a session management thing. It doesn't actually authenticate anyone, it just looks for a signed cookie and populates the
caddyauth.User
based on this. The user must have been authenticated somewhere else to obtain that JWT token.
basicauth
is doing both authentication flow (e.g. setting up the header to make the browser show a password prompt) and authentication backend (the username/password list that's stored in the JSON). There's no reasonbasicauth
couldn't take its username/password list from another source/backend (a database, an LDAP, etc...).If we had a properly separated authentication backend module, it could then be shared with the authentication flow modules (e.g. ssh password, HTTP basicauth, HTTP login form...). Of course more special types of authentication flows (such as ssh pubkey) won't work with this system, but I think that's fine.
That seems like a proper separation of concerns. With that, I imagine the config would be something like:
{
"authentication": {
"flow": {
"handler": "username_password"
},
"credentials": {
"provider": "ldap",
"hostname": "ad.contoso.com",
"dn": "cn=contoso,dc=foo,dc=bar",
"scope": "",
"username": "of_the_ldap_server",
"password": "to_the_ldap_server"
},
"session": {
"module": "jwt"
// ...
}
}
}
The basicauth
could be a special handler or plugin that is in fact a shortcut which expands to something like the above.
The basicauth could be a special handler or plugin that is in fact a shortcut which expands to something like the above.
@mholt , this is basically https://github.com/greenpau/caddy-auth-forms with LDAP backend.
https://github.com/greenpau/caddy-auth-forms/blob/master/assets/conf/Caddyfile.json#L48-L53
{
"type": "ldap",
"hostname": "ad.contoso.com",
"dn": "cn=contoso,dc=foo,dc=bar",
"scope": "",
"username": "of_the_ldap_server",
"password": "to_the_ldap_server",
"realm": "local"
}
@Mohammed90 @roblabla I definitely like the idea of separating the list of users into various modules, and having the actual authenticating be done by their own modules.
@greenpau That's pretty slick -- shall we converge on something like that proposed?
shall we converge on something like that proposed?
@mholt , I think current auth plugin structure works for me ... each plugin may abstract the auth*
the way it works for that plugin.
The conversation about "flow - credentials - session" is another angle/perspective on AAA (authentication, authorization, and accounting).
"session": {
"module": "jwt"
// ...
}
In the above example, the "session" is handled by module jwt
. What is being said there.
Is it a validator or grantor? Are we trying to consolidate auth* in a single plugin (monolith) or do we want modularity?
I am for modularity.
For example, when I implemented jwt plugin, I created a grantor object (and validator object).
I also did it so that both the grantor and validator share a set of common parameters.
Any developer from other plugins may implement issuing tokens using this object. Another plugin may import jwt
module and access AuthzProviderPool
.
Importantly, the AuthzProviderPool
contains Masters
for each authorization context.
Therefore, if you are a plugin developer and you want to issue a token, you copy
common parameters from the master
for a context (that means you get the secret/key),
and issue tokens.
Here is how forms
plugin issues tokens:
claims, err := jwt.NewUserClaimsFromMap(userMap)
if err != nil {
return nil, 500, fmt.Errorf("failed to parse user claims: %s", err)
}
if claims.Subject == "" {
claims.Subject = user.Username
}
guestRoles := map[string]bool{
"guest": false,
"anonymous": false,
}
...
TokenProvider
I should have called it TokenGrantor
; I was mocking the interface on a fly ... that's why it happened).https://github.com/greenpau/caddy-auth-forms/blob/master/plugin.go#L446
userToken, err := userClaims.GetToken("HS512", []byte(m.TokenProvider.TokenSecret))
In this example, I do not go to the AuthzProviderPool
to get secret .. I have a configuration section for that. (but I could do it). It probably makes sense when everything is in "default" context. It could be useful when you have a single instance of a server.
Hey @mholt, Would you be open to adding an auth plugin to enable external authentication/authorization? This goes along the lines of auth request module in Nginx and external authorization filter in envoy.
@hbagdi I think I might be able to write an external auth plugin (I also need it). @mholt Is there anything I should read before starting it?
Would you be open to adding an auth plugin to enable external authentication/authorization?
@hbagdi , you can do things of this nature https://github.com/greenpau/caddy-auth-portal#google-identity-platform It will not be straighthrough, but will likely follow a redirect.
@hbagdi You can have a look at this: https://github.com/trusch/caddy-extauth Its working for me and there is a example compose.yaml and a Caddyfile to get you started. If you run into any problem just open an issue and ask me to add a good readme ;)
HOTP/TOTP In fact, I rely more on this simple offline Authentication Scheme.
You can have a look at this: trusch/caddy-extauth Its working for me and there is a example compose.yaml and a Caddyfile to get you started. If you run into any problem just open an issue and ask me to add a good readme ;)
What would it take to get something like this merged into Caddy main? The auth_request
style seems to cover enough of the authentication space, through Vouch or just writing something custom.
What would it take to get something like this merged into Caddy main?
Unlikely to happen -- specific implementations (other than the simple standard HTTP basic auth) belong as separate plugins. The standard Caddy modules already facilitate general/abstract authentication, so it does its job already since you can add any other auth on top of it.
What would it take to get something like this merged into Caddy main?
Unlikely to happen -- specific implementations (other than the simple standard HTTP basic auth) belong as separate plugins. The standard Caddy modules already facilitate general/abstract authentication, so it does its job already since you can add any other auth on top of it.
One of the biggest benefits of providing something like auth_request
is that it can allow plugging into other auth frameworks without writing new plugin code. It is my understanding that with the current caddy system, adding anything other than basic auth requires code, and not just writing a configuration file.
I mean, even with auth_request
you still have to write code: it just wouldn't be in Caddy. But that's actually counter to Caddy's philosophy, which is to plug the code directly into the server: fewer moving parts.
Fair point. I guess a plug-in that implements auth_request
would be the best approach.
The standard Caddy modules already facilitate general/abstract authentication, so it does its job already since you can add any other auth on top of it.
Caddy does have the necessary authentication abstractions but there is no way to integrate authentication proxies without writing some code in form of a Caddy plugin and then bundling that with Caddy. That leads to a higher friction than necessary.
If there is enough demand, would you be open to bundling a module like auth_request
or envoy's ext-authz
into mainline Caddy? That way, users write only configuration, and not code to get up and running.
I am not sure what is involved to support authentication proxies, but feel free to draft up a proposal for design discussion to augment Caddy's standard http.handlers.authentication
module: https://caddyserver.com/docs/modules/http.handlers.authentication
No guarantees but if it's fairly unintrusive (and introduces no new dependencies, especially), extensible, and generally useful, there's a good chance it could make it in.
No guarantees but if it's fairly unintrusive (and introduces no new dependencies, especially), extensible, and generally useful, there's a good chance it could make it in.
Based on caddy-extauth, this should be doable without any new dependencies – the only direct non-Caddy dependency is zerolog, which should be unnecessary.
If nobody else writes something up, I'm willing to toss my hat in... eventually.†
† At least three to six months from now.
the only direct non-Caddy dependency is zerolog, which should be unnecessary.
Yeah, it should use zap
which is the logger Caddy uses. https://caddyserver.com/docs/extending-caddy#logs
+1 for supporting generic HTTP capabilities around header/url rewriting + request-auth (status code check on a dispatched HTTP call), or a clean & core declarative abstraction on top.
Breaking down a bit more over a few projects I've used Caddy 1 as a secure reverse proxy:
Modular design & non-proprietary abstractions: We want a thin server with thin plugins for using modular auth systems we pick+control. Otherwise, the tail is wagging the dog
Core & declarative: We do want to use Caddy for router-level controls to provide a coarse defense-in-depth layer around ingress/egress, and optional mesh for internal calls. That + large files is why we still don't use Caddy for our innermost layer in our own sw. It'd be a high security concern to have these outside of the caddy org and to not be declarative. (I published multiple top-tier security papers on ways to push the expressive boundaries for scripted policies, including leading to how Amazon now does IAM, so am quite serious about this point, despite years of work with top teams trying to enable doing otherwise..)
request-auth
+ header rewrites is a sufficient & powerful building block for most apps today
I can see value in doing something cleaner on top, like a distinct & declarative RBAC policy layer paired with some admin-defined request<>auth config... but that should basically still just be sugar for request-auth
, and would start there...
Support internal TLS: I'm not well-versed in internal TLS wrt rewriting. We currently only do TLS at the perimeter, but will be doing internally as well, and I have no idea of the implications of that. Somewhere between caddy/docker/k8s, we'll need to figure that out, and it seems like something caddy would be in a good position for, esp. when k8s is overkill.
Apps generally expose+use HTTP routes for authentication: /check/roleX/
, /login?redirect_on_success=...
, etc. . JWT+CSRF via body/header/cookie, generally with customization around names and fields, but consistent in 2xx vs 4xx codes.
End-admin SSO: Many projects are composing software, where one of them might have an account system with custom HTTP auth check & login routes. (Ex: Generic Prometheus/Grafana + Django app w/ custom accounts.) Something like nginx's auth-request
primitive makes quick work of SSO for stitching these together.
Internal app dev SSO: Same as above, but with more careful knowledge of routes. Because this is a second persona, being able to separate the policies here from end-admin's helps. Currently possible today by say chaining 2 proxies together: exposed + internal.
Request normalization: cookie -> header, csrf -> jwt, ...
Most authorization is handled internally, with a few exceptions:
Network: End-user admins may set coarse custom role-based policies, such as for custom sidecar apps
Defense in depth: Internal firewall/mesh for protecting app routes <> fixed roles (web / logged in / staff) . App code is ultimately responsible for sending/receiving resource requests through the PDPs
WAF: We'd like to be able to do policies on ingress/egress, but a much bigger conversation
Just as a "state of the market" thing... Nginx, traefik, etc. show that all of the above can be simple via header rewriting + request-auth
check. I haven't researched WAFs, but would not be surprised to see separating that out as a paid tier that we / our users can enable - I think that's the idea for why f5 bought nginx.
@lmeyerov Thanks for this great discussion.
Modular design & non-proprietary abstractions: We want a thin server with thin plugins for using modular auth systems we pick+control. Otherwise, the tail is wagging the dog
Yeah, agreed. Caddy core (i.e. a Caddy built without modules) is pretty bare-bones by itself, and you can plug in only the modules you need, especially with a custom build. Caddy just ships with a bunch of HTTP/TLS stuff because it's useful to most people.
That + large files is why we still don't use Caddy for our innermost layer in our own sw. It'd be a high security concern to have these outside of the caddy org and to not be declarative.
Can you elaborate on this? Do you mean using code outside of the caddyserver
github organization? i.e. third-party plugins are unacceptable? And what is it about large files?
Support internal TLS: I'm not well-versed in internal TLS wrt rewriting. We currently only do TLS at the perimeter, but will be doing internally as well, and I have no idea of the implications of that. Somewhere between caddy/docker/k8s, we'll need to figure that out, and it seems like something caddy would be in a good position for, esp. when k8s is overkill.
Caddy (v2) is very good at all things TLS, and even setting up your own, internal PKI with its embedded ACME server and certificate signer. You can simplify a lot of your infrastructure this way.
Happy to help, I want caddy to keep growing :)
Do you mean using code outside of the caddyserver github organization
Yes, especially when not from another reasonably trusted org. It should be practical to do TLS/JWT/etc. without introducing significant plugin software supplychain risks that could fail basic security audits.
I advocate core routing, rewriting, TLS, etc. should be in Caddy or a partner org/group who is trusted to do the necessary vetting & upkeep. For example, an individual's third-party JWT plugin experiment is a supplychain shocker for users who'd want their use of caddy to pass a minimal security audit like SOC I. If the JWT plugin was vetted, administered, + actively maintained by caddy
or auth0
, it wouldn't be as much of a leap. Alternatively, if JWT support was done by regular request rewriting + request-auth
by the Caddyfile writer (so nothing specific to JWT/CSRF/etc.), it'd also be boring audit-wise.
Not be declarative
This gets into deeper security. But basically, scripted policies are generally error-prone even by security experts with a lot of time and $, and so should not be necessary for the typical case. If the pandemic ever dies down, this would be a good 🍻 topic as I could share both web-scale deployment horror stories & security research attempts. I'm not saying disallow scripted stuff... but make the recommended security default follow security best practices.
Part of why I like rewriting / request-auth
is because those are limited DSLs. They're still error prone, but at least in more limited areas and quite practical to write a policy tester and even a policy verifier: It's logic over regex.
Large files
Not related to auth, and something we need to research as I hadn't seen Caddy articles explicitly exploring working with them. Headaches happen with larger scales -- size caps, buffer management / optimization, backpressure. Nginx has specific primitives designed for large files and file transfers. Caddy might already be great here, but I hadn't seen articles exploring the happy path + ceiling, so I suspect this needs research.
Caddy (v2) is very good at all things TLS ... You can simplify a lot of your infrastructure this way
Yes, I suspect so! That may be an interesting tutorial -- how to add internal TLS in X lines without running k8s / sidecar app X!
Gotcha. I think you should give Caddy 2 a little more credit, it's pretty good at TLS (including mutual), handling large files, etc. :)
Yes, I suspect so! That may be an interesting tutorial -- how to add internal TLS in X lines without running k8s / sidecar app X!
Awesome -- I have one written up already, actually, still finishing it and polishing it, and will be publishing it for sponsors before too long.
I am going to close this issue since, by my understanding, @greenpau's https://github.com/greenpau/caddy-auth-portal suite of modules seems to address most of the points here. It offers a variety of authentication methods, complete with a web UI and forms for it.
I suppose if there's any specific, actionable requests for new authentication modules, they can easily be put into new issues or just made by someone in a different repository.
Discussion that follows the above conversation is still welcomed; but I'm closing since I'm not sure how much is actionable at this point.
For anyone who'd been waiting for it, we've implemented a forward_auth
directive in Caddy https://github.com/caddyserver/caddy/pull/4739, and it's released in v2.5.1!
1. What would you like to have changed?
Caddy 2 needs more authentication modules. Right now, it supports HTTP Basic Authentication, but there are more methods we need to support:
This issue is to discuss the design of more authentication modules.
Once a user is authenticated, the server should be able to forward/expose the user info (e.g. GitHub auth token, if authenticated via GitHub, for example) to the upstream via:
so that it can know how to compute authorization (permissions).
What are your authentication needs / workflows? Be as specific as possible so that we can implement exactly what you need into Caddy. One or two sentences is probably not enough. Linking to how other software works is better, but not great, because we do not simply want to repeat what other software does -- the goal is to do it better, if possible.
The community is invited to collaborate in building these, so if you're interested, please get involved!
2. Why is this feature a useful, necessary, and/or important addition to this project?
Many companies are using service meshes primarily for authentication, when they do not actually need a whole service mesh.
If they could use Caddy as an ingress controller / reverse proxy with these authentication features, they would likely be able to replace their entire service meshes with just Caddy, and vastly simplify their infrastructure and lower their costs.
3. What alternatives are there, or what are you doing in the meantime to work around the lack of this feature?
Use more complex infrastructure to deliver authentication.
4. Please link to any relevant issues, pull requests, or other discussions.
HTTP Basic Auth was implemented in https://github.com/caddyserver/caddy/commit/f8366c2f09c77a55dc53038cae0b101263488867.