OpenLDAP kerberizado
Hasta ahora se mostró como utilizar OpenLDAP con autenticación anónima, es decir, sin autenticación (ver Instalar y configurar el directorio OpenLDAP  y Autenticar con kerberos y almacenar información de usuarios con LDAP en GNU/Linux). Si bien este modelo puede servir en algunos casos donde el servicio de directorio es público, en general se desea que la información sea accesible sólo para ciertos usuarios. De las autenticaciones soportadas en LDAP, la más segura es SASL-GSSAPI con kerberos y sobre conexión encriptada.
En este artículo veremos cómo utilizar GSSAPI, y cómo definir los accesos al directorio en base a los usuarios autenticados con kerberos para obtener una estructura de usuarios centralizada con autenticación segura, similar a Active Directory.
Con esto cierro la serie de artículos Autenticación y administración centralizada de usuarios en GNU/Linux, la cual comencé a publicar en julio del año pasado y contempla todos los pasos necesarios para armar dicha estructura. En el artículo principal pueden encontrar los links a todos los artículos de la serie.


Autenticar con SASL-GSSAPI kerberos

Para poder utilizar SASL-GSSAPI en LDAP, es necesario instalar el API Cyrus SASL compilado para el kerberos de MIT, tanto en el servidor como en clientes:
# apt-get install libsasl2-modules-gssapi-mit

Como vimos en la descripción de kerberos, cada servicio que desee kerberizarse deberá tener un principal en la base de datos. Entonces, para poder utilizar GSSAPI con kerberos necesitamos crear el principal correspondiente. Esto se logra utilizando kadmin o kadmin.local de la siguiente manera:
# kadmin.local
Authenticating as principal root/admin@DEMASIADOVIVO.ORG with password.
kadmin.local: addprinc -randkey ldap/ldap01.demasiadovivo.org@DEMASIADOVIVO.ORG
WARNING: no policy specified for ldap/ldap01.demasiadovivo.org@DEMASIADOVIVO.ORG; defaulting to no policy
Principal "ldap/ldap01.demasiadovivo.org@DEMASIADOVIVO.ORG" created.
donde ldap01.demasiadovivo.org es la dirección del servidor ldap y -randkey permite generar un password random.
El siguiente paso es guardar la clave del principal recién creado en un archivo (keytab), el cual luego se utilizará desde OpeLDAP para la autenticación:
kadmin.local: ktadd -k /root/ldap.keytab ldap/ldap01.demasiadovivo.org
Entry for principal ldap/ldap01.demasiadovivo.org with kvno 5, encryption type AES-256 CTS mode with 96-bit SHA-1 HMAC added to keytab WRFILE:/root/ldap.keytab.
Entry for principal ldap/ldap01.demasiadovivo.org with kvno 5, encryption type ArcFour with HMAC/md5 added to keytab WRFILE:/root/ldap.keytab.
Entry for principal ldap/ldap01.demasiadovivo.org with kvno 5, encryption type Triple DES cbc mode with HMAC/sha1 added to keytab WRFILE:/root/ldap.keytab.
Entry for principal ldap/ldap01.demasiadovivo.org with kvno 5, encryption type DES cbc mode with CRC-32 added to keytab WRFILE:/root/ldap.keytab.
Si no especifican el nombre del archivo (-k), la clave se escribe en /etc/krb5.keytab. Esto no es aconsejable porque este archivo será utilizado por OpenLDAP, para lo cual debemos darle permiso de lectura/escritura, y al hacerlo, le damos acceso a todas las claves que se encuentren en el. Por ello, es mejor separar la clave de OpenLDAP en un archivo propio.

Una vez creado el principal y exportado a un archivo, hay que copiar el mismo al servidor donde se encuentre OpenLDAP y darle permiso de lectura/escritura al usuario openldap, que es el usuario con el que se ejecuta el servicio. Un buen lugar para ubicar el archivo es dentro del mismo directorio de ldap.
/etc/ldap# chown openldap:openldap ldap.keytab
/etc/ldap# chmod 750 ldap.keytab
A continuación hay que editar el archivo /etc/default/slapd para indicar donde se encuentra el keytab para utilizar con SASL, ya que por default intentará usar /etc/krb5.keytab. Esto se hace editando o agregando la siguiente línea:
export KRB5_KTNAME=/etc/ldap/ldap.keytab

OpenLDAP mapea los principals de kerberos a DNs especiales, que tienen el siguiente formato:
uid=<nombre de="" usuario="">,cn=<realm>,cn=<mecanismo>,cn=auth
<nombre de="" usuario=""><realm><mecanismo>
<nombre de="" usuario=""><realm><mecanismo>
o
<nombre de="" usuario=""><realm><mecanismo> uid=<nombre de="" usuario="">,cn=<mecanismo>,cn=auth
<nombre de="" usuario=""><realm><mecanismo><nombre de="" usuario=""><mecanismo>
Por ejemplo, si el principal es demasiadovivo@DEMASIADOVIVO.ORG, éste se mapeará con el DN:
uid=demasiadovivo,cn=DEMASIADOVIVO.ORG,cn=gssapi,cn=auth
es decir, para poder utilizar autenticación kerberos en LDAP, debe existir una entrada en el directorio con el formato mostrado.

Si están utilizando LDAP como directorio de usuarios, el formato de los DN para estos usuarios (uid=demasiadovivo,ou=People,dc=demasiadovivo,dc=org) es muy diferente al recién mostrado (uid=demasiadovivo,cn=DEMASIADOVIVO.ORG,cn=gssapi,cn=auth). Esta diferencia se debe a que una entrada es para usuarios de LDAP y el otro es para usuarios almacenados en LDAP. Una entrada es para autenticar quién es el usuario y qué puede hacer en el directorio, y la otra es para almacenar datos de un usuario.
En el caso de utilizar autenticación kerberos y tomar los datos del usuario de LDAP con NSS, ambos usuarios son el mismo usuario, entonces se necesita una forma de mapearlos.
Para esto, OpenLDAP provee reemplazo de nombres de autenticación utilizando expresiones regulares. Esto es, en lugar de tener que definir dos entradas para el mismo usuario, es posible definir la entrada con los datos del usuario y mapear el DN de autenticación al DN real. La forma de hacerlo es ingresando una regla authz-regexp en el archivo slapd.conf (/usr/share/slapd/slapd.conf en debian). La directiva utiliza dos parámetros:
authz-regexp <patron buscado=""> <patron de="" reemplazo="">
Entonces, ingresando la siguiente regla, se tiene el reemplazo buscado:
authz-regexp
    uid=([^,]*),cn=DEMASIADOVIVO.ORG,cn=gssapi,cn=auth
    uid=$1,cn=People,dc=demasiadovivo,dc=org
Finalmente hay que reiniciar el servicio slapd para que tome los cambios:
# /etc/init/slapd restart
Para probar que la autenticación funciona, adquirimos un token y ejecutamos un ldapwhoami:
$ kinit demasiadovivo
Password for demasiadovivo@DEMASIADOVIVO.ORG:
$ ldapwhoami
SASL/GSSAPI authentication started
SASL username: demasiadovivo@DEMASIADOVIVO.ORG:
SASL SSF: 56
SASL data security layer installed.
dn:uid=demasiadovivo,cn=gssapi,cn=auth
CUIDADO: Asegúrense de que el servidor LDAP tiene resolución DNS inversa (registro PTR), caso contrario pueden encontrarse con un error como el siguiente:
Cannot determine realm for numeric host address
Para ver los mecanismos de autenticación habilitados, se puede ejecutar:
$ ldapsearch -s base -b "" supportedSASLMechanisms

Configurar permisos para acceder LDAP

Hasta ahora la configuración cuenta con autenticación GSSAPI-kerberos que es mucho mejor a tener autenticación simple, o no tener autenticación, pero todavía no se definió ningún rol de usuario que determine qué usuario puede hacer qué.
Por default OpenLDAP cuenta con el usuario admin que posee control total sobre el directorio, y se autentica utilizando autenticación simple con password. Además cuenta con un perfil público a través del cual cualquier persona, utilizando autenticación simple, puede leer los datos del directorio. Como se mostró en el artículo de instalación y configuración de LDAP, estos permisos se pueden observar ejecutando:

# slapcat -b cn=config -a olcDatabase={1}hdb
Como la autenticación la realizaremos a través de kerberos, una configuración interesante es permitir acceso de lectura de atributos no confidenciales a todo usuario autenticado mediante kerberos (en lugar de acceso anónimo), y acceso total a un grupo administrador, cuyos miembros sean usuarios autorizados y autenticados con kerberos.
Para lograr esta configuración, los pasos a realizar son los siguientes:

1. Eliminar al usuario admin y accesos default.
2. Crear un grupo administrador de dominio.
3. Otorgar control total al grupo del paso anterior (como suele ser el grupo Domain Admins en Active Directory).
4. Asignar acceso de lectura a usuarios autenticados.


1. Eliminar usuario admin y accesos default

Por defecto, OpenLDAP provee acceso de escritura para el usuario admin y de lectura sin autenticación. En la configuración de un servicio centralizado de usuarios con kerberos, estos accesos son poco flexibles e inseguros, por lo tanto hay que eliminarlos, y luego plantear nuevos accesos que utilicen usuarios autenticados con kerberos.
Al utilizar la estructura kerberos, los atributos userPassword y shadowLastChange no son necesarios, por lo que se puede eliminar dicho acceso.

Para lograr este objetivo, crear el siguiente LDIF llamado delete-default.ldif:

dn: olcDatabase={1}hdb,cn=config
changetype: modify
#
# Eliminar acceso al usuario admin
delete: olcAccess
olcAccess: {2}to *
by self write
by dn="cn=admin,dc=dvpem,dc=org" write
by * read
-
# Eliminar acceso de lectura sin autenticacion
delete: olcAccess
olcAccess: {1}to dn.base=""
by * read
-
# Eliminar accesos a los atributos password de los usuarios
delete: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange
by self write
by anonymous auth
by dn="cn=admin,dc=dvpem,dc=org" write
by * none
-
# Prohibir acceso al atributo password
add: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange
by * none
-
# Eliminar el usuario admin
delete: olcRootPW
-
y ejecutar el comando:
$ ldapmodify -f delete-default.ldif -x -D cn=admin,cn=config -W
Se puede verificar que todo salió como se deseaba con el comando:
# slapcat -b cn=config -a olcDatabase={1}hdb

2. Crear grupo administrador del dominio

Como muchos sabrán, en Active Directory existe un grupo denominado Domain Admins, el cual tiene permiso de administración sobre todo el directorio y las máquinas unidas al dominio.
En el caso del dominio que se está configurando, es muy útil tener un grupo con las mismas características, que permita administrar el directorio. El siguiente LDIF denominado domain-root.ldif crea dicho grupo, cuyo único miembro (por ahora) es el usuario demasiadovivo:

dn: cn=domain_root,ou=Group,dc=demasiadovivo,dc=org
cn: domain_root
gidNumber: 1000
objectClass: top
objectClass: posixGroup
memberUid: demasiadovivo
Como todavía el único usuario con permiso de administración es admin, se debe utilizar el mismo con la herramienta ldapadd:
$ ldapadd -x -D cn=admin,cn=config -W -f domain-root.ldif
Enter LDAP Password:
adding new entry "cn=domain_root,ou=Group,dc=demasiadovivo,dc=org"

3. Otorgar control total al grupo domain_root sobre el directorio

Este punto es uno de los más complejos. Como vimos, OpenLDAP es muy flexible en cuanto a otorgar permisos y posee una facilidad para otorgar permisos por grupo (by group). El problema es que este se aplica a la clase groupOfNames y no sirve para posixGroup.
Se me ocurrieron varias alternativas para solucionar este problema:

1- Utilizar groupOfNames y mapear los request NSS_LDAP con una expresión regular que sustituya los pedidos, como se mostró en la sección anterior.
2- Editar el esquema de posixGroup para agregar el atributo member.
3- Utilizar la definición de posixGroup del RFC 2307bis, la cual expiró y nunca se aprobó. Esta RFC incluye el campo member en la definición.
4- Otorgar permisos basados en sets.
5- Cambiar el formato de las consultas LDAP de la librería NSS_LDAP para que utilice groupOfNames.
Como se puede observar, opciones hay, pero ninguna satisfactoria del todo. Yo opté por la 4ta, dado que encontré una buena definición del permiso por parte de Pierangelo Masarati. De todas, creo que es la que menos complicaciones trae, al menos para este setup inicial. Si bien el acceso que defino a continuación tiene la misma base que el de Pierangelo, tuve que cambiar el DN del conjunto, porque el original no me funcionó.

Para el permiso definimos el siguiente LDIF denominado domain_root-access.ldif:

dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to dn.subtree="dc=demasiadovivo,dc=org"
by set="([uid=] + ([cn=domain_root,ou=Group,dc=demasiadovivo,dc=org])/memberUid + [,cn=gssapi,cn=auth])/entryDN & user" write
Al principio puede resultar confuso, pero analicemos un poco qué es lo que se está haciendo.
En la primer línea se indica que se está otorgando permiso a todo el dominio demasiadovivo.org. dn.subtree es el scope, e indica que se aplique a todo lo que contiene el dominio.
En la segunda línea se indica a quién se otorga el permiso y qué tipo de permiso (write). Esta es la parte más confusa.
Primero hay que entender qué es lo que se desea otorgar. Lo que debemos hacer es otorgar permiso de escritura a todos los miembros del grupo domain_root. Los miembros se obtienen del atributo memberUid, pero este atributo no es un DN, sino sólo el nombre del usuario. Como el permiso debe otorgarse a un DN, primero hay que armarlo.
El DN de un usuario consta de uid=,cn=gssapi,cn=auth. Lo único que tenemos es el nombre de usuario, por lo tanto, hay que armar el resto del string (los strings se concatenan con el signo +). Para hacerlo se utilizan los siguientes valores:

  • [uid=], para agregar el string uid=
  • ([cn=domain_root,ou=Groups,dc=demasiadovivo,dc=org])/memberUid para agregar el nombre de usuario de los miembros del grupo. Se utilizan los parentesis porque los mismos indican precedencia, de esta forma se evalúa primero el grupo y luego se obtienen los miembros.
  • [,cn=gssapi,cn=auth] para agregar la parte restante del DN
Para obtener el DN definitivo, se encierra todo entre paréntesis y se obtiene el atributo entryDN. El resultado es la lista de todos los usuarios que integran el grupo domain_root. Este conjunto se intersecta (operador &) con la base "user" que referencia al usuario autenticado actualmente. La intersección retornará el DN del usuario, si el usuario está en el grupo, o vacío si no lo está, otorgando o negando el permiso.

Para agregar este nuevo acceso, utilizar ldapmodify con el usuario administrador de la base de datos de configuración (cn=admin,cn=config):

$ ldapmodify -f domain_root-access.ldif -x -D cn=admin,cn=config -W

4. Asignar acceso de lectura a usuarios autenticados

Llegado este punto, ya tenemos el grupo de administración del dominio, pero no los accesos de lectura para que los usuarios lean las entradas. Esto se logra definiendo el siguiente LDIF denominado read-access.ldif:

dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {1}to *
by users read
by * none
que se agrega con:
$ ldapmodify -f read-access.ldif -x -D cn=admin,cn=config -W

Configurar las máquinas cliente para usar GSSAPI

De la configuración anterior tenemos que ahora sólo se puede consultar al servidor LDAP si el cliente se autenticó previamente ante kerberos. Debido a esto, hay que configurar NSS-LDAP de forma especial, para que el servicio pueda consultar el directorio y obtener los datos del usuario que se está autenticando.
Hasta ahora, cuando un usuario se autentica ante el sistema, primero PAM valida las credenciales con kerberos, y luego NSS se conecta de forma anónima al servidor LDAP para traer los datos del usuario. Como ahora no es posible obtener datos de forma anónima, es necesario que NSS utilice GSSAPI con un ticket kerberos. El problema es qué ticket utilizar? dado que no podemos utilizar el ticket que se obtiene al validar al usuario.

Para solucionar este problema, es necesario realizar los siguientes pasos:

1. crear un principal por cada host,
2. exportar la clave de cada principal a keytabs,
3. mover cada keytab al host correspondiente,
4. configurar NSS de cada host para utilizar kerberos y realizar la autenticación con el keytab correspondiente.

Crear principal para host y exportar clave en keytab

Para crear el principal del host maquina01 y exportarlo en el keytab maquina01.keytab, nuevamente hacemos uso de la herramienta kadmin.local (o kadmin si se conectan remoto), y ejecutamos lo siguiente:

kadmin.local: addprinc -randkey host/maquina01.dvpem.org@DVPEM.ORG
WARNING: no policy specified for host/maquina01.dvpem.org@DVPEM.ORG; defaulting to no policy
Principal "host/maquina01.dvpem.org@DVPEM.ORG" created.
kadmin.local: ktadd -k /root/maquina01.keytab host/maquina01.dvpem.org
Entry for principal host/maquina01.dvpem.org with kvno 2, encryption type AES-256 CTS mode with 96-bit SHA-1 HMAC added to keytab WRFILE:/root/maquina01.keytab.
Entry for principal host/maquina01.dvpem.org with kvno 2, encryption type ArcFour with HMAC/md5 added to keytab WRFILE:/root/maquina01.keytab.
Entry for principal host/maquina01.dvpem.org with kvno 2, encryption type Triple DES cbc mode with HMAC/sha1 added to keytab WRFILE:/root/maquina01.keytab.
Entry for principal host/maquina01.dvpem.org with kvno 2, encryption type DES cbc mode with CRC-32 added to keytab WRFILE:/root/maquina01.keytab.
Se utiliza -randkey para generar una clave aleatoria, dado que se utilizará directamente desde el archivo maquina01.keytab.

El paso anterior podría haberse realizado directamente desde el host maquina01, utilizando el comando kadmin y descargando la clave a un keytab local (/etc/krb5.keytab). En resultado es el mismo.


Configurar NSS-LDAP para utilizar kerberos en los clientes

Una vez generado el principal del host maquina01, hay que mover el keytab recién creado al host correspondiente. Un buen lugar para colocar el keytab es en /etc/krb5.keytab, que es el archivo default de kerberos. Por seguridad, el archivo debe poseer permisos 640:

# chmod 640 /etc/krb5.keytab
Con la clave en su lugar, procedemos a configurar NSS-LDAP. En debian el archivo de configuración se encuentra en /etc/nslcd.conf. En dicho archivo se deben agregar las siguientes líneas:
sasl_mech gssapi
krb5_ccname FILE:/tmp/krb5cc_0

donde:

- sasl_mech indica el mecanismo SASL a utilizar (GSSAPI)
- krb5_ccname informa donde se encuentra el ticket kerberos
Una vez especificados los parámetros anteriores, nslcd comenzará a utilizar k5start para obtener el ticket del host antes de acceder a LDAP. Este demonio permite configurar algunos aspectos de cómo utilizar k5start a través del archivo /etc/default/nslcd. En el mismo se puede forzar la ejecución de k5start, como configurar la ubicación de la clave, el nombre del principal, y cada cuánto refrescar el ticket. La configuración por default es buena para la mayoría de los casos, aunque recomiendo agregar las siguientes líneas:
K5START_START="yes"
K5START_PRINCIPAL="host/maquina01.dvpem.org"
donde K5START_START="yes" fuerza el uso de k5start, y K5START_PRINCIPAL="host/maquina01.dvpem.org" indica cuál es el nombre del principal.
Si todavía no tienen instalado kstart, pueden hacerlo con:

# apt-get install kstart
Al finalizar la configuración, reiniciar el demonio nslcd para que tome los cambios:
# /etc/init.d/nslcd restart
Para probar, simplemente realizar un login con una cuenta que esté definida en LDAP. También pueden ver que se consulta LDAP al utilizar el comando:
$ getent passwd
En caso de que no esté funcionando y debamos revisar la configuración, primero detener el demonio nscd, dado que este cachea los resultados de las consultas, y por lo tanto, el resultado que se obtiene puede no ser el real:
# /etc/init.d/nscd stop
Luego revisar los siguientes logs:
- en el cliente:
/var/log/auth.log
- en el servidor:
/var/log/debug
Otra ayuda es observar en el cliente si el proceso login se conecta al servidor LDAP al momento de autenticar, con el siguiente comando:
# lsof -r 2 -i
donde:
-r 2 pone lsof en modo repetición. De esta forma lsof lista lo seleccionado por otros argumentos, espera 2 segundos y vuelve a listar.
-i lista los procesos que estan escuchando en sockets tcp o udp.

Referencias

- OpenLDAP - 15. Using SASL 

- LDAP, Kerberos 5, SASL and Passwords 
- Debian OpenLDAP with Kerberos Authentication
- Red Hat Documentation - 10.2.6. Setting Up Kerberos Authentication
- OpenLDAP - 5. Configuring slapd
- LDAP Administration Guide
- OpenLDAP FAQ - Sets in Access Controls
- Integrated Kerberos-OpenLDAP client on Debian lenny
- Configuring LDAP Authentication

- FreeBSD Forum - nss_ldap sasl gssapi authentication?
- RedHat mailing list - nss_ldap using sasl with gssapi. Kerberos credentials cache problem[Scanned]