Trick789 - Hi ๐ I'm running homeassistant (HA)...
Hi ๐ I'm running homeassistant (HA), lldap and the cisco duo authentication proxy (DUO) on k8s. I can't use the example config as I'm using HA > LDAP > DUO > LDAP > LLDAP. It's working beautifully using a a custom auth provider for HA (python script using ldap3 library: https://gist.github.com/yumenohikari/8440144023cf33ab3ef0d68084a1b42f) , but the only thing I can't get right is the filter so that only members of a group cn=ha_rw,ou=groups,dc=example,dc=com can authenticate.
I've tried a bunch of flavors for the filter, but the lldap log continues to throw [warn]: Ignoring unknown group attribute ""memberof"" in filter messages. I thought memberof was a person attribute so I loaded up an LDAP browser (Apache DS) but can't find the attribute on either groups or people. But it must work because DUO is also pulling a memberof query to allow certain LLDAP users through and that's not generating a log entry on the LLDAP server.
This is the original filter in the script (basically for AD):
FILTER = """ (& (objectClass=person) (| (sAMAccountName={}) (userPrincipalName={}) ) (memberOf=CN=Home Assistant,OU=Security Groups,OU=Accounts,DC=ad,DC=example,DC=com) )"""and that's what I've attempted to change it to..
FILTER = """ (& (objectClass=person) (| (uid={}) (mail={}) ) (memberOf=CN=ha_hw,ou=groups,dc=example,dc=com) )"""Any idea what the filter should look like? (running lldap 0.5.1-alpha). Thank you.
Solution:Jump to solution
@nitnelave - hey, just wanted to let you know that with the right filter in place I got it to work..
filter in ldap-auth.py
safe_username = escape_filter_chars(os.environ['username']) FILTER = f"(&(uid={safe_username})(memberOf=cn=ha_rw,ou=groups,dc=example,dc=com))"...
64 Replies
You're right that memberOf is a person attribute. It's also not technically an attribute, which is why the LDAP browser didn't show it (it's also possible that the browser doesn't work well with LLDAP).
The reverse direction, for groups, is "member".
@nitnelave - thanks for the excellent work. love the project!
Enabled verbosity and am seeing the following entries..
[debug]: | request.base: "DC=example,DC=com" | scope: Global get_user_list [ 607ยตs | 0.03% / 0.34% ] [debug]: | filters: And([And([]), MemberOf("ha_rw")])so I take it the filter is working and I can just ignore those warnings
The warning might be a false alarm: since your query probably has a base DN of just the DC (without the OU), LLDAP searches both groups and users (although it's not going to find any group with the object class condition)
oh right, I could finetune the base DN in the script to avoid that.
If you can configure the base DN of the request, adding "ou=people" at the beginning should make the warnings go away
just noticed, after having added a couple of users to the group, that no matter which user I enter in HA, it's trying to login with the first user (alphabetically) in the group. Bummer but that has nothing to do with LLDAP I suppose ๐
Look at the query sent to LLDAP and make sure it has the login name
(you can see it in the verbose logs)
When I see:
get_user_list [ 607ยตs | 0.03% / 0.34% ] [debug]: | filters: And([And([]), MemberOf("ha_rw")])It seems that there's no login name in there
oh yeh I'm monitoring the logs of all 3 apps while trying to sign in..
HA: authentication failed for user B (the one I'm trying to login with).
DUO: passes user A (first user of group hw_rw) on to LLDAP.
LLDAP: errors out on user A.
I don't see the whole "or" block from your filter
The second "And([])" is just "true", that's the object class= person filter, it's always true for users
I guess the issue is in the duo configuration
This is what I'm seeing when DUO checks if a user is allowed to use the service (DUO)
[debug]: | filters: And([And([Or([MemberOf("duo_access")]), Or([And([Not(And([])), Not(And([]))]), And([]), Not(And([]))])]), UserId(UserId("userB"))])I didn't define the filter, that's all DUO.
It's getting hard to read ๐
can you post the LDAP just above? It has the LDAP filter before processing
[debug]: | filters: And([And([]), MemberOf("ha_rw")]) list_users [ 509ยตs | 0.26% ] filters: Some(And([And([]), MemberOf("ha_rw")])) | _get_groups: false [debug]: | return: [UserAndGroups { user: User { user_id: UserId("userB"), ..
No, I mean a couple of lines above the filter, for the same query, you have the full request
And the filter looks different
gotcha
[debug]: | msg: LdapMsg { msgid: 183, op: SearchRequest(LdapSearchRequest { base: "CN=duo_access,OU=groups,DC=example,DC=com", scope: Base, aliases: Never, sizelimit: 1, timelimit: 0, typesonly: false, filter: Present("objectClass"), attrs: ["objectsid"] }), ctrl: [] }followed by
[debug]: | request.base: "CN=duo_access,OU=groups,DC=example,DC=com" | scope: Group(Equality("cn", "duo_access"))
That's the request for the duo_access group
(no idea about the objectsid attribute though)
(maybe it's important?)
I do see that that attr gets resolved?
[debug]: | resolved_attributes: ["objectsid"]
That request should return exactly one entry (if the group exists)
You don't get a warning for unknown group attribute objectsid?
Yes I do. Right below that entry
objectsid, objectcategory and memberof are all unknown group attributes
Right. That's because we don't have those attributes (disregard memberOf)
If they're important, you'll have to look into custom attributes
but uhm, its all working though :p
Which we support, we just don't have a UI for it
Great, so they're probably not important!
both the check on group member of the duo_access and ha_ra groups is ๐
membership*
I just think I'm stuck with the limitation of only having a single user in the ha_rw group
Okay, so where does it get the names of the members of the group? Since it's sending a query as the first user of the group, it means that it got the list somewhere
let me have a look
(user A and B in your example)
I'm adding them back in
Does a bindrequest with userA while I'm trying to login with userB
I'm assuming it's because the request isn't passing on a uid and simply going with a memberOf filter so it just grabs the first user it finds?
So, it requests all the members of the group, but gets the display name, that's a bit unusual
You'd expect the uid
Can I see your duo config?
ofc
but duo's config doesn't query display name
the HA script does..
search = conn.search(BASEDN, FILTER, attributes='displayName')
if len(conn.entries) > 0: # search is True on success regardless of result size
eprint("search success: username {}, result {}".format(os.environ['username'], conn.entries))
user_dn = conn.entries[0].entry_dn
user_displayName = conn.entries[0].displayName
I guess just to display in the app
DUO config:
[ad_client] host=lldap.lldap.svc.cluster.local port=3890 bind_dn=CN=svcduo,OU=people,DC=example,DC=com service_account_username=CN=svcduo,OU=people,DC=example,DC=com service_account_password=### search_dn=OU=people,DC=example,DC=com security_group_dn=CN=duo_access,OU=groups,DC=example,DC=com transport=clear auth_type=plain username_attribute=uid at_attribute=mail
[ldap_server_auto] ikey=### skey=### api_host=###.duosecurity.com port=389 factors=auto failmode=safe
Huh, that looks good
Maybe add uid and mail to the attributes?
wouldn't I want to pass the user I'm trying to login with with filter uid=$username?
instead*
Oh yeah, you can try that
I'll give that a go tomorrow. Thanks for your help! ๐
My pleasure!
Solution
@nitnelave - hey, just wanted to let you know that with the right filter in place I got it to work..
filter in ldap-auth.py
safe_username = escape_filter_chars(os.environ['username']) FILTER = f"(&(uid={safe_username})(memberOf=cn=ha_rw,ou=groups,dc=example,dc=com))"DUO
LDAPMessage(id=3, value=LDAPSearchRequest(baseObject=b'ou=people,dc=example,dc=com', scope=2, derefAliases=3, sizeLimit=0, timeLimit=0, typesOnly=0, filter=LDAPFilter_and(value=[LDAPFilter_equalityMatch(attributeDesc=BEROctetString(value=b'uid'), assertionValue=BEROctetString(value=b'userB')), LDAPFilter_equalityMatch(attributeDesc=BEROctetString(value=b'memberOf'), assertionValue=BEROctetString(value=b'cn=ha_rw,ou=groups,dc=example,dc=com'))]), attributes=[b'displayName']), controls=None)LLDAP
[debug]: | filters: And([UserId(UserId("userB")), MemberOf("ha_rw")]) [debug]: | resolved_attributes: ["displayName"]Also set BaseDN to "ou=people,dc=example,dc=com" and the event Ignoring unknown group attribute ""memberof"" in filter stops showing up in the logs. Does still show Ignoring unknown user attribute "objectcategory" in filter and Ignoring unrecognized group attribute: objectsid, but whatever ๐ I know that in your homeassistant example you have people targeting the graphql API (and that may be the prefered way). Would you think it's also worth adding a section for those who need to use LDAP for certain edge cases? If you do, I wouldn't mind writing something up and sending it to you.
So, objectsid and category are good candidates for adding them to the list of ignored attributes: you know that they don't work and it doesn't matter
That's kinda the point of that list ๐
Oh, but it's in filters, sorry
Yeah, will add them
Does that work with filters?
I don't remember
And yeah, more documentation is always welcome, there's bound to be someone who's going to try the same thing as you
the thing is, those events show up while lldap is still dealing with whatever duo is requesting
So feel free to add to the existing HA docs
and I have no control over that
Right, I was just talking about silencing the warnings
In LLDAP there's a mechanism for saying "don't warn me about this unknown attribute, it's a known unknown"
But I don't remember if it works for filters
I'll add it to the ignore list and will let you know if it does
Works for filters ๐
added to conf file:
ignored_user_attributes = [ "objectcategory" ] ignored_group_attributes = [ "objectsid" ]
Great! To be honest, you're the first person who I know uses that feature ๐คฃ
the authentication & authorization aspect of my homelab is my biggest annoyance with it ๐
I hope LLDAP was useful!
I remember spending weeks setting up openLdap (it took me several tries, and I probably found the worst tutorials in terms of explaining concepts)
You have services who offer no user management at all (longhorn offers write access! without authentication to the block storage), services who only offer local users (no external identity providers), then those who do but no ldap, then those who offer OIDC I can count on one hand, etc.
Yeah... Gating stuff behind authelia sometimes works
You can even sometimes get trusted header support for SSO
so now I have my reverse proxy forwardAuth to Authelia + LLDAP for those who offer no direct integration with external identity providers
and for those with LDAP support (Nextcloud, etc.), I'll skip the Authelia step and offer MFA through DUO+LLDAP
(Authelia also goes through DUO before reaching LLDAP for requests coming from non-trusted networks)
How does it look like with duo? What's the user flow?
For the second factuur
Factor
Oh, you get a push notification
And it just takes super long for duo to respond to the bind request
It's amazing. DUO offers up to 10 users for free. That plan allows me to create an application for Authelia for that integration and also an LDAP proxy application. The DUO authentication proxy simply runs on my k8s cluster and whenever a request to authenticate a user comes in, it reaches out to the "Cisco managed control plane" API to check 1) if the user is enrolled, 2) which policy it needs to follow, 3) which devices the user has, etc.
You can set timeouts at practically every step
it's either push, or sms, etc.
whatever you define for earch user on the control plane
Right. But if the client service expects a bind response in 3s, it'll time out I guess
(although no one sets timeouts...)
Yeah, I set the timeout value in the HA script to 30s
A bit tight for sms ๐
But push should work
Good to know!
true ๐
I'll recommend that in the MFA issue
(although it'd be nice to have a free option baked in, with just totp )
The important bit for me is that I can just have an ldap proxy deal with it
as long as the app supports the protocol, I'm golden
There's one place you can't put MFA: in LLDAP itself
As in, your admin login to LLDAP can't be protected
Which I why I wanted to have at least support for totp
How do you usually deal with SSO?
Currently, you don't
I've kind of given up on it seeing how many apps lack support for OIDC
There's a request for authelia trusted headers support
It "just" needs to be implemented
ok, I'll keep an eye on that ๐
Thanks again for your time! I'll write-up a piece for HA w/ LDAP this week.