Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP + Traefik #14

Closed
jbperrin88 opened this issue Jun 20, 2019 · 40 comments
Closed

LDAP + Traefik #14

jbperrin88 opened this issue Jun 20, 2019 · 40 comments

Comments

@jbperrin88
Copy link

Hi there ,

Such a nice project , something i was looking for a long long time.

I'm trying to handle Basic auth in traefik reverse proxy and forward auth to LDAP AD via your services

Can you please , give me some tips . some help to configure all the stuff ?

I want to use it in a single docker env
dynamicly forward auth via label https://docs.traefik.io/v2.0/middlewares/forwardauth/
Try to allow or not by LDAP valid user , or group mapping .

Thanks you !

@travisghansen
Copy link
Owner

Sure! Let's get you going!

I'm not sure how far you've gotten so far but fir you need to get the server running. Presumable in pure docker so have a look here: https://github.com/travisghansen/external-auth-server#docker

After it's running somewhere that will be accessible by traefik you need to generate an appropriate config_token: https://github.com/travisghansen/external-auth-server#generate-a-token

With ldap you'll want to populate the plugins property with an appropriate block from here: https://github.com/travisghansen/external-auth-server/blob/master/PLUGINS.md#ldap

Which subsequently uses config options from here: https://github.com/vesse/node-ldapauth-fork#ldapauth-config-options

With the config_token in hand simply turn on forward auth with the proper labels in traefik and point it to: http://<eas server ip>:8080/verify?config_token=<token output from above>

My testing with ldap was openldap so once you get things rolling with AD I'd like to get feedback and document any nuances. If possible I'll have you send a sample config over so that I can share with other what AD would look like.

If you're stuck somewhere in the process and need more detailed info for a particular stage let me know. Thanks!

@jbperrin88
Copy link
Author

The basic auth works successfully for a user via LDAP Active Directory ! Nice

This is the config :

plugins: [
{
type: "ldap",
//realm: "my realm", // optional
session_cache_ttl: 900, // seconds to cache successful logins
connection: {
url: "ldap://XXXXX:389",
bindDN : "XXXXX",
bindCredentials : 'XXXXX',
searchBase : 'DC=XXXXX,DC=XXXXX,DC=XXXXX,DC=XXXXX',
searchFilter : '(sAMAccountName={{username}})',
bindProperty : 'sAMAccountName'
}
}
]

Next step : if i want to only only a user from a AD group ?

What can i do ?

Play with header maybe ?

I also wonder how to use the realm parameter ?

@travisghansen
Copy link
Owner

I intend to allow assertions on the ldap userinfo, but you should be able to use the built-in functionality of the ldap lib too. Have a look here and let me know if you get it to work correctly: vesse/passport-ldapauth#10

@jbperrin88
Copy link
Author

I've already try to make a 'searchFilter' which handle the require group . Without success.
To be sure of this filter, i've test it with ldapsearch with two user not in the same group.

Still, Valid user can connect every time . But it's maybe some cache somewhere ...

@travisghansen
Copy link
Owner

Well, there are other params. Did you see the groupSearchFilter and associated options in the doc?

@travisghansen
Copy link
Owner

Also, yeah during testing set cache to 0.

@travisghansen
Copy link
Owner

The realm parameter has nothing to do with AD realms, it's just the http basic auth realm sent back to the browser... basically meaningless especially in this context.

@jbperrin88
Copy link
Author

Hum , another stupid thing , but all my AD group contains parenthesis in DN .....

So ii try to espace it thi /(XXXX/) or /28XXX/29 but in log every time it's not escape :

memberOf: CN=XXX,OU=XXX (YYYY),OU=XXX,DC=XXX,DC=XXX,DC=XXX,DC=XXX

@travisghansen
Copy link
Owner

Is it possible to try with a group that doesn't need escaping? I'll dig into the escaping behavior to see what's needed.

My LDAP skills are limited so I appreciate the patience and help making it work :)

@jbperrin88
Copy link
Author

jbperrin88 commented Jun 26, 2019

Great Github only escape ... WTF !

Unfortunatly , i haven't found any group Without parenthesis in DN ....

  • When i set '\(XXXX\)' in the configuration , the debug log show '(XXXX)' : not working

  • When i set '\\(XXXX\\)' in the configuration , the debug log show '\\(XXXX\\)' : not working

  • When i set '\28XXXX\29' in the configuration , the debug log show ' \u00028GG\u00029' : not working

What i expect to see in the log is either '\(XXXX\)' or '\28XXXX\29' , cause it is working with ldapsearch

I think there is two thing happend there :

  • url encode from the genration of the config
  • maybe some Javascript tricks

From the ldap module , i've seen a sanitize input fonction only for username (https://github.com/vesse/node-ldapauth-fork/blob/master/lib/ldapauth.js)

var sanitizeInput = function(input) { return input .replace(/\*/g, '\\2a') .replace(/\(/g, '\\28') .replace(/\)/g, '\\29') .replace(/\\/g, '\\5c') .replace(/\0/g, '\\00') .replace(/\//g, '\\2f'); };

@travisghansen
Copy link
Owner

I've got my dev environment back up and testing out those parameters locally. Couple notes:

  • I found a minor bug in the ldap code regardless, I'll commit shortly
  • The group filters don't actually filter anything, they just add an _groups property to the lookup that includes only the groups the user belongs to
  • I had to explicitly add memberOf to searchAttributes (seemingly all doesn't include computed properties
  • after adding memberOf I was able to enforce membership by using searchFilter with the appropriate value (ie: &(uid={{username}})(objectClass=inetOrgPerson)(memberOf=cn=somegroup,ou=Groups,dc=example,dc=com)
  • I'm also going to add config options to support the exact same style of userinfo assertions on the user data as is allowed in the oauth2 and oidc plugins. I'll commit that with the above fix. This will allow complex filtering rules outside of pure ldap logic/filters.

I'll test a group name with parens shortly..

@jbperrin88
Copy link
Author

Waoooo .... quick answer and debug ... thanks !

Can you please keep me updated ?

@travisghansen
Copy link
Owner

Yeah, still digesting the behavior of the options but I'm close. Just so my test is sane is this an example of what a group looks like for you?

cn=(test),ou=Groups,dc=example,dc=com?

@travisghansen
Copy link
Owner

I just tried a filter like this and it worked as expected:

&(uid={{username}})(objectClass=inetOrgPerson)(memberOf=cn=(test),ou=Groups,dc=example,dc=com)

@travisghansen
Copy link
Owner

Be careful with spaces between the filters, I noticed if I had anything in there it was behaving badly..

@jbperrin88
Copy link
Author

almost :
&(uid={{username}})(objectClass=inetOrgPerson)(memberOf=cn=test,ou=Groups (TEST),dc=example,dc=com)

Something like that

@jbperrin88
Copy link
Author

in my plugin configuration

searchFilter : "(&(sAMAccountName=blabla)(memberOf=CN=GG-BLABLA,OU=BLABLA BLIBLI ,OU=Groupes (GG),OU=XXXX,DC=XXXX,DC=XXXX,DC=XXXX,DC=XXXX))"

@jbperrin88
Copy link
Author

i've try also with another Attribute -> manager

This one doesn't contains parenthesis but still space inside DN.

It's not working

@travisghansen
Copy link
Owner

Yeah, maybe it's the space, let me do some testing with that..

@travisghansen
Copy link
Owner

Space worked here too. You're probably getting bit by the bug I found. Let me get that fix committed real fast and get an updated docker image built..

@jbperrin88
Copy link
Author

Ok ! Thanks so much , will try soon and keep you informed !

@travisghansen
Copy link
Owner

When this build completes force the container(s) to update and try again: https://travis-ci.org/travisghansen/external-auth-server/builds/550907791

@travisghansen
Copy link
Owner

OK, it's built..

@jbperrin88
Copy link
Author

jbperrin88 commented Jun 27, 2019

I've just try the new build .

Same behavious .
Every valid user can connect even if they aren't in the group from the filter.

From the RFC LDAP , i've read that parenthesis inside DN need to be espace
https://tools.ietf.org/html/rfc4515#section-4

I've also try to set the filter with another attribute (my personal CN) and it is neither working.

I wonder if the filter is used .....

This is my last try search filter :
"searchFilter":"(&(sAMAccountName={{username}})(cn=PERRIN Jean-Baptiste))"

(work well in ldapsearch)

@jbperrin88
Copy link
Author

Last try done .
I set the filter with my personal sAMAccountName . Other user can auth .......

I'm pretty sure filter is not used at all .

@jbperrin88
Copy link
Author

Hum , i've found something interesting !

If the ldap cache parameter is not set , the node-ldapauth push the default value to False

https://github.com/vesse/node-ldapauth-fork
cache - Optional, default false. If true, then up to 100 credentials at a time will be cached for 5 minutes.

You set it to true
https://github.com/travisghansen/external-auth-server/blob/master/src/plugin/ldap/index.js line 23

I need to do more test ^^

@jbperrin88
Copy link
Author

Do you have a way to make the node-ldapauth more versbose .

I see the log parameter , but i've no idea how to use it

@travisghansen
Copy link
Owner

Let me see if there's a way to enable more logging.

Regarding cache, don't set the cache property from the library options block connection, I'm referring to the session_cache_ttl option just below the realm param in the example...set that to 0.

I'm certain the filters do work in my setup. I changed them to various options and they do take effect.

@travisghansen
Copy link
Owner

I've integrated the ldap logging into the app, simply set the connection.log value to true (ignore the upstream documentation and just set it to a bool. Change the image tag to next as I haven't merged it into master/latest yet.

Let me know if it helps you find anything.

@jbperrin88
Copy link
Author

image set to next , connection.log set to true .

Nothing more with docker logs .

This is the tokenized output from docker logs

info: starting verify for plugin: ldap
verbose: ldap userinfo: undefined
debug: plugin response

{
"statusCode": 200,
"statusMessage": "",
"body": "",
"cookies": [],
"clearCookies": [],
"headers": {},
"authenticationData": {},
"plugin": {
"server": {},
"config": {
"type": "ldap",
"realm": "my realm",
"session_cache_ttl": 0,
"connection": {
"url": "ldap://XXXX:389",
"bindDN": "ZZZZ",
"bindCredentials": "ZZZZ",
"searchBase": "DC=AAAA,DC=BBBB,DC=CCCC,DC=DDDD",
"searchAttributes": ["cn", "sn", "manager", "memberOf", "sAMAccountName"],
"searchFilter": "(sAMAccountName=EEEE)",
"bindProperty": "sAMAccountName",
"cache": false,
"log": true,
"reconnect": true,
"timeout": 3000,
"connectTimeout": 10000,
"idleTimeout": 10000
},
"pcb": {}
}
}
}

info: end verify pipeline with status: 200

@travisghansen
Copy link
Owner

Something is off :( I get a whole slew of debug messages from the authenticate method. Are you running this in pure docker? If so would you be comfortable adding debug lines into the code after starting the image? or alternatively running from source?

@jbperrin88
Copy link
Author

GOT IT !

After a clean reboot , some refactoring , and set the basic stuff . It works

Thanks to your debug ! My Active directory account (service account ) is not working with your container

Really don't knwo why ! (Maybe the slash in the password.....)

But when i bind with my personnal account , it works !

I can also use member filtering ! GREAT

Thanks you so much

@travisghansen
Copy link
Owner

Nice! Can you share your final config minus secrets? I'll add it to a list of examples.

Also, I'll debug bad bind creds..that should fail not be allowed through so I'll make sure to fix that up if I find anything.

@jbperrin88
Copy link
Author

yeap, i'm wondering if the encoreURI component play well with single slash ....

This is my configuration:

let config_token = { eas: { plugins: [ { type: "ldap", realm: "my realm", session_cache_ttl: 0, connection: { url: 'ldap://ldap.example.com:389', bindDN: 'myaccount@ldap.example.com', bindCredentials: 'HOOOOThatAGreatSecret', searchBase: 'DC=base,DC=example,DC=com', // valid_user by Active Directory Account searchFilter: '(sAMAccountName={{username}})', // valid_user by Active Directory Account or mail searchFilter: '(|(sAMAccountName={{username}})(mail={{username}}))', // valid user in the group onegroup searchFilter: '(&(sAMAccountName={{username}})(memberOf=CN=onegroupe,OU=subgroupe,DC=base,DC=example,DC=com))', cache: false, log: true } } ], } };

@travisghansen
Copy link
Owner

Mind sharing which direction the slash is? I'll test it out..

@travisghansen
Copy link
Owner

I'll review your config and send over some suggestions for prod usage later as well..

@jbperrin88
Copy link
Author

bindCredentials : 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\rxxxxxxxxxxx',

don't ask me why this happend in my password ....

@travisghansen
Copy link
Owner

Ok I'll have a look..

@travisghansen
Copy link
Owner

@jbperrin88 ok, here's my findings:

  1. The backslash \ is an escape character in a node string. Whether you're single or double quoting the value you need to use a double \\ to represent a single \ in the represented value. Try that out and let me know how it goes. The uri encoding shouldn't be problematic...

  2. I'm a little baffled by the fact that you were able to successfully authenticate with invalid bind credentials. Are you certain the app was returning a 200 response code when you had the 'bad' configuration? I cannot reproduce this. I have however changed the logic a bit to return a 503 instead of a 401 when credentials are bad or generally when there are connection errors to the ldap server.

  3. I'm still working on documenting the behavior of the group settings and will also be implementing userinfo assertions based on ldap user data in the next release.

  4. For your production config_token I would use something like the following:

let config_token = {
  eas: {
    plugins: [
      {
        type: "ldap",
        //realm: "my realm",
        session_cache_ttl: 900,
        connection: {
          url: "ldap://ldap.example.com:389",
          bindDN: "myaccount@ldap.example.com",
          bindCredentials: "HOOOOThatAGreatSecret",
          searchBase: "DC=base,DC=example,DC=com",

          // valid_user by Active Directory Account
          searchFilter: "(sAMAccountName={{username}})",

          // valid_user by Active Directory Account or mail
          searchFilter: "(|(sAMAccountName={{username}})(mail={{username}}))",

          // valid user in the group onegroup
          searchFilter:
            "(&(sAMAccountName={{username}})(memberOf=CN=onegroupe,OU=subgroupe,DC=base,DC=example,DC=com))"
        }
      }
    ]
  }
};

Specifically set the session_cache_ttl as appropriate. Just remove the cache and log values altogether for the connection.

@travisghansen
Copy link
Owner

I just landed all the tweaks I've made to ldap into master including userinfo asserstions as an FYI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants