What is the easiest way to set up composable POSIX groups for SSH & Samba authentication?

Background

I'm putting together what I would consider to be a fairly ordinary chunk of infrastructure, but have been running into so many problems that I can't help but wonder if there's an easier way.


I need to be able to do the following:

  • Securely share files from a Linux server to several hundred users on OSX, Linux, and Windows machines with delegated authentication and group-based permissions.
  • Permit group-based SSH access to dozens of Linux servers, with delegated authentication and group-based permissions.
  • Be able to create groups composed of both other groups and users, preferably to an arbitrary depth.
  • Be able to permit self-service for basic tasks (password changing and recovery, limited editing of one's own information, etc)
  • Be able to migrate existing servers (running various flavors of Linux) with minimal configuration - so sticking to "standard" configuration wherever possible, especially client-side.

I'd like to be able to have the options to do the following:

  • Support public key authentication in addition to password-based authentication

All of the above sounds to me like LDAP + Samba is the only practical way to go, especially since the previous implementation used FreeIPA/Samba. The team settled on using OpenLDAP with LDAP Account Manager to provide the authentication and directory services, but the implementation has been a nightmare.


OpenLDAP setup

My directory tree has the following structure:

 - dc=example,dc=com
   - ou=groups
     - cn=groupA
     - ...
   - ou=policies
     - cn=passwordDefault
     - ...
   - ou=services
     - cn=service1
     - ...
   - ou=users (
     - uid=user1
     - ...

Users have the following classes:

  • inetOrgPerson (structural)
  • posixAccount
  • shadowAccount
  • sambaSamAccount
  • ppolicyUser
  • passwordSelfReset
  • ldapPublicKey
  • generalInformation

Users work great. Most of the Linux machines are using sshd -> libpam-ldap -> libnss-ldapd -> nscd -> nslcd and coreutils -> libnss-ldapd -> nscd -> nslcd, so getent passwd and id [username] work without any special configuration at all.

Services have the following classes:

  • applicationProcess (structural)
  • simpleSecurityObject

Services are just simple DN/pw entities for in-house services like Gitlab such that contact LDAP directly, so that we can turn off anonymous binding without cutting them off. They also work wonderfully. (Note - a few services actually need to exist as POSIX accounts, so they're configured as users for simplicity)

Policies are entities such as password policies, which fall outside the scope of this question.

Groups have been the real problem, and so I will discuss them in detail in the next section.


The Problem With Groups

No matter what I do, I cannot seem to create composable POSIX-compliant groups. In this context, I'm using "composable" to mean the following. Suppose these groups are configured - GroupA (User1, User2, User3), GroupB (User4, User5), and GroupC (User1, User5). Composable groups would allow the creation of a dynamic GroupD (GroupA, GroupC), with an effective membership of (User1, User2, User3, User5). Ideally, GroupD could also be defined as (GroupA, GroupC, User6). Also, ideally you could continue nesting groups more than one layer deep.

OpenLDAP and LAM have some tools that seem intended for this sort of thing, but I keep running into schema issues, design issues, implementation issues, or some combination of the three.

  • dynlist - This OpenLDAP overlay allows you to populate lists of attributes on-the-fly based on an LDAP filter. You can even have hybrid groups made up of both users and other groups! Unfortunately, the overlay only triggers when the entity it's attached to is being viewed directly. This means that getent group [group] (which looks at one or more groups directly) works, but groups [user] (which searches for a user's presence in groups) does not. This makes it impossible to SSH based on a dynamic group without writing your own mapping layer into nss or other LDAP consumers. Also, probably for similar reasons, you can't create a dynlist group from other dynlist groups.

  • memberof - This OpenLDAP overlay will automatically update memberOf attributes for users to correspond to the user's addition to and removal from groups. It seems to require using a group class other than the RFC2307-based posixGroup class, however, since memberof requires DN-based membership, rather than uid-based membership. It also doesn't actually help with managing the groups themselves.

  • autogroup - This OpenLDAP overlay will automatically add or remove user DNs from a configurable attribute, corresponding to the user's presence or absence in the results of a configurable LDAP filter. This overlay seems to require that memberof be installed, as installing autogroup fails unless I included the olcAGmemberOfAd configuration attribute. It also seems to require DN-based membership, so like memberof it needs a class other than RFC2307-based posixGroup

I initially tried to implement static groups using the class posixGroup (structural), and dynamic groups with an additional labeledURIObject class to attach a dynlist filter. This basically worked, but I ran into dynlist's shortcomings outlined above.

I then tried using the autogroup overlay instead, but ran into the issue where it seemed to expect DN-based memberships. At this point, I felt I had to try to get away from posixGroup as the structural class, as it seemed nothing was written to support it. LAM seems to prefer using a combination of groupOfNames and RFC2307bis-posixGroup, as it includes a feature to automatically sync member attributes with memberUid attributes when both are present on the same object. So I modified the posixAccount schema that ships with OpenLDAP to make it auxiliary instead of structural, and made groups with the groupOfNames (structural) and modified posixAccount classes. Hacking apart OpenLDAP's objectClass schemas to trick it with an RFC2307bis-esque class felt like Not The Right Thing To Do™, but OpenLDAP/LAM appeared to accept it. Doing this also allowed me to install memberof successfully, which was nice.

autogroup, however, only half works. While it will add users to dynamic groups (which are visible with groups [user]), it won't remove them when they're removed from the base group. groupOfNames also requires that its members field have at least one user in it at all times, which is problematic for use with autogroup. You have to set it up with an initial member, but then can't remove the dummy member once autogroup locks the member attribute.


Config

cn=module{0},cn=config

dn: cn=module{0}
objectClass: olcModuleList
cn: module{0}
olcModulePath: /usr/lib/ldap
olcModuleLoad: {0}back_mdb
olcModuleLoad: {1}ppolicy
olcModuleLoad: {2}autogroup
olcModuleLoad: {3}memberof

olcOverlay={1}memberof,olcDatabase={1}mdb,cn=config

dn: olcOverlay={1}memberof
objectClass: top
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcMemberOf
olcOverlay: {1}memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf

olcOverlay{2}autogroup,olcDatabase={1}mdb,cn=config

olcOverlay: {2}autogroup
structuralObjectClass: olcAutomaticGroups
olcAGattrSet: {0}labeledURIObject labeledURI member
olcAGmemberOfAd: memberOf

The Questions

Most specific to least:

  1. Why is autogroup able to add users to dynamic groups, but not remove them?
  2. Is there an easier way to implement this use-case?
  3. Am I using the wrong tools for the job? Would this use-case be significantly easier with a different LDAP implementation, some simple client-side configuration, or something non-LDAP altogether?

It turned out that the best approach here was to let the LDAP clients do the heavy lifting:

I set up groups as consisting structurally of groupOfNames with RFC2307bis-posixGroup attached. As my environment didn't support RFC2307bis-posixGroup directly, I put together an LDIF to make OpenLDAP remove its existing definition of posixGroup and replace it with an auxiliary copy:

DO NOT modify your configuration with this as-is! If you choose to go this route, locate your server's posixGroup configuration, and swap it with an equivalent configuration with STRUCTURAL changed to AUXILIARY.

dn: cn={2}nis,cn=schema,cn=config
changetype: modify
delete: olcObjectClasses
olcObjectClasses: {2}( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )
-
add: olcObjectClasses
olcObjectClasses: {2}( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top AUXILIARY MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )

The server could then pretend to support the RFC2307bis-posixGroup specification. From there, I was able to add memberOf and complete the OpenLDAP configuration.

The key was that modern versions of NSLCD and SSSD both contain flags permitting recursive group searching. Apparently this feature is called "nested" grouping, which is a bit confusing in the context of an LDAP directory structure that also permits groups to be nested inside each other. These features aren't about the LDAP structure, however, they're about being able to follow references to groupOfNames inside of other groupOfNames.

nslcd: nss_nested_groups yes|no

If this option is set, the member attribute of a group may point to another group. Members of nested groups are also returned in the higher level group and parent groups are returned when finding groups for a specific user. The default is not to perform extra searches for nested groups.

sssd: ldap_group_nesting_level (integer)

If ldap_schema is set to a schema format that supports nested groups (e.g. RFC2307bis), then this option controls how many levels of nesting SSSD will follow. This option has no effect on the RFC2307 schema. Default: 2

There are some caveats - Red Hat prefers that you use SSSD, so the version of nslcd in the Red Hat repository isn't new enough to support the feature. But this approach should fit most use cases. Ultimately, reconfiguring a flag on all the servers using sssd or nslcd was much easier than trying to get OpenLDAP to do "nested" grouping all on its own.

I can now reference groups in other groups in the LDAP tree, and getent group will now list each group and a flattened list of all the users that are referenced in the group directly, or within referenced groups recursively.