I wanted to allow users to log in with their personal SSH keys on our AWS instances so that I wouldn’t have to neither hand over the main key to everyone and neither copy everyone’s key to all machines. The OS for this were Amazon Linux and CentOS.
Install OpenLDAP
[user@host]# yum install openldap-servers
Set root DN and admin
Create password hash with slappasswd:
[user@host]# slappasswd New password: Re-enter new password: {SSHA}Ky1iHfH03xYvjCjkZcm1e0sWtlS7T/KY
Create a file init.ldif with this content:
dn: olcDatabase={2}bdb,cn=config changetype: modify replace: olcSuffix olcSuffix: dc=example,dc=com - replace: olcRootDN olcRootDN: cn=Manager,dc=example,dc=com - replace: olcRootPW olcRootPW: {SSHA}Ky1iHfH03xYvjCjkZcm1e0sWtlS7T/KY
Copy the password hash you created with slappasswd and replace the one in the example above. If you copied the hash, the password is “1” (the number one). You should also pick the correct root DN for your system. This is usually a domain name or an organization name and the country.
Load this file into OpenLDAP:
[user@host]# ldapmodify -Y EXTERNAL -H ldapi:/// -f init.ldif
Now create the base structure. We start with the organization. Copy the text below into a file called org.ldif:
dn: dc=example,dc=com objectClass: top objectClass: dcObject objectClass: organization dc: example.com o: example.com
And we load it with
[user@host]# ldapadd -Y EXTERNAL -D "cn=Manager,dc=example,dc=com" -W -H ldapi:/// -f org.ldif
Repeat the same for the base entry for user accounts in user-org.ldif:
dn: ou=Users,dc=at ou: Users objectClass: top objectclass: organizationalunit
[user@host]# ldapadd -Y EXTERNAL -D "cn=Manager,dc=example,dc=com" -W -H ldapi:/// -f user-org.ldif
Now we load the schema that will allow us to store the SSH key in LDAP. Create the file openssh-lpk.ldif with this content:
dn: cn=openssh-lpk,cn=schema,cn=config objectClass: olcSchemaConfig cn: openssh-lpk olcAttributeTypes: ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' DESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) olcObjectClasses: ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY DESC 'MANDATORY: OpenSSH LPK objectclass' MAY ( sshPublicKey $ uid ) )
And load it once more with:
[user@host]# ldapadd -Y EXTERNAL -D "cn=Manager,dc=example,dc=com" -W -H ldapi:/// -f openssh-lpk.ldif
Now everything is in place to start creating users. I recommend that you do this with a UI or a script. But for the purpose of this post we’ll once again create an LDIF. This file is called user.ldif:
dn: uid=username,ou=Users,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount objectClass: ldapPublicKey cn: username sn: username loginShell: /bin/bash uidNumber: 1000 gidNumber: 1000 homeDirectory: /home/username sshPublicKey: dGhpcyBpcyBqdXN0IGFuIGV4YW1wbGUK
Here you must change all occurrences of “username” to match the username you want to add. You might also note the absence of a password. “userPassword” is marked “optional”, so we can omit it because we’re using a SSH key.
The key itself is the public key of the user, Base64-encoded. You can do this on the command line:
[user@host]# base64 id_rsa.pub dGhpcyBpcyBqdXN0IGFuIGV4YW1wbGUK
Once everything is in place we can load the user:
[user@host]# ldapadd -Y EXTERNAL -D "cn=Manager,dc=example,dc=com" -W -H ldapi:/// -f user.ldif
Now let’s test if we can retrieve the key!
[user@host]# ldapsearch -x -H ldap://hostname:389 '(&(objectClass=posixAccount)(uid='"username"'))' -b "ou=Users,dc=at" 'sshPublicKey'
Once again, substitute “username” for your username. Also change “hostname” to the address of your OpenLDAP server.
This should return the key we just added:
# extended LDIF # # LDAPv3 # base <ou=Users,dc=at> with scope subtree # filter: (&(objectClass=posixAccount)(uid=kengelke)) # requesting: sshPublicKey # # kengelke, Users, at dn: uid=username,ou=Users,dc=example,dc=com sshPublicKey:: dGhpcyBpcyBqdXN0IGFuIGV4YW1wbGUK # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1
Now it’s time to configure SSHd to use LDAP to authenticate users.
The first part is easy, setting up SSHd itself.
The first step is to make sure we have the LDAP clients, like ldapsearch:
[user@home]# yum install openldap-clients nss-pam-ldapd
Next we create a script that will query LDAP and return the decoded SSH key. Create the file /usr/local/bin/query-ldap.sh:
#!/bin/sh ldapsearch -x -H ldap://10.0.1.217:389 '(&(objectClass=posixAccount)(uid='"$1"'))' -b "ou=Users,dc=at" 'sshPublicKey' | sed "/^sshPublicKey:: /{:l N;/\n./{s/\n //;bl}}" | egrep "^sshPublicKey" | sed 's/sshPublicKey:: //' | base64 -d
This script takes the LDIF above, extracts the SSH key and decodes the Base64. The result is something SSHd can work with. Now we tell the daemon to use this script. Edit /etc/ssh/sshd_config and add these two lines:
AuthorizedKeysCommand /usr/local/bin/query-ldap.sh AuthorizedKeysCommandUser nobody
The second lines tells the daemon to run the script as a user with low privileges, which is good practice.
Now we can already log in with our key but only if “username” exists on the local system. We also need to set up PAM to query LDAP for information like uid, gid and home directory.
We start with /etc/nsswitch.conf. Open it and set “ldap” for “passwd”, “shadow” and “group”:
passwd: ldap files shadow: ldap files group: ldap files
In this example LDAP is the primary source for authentication information. You can also configure it to check local files first.
Next we edit /etc/pam.d/system-auth. Mine looks like this:
auth sufficient pam_ldap.so use_first_pass auth required pam_env.so auth sufficient pam_unix.so nullok try_first_pass auth requisite pam_succeed_if.so uid >= 1000 quiet_success auth required pam_deny.so account sufficient pam_ldap.so account required pam_unix.so account sufficient pam_localuser.so account sufficient pam_succeed_if.so uid < 1000 quiet account required pam_permit.so password sufficient pam_ldap.so use_first_pass password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type= password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok password required pam_deny.so session optional pam_keyinit.so revoke session required pam_limits.so -session optional pam_systemd.so session optional pam_ldap.so session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid session required pam_unix.so
TODO add pam_mkhomedir
TODO /etc/nslcd.conf
base dc=example,dc=com base passwd ou=Users,dc=example,dc=com base shadow ou=Users,dc=example,dc=com uri ldap://hostname/ scope sub