My experience/guide setting up OpenLDAP for PC/webapp authentication on Ubuntu 20.04.
This was originally posted Here but I'm cross-posting it to my site for preservation and self-reliance.
I've been experimenting with user and account management solutions for my home lab, and I was looking for something that could manage posix users and web users. I wanted something with group management and OU hierarchies that sort of follow what you would use in Active Directory. I found a lot of issues with some of the more well recommended solutions out there:
- Samba: It's basically an AD Domain Controller which was more than I needed; also while being feature-familiar with AD is nice, I'm not looking to implement AD, I'm looking to really stick closer to just LDAP.
- GLAuth: Interesting project, but is somewhat limited in the attributes it can handle, how group management works, how the OU structure is reflected in group membership, and usability. GLauth-ui is an interesting component for usability in that it allows for user password resets and such, however it's not mature enough yet for what I need.
- FreeIPA: It isn't an option on Ubuntu Servers anymore which is my target platform; since I believe 18.04 there are dependency resolution issues and the issue trackers indicate there is no path to resolution.
- Æ-DIR: This seemed interesting, but at the time I looked into it there was a deal breaker which I can no longer remember; I think it didn't do both posix auth and web-app integration well enough, but it did one or the other well? I'm not sure, it's been a while.
Part of all this was wanting to learn more about how LDAP worked in general, so I had a driver in the back of my mind to stick to pure openLDAP. While that seemed very intimidating at first, it's become the solution at the top of my list. That being said, I don't think it's great out-of-the-box for the average homelab setup. The default objectClasses inherit from top and have attributes that overlap in undesireable ways, so a user created in one of these classes for posix authentication may not work well for, say, a webapp whose privileges are assigned by OU or group. To this end, I was driven to create a custom schema with objectClasses that are more reflective of what I believe someone would want in a homelab. There's a lot of counter-intuitive stuff here, which I want to document for anyone else going down this path. This link has been a tremendous resource: https://www.openldap.org/doc/admin22/schema.html
Custom LDAP schemas should be globally unique, and that is handled through an OID. To avoid OID conflict, you should go here and request your own OID: https://pen.iana.org/pen/PenApplication.page The turnaround is like a week. With this, I could create objectClasses like "surfrock66User" just for me, or if someone wanted to be more enterprising and create a worldwide-useful objectClass like "homeLabUser" they could get an OID to hold. For my purposes, since I'm experimenting with changing schema, it makes sense to have my own id space to play in. There is also room for using 1.1 based OID's as kind of unreserved space, kind of like 192.168.1.* for IPs. When you get your final OID, it'll be of the format "1.3.6.1.4.1._____." and you append your structure after that.
I need 2 custom objectClasses (Users and Groups), but will be experimenting with a third as well later (Devices); I separate the third as I don't really have a use case, so I don't feel confident in an attribute list that would represent a custom class yet. One thing that is an issue, I expect group membership to be reflected both in the group and the user; that's not part of OpenLDAP to start, you need to install the memberOf overlay. Here's a good guide: https://kifarunix.com/how-to-create-openldap-member-groups/
Below are the attributes in the default relevant objectClasses:
objectclass ( 2.5.6.6 NAME 'person'
DESC 'RFC2256: a person'
SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )
objectclass ( 2.5.6.7 NAME 'organizationalPerson'
DESC 'RFC2256: an organizational person'
SUP person STRUCTURAL
MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $
postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )
objectclass ( 2.5.6.10 NAME 'residentialPerson'
DESC 'RFC2256: an residential person'
SUP person STRUCTURAL
MUST l
MAY ( businessCategory $ x121Address $ registeredAddress $
destinationIndicator $ preferredDeliveryMethod $ telexNumber $
teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ preferredDeliveryMethod $ street $
postOfficeBox $ postalCode $ postalAddress $
physicalDeliveryOfficeName $ st $ l ) )
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount'
DESC 'Abstraction of an account with POSIX attributes'
SUP top AUXILIARY
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $ description ) )
objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount'
DESC 'Additional attributes for shadow passwords'
SUP top AUXILIARY
MUST uid
MAY ( userPassword $ shadowLastChange $ shadowMin $
shadowMax $ shadowWarning $ shadowInactive $
shadowExpire $ shadowFlag $ description ) )
objectclass ( 2.16.840.1.113730.3.2.2
NAME 'inetOrgPerson'
DESC 'RFC2798: Internet Organizational Person'
SUP organizationalPerson
STRUCTURAL
MAY (
audio $ businessCategory $ carLicense $ departmentNumber $
displayName $ employeeNumber $ employeeType $ givenName $
homePhone $ homePostalAddress $ initials $ jpegPhoto $
labeledURI $ mail $ manager $ mobile $ o $ pager $
photo $ roomNumber $ secretary $ uid $ userCertificate $
x500uniqueIdentifier $ preferredLanguage $
userSMIMECertificate $ userPKCS12 ) )
objectclass ( 2.5.6.9 NAME 'groupOfNames'
DESC 'RFC2256: a group of names (DNs)'
SUP top STRUCTURAL
MUST ( member $ cn )
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
objectclass ( 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 ) )
Additionally, I wanted the sshPublicKey option from an optional schema http://pig.made-it.com/ldap-openssh.html
Out of this, I am proposing the following new schema for my lab...it has a lot of the other attributes in as MAY even though I have no plan to populate them, that being said if a day comes where I use something requiring an LDAP attribute I didn't build in I could see it not working:
objectclass ( 1.1.2.2.1 NAME 'domainAccount'
DESC 'A user/account/person in the organization'
SUP top STRUCTURAL
MUST ( cn $ name $ sn $ uid )
MAY (
audio $ businessCategory $ carLicense $ departmentNumber $
description $ destinationIndicator $ displayName $
employeeNumber $ employeeType $ facsimileTelephoneNumber $
gecos $ gidNumber $ givenName $ homeDirectory $ homePhone $
homePostalAddress $ initials $ internationaliSDNNumber $
jpegPhoto $ l $ labeledURI $ ldapPublicKey $ loginShell $
mail $ manager $ mobile $ o $ ou $ pager $ photo $
physicalDeliveryOfficeName $ postalAddress $ postalCode $
postOfficeBox $ preferredDeliveryMethod $
preferredLanguage $ registeredAddress $ roomNumber $
secretary $ seeAlso $ shadowExpire $ shadowInactive $
shadowLastChange $ shadowMax $ shadowMin $ shadowWarning $
sshPublicKey $ st $ street $ telephoneNumber $
teletexTerminalIdentifier $ telexNumber $ title $
uidNumber $ userCertificate $ userPassword $ userPKCS12 $
userSMIMECertificate $ x121Address $ x500uniqueIdentifier ) )
objectclass ( 1.1.2.2.2 NAME 'domainGroup'
DESC 'A group of names in the organization'
SUP top STRUCTURAL
MUST ( cn $ member )
MAY (
businessCategory $ description $ gidNumber $ o $ ou $
owner $ seeAlso ) )
Here's a more commented breakdown of the classes:
- https://www.surfrock66.com/ldap/domainAccount.description.txt
- https://www.surfrock66.com/ldap/domainGroup.description.txt
Per the documents, it's recommended to add this to your schema directory in its own .schema file, then include that in the slapd config AFTER the other includes (as attributes need to be previously defined). On ubuntu, you can install the schema2ldif package and it will help to install custom schema; you put the .schema file in the schema directory (/etc/ldap/schema/) and do "ldap-schema-manager -i /etc/ldap/schema/surfrock66Schema.schema".
To manage an LDAP environment, especially for things like user password resets, you'll want a simple web interface. Everyone will tell you to use LDAP Account Manager (LAM) which is awesome and more modern, however if you're using custom schema it will not support it unless you pay for LAM Pro which is like $260/year, and is not worth it in my opinion for a small homelab. To that end I fell back to phpLDAPAdmin which is kind of abandoned, but still works. The OS packages in the Ubuntu repositories won't work as they don't work with php7.2+, but there are installation instructions for the 1.2 branch on the github here: https://github.com/leenooks/phpLDAPadmin If you have an existing apache environment, it is very straightforward, just extract the zip file, create a site in apache, and edit the config.php. You'll do initial configuration with the admin user, using session bind. Once we get down the road, users can reset their passwords using phpLDAPAdmin, but it's not super easy...given the scope of my home lab this guide is good enough: https://www.linvirtshell.com/2017/11/openldap-user-password-reset-by-using.html
If I remove all the comments from my config.php file, this is the resultant configuration and it works well for me:
custom->appearance['hide_template_warning'] = true;
$config->custom->appearance['friendly_attrs'] = array(
'facsimileTelephoneNumber' => 'Fax',
'gid' => 'Group',
'mail' => 'Email',
'telephoneNumber' => 'Telephone',
'uid' => 'User Name',
'userPassword' => 'Password'
);
$servers = new Datastore();
$servers->newServer('ldap_pla');
$servers->setValue('server','name','Surfrock66\'s LDAP Server');
$servers->setValue('server','host','127.0.0.1');
$servers->setValue('server','port',389);
$servers->setValue('server','base',array('dc=example,dc=com'));
$servers->setValue('login','auth_type','session');
$servers->setValue('login','attr','dn');
$servers->setValue('login','base',array('ou=accounts,dc=example,dc=com'));
$servers->setValue('login','class',array('domainAccount'));
$servers->setValue('login','bind_dn_template','cn=%s,ou=accounts,dc=example,dc=com');
$servers->setValue('appearance','show_create',false);
$servers->setValue('appearance','open_tree',true);
$config->custom->session['reCAPTCHA-enable'] = false;
$config->custom->session['reCAPTCHA-key-site'] = '';
$config->custom->session['reCAPTCHA-key-server'] = '';
?>
Since we're using custom schema, we need to add templates for them so the UI can create my users. I'm not gonna dump the xml for that here, but it's well documented, you just create a copy of existing templates. http://phpldapadmin.sourceforge.net/wiki/index.php/Templates I will provide links to my templates below.
SO. TLDR; Let's say you just want to do everything I did. Here's some prototype commands that should get you started, with downloads of the files I made/collected:
sudo apt update
sudo apt -y install slapd ldap-utils schema2ldif
# cd web root
sudo wget https://github.com/leenooks/phpLDAPadmin/archive/refs/tags/1.2.6.2.zip
sudo unzip 1.2.6.2.zip
sudo rm 1.2.6.2.zip
sudo mv phpLDAPadmin-1.2.6.2 phpLDAPadmin
sudo cp phpLDAPadmin/config/config.php.example phpLDAPadmin/config/config.php
sudo vi phpLDAPadmin/config/config.php
# Configure for your needs, ask google and there are lots of guides
sudo cd phpLDAPadmin/templates/creation
sudo wget https://www.surfrock66.com/ldap/custom_domainAccount.xml
# Edit the above file if you want to re-order or remove any attributes from the account creation form
sudo wget https://www.surfrock66.com/ldap/custom_domainGroup.xml
# Edit the above file if you want to re-order or remove any attributes from the group creation form
sudo cd ../../../
# if your apache is not running under www-data, change this command to that user
sudo chown -R www-data:www-data phpLDAPadmin
sudo cd /etc/ldap/schema
sudo wget https://www.surfrock66.com/ldap/2.schema.attribute.sshPublicKey.schema
sudo wget https://www.surfrock66.com/ldap/3.schema.objectClass.domainAccount.schema
# Edit the above file with your own OID if you requested one
sudo wget https://www.surfrock66.com/ldap/4.schema.objectClass.domainGroup.schema
# Edit the above file with your own OID if you requested one
mv 2.schema.attribute.sshPublicKey.schema ldapPublicKey.schema
mv 3.schema.objectClass.domainAccount.schema domainAccount.schema
mv 4.schema.objectClass.domainGroup.schema domainGroup.schema
cd ~/
wget https://www.surfrock66.com/ldap/1.config.InstallMemberOf.ldif
wget https://www.surfrock66.com/ldap/5.ous.ldif
# Edit this file with your actual domain name
sudo ldapadd -Y EXTERNAL -H ldapi:/// -f 1.config.InstallMemberOf.ldif
sudo ldapadd -Y EXTERNAL -H ldapi:/// -f 5.ous.ldif
sudo ldap-schema-manager -i /etc/ldap/schema/ldapPublicKey.schema
sudo ldap-schema-manager -i /etc/ldap/schema/domainAccount.schema
sudo ldap-schema-manager -i /etc/ldap/schema/domainGroup.schema
sudo systemctl restart slapd
At this point, you should be able to log into phpLDAPadmin using the admin account created in the slapd package config. You should make your groups before making your users; this could be changed if you moved the gidNumber from the "Must" to "May" of the domainAccount schema.
Once this is done, create 2 users using the GUI to test that things work; I made one for me, and one for ldap binds (so I can tattoo a minimally privileged account with an unimportant password to apps for binding) called ldapbinduser. The last step is to tattoo olcAccess rules to your configuration. My desired rules are the following:
- My user, for now, is like a Domain Admin that can manage everyting
- The ldapbinduser account will be used for binds, meaning it needs to authenticate and read the users and groups (basically the whole directory)
- Any user should be able to read their own attributes, authenticate, and change their own password
- Everything else is denied.
To that end, I made an ldif file called "accessRules.ldif" with the following contents, this article was instrucmental in making this: https://medium.com/@moep/keeping-your-sanity-while-designing-openldap-acls-9132068ed55c :
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to *
by dn="cn=admin,dc=example,dc=com" manage
by dn="cn=surfrock66,ou=accounts,dc=example,dc=com" manage
by dn="cn=ldapbinduser,ou=accounts,dc=example,dc=com" read
by * break
-
add: olcAccess
olcAccess: {1}to dn.children="ou=accounts,dc=example,dc=com" attrs=userPassword,shadowExpire,shadowInactive,shadowLastChange,shadowMax,shadowMin,shadowWarning
by self write
by anonymous auth
-
add: olcAccess
olcAccess: {2}to dn.subtree="dc=example,dc=com"
by self read
To get that into your configuration, use the following command:
ldapmodify -Y EXTERNAL -H ldapi:/// -f accessRules.ldif
With that, I have a working openLDAP setup. Applications and Linux computers are authenticating and pulling attributes. Based on what I've done here, it can be pretty easily adapted at a bunch of different points, but this is all the info I collected over the course of my research and should help to guide someone starting a similar journey.