sábado, 10 de abril de 2021

Servidor DNS con backend OpenLDAP

Hoy continuamos con la serie sobre OpenLDAP y después de la anterior entrada sobre el bastionado básico de la arquitectura, pasamos a añadir una nueva pieza a la solución, concretamente un servidor DNS con backend LDAP.

Al añadir este nuevo elemento, la arquitectura de alto nivel quedaría más o menos así:

Relaciones entre los componentes de la solución.

Como podemos ver en el diagrama, el servidor DNS será un nuevo cliente LDAP que empleará el servidor OpenLDAP como backend para la información de zonas DNS. La principal ventaja de utilizar esta solución es que no necesitamos un servidor DNS que replique sus bases de datos, ya que la capacidad de replicación la proporciona directamente el servidor OpenLDAP.

Dentro de todas las posibles opciones de servidores DNS que puedan emplear un servidor OpenLDAP como backend, vamos a probar PowerDNS. Este servidor DNS dispone de un plugin específico para soportar servidores LDAP como backend y en general está disponible en cualquier distribución Linux. Otra característica adicional que puede ser intersante en ciertos despliegues, es que PowerDNS separa las funciones de servidor DNS autoritativo de las funciones del servidor DNS que realiza consultas recursivas. En el caso quue nos ocupa, usaremos el servidor autoritativo, ya que queremos crear un pequeño dominio DNS de pruebas, que nunca realizará consultas a servidores DNS externos.

En el caso de CentOS 7.8, PowerDNS se encuentra disponible en el repo EPEL, así que tras instalar y habilitar dicho repo, podemos instalar el servidor PowerDNS así como las herramientas y el backend LDAP:

Instalación de PowerDNS en CentOS.

De forma similar, podemos instalar PowerDNS en Debian empleando los comandos de gestión de paquetes de Debian.

Paquetes PowerDNS en Debian.

Al igual que hicimos al integrar Kerberos, es necesario ampliar el esquema de OpenLDAP para poder almacenar las clases de objeto y atributos requeridos por PowerDNS. Los ficheros de esquema correspondientes están incluidos con la instalación, encontrándose en la ruta /usr/shared/doc/pdns-backend-ldap-4.1.14 en el caso de CentOS mientras que, en el caso de Debian, se almacenan directamente en la ruta /etc/ldap/schema de instalación del servidor OpenLDAP:


Ficheros de esquema necesarios en CentOS.

Ficheros de esquema necesarios en Debian.

En el caso de CentOS hay dos ficheros de esquema diferentes, uno extiende el esquema del servidor incluyendo información necesaria para la integración de PowerDNS y el otro define los atributos y objetos necesarios para almacenar infomación de registros y zonas DNS. En el caso de Debian solo existe un fichero, el cual define  los atributos y objetos necesarios para almacenar infomación de registros y zonas DNS. Ya que estamos ampliando el esquema de ambos servidores, crearemos un único fichero de extensión de esquema y lo usaremos en ambos.

Para extender el esquema de un servidor OpenLDAP necesitamos crear una nueva entrada en el árbol de configuración cn=config, para lo cual es necesario que usemos el comando ldapadd para cargar el esquema. Es importante tener en cuenta que los ficheros proporcionados con las instalaciones, tanto en CentOS comno en Debian no están en formato LDIF, con lo que no podemos usar los ficheros directamente con el comando ldapadd. Es necesario corregir dichos ficheros, añadiendo las modificaciones necesarias para pasarlos a formato LDIF y así poder usarlos con los comandos ldapadd y ldapmodify. Tras un poco de vi, los ficheros quedarán más o menos así:

Modificación ficheros de schema PowerDNS.
 
Modificación ficheros schema PowerDNS.

Es muy importante tener en cuenta que, al estar dividido en dos ficheros, es necesario continuar los índices numéricos de cada atributo y clase de objeto para poder hacer la carga de forma correcta en el árbol de configuración dinámica.

Para cargar el primer fichero utilizaremos el comando ldapadd y para el segundo ldapmodify, ya que estaremos modificando una entrada ya existente. Los comandos a utilizar, teniendo en cuenta que ahora los servidores requiren TLS para las conexiones, serán más o menos así:

Carga de schema pdns-domaininfo

Carga de schema dnsdomain2

Podemos comprobar que el esquema se ha ampliado correctamente comprobando que aparece dentro de la rama cn=schema del árbol de configuración dinámica cn=config:

 

Esquema del servidor OpenLDAP.

Una vez que hemos realizado la extensión del esquema de ambos servidores OpenLDAP correctamente, podemos empezar a configurar PDNS. El fichero principal de configuración es /etc/pdns/pdns.conf y en este fichero debemos establecer, al menos, las siguientes variables de configuración:

  • local-address, establece la dirección IP en la que escuchará el servicio PDNS.
  • launch, establece que backend empleará PDNS. En nuestro caso especificaremos ldap.
  • ldap-host, nos permite establecer la URL del servidor LDAP que actuará como backend.
  • ldap-starttls, para indicar que debe usarse StarTLS en las conexiones con el servidor LDAP.
  • ldap-bindmethod, esta opción puede tomar diferentes valores. De momento especificaremos simple para usar un DN específico para la conexión y consultas del servidor LDAP por parte de PDNS.
  • ldap-binddn y ldap-secret, DN y password usada para la conexión y consultas de PDNS con el servidor OpenLDAP.
  • ldap-basedn, DN a partir del cual PDNS realziará la búsqueda de entradas. 
  • ldap-method, esta opción puede tomar varios valores y básicamnte establece como se traslada una query DNS a la estructura de la zona dentro del servidor de directorio. La estableceremos en strict y luego analizaremos más detenidamente que significa.

Es importante resaltar que, al utilizar las bibliotecas LDAP cliente del sistema operativo, el fichero de configuración LDAP (/etc/ldap.conf o /etc/openldap/ldap.conf), debe estar correctamente configurado. Para poder iniciar correctamente la conexión StartTLS es muy importante la opción TLS_CACERT. Esta opción debe establecer la ruta completa al fichero que contenga el certificado de la CA que haya firmado el certificado usado por el servidor OpenLDAP.

El fichero de configuración de PowerDNS, en lo que respecta solamente a la configuración del backend LDAP, quedaría más o menos así:

Configuración PowerDNS para backend LDAP.

Una vez configuradas todas estas opciones, antes de arrancar el servicio PDNS, necesitamos construir la información básica de la zona DNS en el servidor OpenLDAP. Es en este punto donde necesitamos distinguir los diferente valores de la opción de configuración ldap-method, siendo el significado de cada uno de ellos el siguiente:

  • En modo simple podremos crear la estructura del árbol de directorio como deseemos. Cada consulta al servidor DNS se traducirá en una búsqueda del atributo associatedDomain de las entradas de la clase de objeto domainRelatedObject.
  • El modo strict es como el modo simple pero permite que PowerDNS devuelva las resoluciones inversas, las entradas PTR, a partir de las entradas directas, con lo que no es necesario crear entradas de tipo PTR.
  • Por último, el modo tree nos obliga a crear una estructura de directorio que coincida con nuestro dominio, es decir, una entrada A para un host como server1.lab.com se traduciría a un DN que sería dc=server1,dc=lab,dc=com,.... con lo que debemos crear las entradas teniendo en cuenta este modo de operación.

Al configurar la opción ldap-method en modo strict, podemos crear una unidad organizativa, que será la base de la zona o zonas de nuestro servidor DNS, añadiendo el objeto que define la zona y varias entradas de servidores, de una forma tan simple como la siguiente:

Estructura básica de zona DNS.

Con esta información básica ya creada en el servidor OpenLDAP, podemos arrancar el servicio pdns y comprobar que el servidor responde a las queries DNS que realicemos desde clientes.

Es importante tener en cuenta que PowerDNS considera que el servidor, con este tipo de configuración no debe ser configurado como master, ya que el backend es el encargado de realizar la replicación de los objetos de la zona DNS.

Con esto ya tenemos listo un servidor DNS integrado con el resto de la infraestructura basada en OpenLDAP. Los siguientes pasos a seguir, para continuar bastionando el sistema, será comenzar a usar Kerberos para las conexiones entre los diferentes elementos, lo cual nos permitirá eliminar contraseñas de ficheros de configuración, así como seguir aplicando opciones recomendadas de seguridad.

jueves, 1 de abril de 2021

Integración de OpenSSL con GnuTLS y el atributo olcTLSCipherSuite

Hoy una entrada rápida sobre OpenSSL y GnuTLS o, más concretamente, sobre los casos en los que, en una arquitectura, existen sistemas que emplean una u otra.

Ambas soluciones son bibliotecas que implementan los protocolos SSL y TLS que nos permiten, entre otras cosas, cifrar las comunicaciones entre sistemas. Es importante saber que hay distribuciones Linux que emplean OpenSSL, como CentOS, mientras que otras emplean GnuTLS, como es el caso de Debian. Por tanto, si en una arquitectura ya existente, es necesario integrar un nuevo servicio que empleará cifrado o si estamos bastionando la arquitectura por motivos de seguridad, es muy importante tener en cuenta las diferencias entre ambas bibliotecas.

La primera diferencia a tener en cuenta entre ambas bibliotecas es como se definen las cifras y protocolos, que luego podremos configurar en cualquier servicio empleando las opciones correspondientes. De forma simple, podemos consultar las cifras soportadas o disponibles en ambos casos del siguiente modo:

Consultando cifras disponibles en OpenSSL.

Consultndo cifras disponibles en GnuTLS.

Una de las primeras diferencias que podemos observar, es como se especifican las cifras en el caso de GnuTLS. Para poder ver la lista de cifras soportadas, así como protocolos y su versión, es necesario que especifiquemos una cadena de prioridad, las cuales son una manera simple y compacta de especificar un conjunto de cifras, protocolos, algortimos de intercambio de claves, etc. Las cadenas de pioridad disponibles, así como su significado, están disponibles aquí y quizás son la parte más importante para integrar correctamente un sistema que está trabjanado con OpenSSL con otro que emplea GnuTLS.

En concreto ¿cual es el problema al hacer este tipo de integraciones? Recordando el post en el que comenzábamos el bastionado de una arquitectura basada en OpenLDAP, desplegábamos unos certificados de servidor para cifrar las comunicaciones entre los diferentes elementos, principalmente para proteger el tráfico de replicación entre ambos servidores OpenLDAP.

Si generamos el certificado de servidor con OpenSSL, utilizando una configuración por defecto y luego fijamos que conjuntos de cifras queremos utilizar en cada extremo, en este caso en cada servidor OpenLDAP, nos encontraremos con problemas de compatibilidad derivados del hecho de que no todas las cifras o algoritmos estarán incluidas en cada implementación.

Recordando que un servidor OpenLDAP que replica información desde otro podemos verlo como un cliente LDAP, al establecer la conexión y solicitar la extensión StartTLS, realizará una comprobación del certificado proporcionado por el servidor y dicha conexión fallará en caso de encontrarse algún algoritmo o cifrado no soportado.

Podemos comprobar este problema de una manera muy sencilla utilizando el comando gnutls-cli, con las opciones necesarias para establecer una conexión StartTLS con un servidor remoto y especificando la cadena de prioridad que queremos utilizar en el lado cliente. Por ejemplo, desde el servidor OpenLDAP réplica, podemos conectar con el servidor OpenLDAP master y comenzar uan conexión StartTLS con un comando como el siguiente:

Conexión mediante gnutls-cli - SECURE128.

En este ejemplo, especificando como parámetros la ruta al fichero que contiene el certificado de la CA que ha firmado el certificado del servidor remoto, así como que queremos establecer una conexión TLS mediante protocolo LDAP, el certificado se valida y la conexión TLS se establece correctamente. El punto importante del comando es la cadena de prioridad que hemos especificado, en este caso la denominada SECURE128. La documentación de GnuTLS establece que al especificar SECURE128, se están empleando aquellas cifras consideradas seguras que ofrecen un nivel de seguridaad de 128 bits o superior y que se establece un perfil de comprobación de certificado bajo.

Si repetimos el comando pero pasamos a especificar una cadena de prioridad SECURE192, en la cual se están empleando cifras consideradas seguras que ofrecen un nivel de seguridaad de 192 bits o superior y que se establece un perfil de comprobación de certificado alto, veremos que sucede lo siguiente:

Conexión mediante gnutl-cli - SECURE192.

Por tanto, al incrementar el nivel de seguridad que deseamos usar, el certificado del servidor ya no es válido por usar un algoritmo inseguro, basado en la cadena de prioridad que hemos especificado en este caso.

Esta configuración es la que establecemos en el atributo olcTLSCipherSuite de OpenLDAP, el cual debe estar soportado por la biblioteca SSL usada para comnpilar el servidor OpenLDAP. Así, para un OpenLDAP que emplea OpenSSL, el valor de dicho atributo podría ser TLSv1.2, mientras que para un servidor OpenLDAP que emplea GnuTLS, el valor de dicho atributo debería ser una de las cadenas de prioridad soportadas, por ejemplo SECURE128.

Por tanto y en resumen, a la hora de tener que integrar sistemas que usen diferente biblioteca SSL, es muy importante analizar que algoritmos, cifras, protocolos de intercambio de claves, etc. están soportados en cada caso y cuales están incluidos si usamos las cadenas de prioridad de GnuTLS, pero, en la medida de lo posible, lo más sencillo es usar la misma distribución para evitar este tipo de problemas.


domingo, 28 de marzo de 2021

Seguridad en arquitecturas basadas en OpenLDAP

Hace ya unos cuantos meses vimos como podíamos integrar Kerberos con un servidor OpenLDAP, creando así un servicio de nombres y autenticación en el cual OpenLDAP se utiliza como base de datos para los principales de Kerberos, así como para almacenar información de cuentas de usuarios y grupos para sistemas Unix/Linux. Aprovechando las funcionalidades de replicación de OpenLDAP, conseguíamos posteriormente un despliegue con tolerancia a fallos.

Para continuar mejorando lo que hicimos entonces, vamos a establecer las medidas de seguridad necesarias que aseguren que las comunicaciones entre los diferentes elementos que forman la solución se encriptan siempre que sea posible.

Empezando por lo más sencillo, es necesario establecer unas reglas en los cortafuegos que solo permitan las conexiones a los puertos requeridos de cada uno de los servicios proporcionados. En concreto es necesario crear reglas para permitir las conexiones a los siguientes puertos:

  • Puertos 389 y 636. Puertos de servicio de OpenLDAP para establecer conexiones cifradas mediante StartTLS y ldaps respectivamente.
  • Puerto 88. Puerto de servicio del KDC de Kerberos. Necesario para realizar la autenticación de usuarios mediante la petición y expedición de tickets.
  • Puertos 464 y 749. Puertos del servicio kadmin de Kerberos para el proceso de cambio de contraseñas.
Una vez establecidas estas reglas básicas de cortafuegos, el siguiente paso es cifrar las comunicaciones entre los servidores OpenLDAP y los clientes que deban acceder a los mismos. Llegados este punto puede resultar interesante que consideremos el siguiente diagrama:

Relaciones entre los componentes de la solución.

Analizando el diagrama anterior y teniendo en cuenta que los servidores OpenLDAP son el repositorio central de toda la arquitectura del servicio de nombres y validación que queremos desplegar, está claro que es necesario cifrar todas las comunicaciones que se realicen desde cualquier cliente. Es importante señalar que, desde el punto de vista de OpenLDAP, Kerberos es un cliente de LDAP más, ya que este realizará consultas para buscar los principales correspondientes a todas aquellas peticiones de validación que reciba. También es importante señalar que para realizar el passthrough de autenticación, OpenLDAP delega el proceso de autenticar un usuario contra el KDC a través del servicio saslauthd que se encuentra corriendo en el mismo servidor.

Aunque posteriormente volveremos sobre este diagrama, empecemos por cifrar las comunicaciones entre OpenLDAP y los clientes existentes, incluido el servidor OpenLDAP secundario o réplica ya que, para utilizar el mecanismo de replicación syncrepl, se realizan consultas LDAP estándar así que, estas comunicaciones también podemos entenderlas como procedentes de un cliente LDAP.

Para cifrar las comunicaciones lo que necesitamos es disponer de un par clave privada-certificado por cada servidor. Podemos crear nuestra propia CA de forma simple con OpenSSL, crear certificados autofirmados o, si disponemos de una CA corporativa, generar dichos certificados en ella. En resumen, al final terminaremos con un par de ficheros en formato PEM que tendremos que copiar a nuestros servidores y configurar OpenLDAP para que los utilice para el cifrado de las comunicaciones.
 
Una vez copiados los ficheros y aprovechando el backend config, configuramos de forma dinámica los servidores, cambiando los siguientes atributos de configuración:
  • olcTLSCACertificateFile, este atributo establece la ruta y fichero que contiene los certificados de todas las CAs en las que se confía.
  • olcTLSCACertificatePath, este atributo establece la ruta que contiene los ficheros con los certificados de todas las CAs en las que se confía. Este parámetro es complementario al anterior y probablemente no sea necesario usar ambos.
  • olcTLSCertificateFile, este atributo establece la ruta y fichero que contiene el certificado del servidor OpenLDAP.
  • olcTLSCertificateKeyFile, este atributo establece la ruta y fichero que contiene la clave privada correspondiente al certificado del servidor OpenLDAP.
  • olcTLSCipherSuite, este atributo establece que cifrados acepta el servidor OpenLDAP, así como el orden de los mismos. Es muy importante tener en cuenta que, el valor de este atributo, depende de la biblioteca SSL que se haya utilizado para la compilación de OpenLDAP. Esto quiere decir que, la cadena que utilicemos como valor para este atributo, debe ser entendida por la biblioteca. Como apunte para tenerlo en cuenta, en el caso de CentOS, OpenLDAP está compilado contra OpenSSL, mientras que en Debian está compilado contra GnuTLS. 
Debido al impacto que tiene en las comunicaciones, de momento dejaremos de lado el atributo olcTLSCipherSuite así que, usando nuestro editor favorito de servidores LDAP, cambiamos adecuadamente el valor del resto de atributos de configuración en la rama cn=config y tendremos algo similar a lo siguiente:

Configuración de certificados en OpenLDAP.

Es necesario asgurar que la ruta y permisos de estos ficheros son correctos o recibiremos un error al modificarlos, ya que el proceso slapd intentará acceder a los mismos y al no poder hacerlo, rechazará la modificación de dichos atributos.
 
A continuación, una vez configurados los certificados de ambos servidores, tenemos dos opciones para realizar el encriptado de las comunicaciones, levantar slapd con el puerto adicional 636 para el uso de ldaps o habilitar el uso de StartTLS sobre el puerto estándar 389. Veamos un poco las diferencias entre ambas soluciones:
  • ldaps, también conocido como LDAP seguro, es el mecanismo diseñado originalmente para LDAPv2 que permite confidencialidad en las comunicaciones entre un servidor OpenLDAP y un cliente. Se inicia en el momento de establecer la conexión entre el servidor y el cliente y requiere el uso de un puerto adicional que, por defecto, es el 636. 
  • StartTLS es el mecanismo estándar defiinido en la RFC 2830 para LDAPv3. En esta RFC se establece el procedimiento para que, una vez que se ha establecido correctamente la conexión LDAP entre cliente y servidor, se habilite el uso de TLS/SSL para cifrar la comunicación sobre el puerto 389 estándar de cualquier servidor LDAP.
Lo cierto es que, una vez establecida la conexión cifrada, no hay diferencia entre ambas soluciones, salvo por el uso de un puerto adicional en el caso de ldaps. Como el uso de una u otra solución dependerá de las necesidades de los clientes que vayan a conectarse, es importante considerar ambas para fijar una u otra, pero es importante tener en cuenta que es preferible usar StartTLS siempre que sea posible.

Teniendo en cuenta las diferencias entre ldaps y StartTLS, de momento vamos a configurar ambas empezando por la sencilla. Para configurar ldaps, solo tenemos que levantar el servidor OpenLDAP especificando una URL adicional para ldaps. En general, haremos esto modificando el fichero slapd que define los parámetros utilizado para arrancar el servicio. En función de la distribución utilizada, este fichero se encontrará en una ruta u otra. En el caso de usar CentOS o Debian, estos ficheros están en /etc/sysconfig y /etc/default respectivamente:

Configuración URL ldaps - CentOS.

Configuración URL ldaps - Debian.

Tras realizar este cambio y reiniciar el servicio slapd, este escuchará en el puerto 636 presentando el certificado obtenido inicialmente para cada servidor. Al conectar de nuevo al servidor desde un cliente LDAP, debemos escoger como puerto de conexión el 636 y SSL como método de encriptación, con lo que estaremos usando ldaps para conectarnos con el servidor. Usando Apache Directory Studio recibiremos un mensaje, acerca del certificado presentado por el servidor OpenLDAP, como el siguiente al establecer la conexión por primera vez al puerto 636:
 
Verificación de certificado de servidor.

De este modo tan simple, podemos asegurar que la conexión entre nuestro cliente y el servidor OpenLDAP está cifrada protegiendo de momento mediante ldaps, las tareas de administración que realicemos.

A continuación vamos a cifrar el tráfico de replicación entre el servidor master y el servidor réplica de la infraestructura, para lo cual usaremos StartTLS en vez de ldaps. Para esto, una de las primeras comprobaciones que debemos hacer ,es verificar que el servidor soporta la extensión StartTLS definida en la RFC 2830. Podemos comprobarlo mediante una búsqueda sencilla con ldapsearch o bien usando un editor LDAP. En general, mediante un comando ldapsearch, el resultado que debemos obtener es similar al siguiente:
 
Lista de controles, características y extensiones soportadas.
 
Entre todos los OIDs que se muestran en la salida anterior, el correspondiente a StartTLS es el 1.3.6.1.4.1.1466.20037 como se describe en la RFC 2830. Esto indica que cuando configuremos cualquier cliente para emplear StartTLS, este enviará este OID en una petición extendida solicitando el uso de dicha extensión. Al estar soportada por el servidor, la conexión LDAP establecida pasará a estar cifrada entre ambos servidores para el tráfico de replicación.
 
Para configurar el uso de StartTLS entre ambos servidores, solo es necesario modificar el atributo olcSyncRepl existente en el servidor réplica, ya que hay que recordar que en OpenLDAP, la operación de replicación se inicia desde los servidores réplica y que en el servidor maestro solo es necesario habilitar el overlay syncprov para permitir las replicaciones.

En este caso, lo más recomendable es utilizar un editor gráfico LDAP para realizar la modificación de este atributo, el cual está definido en la rama cn=config de configuración dinámica del servidor. Para forzar el uso de StartTLS entre los servidores, las opciones que debemos añadir al atributo olcSyncRepl son las siguientes:
  • starttls=yes o critical, para establecer la sesión TLS antes de realizar la operación de autenticación con el servidor LDAP maestro. Si usamos la opción critical, la replicación fallará en caso de no poder iniciarse la sesión TLS. Si especificamos yes, pasará a usar ldap no cifrado en caso de fallo en el establecimiento de la sesión TLS.
  • tls_cert, tls_key, tls_cacaert y tls_cacertdir, para establecer la ruta a los ficheros de certificado, tanto de CAs como del propio servidor, así como al fichero que contiene la clave privada del servidor LDAP réplica.
  • tls_reqcert=demand, para forzar al servidor maestro el presentar su certificado para la conmprobación del mismo.
Teniendo en cuenta todas estas opciones, lo único que necesitamos es cambiar el atributo del siguiente modo:
 
Modificación de olcSyncRepl en servidor réplica.

Aunque hemos usado el backend de configuración dinámica y estas opciones se empiezan a utilizar en el momento de establecerlas, es necesario reiniciar el servidor réplica. Una vez hecho esto, lo primero es comprobar que la replicación funciona correctamente, para lo cual basta con añadir una nueva entrada o modificar una ya existente y confirmar que los cambios se replican adecuadamente.
 
Podemos comprobar que, efectivamente, se está realizando la replicación MASTER-RÉPLICA de forma cifrada mediante el comando tcpdump:
 
Replicación cifrada entre master y réplica.
 
Estos paquetes los veremos en el momento de realizar cualquier modificación en el servidor master y, como se puede apreciar, el tráfico de replicación está cifrado entre ambos servidores. Adicionalmente,  podemos comprobarlo en los logs de ambos servidores:
 
Sesión TLS entre servidor master y réplica.
 
Y por último, aseguramos que la replicación es correcta verificando que el contextCSN de ambos servidores es idéntico:
 
ContextCSN del servidor MASTER.
 
ContextCSN del servidor RÉPLICA.

Por tanto, llegados a este punto, tenemos cifradas las comunicaciones entre los clientes LDAP como pueden ser las herramientas administrativas así como la replicación entre el servidor MASTER y los servidores RÉPLICA existentes.

Ahora, recordando el análisis realizado al principio del post y para terminar, es necesario que establezcamos la seguridad de las comunicaciones necesarias entre los servicios de Kerberos y el servidor OpenLDAP. Como ya se estableció, desde el punto de vista de OpenLDAP, tanto el servicio kdc como el servicio kadmin, son clientes LDAP, los cuales debemos configurar adecuadamente para o bien utilizar ldaps o emplear StartTLS.

Como ya vimos en el post en el que integrábamos Kerberos con un backend OpenLDAP, toda la configuración necesaria debemos realizarla en el fichero kdc.conf, el cual se encuentra en la ruta /var/kerberos/krb5kdc en sistemas CentOS o en la ruta /etc/krb5kdc en el caso de sistemas Debian. El fichero que generamos entonces es similar al siguiente:

Fichero de configuración kdc.conf.

Revisando la documentación de Kerberos comprobamos que, a la hora de especificar la URL de los servidores LDAP, lo recomendado es emplear ldaps para asegurar que la comunicación entre los servicios de Kerberos y OpenLDAP está cifrada.

En este caso, como Kerberos hará uso de las bibliotecas LDAP del sistema operativo, lo primero que debemos hacer es configurar el fichero ldap.conf. En este fichero se establece la configuración del cliente ldap del sistema, indicando la ruta al fichero que contendrá el certificado de la CA que ha firmado el certificado empleado por OpenLDAP. Para realizar esta configuración solo necesitamos modificar la opción TLS_CACERT indicando la ruta al fichero, o la opción TLS_CACERTDIR, para indicar la ruta al directorio donde se encuentran los ficheros con los certificados de las CAs en las que se confía:
 
Fichero ldap.conf.
 
Con esta configuración establecida, solo necesitamos modificar el fichero kdc.conf e indicar como URL de acceso ldaps:// en el parámetro de configuración ldap_servers. El fichero quedaría más o menos así:

Fichero kdc.conf modificado para usar ldaps.
 
Una vez realizado este cambio solo nos falta reiniciar los servicios y comprobar que estos arrancan y se conectan al puerto 636 correctamente:

Conexiones del servicio kdc mediante ldaps.

Conexiones del servicio kadmin mediante ldaps.

Llegado este punto, las comunicaciones entre los diferentes elementos del sistema que establecen conexiones con el servidor OpenLDAP están cifradas pero, para terminar de asegurar que solo se permiten este tipo de conexiones, es necesario que configuremos el propio servidor OpenLDAP para que exija el uso de confidencialidad en todas las conexiones. Para esto, lo único necesario es que establezcamos el atributo de configuración olcSecurity con el valor ssf=1. Al establecer este atributo, cualquier conexión no cifrada se rechazará con el mensaje confidentiality required sin necesidad de reiniciar el servidor OpenLDAP:

Mensaje de error para conexiones no cifradas.

En resumen, hemos establecido las configuraciones mínimas necesarias para establecer la encriptación y por tanto confidencialidad de las comunicaciones entre el servicio OpenLDAP y los clientes existentes, incluyendo los servicios de Kerberos que emplean el servidor LDAP como backend. Además hemos establecido que el servidor OpenLDAP exija siempre el uso de encriptación en cualquier conexión que se establezca, lo que provoca que no puedan realizarse conexiones no encriptadas al puerto 389. Teniendo esto último en cuenta, para comprobar el contextCSN de cada uno de los servidores, ahora necesitamos realizar el comando ldapsearch especificando la opción -Z del siguiente modo:

Comprobación del contextCSN mediante StartTLS.

Tengamos en cuenta que, al haber configurado el cliente ldap del sistema, especificando la ruta al fichero que contiene el certificado de la CA en ldap.conf, las herramientas como ldapsearch usarán dicha configuración para establecer la comunicación mediante StartTLS correctamente.

Para terminar, solo falta impedir que se puedan realizar operaciones bind anónimas, para lo cual solo es necesario que cambiemos el atributo de configuración dinámica olcDisallows y especifiquemos bind_anon lo cual queda más o menos así:

Deshabilitando el bind anónimo.

También es recomendable establecer que se requiere siempre autenticación, para realizar cualquier tipo de operación en el servidor de directorio, así como requerir siempre el uso del protocolo LDAPv3. Estas opciones podemos establecerlas modificando el atributo olcRequires como se ve en la imagen anterior. Con estos cambios, al intentar hacer una búsqueda empleando un bind anónimo, recibimos un mensaje como el siguiente:

Error en operaciones con bind anónimos.

Este cambio provoca que cualquier cliente que hayamos configurado, que realice operaciones bind anónimas, requiera ahora de un usuario del directorio, especificado por su DN y una contraseña. Como veremos en futuras entradas, esta configuración es muy importante cuando configuramos cualquier sistema Unix/Linux para que utilice un servidor OpenLDAP como servicio de nombres y requerirá la correcta configuración del servicio nslcd o sssd.

En próximas entradas, veremos como integrar un servidor DNS con OpenLDAP así como emplear tickets Kerberos para el acceso a los diferentes servicios, especificando diferentes mecanismos SASL para la validación al realizar operaciones.

viernes, 25 de diciembre de 2020

Kubernetes - Notas adicionales sobre ReplicaSets

Continuando con los objetos de tipo ReplicaSets, hoy vamos a ver unas notas adicionales que pueden ser de interés y que debemos tener en cuenta.

Como ya vimos en la anterior entrada sobre los objetos ReplicaSet, este tipo de objeto permite controlar de forma automática un conjunto de PODs. Esto implica que el KCP monitorizará en todo momento el número de PODs del ReplicaSet, asegurando que el estado del cluster coincida con la configuración que hemos aplicado y manteniendo el número de PODs que hayamos fijado en la configuración del objeto ReplicaSet.

En resumen, partiendo de una situación como la siguiente:

Objeto ReplicaSet en el cluster.

Vemos que hemos definido un ReplicaSet en el que hemos fijado que siempre debe haber un total de 2 PODs corriendo en todo momento. Evidentemente este caso es muy sencillo y vemos rápidamente la relación entre los PODs y el ReplicaSet correspondiente, pero en entornos reales donde podemos tener cientos de PODs corriendo esto puede ser bastante más confuso, con lo que podemos comprobar como está gestionado un POD haciendo un describe del mismo:

Describe de un POD controlado por un ReplicaSet.

Como vemos en la líinea Controlled By, este POD está controlado por el objeto superior ReplicaSet/webapp.

Sabemos que los PODs controlados por un objeto de tipo ReplicaSet están dados por el selector del ReplicaSet y la etiqueta de los PODs, es decir que todos los PODs que contengan la etiqueta que coincida con el selector definido serán controlados por un ReplicaSet determinado.

Esto nos puede llevar a situaciones en las que, si por alguna razón hay PODs con etiquetas coincidentes con las del campo selector de un ReplicaSet, dichos PODs pasen a estar bajo el control de dicho ReplicaSet. Por ejemplo, partiendo de la situación anterior, si arranco manualmente un POD basado en la siguiente descripción:

Definición de un POD simple.

Al aplicar esta configuración debería existir un nuevo POD, con el nombre webserver-v1, corriendo en el cluster:

Estado del cluster.

Pero, como podemos ver, el nuevo POD no aparece en la salida del comando get all así que ¿donde está el nuevo POD? Es importante que nos fijemos en que el campo selector del ReplicaSet coincide con el campo label asignado en el POD que hemos descrito, por tanto el KCP lo ha puesto directamente bajo el control del ReplicaSet webapp. Como el número de PODs establecido del ReplicaSet ya coincide con el número de PODs corriendo en el sistema, el nuevo POD con nombre webserver-v1 se ha borrado nada más arrancar. Podemos confirmar este hecho consultando la sección de Events del ReplicaSet con el subcomando describe de kubectl:

Descripción del ReplicaSet webapp.

Podemos observar que la última línea muestra que se ha borrado el POD webserver-v1, lo que nos indica que nada más aplicar el fichero de descripción del mismo, el KCP ha puesto dicho POD bajo control del ReplicaSet webapp debido a que su etiqueta coincide con el campo selector del mismo.

Este punto debe tenerse en cuenta ya que implica que, si no identificamos correctamente las templates de PODs de diferentes objetos, podemos provocar conflictos en los cuales objetos de bajo nivel como PODs estén controlados por otros de alto nivel a los que no corresponden.

Hasta aquí lo más básico relacionado con los objetos de tipo ReplicaSet, en próximas entradas veremos como controlar este tipo de objetos con Deployments.

Kubernetes - Conceptos básicos III

Siguiendo con los conceptos básicos de Kubernetes, vamos a estudiar de forma simple la arquitectura de un cluster de Kubernetes.

Vimos en la primera entrada de esta serie de posts que podíamos distinguir entre nodos master y nodos worker, llamados anteriormente minions. En general, dentro de la terminología de Kubernetes, un nodo es directamente un worker, el cual se caracteriza porque dispone del motor de ejecución de contenedores, los procesos kubelet y kube-proxy y es gestionado por los componentes del master.
 
En general, la arquitectura de un cluster de Kubernetes estará formado por uno o más nodos master y varios nodos worker. Los administradores usarán el comando kubectl para comunicarse con un balanceador de carga, que repartirá las conexiones entre todos los nodos master, controlando así el cluster y estableciendo el estado deseado del cluster. La forma más sencilla de ver esta arquitectura es la siguiente:
 
Arquitectura básica de un cluster de Kubernetes.

Esta gestión de los nodos worker por parte del master, se realiza mediante la comunicación entre el apiserver del nodo master y el proceso kubelet de los nodos worker. Esta comunicación permite al master obtener los logs del nodo worker, la ejecución de contenedores y proporcionar la característica de reenvío de puertos de kubelet.

Esta comunicación entre el apiserver y kubelet se realiza mediante el protocolo HTTPS, pero el apiserver por defecto no comprueba el certificado ofrecido por kubelet. Para forzar la comprobación del certificado ofrecido por kubelet, es necesario usar la opción --kubelet-certificate-authority del apiserver especificando un conjunto de certificados raíz que permitan la comprobación del certificado de kubelet.

El apiserver también se conecta con los nodos y contenedores usando el protocolo HTTP y aunque, puede cambiarse a HTTPS no se realiza ningún tipo de validación de credenciales ni de comprobación de certificados en estas conexiones.

Adicionalmente, los nodos worker y servicios del propio master se comunican con el apiserver, que por defecto acepta peticiones HTTPS en el puerto 443. Para añadir más seguridad a estas comunicaciones es importante utilizar autenticación de clientes, por ejemplo mediante certificados de cliente, así como autorizaciones para dichas conexiones.

Podemos obtener el estado de un worker, en base a una serie de características del mismo, utilizando el comando siguiente:

Descripción de estado de un nodo.
 
La salida de este comando es bastante extensa y entre toda la información que proporciona, nos devolverá el rol del nodo, su hostname y dirección interna y externa, la capacidad del nodo en terminos de CPU y memoria disponible, la versión del motor de ejecución de contenedores, una lista de los últimos eventos y el estado de condiciones como falta de memoria o disco.

Dentro de los tipos de condiciones de un nodo worker, es muy importante la condición Ready. Esta condición indica al master si el nodo worker es capaz de ejecutar contenedores y puede tener tres valores diferentes, True, False o Unknown. Si la condición Ready de un nodo worker permanece en estado False o Unknown durante más de un tiempo determinado, que por defecto son 5 minutos y se denomina pod-eviction-timeout, el kube-controller-manager del master lanza un borrado de los contenedores que se estén ejecutando en el nodo worker. Si el master no puede comunicarse con el proceso kubelet del nodo worker, es posible que los contenedores sigan ejecutándose en el nodo fallido hasta que la comunicación con el kube-apiserver vuelva a establecerse.

En caso de querer realizar algún tipo de operación de mantenimiento con un nodo worker, que implique que el nodo no puede aceptar contenedores, podemos hacer el nodo worker no disponible con el comando:

Marcando un nodo como no disponible.

El proceso kube-controller-manager del master ejecuta controladores que operan sobre recursos y objetos del cluster de Kubernetes, siendo uno de estos el controlador de nodos. Este controlador de nodos es responsable, entre otras cosas, de monitorizar la salud de todos los nodos del cluster y en caso de ser necesario, de mover los contenedores de un nodo que no responde a otro cuyo estado Ready sea True.

domingo, 20 de diciembre de 2020

Kubernetes y el soporte de Docker

Tras el anuncio por parte del equipo de Kubernetes de no continuar soportando Docker como motor de ejecución de contenedores, muchos hemos pensado ¿y ahora que es lo que debemos hacer? Pues para empezar, lo mejor es leer el anuncio oficial de Kubernetes, el cual podéis encontrar en el siguiente enlace.

Otro artículo interesante, publicado por Red Hat, explica un poco más las razones detrás de este cambio y en el cual podemos ver que está muy relacionado con la complejidad del desarrollo que implica integrar Docker como motor de contenedores.

Lo importante que sacamos en claro de ambos artículos, es que podemos seguir usando todas las imágenes que hemos desarrollado hasta hora utilizando Docker con lo que, en principio, el impacto debería ser mínimo.

Sin embargo, para ir adelantándonos un poco, vamos a ver de forma rápida como podemos adaptar una instalación que tengamos de minikube para que el motor de ejecución de contenedores sea otro diferente a Docker.

En un post anterior vimos como instalar minikube usando un host con Docker. Aquella instalación utilizaba Docker como motor para la ejecución de contenedores. Ahora, lo que vamos a hacer es sencillamente volver a crear un cluster de Kubernetes mediante minikube, pero en este caso vamos a especificar que el runtime de contenedores será otro diferente, en mi caso he escogido containerd ya que se instala por defecto con Docker. El comando en cuestión es el siguiente:

Creación del "cluster" con minikube.

Es recomendable actualizar a las últimas versiones disponibles de minikube y containerd, además de especificar en el comando que la versión de Kubernetes a usar es la 1.20.0. Para el correcto funcionamiento de la red, en mi caso ha sido necesario copiar el contenido de la ruta /usr/libexec/cni a /opt/cni/bin. Esto se debe a que, por defecto, los plugins CNI se buscan en /opt/cni/bin pero el paquete containernetworking-plugins los instala en /usr/libexec/cni. Como referencia, podemos ver que el fichero de configuración de containerd (/etc/containerd/config.toml) especifica que la ruta para la búsuqeda de plugins de red es /opt/cni/bin así que podemos cambiarlo igualmente a la ruta de instalación del paquete containernetworking-plugins:

Configuración de los plugins CNI.

Una vez creado correctamente nuestro cluster, podemos comprobar que el motor de contenedores ya no es Docker con solo comprobar si hay contenedores corriendo bajo su control:

Contenedores controlados por Docker.
 
Podemos ver, sin embargo, que los PODs de los servicios de infraestructura de Kubernetes se encuentran corriendo correctamente:
 
Servicios de infraestructura de Kubernetes.

Como hemos cambiado el runtime de contenedores a containerd, podemos comprobar que estos se están ejecutando correctamente usando el comando siguiente:

Contenedores bajo el control de containerd.

Un punto importante del comando anterior, es que he tenido que especificar el namespace del cual quiero obtener información, siendo por defecto k8s.io para containerd. En este namespace veremos tanto los contenedores de infraestructura de Kubernetes, así como aquellos correspondientes a nuestros servicios.

Por tanto hemos cambiado el runtime de nuestro cluster a containerd correctamente. Ahora solo tenemos que comprobar que, cualquiera de nuestras imágenes usadas hasta ahora con Docker, siguen funcionando correctamente. Por ejemplo, creando un replicaset con la imagen de un servidor Apache simple almacenada en Docker Hub, tenemos lo siguiente:

Despliegue de un replicaset usandoo una imagen en Docker Hub.

Por tanto, nuestras imágenes pueden seguir usándose sin problemas y el impacto que podemos tener es menor de lo que podíamos pensar. De todos modos, es importante revisar el impacto que implica en clusters reales el cambio del runtime de contenedores de Docker a containerd o cri-o. Además será importante investigar un poco más como funcionan y como podemos interactuar coon estos motores de contenedores.