sábado, 27 de enero de 2024

Ansible - Estructura de proyectos I

Cuando trabajamos en proyectos de automatización con Ansible, lo normal es que tengamos una carpeta para cada uno de los proyectos. En dichas carpetas guardaremos el fichero de configuración de Ansible, el fichero de inventario, playbooks, etc.

En este post veremos como organizar dicha carpeta para aprovechar las funcionalidades de Ansible de la forma más simple y eficiente posible, además de ser la aproximación recomendada.

Hasta ahora, hemos tenido una carpeta donde, al menos, necesitamos el fichero de configuración de Ansible y el inventario. En el fichero de inventario, para comandos ad-hoc simples, hemos añadido en ocasiones variables, ya fuese a grupos o a hosts, para poder realizar determinadas operaciones. Para poder asignar de forma mucho más ordenada y clara variables a cada grupo de hosts, o a hosts individuales, Ansible recomienda que, en nuestra carpeta de proyecto, tengamos presentes las carpetas group_vars y host_vars. Con esto en mente, la estructura más básica de una carpeta de proyecto Ansible sería algo como lo siguiente:

Estructura mínima de un proyecto de Ansible.

Como es lógico y hemos visto hasta ahora, el contenido del fichero ansible.cfg debería tener las variables de configuración necesarias, en función de nuestro entorno y necesidades, pero al menos, debería incluir la ruta a nuestro fichero de inventario, el nombre del usuario remoto empleado para la conexión así como las opciones de escalado de privilegios que necesitemos.

¿Cual es la función de las carpetas group_vars y host_vars? contener ficheros cuyos nombres coincidirán con los nombres de grupos de hosts, en el caso de la carpeta group_vars, o de hosts individuales en el caso de la carpeta host_vars, de acuerdo con las entradas de nuestro inventario. En dichos ficheros podremos incluir variables, tanto de ansible como definidas por nosotros, para su uso durante la ejecución de playbooks o comandos ad-hoc. Es importante tener en cuenta que estos ficheros deben estar en formato YAML.

Para verlo con un ejemplo simple, supongamos que tenemos el grupo de hosts de bases de datos y el de servidores web. Definimos que, el paquete que debe instalarse en los servidores web, será la última versión disponible de Apache, lo cual podemos definir en el fichero correspondiente dentro de group_vars del siguiente modo:

Contenido de fichero group_vars.

El contenido del fichero, en formato YAML, contiene el nombre que he definido para la variable y el valor de la misma. Ahora, si uso un comando ad-hoc o un playbook en el que lanzo la instalación del paquete dado por el nombre de la variable sobre el grupo webservers, tengo lo siguiente:

Instalación de Apache usando variable.

Aunque no está la salida completa del comando, Ansible ha realizado la instalación de la última versión del paquete Apache en todos los hosts del grupo webservers. A partir de esto, entendemos lo siguiente:

  • Ansible busca por defecto la carpeta group_vars y, dentro de la misma, un fichero cuyo nombre coincida con el del grupo de hosts sobre el que estamos actuando.
  • Como no hemos definido nada relacionado con dicha carpeta en nuestro fichero de configuración, está claro que es una característica propia de Ansible.
  • Para usar una variable definida en un fichero de variables, la definimos entre dobles llaves {{}} como podemos ver en el comando anterior.

Si intentamos utilizar una variable de un grupo, con otro grupo diferente, recibiremos un error ya que, como acabamos de ver, Ansible buscará en el fichero de variables con el mismo nombre que nuestro grupo:

Usando una variable de otro grupo.

Como vemos, Ansible busca la variable en el fichero group_vars correspondiente al grupo sobre el que queremos actuar y, al no encontrarla, nos devuelve un error.

La única excepción a este comportamiento, es el fichero group_vars/all, el cual, si existe, será leido por Ansible en todas las ejecuciones. Así, por ejemplo, podemos incluir en el fichero group_vars/all la password para realizar sudo en nuestros hosts, de una manera similar a la siguiente:

Variables comunes a todos los hosts.

En este caso estamos definiendo una variable de Ansible, con lo que, al lanzar un comando ad-hoc simple como el módulo ping, será leido y utilizado directamente por Ansible:

Ejecución de comando ad-hoc.

Como podemos ver en la salida anterior, utilizamos el grupo dbservers, pero no la opción -k, porque estoy usando claves SSH, ni la opción -K, porque la password para sudo ya está definida en el fichero group_vars/all. Por tanto, Ansible busca la carpeta group_vars y, dentro de ella, leerá el fichero all y aplicará todas las variables que encuentre para, después, buscar el fichero con el nombre coincidente con el grupo de hosts sobre el que estamos actuando, para leer las variables definidas en el mismo.

Como es lógico, el directorio host_vars tiene un uso idéntico pero está pensado para hosts individuales, con lo que los ficheros que creemos en dicha carpeta, tendrán el nombre o dirección IP de hosts para los que queramos definir variables especiales.

Más adelante, veremos como usar vault con el inventario y como extenderlo aún más, con el uso de roles.

sábado, 20 de enero de 2024

Ansible - Autenticación, conexión y escalado de privilegios

En la entrada de hoy vamos a tratar los tipos o métodos de conexión que soporta Ansible, así como la autenticación y escalado de privilegios. Por defecto, Ansible siempre trabaja con OpenSSH y, si por algún motivo, nuestra máquina de control dispone de una versión antigua de OpenSSH que no soporta la característica ControlPersist, entonces empleará una implementación Python de OpenSSH llamada paramiko.

De forma muy sencilla, la característica ControlPersist de OpenssH está disponible desde la versión 3.9 y permite el multiplexado de conexiones. Mediante esta característica, varias sesiones SSH pueden compartir la misma conexión física lo cual, para entornos en los que estemos usando Ansible, presenta como principal ventaja, que se elimina la sobrecarga de crear nuevas conexiones y el negociado de las características de la conexión segura con lo que, en resumen, se consigue una mejora de rendimiento.

Ya vimos en el post anterior que, por defecto, Ansible usará siempre el usuario con el que estamos lanzando el comando o playbook, para establecer la conexión remota desde nuestra máquina de control. Podemos cambiar este comportamiento usando la opción de configuración remote_user, dentro de la sección defaults del fichero de configuración general de Ansible, o bien usando la variable ansible_user dentro del inventario, ya sea para un host específico a para un grupo de ellos.

Por tanto, y como regla general, es buena idea que especifiquemos el usuario de conexión en nuestro fichero de configuración, de una forma similar a la siguiente:

Fichero de configuración básico.
De esta manera nos aseguramos que siempre usaremos dicho usuario, para establecer conexión con los hosts que forman parte de nuestro inventario.

Ahora bien, aunque el método recomendado por Ansible es el uso de claves SSH, lo cual veremos en un momento, hasta ahora hemos estado conectándonos usando la passsword del usuario remoto. ¿Como le decimos a Ansible que debe solicitar la password del mismo? mediante las dos opciones que aparecen en todos los comandos ejecutados hasta ahora, que son -k y -K, las cuales, revisando la ayuda del comando ansible, se corresponden con la password de conexión y la password de escalado de privilegios:
 
-K, --ask-become-pass           ask for privilege escalation password
-k, --ask-pass        ask for connection password

Como es lógico, la password de conexión, es la que Ansible emplea cuando establece la conexión SSH con el host remoto y evidentemente, recibiremos un mensaje de error al intentar conectar con la password incorrecta.

Contraseña SSH incorrecta.

Si lanzamos manualmente comandos ad-hoc o playbooks, puede que resulte más seguro que, si no estamos empleando claves SSH, escribamos la password para realizar la conexión. Pero, como es lógico, esto impediría la ejecución de tareas desatendidas como, por ejemplo, aquellas que tengamos programadas en cron.

Una opción, que tiene importantes implicaciones de seguridad, es que fijemos la password como variable en el fichero de inventario. Por ejemplo, podemos hacer algo como lo siguiente:

Password SSH como variable.

Con esta configuración, ya podemos ejecutar comandos ad-hoc o lanzar playbooks sin necesidad de introducir la contraseña SSH cuando se solicite y, por tanto, podemos lanzar los comandos sin emplear la opción -k:

Ejecución sin password SSH.
 
Como vemos en la ejecución anterior, ahora Ansible solo solicita la password para el escalado de privilegios, usando para la conexión la especificada en el inventario.
 
Si optamos por no utilizar claves SSH, esta será la opción más cómoda, si no queremos introducir la password en cada ejecución, y totalmente requerida para el caso de tareas programadas. Aunque ya veremos más adelante como proteger esta información, es fundamental asegurar que, el fichero de inventario donde especifiquemos la password de conexión SSH, sea accesible solo por el usuario o usuarios requeridos.

Tras la conexión SSH, esta claro que queremos realizar alguna tarea en el host o hosts remotos. Esta tarea, por lo general, requerirá permisos de otro usuario para su ejecución. Como es lógico, si hablamos de tareas de administración, lo normal es que usemos el usuario root o un usuario con privilegios de administración para la ejecución de dichas tareas. En este punto es donde entra el escalado de privilegios, el cual, siguiendo la terminología utilizada por Ansible, se denomina become.

El uso del escalado de privilegios se puede configurar, de manera global, en el fichero ansible.cfg o al nivel de playbook o tarea. De forma general, podemos tener lo siguiente en el fichero de configuración ansible.cfg:

Configuración de become general.

Con la configuración definida dentro de la sección privilege_escalation, fijamos el comportamiento de ansible de forma general para todos los casos. En esta configuración estamos fijando lo siguiente:
  • become = true: Siempre se utilizará become, es decir, se realizará un cambio de usuario para el escalado de privilegios.
  • become_method = sudo: Especificamos que usaremos sudo para realizar el escalado de privilegios. Esto implica que sudo debe estar correctamente configurado en todas las máquinas remotas.
  • become_user = root: El usuario que se empleará para la ejecución de las tareas, es decir, el usuario que dispone de los permisos requeridos para realizar las tareas deseadas.

Es muy importante tener en cuenta que, si become es false, da igual el resto de opciones presentes de become, ya que no se hará el escalado de privilegios. Por ejemplo:

Fallo en la ejecución de la tarea remota. No se ha utilizado become.

Como se ve en la imagen anterior, aunque utilicemos la opción -K, para proprocionar la password para become, no se realiza el cambio al usuario root por la configuración que hemos fijado.

De la misma manera que hemos hecho con la password para la conexión SSH, también podemos especificar la password para el escalado de privilegios como una variable en el inventario:

Password para sudo como variable.
Ejecución sin especificar la password de sudo.
 
Con esta configuración, cualquier tarea programada funcionaría sin problemas ya que no sería necesaria intervención para introducir la clave. Pero, de nuevo, es muy importante controlar el acceso al fichero o ficheros de inventario que contengan las passwords necesarias.
 
Como ya hemos dicho al principio del post, Ansible recomienda el uso de claves SSH para establecer las conexiones con los hosts remotos. Como es evidente, esto pasa por crear el fichero authorized_keys correspondiente en el home del usuario remoto si este no existe. Una forma sencilla de copiar la clave pública de nuestra máquina de control, sería algo como lo siguiente:

Creando el fichero authorized_keys.

Con el comando ad-hoc anterior, copiamos la clave pública RSA del usuario que empleamos para conectarnos a los hosts remotos, usando el módulo copy de Ansible. Una vez hecho esto, podemos eliminar la variable ansible_password de nuestro inventario y lanzar comandos ad-hoc o ejecutar los playbooks sin necesidad de usar o especificar la password del usuario remoto:

Ejecución de comandos ad-hoc empleando claves SSH.
 
Como es lógico, el uso de claves SSH implica que, cuando creemos la plantilla o plantillas de nuestras máquinas, ya sean estas físicas o virtuales, sería recomendable que ya incluyeran el fichero authorized_keys necesario.

Para que nuestro entorno sea lo más seguro posible, lo recomendado para el caso de los ficheros que contienen nuestras claves SSH es:
  • Asegurar que los permisos de acceso a los ficheros son los correctos. En general, los permisos de los ficheros de clave SSH debería ser siempre 600 y el home del usuario empelado par ala conexión, debería tener permisos 700.
  • La clave SSH debería estar encriptada.

Respecto al último punto, si nuestra clave SSH esta encriptada, nos pasará algo como lo siguiente:

 

Usando una clave SSH encriptada.

Para evitar que nos pida la clave de desencriptación para cada conexión, podemos usar ssh-agent, cargar la clave y podremos trabajar de nuevo sin necesidad de introducir la contraseña necesaria:

Usando ssh-agent para cargar las claves encriptadas.
 
Este método es el recomendado por Ansible para el uso de claves SSH encriptadas, lo único que necesitamos tener en cuenta es que, si usamos diferentes claves para diferetens entornos o grupos de máquinas, tendremos que añadir la adecuada en cada momento.
 
Como último apunte de este post, es necesario que consideremos las implicaciones de seguridad relacionadas con el uso de Ansible. Es muy importante tener en cuenta que, el nodo principal de Ansible, es capaz de conectarse a toda nuestra infraestructura y realizar tareas administrativas, lo que la convierte en un sistema que debe ser protegido todo lo posible, con lo que, lo minimo recomendable es:
  • Restringir el acceso, tanto remoto como local, al grupo de administradores designados para el uso de Ansible.
  • Mantener el nodo de control actualizado y con todos los parches de seguridad instalados.
  • Configurar syslog para que registre toda la actividad del sistema, principalmente los accesos, tanto remotos como locales. Estos eventos deben enviarse a ficheros de log locales y a un servidor syslog centralizado.
Adicionalmente, en todas las máquinas controladas con Ansible, es muy importante asegurarnos de que todas las conexiones SSH que se realizan, se registran convenientemente en los logs del sistema. Para esto, es necesario que configuremos syslog para registrar los acceso SSH tanto en los ficheros de log locales como en un servidor syslog centralizado.
 
Como seguramente emplearemos sudo, la configuración de los sistemas controlados por Ansible que deberíamos plantearnos, debería cumplir al menos lo siguiente:
  • Emplear un usuario con permisos de administración diferente de root. Esto implica crear un usuario en el sistema, adicional al que usaremos para la conexión SSH.
  • Crear las reglas de sudo necesarias para dicho ususario, evitando usar una regla ALL para permitir la ejecución de todos los comandos.
  • Configurar syslog para que registre toda la actividad del sistema, principalmente los accesos, tanto remotos como locales, así como la ejecución de sudo. Estos eventos deben enviarse a ficheros de log locales y a un servidor syslog centralizado.
En próximos posts veremos como poder usar las contraseñas en nuestros ficheros, pero encriptar los mismos y desencriptarlos en el momento de lanzar nuestros comandos o playbooks.

miércoles, 1 de noviembre de 2023

Introducción a Ansible - Inventario y variables

Hoy, después de mucho tiempo, continuamos la serie sobre Ansible analizando el inventario y como asignar variables a los hosts, o grupos de hosts, que forman parte del mismo.

Como vimos en la entrada anterior, al utilizar Ansible, este busca su fichero de configuración ansible.cfg en una serie de rutas. Dentro de este fichero podremos especificar la ruta y nombre del fichero de inventario usando la opción de configuración inventory. Recordemos que el fichero de configuración por defecto de Ansible, /etc/ansible/ansible.cfg, fija como inventario el fichero /etc/ansible/hosts.

El fichero de inventario debe contener el nombre o dirección IP de todos los sistemas bajo el control de Ansible y como vimos, podemos hacer grupos con ellos de forma muy simple especificando etiquetas. Así, comenzamos a realizar pruebas con una serie de contenedores y comprobamos que podíamos acceder correctamente a los mismos. En ese momento, el contenido del fichero de inventario era el siguiente:

Fichero de inventario.

Revisando la imagen anterior podemos observar que hay definidos 5 hosts, cuyas direcciones IP están en el rango 172.17.0.2-172.17.0.6 y un grupo adicional llamado all, el cual está formado por el rango de direcciones IP 172.17.0.2-172.17.0.7. Usando un comando ad-hoc, podemos comprobar las direcciones IP de cada contenedor del siguiente modo:

Direcciones IP de cada contenedor usado.

Por tanto, cada uno de los contenedores que estoy empleando están registrados en el inventario, lo que me permite hacer comandos ad-hoc o utilizar playbooks con cada uno de ellos individualmente. Como vemos, en la definición de inventario no es necesario tener definidos los hosts en una lista inicial y luego usarlos en grupos, como en mi caso donde, el contenedor con la IP 172.17.0.7, solo aparece en el grupo all. Si elimino dicha entrada del grupo all y lanzo un comando ad-hoc directamente contra esa dirección IP el resultado es el siguiente:

Comando ad-hoc contra 172.17.0.7.

El resultado anterior nos muestra un punto muy importante sobre el inventario y es que, es necesario  que todos los hosts estén incluidos en el inventario si queremos poder controlarlos mediante Ansible. 

Si resumimos lo poco que hemos visto hasta ahora del fichero de inventario, podemos decir que es un fichero de texto plano, en el cual registramos las direcciones IP o nombres de todos los hosts que queremos controlar con Ansible.

Una de las características de Ansible en lo referente al fichero de inventario, es que podemos definir categorías o grupos dentro del mismo identificándolas con una etiqueta. En el fichero que hemos usado hasta ahora solo hay una etiqueta llamada [all], que engloba todas las direcciones IP, pero por ejemplo, si hacemos lo siguiente:

Nuevo grupo de hosts.

Ahora podemos repetir el comando ad-hoc empleando el módulo ping, pero especificando que el grupo sobre el que queremos lanzar dicho comando es el nuevo grupo:

Comando ad-hoc con el nuevo grupo.

¿Que nos muestra esta prueba tan sencilla? que podemos poner nombres de etiquetas, totalmente arbitrarias, para agrupar los hosts que luego controlaremos desde nuestro nodo de gestión. Así, por ejemplo, podemos necesitar una clasificación como la siguiente:

Un inventario "más complejo".

Ahora, al comprobar la conectividad con el grupo de hosts webservers:

Comprobación de un grupo de hosts.

En este caso, la salida del comando, muestra que solamente se ha lanzado la tarea contra los hosts que forman parte de dicho grupo.

En general, y de forma mucho más simple, siempre que queramos consultar la lista de hosts que forman parte de un grupo, podremos usar directamente el comando ansible, con la opción --list-hosts, de la siguiente manera:

Listando los miembros de los grupos de hosts del inventario.

De esta manera, podemos consultar los miembros de un grupo sin necesidad de editar el fichero de inventario. La salida del comando nos mostrará todas las direcciones IP, o nombres de los hosts, que forman dicho grupo.

Como es lógico, recibiremos un mensaje de error si intentamos lanzar alguna acción sobre un grupo no está presente en el fichero de inventario:

Grupo no existente.
 
Esto se debe a que, en general, el comando ansible buscará en el fichero de inventario el patrón que aparece al final del comando, ya que se entiende que se corresponde con el nombre del host o grupo de hosts sobre los que queremos actuar. Esto lo comprobamos del siguiente modo:
 
 
Usando expresiones para especificar los hosts.

Como vemos en la salida anterior, claramente el comando ansible buscará en el fichero de inventario los patrones que coincidan con el que pasamos por línea de comandos.

Bueno, y ahora la duda es ¿y el inventario sirve para algo más? Afortunadamente, el fichero de inventario nos permite establecer configuraciones específicas para hosts o comunes a grupos de hosts. Para esto, podemos definir variables de configuración en el inventario en el caso de ser necesario, tanto por host individual como por grupos. Imaginemos que necesitamos instalar un paquete determinado en un grupo de hosts, para esto podríamos hacer lo siguiente:

Uso de variables en fichero de inventario.
 
Como podemos ver he definido una variable, con un nombre arbitrario, para especificar el paquete de servidor de base de datos que quiero instalar en cada uno de los hosts que forman parte de dicho grupo. Ahora, puedo lanzar la ejecución del mismo en todos los hosts del grupo usando un comando ad-hoc como el siguiente:

Instalación de paquete especificando una variable.

Aunque no se muestra toda la salida, el resultado es la instalación del paquete especificado en la variable db_package que está definida en el fichero de inventario.

En el comando ad-hoc usamos el módulo apt, al que pasamos dos argumentos; el primero, state=present, establece que lo que queremos es que el estado final del sistema incluya el paquete; el cual está especificado por el segundo argumento, dado por name y que es igual a la variable que hemos definido. Como podemos ver, las variables las especificamos entre llaves dobles {{ NOMBRE_VARIABLE }}, aunque ya veremos más adelante como trabajar con variables.

Pero esto, podríamos pensar, es bastante incómodo de manajer ya que, si tengo 20 hosts en dicho grupo, tendré que añadir la variable a todos y cada uno de ellos. Afortunadamente, para evitar este trabajo innecesario, podemos definir variables de grupos del siguiente modo:

Definiendo variables para grupos de hosts.

Por ejemplo, supongamos que en una serie de hosts, el usuario que se emplea para las conexiones desde el servidor Ansible no es el mismo que en el resto. Para controlar este tipo de situaciones, Ansible nos deja especificar opciones de configuración de ansible en nuestro fichero de inventario, por ejemplo del siguiente modo:

Especificando el usuario de conexión
Un punto importante a tener en cuenta, ya que estamos hablando de usuarios de conexión, es que por defecto, ansible usará para conectarse con el host remoto el usuario con el que se está ejecutando el comando. Si queremos cambiar este comportamiento, es necesario que fijemos la opción de configuración remote_user en la sección defaults del fichero de configuración general ansible.cfg o bien la variable ansible_user, como una variable específica para aquellos hosts os grupos que utilicen un usuario diferente, dentro del inventario.
 
Con el cambio anterior, al intentar conectar a los hosts del grupo webservers, encontramos el siguiente fallo ya que el usuario especificado no existe:
 
Fallo de conexión al usar un usuario incorrecto

Por tanto, si queremos hacer algo así, necesitamos crear el usuario primero, lo que podríamos hacer del siguiente modo. Primero definimos dos variables, que podemos llamar admin_user, para el usuario, y admin_password, para la password de dicho usuario. Estas variables las definimos para los hosts del grupo webservers, añadiéndolas del siguiente modo:

Variables para creación de usuario

Ahora creamos dicho usuario en las máquinas mediante un comando ad_hoc como el siguiente:

Creación del nuevo usuario de conexión

Para poder usar este nuevo usuario, como administrador de las máquinas que forman parte del grupo webservers, añadimos un fichero con una regla de sudoers usando un comando ad-hoc como el siguiente:

Añadiendo un fichero con una regla específica de sudo para el nuevo usuario

Tras esto, al volver a cambiar la variable ansible_user en el fichero de inventario, podemos comprobar que podemos conectar sin problemas con los hosts del grupo webservers:

Comprobación usando el nuevo usuario

En resumen, hemos visto como usar el fichero de inventario y hemos comenzado a usar variables, definidas en este caso en el fichero de invetnario, así como hemos usado varios módulos y empleado variables en los mismos.

Espero que, en breve, más.

sábado, 23 de octubre de 2021

Integración de servicios con Kerberos - PDNS

Aprovechando un rato libre, es momento de continuar con la implementación de un servidor de nombres y autenticación basado en OpenLDAP y Kerberos. 

En las entradas anteriores de esta serie, fuimos añadiendo servicios sobre la base de un servidor OpenLDAP y llegamos a un punto donde teníamos un servidor de nombres implementado mediante un servidor OpenLDAP, un servidor de autenticación implementado mediante Kerberos y un servidor DNS, usando PDNS. Configuramos Kerberos y el servidor DNS para que usaran OpenLDAP como backend, para lo cual creamos DNs específicos dedicados a cada servicio. Estos DNs permiten a cada servicio realizar consultas o modificaciones, en caso de ser necesario, de la información contenida en el servidor OpenLDAP.

Hoy veremos como integrar un servicio, en este caso PDNS, para que utilice tickets de Kerberos para poder realizar consultas al servidor OpenLDAP en vez de emplear un par DN/contraseña. El objetivo de esta integración consiste en aumentar la seguridad de la infraestructura, además de asegurar que no hay contraseñas de servicio, aunque sean encriptadas, en los ficheros de configuración de los servicios.

Para realizar esta integración es importante recordar, de forma resumida, el flujo de autenticación de Kerberos, teniendo en cuenta que las dos entidades implicadas en este caso son dos servicios que se ejecutarán en el mismo host o en dos hosts separados y por tanto, uno de los servicios actuará como cliente:

  1. El servicio cliente, en este caso PDNS, ha sido configurado para autenticarse mediante el uso de GSSAPI, con lo que envía la clave almacenada en su keytab al KDC (Key Distribution Center) solicitando un TGT (Ticket Granting Ticket).
  2. El KDC valida la clave enviada por el servicio remoto y genera el TGT correspondiente.
  3. El servicio PDNS recibe el TGT, lo que le permitirá solicitar tickets para acceder a otros servicios.
  4. Empleando el TGT, el cliente solicita al KDC un ticket de servicio para poder consultar el servidor OpenLDAP.
  5. El KDC recibe la solicitud y tras validarla, devuelve el ticket de servicio correspondiente al cliente.
  6. El servicio cliente consulta al servicio remoto empleando el ticket de servicio proporcionado por el KDC.

Este flujo de validación que acabamos de ver implica los siguientes puntos:

  • En el dominio o reino Kerberos existente debemos crear los principales necesarios para ambos servicios, tanto el del cliente como el del servidor.
  • El servicio cliente debe disponer de la clave necesaria en un keytab accesible por el mismo.
  • El servicio cliente debe configurarse para emplear GSSAPI.
  • El servicio servidor debe estar configurado para permitir el acceso mediante Kerberos y disponer de un keytab.
  • El servicio servidor debe configurarse para acepta GSSAPI como mecanismo de autenticación.

Para implementar todo esto, vayamos punto por punto y empecemos por saber que es un keytab. De forma simple, un keytab es un fichero que almacena claves de kerberos para uno o más principales. En concreto un fichero keytab almacenará la fecha de escritura de la entrada en el fichero, el nombre del principal, un número de versión de la clave que representa la entrada, un tipo de encriptación y la propia clave.

Conviene recordar que un principal o Service Principal Name (SPN) es una entrada en un reino Kerberos, tanto de usuarios como de servicios, con un formato como el siguiente:

Servicio/Nombre de Host@REINO Kerberos

Servicio/Nombre de Host.Dominio@REINO Kerberos

Nombre de Usuario@Reino Kerberos

Los dos primeros tipos suelen emplearse para representar servicios, mientras que el último se utiliza para representar usuarios. Por tanto, en nuestro caso, es necesario que creemos cuatro SPNs nuevos, dos para el servicio LDAP y otros dos para el servicio PDNS, a partir de los cuales podremos crear los keytabs correspondientes.

Teniendo en cuenta lo expuesto hasta aquí, creamos el principal correspondiente para cada servidor LDAP de nuestra infraestructura y los exportamos a un keytab:

Creación principal - Servidor ldap1

Creación principal - Servidor ldap2

Exportación keytab servidor ldap1.

Exportación keytab servidor ldap2.

Ahora realizamos el mismo proceso de creación de SPNs y exportación de los keytab correspondientes para el servicio PDNS, que actuará como cliente del servicio OpenLDAP. ¿Como hacemos esto? Pues como se ve a continuación:

Creación principal - Servidor PDNS1.

Creación principal - Servidor PDNS2.

Ahora, al igual que en el caso de los principales de OpenLDAP, exportamos la clave del principal recién creado a un fichero para su uso por parte de PDNS. Esto debe hacerse desde el servidor master, ya que al realizar la exportación se realiza un modificación en las entradas correspondientes en OpenLDAP y por tanto es necesario que el KDC realice operaciones de escritura:

Exportación keytab servidor PDNS1.

Exportación keytab servidor PDNS2.

Una vez que tenemos listos los keytabs de todos los servicios, pasamos a realizar la configuración de los mismos para que se utilice Kerberos como mecanismo de autenticación entre ellos.

Para empezar de forma sencilla, comenzamos con el servicio PDNS. PDNS permite configurar el tipo de validación a utilizar con el backend LDAP con las siguientes opciones de configuración:

  • ldap-bindmethod, permite especificar las opciones simple, el método básico con un DN y una password o gssapi, para el uso de Kerberos.
  • ldap-krb5-keytab, especifica la ruta al fichero que contiene la clave que se usará para la validación.
  • ldap-krb5-ccache, especifica la ruta completa al fichero que almacenará la cache de credenciales de Kerberos.

Por tanto, la configuración necesaria del servidor PDNS implica añadir las siguientes lineas al fichero /etc/pdns/pdns.conf:

Configuración de PDNS para usar Kerberos.

Como es lógico, utilizaremos el keytab específico para cada servidor en cada caso y es muy importante tener en cuenta los permisos de acceso a dicho fichero si configuramos el servicio para que utilice un usuario diferente a root, que es lo más recomendable.

A continuación, configuramos OpenLDAP para que utilice el fichero keytab al arrancar, para lo cual es necesario cambiar el fichero de configuración correspondiente que dependerá de la distribución utilizada. En este caso tenemos lo siguiente para CentOS y Debian:

Configuración keytab OpenLDAP en CentOS

Configuración keytab OpenLDAP en Debian.

Al reiniciar el servicio OpenLDAP este utilizará el keytab especificado. Como es lógico, es muy importante que tenga los permisos correctos y el propietario del fichero sea el usuario que ejecuta el servicio slapd.

Ahora es necesario que pasemos a configurar OpenLDAP para que soporte mecanismos de validación adicionales. Por tanto, antes de continuar, comprobemos los mecanismos de autenticación soportados por el servidor OpenLDAP. Para esto basta con realizar una consulta como la siguiente:
 
Comprobación de mecanismos SASL soportados.

Como se puede apreciar en la imagen anterior, todavía no hay ninguno disponible, así que tenemos que añadirlos, empezando por el plugin correspondiente de SASL para GSSAPI ya que todos los mecanismos de autenticación adicionales o externos, como Kerberos en este caso, emplean la biblioteca SASL y los plugins proporcionados por dicha biblioteca. Para instalar el plugin, por ejemplo en CentOS, solo es necesario hacer lo siguiente:

Instalación de plugin GSSAPI de SASL.

Con el plugin instalado, debemos cambiar la configuración existente de la biblioteca SASL para incluir los nuevos mecanismos a utilizar por parte de OpenLDAP.

En una entrada anterior de esta serie, concretamente en esta, configuramos OpenLDAP para realizar autenticación passthrough y usar Kerberos para validar usuarios. Como vimos entonces, OpenLDAP utilizaba un servicio de autenticación externo, delegando el proceso de autenticación al proceso saslauthd, para todas aquellas entradas cuyo atributo userPassword tuvieran el formato {SASL}principal@REINO. El contenido del fichero que, por tanto, debemos tener inicialmente es parecido a este:

Configuración SASL para autenticación passthrough.
 
La configuración anterior permite delegar el proceso de autenticación a un servicio externo, pero ahora necesitamos que OpenLDAP soporte otros mecanismos de autenticación, para que clientes puedan acceder usando otros mecanismos de autenticación. Esto requiere que configuremos adecuadamente la biblioteca SASL para OpenLDAP, añadiendo los mecanismos del plugin GSSAPI, lo que permitirá el uso de tickets Kerberos para realizar operaciones de lectura y escritura en el servidor. A partir del fichero anterior, la configuración quedaría del siguiente modo:

Configuración SASL incluyendo mecanismos adicionales.

Con esta configuración, y tras reiniciar el servidor OpenLDAP, podemos comprobar que se soportan todos los casos:

Búsqueda usando autenticación GSSAPI.

Búsqueda usando autenticación SIMPLE y passthrough.

Búsqueda usando autenticación SIMPLE y validación local.

Como podemos apreciar en los ejemplos anteriores, hemos realizado una operación de consulta al servidor OpenLDAP con las siguientes características:

  • En el primer caso, el usuario que realiza la operación debe obtener un ticket de Kerberos. Usando dicho ticket realiza la consulta al servidor OpenLDAP especificando como mecanismo de autenticación GSSAPI. Este proceso es el mismo que queremos configurar para PDNS.
  • En el segundo ejemplo, un usuario realiza una consulta empleando el método de autenticación SIMPLE, es decir, presentando un par DN-contraseña. Como ese usuario tiene delegada la autenticación (atributo userPassword: operator1@LAB.INT), OpenLDAP delega la validación del mismo al proceso saslauthd.
  • En el último caso, muy similar al anterior, comprobamos que la validación empleando em método de autenticación SIMPLE con un usuario cuya contraseña se encuentra en el propio servidor OpenLDAP, y que por tanto no se delega a ningún autenticador externo, también puede acceder correctamente al servidor OpenLDAP.
Por tanto, el servidor OpenLDAP ya está configurado para emplear los mecanismos necesarios para su integración con Kerberos y ahora, siguiendo con la configuración del mismo, tenemos que pasar a realizar un mapeo de dichas autenticaciones a DNs existentes. Pero, ¿que es exactamente esto de los mapeos de autenticación a DNs?
 
Para un servidor OpenLDAP, cualquier operación bind que se realice requiere de un DN. Con esto quiero decir que internamente, una vez que un usuario o una aplicación ha realizado un bind con el servidor OpenLDAP, lo que se espera es tener algo con el formato de un DN, por ejemplo cn=.... o uid=...., que identifique la entidad que ha realizado dicho bind. Esto es así porque, posteriormente existirán ACLs que permitirán o denegarán el acceso a ramas del árbol o a atributos, y las ACLs se basan en la comparación del DN que ha hecho el bind con la especificación de las reglas que las establecen.
 
Al emplear un procedimiento de autenticación externo, la operación de bind que se realiza con el servidor OpenLDAP no devuelve un DN existente en el servidor, sino algo como lo siguiente:
 
Bind con autenticación externa.

Al realizar una búsuqeda empleando un ticket de Kerberos, cuando la validación es correcta, OpenLDAP construye un DN como el que podemos ver en la imagen anterior. Básicamente el DN indica el reino del ticket Kerberos y el mecanismo de autenticación empleado. Como es lógico, con esta configuración básica podríamos validar entidades externas, como el servicio PDNS, pero para poder aplicar ACLs y controlar a que ramas tiene acceso cada uno de los servicios, lo mejor es convertir dicho DN a uno existente en el árbol de directorio. Es en este paso donde entra el mapeo de DNs, el cual se realiza con la confioguración de reglas de mapeo usando el atributo de configuración dinámica olcAuthzRegexp el cual, empleano expresiones regulares, nos permitirá convertir DNs específicos o conjuntos de los mismos, a otros DNs diferentes.
 
De forma muy básica, viendo el DN generado con una autenticación GSSAPI, podemos establecer un mapeo de autorizaciones muy simple usando una expresión como la siguiente:
 
 
Mapeo simple de autenticación GSSAPI.

Al aplicar este atributo en la rama de configuración dinámica y reiniciar el servicio slapd, ahora tenemos lo siguiente al realizar un bind empleando el mecanismo GSSAPI:

Mapeo de DN efectivo.

Una vez establecida toda la configuración necesaria, debemos pensar un momento en la secuencia de arranque del servicio PDNS. Al arrancar el servicio, este necesita solicitar un ticket a partir del keytab de servicio que hemos creado y establecido en su configuración. Al hacer esto, debe generar un fichero donde almacenará la caché de credenciales de Kerberos, usada para posteriormente poder realizar peticiones al servidor OpenLDAP. Pues bien, al menos en la version del servidor autoritativo de  PowerDNS que estoy utilizando, en concreto la 4.5.1, al arrancar el servicio tras realizar toda la configuración, este no es capaz de iniciarse y se recibe un error que indica que los permisos del fichero de caché de credenciales no son conrrectos:
 
Error del fichero de caché de credenciales.
 
Tras bastantes pruebas y analizar el arranque con strace, el error se produce sencillamente porque no es capaz de acceder al fichero que contiene la cache, el cual se está generando durante el arranque y antes de que el proceso cambie a ser del usuario pdns. Con la configuración que he establecido, el servicio cambia al usuario pdns al arrancar y según el análisis realizado, intenta acceder al archivo de credenciales que ya es del usuario root, recibiendo el error indicado. 

Si arrancamos el proceso manualmente, en vez de lanzar el servicio definido mediante systemctl, se recibe el siguiente error que es bastante más esclarecedor de lo que puede estar sucediendo:

Error de caché de credenciales durante el arranque de PDNS.

Este problema se debe, única y exclusivamente, a que el propietario del directorio de instalación de PDNS, que está en /etc, es el usuario root y no el propio usuario pdns. Por tanto, podemos cambiar el propietario de /etc para que sea del usuario pdns, pero es importante tener en cuenta que, quizás la actualización automática del paquete, pueda cambiar de nuevo dichos permisos. Con esta solución, la configuración de pdns quedaría del siguiente modo:

Configuración corregida de PDNS. Fichero /etc/pdns/pdns.conf.

Es muy importante asegurar que, en el fichero de descripción del servicio, no esta incluida la opción ProtectSystem=full, ya que esta evita que el proceso escriba en /etc con lo que es necesario cambiarla a false en caso de estar presente. Esto puede incluir que, al para el servicio usando systemctl, dicho fichero de caché de credenciales no se elimine, lo cual provocará un fallo en el siguiente arranque del servicio, con lo que es necesario modificar la descripción del servicio para borrar dicho fichero. Como referencia, una posible solución a estos problemas sería similar a la siguiente:

Modificaciones de la descripción del servicio pdns.

Con toda esta configuración, ya podemos arrancar el servicio pdns el cual, se valida usando Kerberos para poder realizar operaciones de lectura con el servidor OpenLDAP:

Servicio pdns arrancado.

Hasta aquí esta entrada y en cuanto saque otro rato, seguiremos integrando servicios con Kerberos para poder asegurar más nuestras infraestructuras.