commit inicial

This commit is contained in:
Mauro Torrez 2019-04-07 23:44:01 -03:00
commit 978e630342
9 changed files with 1295 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*~
\#*
.#*
*.pyc
*.bak
__pycache__

146
README.md Normal file
View File

@ -0,0 +1,146 @@
# Rol openldap-docker
Este rol configurar un servicio OpenLDAP dentro de un container
Docker. Permite un esquema de replicación delta-syncrepl [deltasync].
El rol efectúa a grandes rasgos las siguientes tareas:
1. _Configuración general_: setea clave del administrador
`cn=admin,cn=config` y carga esquemas.
2. _Configuración de provider delta-syncrepl_: configura los módulos
`accesslog` y `syncprov` en el provider de replicación.
3. _Configuración de dominios_: Hace un _loop_ sobre cada dominio
especificado en `openldap_domains`, configurando entradas de las
base de datos en `cn=config` y clientes de replicación del dominio.
La siguientes consideraciones deben ser tenidas en cuenta al usar este rol:
* Se deben especificar *primero* los dominios más generales
(`ejemplo.com.ar`) y luego los más específicos
(`sub.ejemplo.com.ar`).
* La base `cn=accesslog` del provider requiere que se especifique un
usuario administrador en la variable `openldap_accesslog_admin_dn`.
Este usuario *debe* pertenecer a alguno de los dominios
configurados en el servidor.
* Se debe setear una clave para el usuario administrador
`cn=admin,cn=config` en la variable `openldap_admin_password`.
## Configuración del rol
Las variables a continuación determinan el comportamiento del rol.
El valor por defecto se muestra entre paréntesis.
### Configuraciones generales
* `openldap_schemas`: Schemas a cargar. Cada schema personalizado se
debe agregar el LDIF correspondiente en
`files/schemas/NOMBRE.ldif` dentro del rol. Valor por defecto:
```
- core
- cosine
- nis
- inetorgperson
- misc
```
* `openldap_create_dit_entries` (`yes`): Especifica si se debe crear
el DIT: dominios, usuarios administradores, OUs, para cada
dominio.
### Replicación
A no ser que se seteen las variables a continuación, el rol no
configura replicación alguna.
* `openldap_provider` (`no`):
En un esquema de replicación, el host es provider.
* `openldap_consumer` (`no`):
En un esquema de replicación, el host es consumer.
* `openldap_replicator_base` (`dc=example,dc=com`):
DN base a partir del cual se replica.
* `openldap_accesslog_dir` (`/var/lib/ldap/accesslog`):
Directorio donde crear la base `cn=accesslog`.
### Credenciales
* `openldap_admin_password` (`password`):
Clave del usuario administrador `cn=admin,cn=config`.
* `openldap_accesslog_admin_dn` (`cn=admin,dc=example,dc=com`):
DN del usuario administrador para la base `cn=accesslog`.
Se debe setear en el _provider_.
* `openldap_replicator_dn` (`cn=replicator,dc=example,dc=com`):
DN del usuario usado para replicación.
Se debe setear en el _provider_ y en el _consumer_.
* `openldap_replicator_password` (`password`):
Clave del usuario usado para replicación.
Se debe setear en el _provider_ y en el _consumer_.
### Valores por defecto para los dominios
* `openldap_default_db_access`: Lista con permisos de acceso.
Valor por defecto:
```
- {0}to attrs=userPassword by self write by anonymous auth by * none"
- {1}to attrs=shadowLastChange by self write by * read"
- {2}to * by * read"
```
* `openldap_default_db_index`: Lista con índices del dominio.
Valor por defecto:
```
- cn,uid eq
- member,memberUid eq
- objectClass eq
- uidNumber,gidNumber eq
```
* `openldap_default_db_limits`: Límites de acceso al dominio.
Valor por defecto: `[]` (lista vacía)
* `openldap_default_domain_ous`: OUs a crear dentro del dominio.
Valor por defecto:
```
- Alias
- Group
- People
```
### Configuración de dominios
La configuración de los dominios se setea en la variable
`openldap_domains` (`[]`). Esta variable es una _lista de
diccionarios_ en la que cada item representa un dominio.
Cada dominio (diccionario) tiene las siguientes claves:
* `name`: Obligatorio. Nombre del dominio en notación `mi.dominio.com`.
Para este ejemplo se crea la organizacion `dc=mi,dc=ejemplo,dc=com`.
* `admincn` (`admin`): cn del administrador, cuyo DN vendrá dado por
`cn=admin,dc=mi,dc=ejemplo,dc=com`.
* `adminpw` (`password`): clave del administrador del dominio.
Se recomienda cambiar este valor, o setear la variable `adminpw_id`.
* `adminpw_id`: ID de la clave del administrador en Rattic.
Sobreescribe el valor de `adminpw` con la clave obtenida de Rattic.
* `access`: lista con permisos de acceso.
Por defecto toma el valor de la variable `openldap_default_db_access`.
* `ou`: unidades organizacionales del dominio.
Por defecto toma el valor de la variable `openldap_default_domain_ous`.
* Si bien se crea un usuario admin para los subdominios, éstos por
defecto no tienen permiso alguno.
## Referencias
[deltasync]: https://openldap.org/doc/admin24/replication.html#Delta-syncrepl

486
action_plugins/ldap.py Normal file
View File

@ -0,0 +1,486 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.action import ActionBase
from ansible.errors import AnsibleError
from ansible.utils.vars import merge_hash
from ansible.module_utils._text import to_text
from ansible.module_utils.parsing.convert_bool import boolean
import sys
import re
import base64
import io
try:
import ldap
import ldif
HAS_LDAP = True
class MyLDIF(ldif.LDIFParser):
'''Clase auxiliar para extraer información de LDIF'''
def __init__(self,input):
ldif.LDIFParser.__init__(self,input)
self._entries = []
def handle(self,dn,entry):
self._entries.append((dn,entry))
def entries(self):
return self._entries
except ImportError:
HAS_LDAP = False
class LDAPException(Exception):
'''Excepción genérica de este módulo'''
pass
# ---- funciones para chequeo de claves, sacado de
# http://www.openldap.org/faq/data/cache/347.html
import os
import hashlib
from base64 import encodestring as b64encode
from base64 import decodestring as b64decode
def makeSecret(password):
salt = os.urandom(4)
h = hashlib.sha1(password)
h.update(salt)
return "{SSHA}" + b64encode(h.digest() + salt)[:-1]
def checkPassword(challenge_password, password):
try:
challenge_bytes = b64decode(challenge_password[6:])
digest = challenge_bytes[:20]
salt = challenge_bytes[20:]
hr = hashlib.sha1(password)
hr.update(salt)
return digest == hr.digest()
except:
pass
return False
class ActionModule(ActionBase):
# Nombre de los argumentos
ARG_DN = 'dn'
ARG_OBJCLS = 'objectClass'
ARG_ATTRS = 'attributes'
ARG_FILTER = 'filter'
ARG_LDIF = 'from_ldif'
ARG_REMOTE = 'remote_src'
ARG_RDN = 'dn_relative'
ARG_CURLY = 'dn_curly_hack'
ARG_BINDDN = 'bind_dn'
ARG_BINDPW = 'bind_pw'
ARG_SRVURI = 'server_uri'
ARG_TLS = 'start_tls'
ARG_VALCRT = 'validate_certs'
ARG_STYPE = 'search_type'
ARG_STATE = 'state'
# Atributos que representan claves
ATTR_PASSWORD = ('olcRootPW', 'userPassword')
# Atributos que no se pueden eliminar
ATTR_FRAGILE = ('olcModuleLoad','olcAttributeTypes')
# Atributos RDN, no se pueden modificar
ATTR_RDN = ()
def __init__(self, *args, **kwargs):
super(ActionModule, self).__init__(*args,**kwargs)
self.result= dict(failed=False,changed=False,action_history=[])
self.dn = None
self.attrs = dict()
self.entries = []
def _read_args(self):
'''
Leer argumentos y efectuar búsqueda inicial del DN
'''
# Leer DN y parametros de busqueda
self.req_dn = self._task.args.get(self.ARG_DN, None)
self.dn_relative = self._task.args.get(self.ARG_RDN, False)
self.search_filter = self._task.args.get(self.ARG_FILTER, '(objectClass=*)')
self.search_type = self._task.args.get(self.ARG_STYPE, 'sub')
self.state = self._task.args.get(self.ARG_STATE, 'present')
self.oc = self._task.args.get(self.ARG_OBJCLS, None)
self.req_attrs = self._task.args.get(self.ARG_ATTRS, {})
if self._task.args.get(self.ARG_LDIF, []):
'''Leer LDIF pasado como argumento y usarlo para configurar entrada'''
self._load_from_ldif()
if self.dn_relative:
'''Setear base de busqueda al parent de req_dn'''
reqdn = ldap.dn.str2dn(self.req_dn)
rdnkey = reqdn[0][0][0]
rdnval = reqdn[0][0][1]
if boolean(self._task.args.get(self.ARG_CURLY,False), strict=False):
rdnval = '{*}' + rdnval
self.search_filter = '(&{}({}={}))'.format(
self.search_filter, rdnkey, rdnval)
self.search_base = ldap.dn.dn2str(reqdn[1:])
else:
self.search_base = self.req_dn
# Validar params obligatorios
if not self.req_dn:
raise LDAPException("'{}' argument is mandatory, got value: {}".format(self.ARG_DN,self.req_dn))
if not self.oc:
raise LDAPException("'{}' argument is mandatory".format(self.ARG_OBJCLS))
# parametros de conexion al servidor
self.bdn = self._task.args.get(self.ARG_BINDDN, None)
self.bpw = self._task.args.get(self.ARG_BINDPW, None)
self.uri = self._task.args.get(self.ARG_SRVURI, 'ldapi:///')
self.tls = self._task.args.get(self.ARG_TLS, False)
self.valcert = self._task.args.get(self.ARG_VALCRT, True)
# Armar comando ldapsearch
self.ldapsearch_cmd = 'ldapsearch -LLL -H {}'.format(self.uri)
if self.bdn and self.bpw:
self.ldapsearch_cmd = '{} -D {} -w "{}"'.format(
self.ldapsearch_cmd, self.bdn, self.bpw)
else:
self.ldapsearch_cmd = "{} -Y EXTERNAL".format(
self.ldapsearch_cmd)
if not self.valcert:
self.ldapsearch_cmd = 'LDAPTLS_REQCERT=never {}'.format(self.ldapsearch_cmd)
if self.tls:
self.ldapsearch_cmd = '{} -ZZ'.format(self.ldapsearch_cmd)
# Armar parametros para módulos ldap_entry, ldap_attr
self.ldap_params = {
'bind_dn': self.bdn,
'bind_pw': self.bpw,
'server_uri': self.uri,
'start_tls': self.tls,
'validate_certs': self.valcert
}
def _load_from_ldif(self):
'''Configurar argumentos a partir de archivo LDIF'''
# copiado en buena parte de https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/unarchive.py
source = self._task.args.get(self.ARG_LDIF)
remote_src = boolean(self._task.args.get(self.ARG_REMOTE, False),
strict=False)
ldif_content = io.StringIO()
# el ldif es local
if not remote_src:
'''encontrar nombre real del archivo'''
try:
source = self._loader.get_real_file(self._find_needle('files',source))
except AnsibleError as e:
raise AnsibleActionFail(to_text(e))
'''leer archivo'''
with open(source,'r') as f:
ldif_content.write(unicode(f.read()))
# el ldif es remoto
else:
try:
slurp = self._execute_module(module_name='slurp',module_args=dict(src=source))
if slurp.get('failed',False):
raise AnsibleError('Failed reading remote file!')
if slurp.get('encoding') == 'base64':
ldif_content.write(unicode(b64decode(slurp['content'])))
else:
raise AnsibleError('Unknown encoding returned from slurp, this is a bug.')
except AnsibleError as e:
raise AnsibleActionFail('Error reading remote file: {}'.format(e.message))
ldif_content.write(u'\n\n')
ldif_content.seek(0)
parser = MyLDIF(ldif_content)
parser.parse()
# Sólo se soporta UNA entrada por archivo LDIF
entry = parser.entries()[0]
self.result['entry'] = entry
self.req_dn = entry[0]
self.req_attrs = entry[1].copy()
del self.req_attrs['objectClass']
self.oc = entry[1].get('objectClass',None)
# self.result['req_attr'] = self.req_attrs
def _ldapsearch(self, args):
'''Ejecutar comando ldapsearch y devolver el resultado'''
command = "{} {}".format(self.ldapsearch_cmd,args)
result = {}
failed = False
try:
result = self._execute_module(
module_name='command',
module_args=dict(_raw_params=command))
failed = False if result['rc'] in (0, 32) else result.get('failed',False)
except Exception as e:
raise LDAPException("Failed command: {}, message: {}".format(
command, e.message))
if failed:
raise LDAPException("Failed command: {}, message: {}".format(
command, result['stderr']))
result['failed'] = failed
return result
def _find_dn(self):
'''
Encontrar DN que matchea rdnkey=rdnval dentro del DN self.parent,
aplicando el filtro de búsqueda en self.sfilter.
'''
self.search_type = 'base'
if self.dn_relative:
self.search_type = 'one'
# Ejecutar comando
entries = self.search()
# DN encontrado
founddn = None
if len(entries):
founddn = entries[0][0]
# registrar resultados y salir
self.result['action_history'].append({
'function': 'finddn',
'outcome': ('found' if founddn else 'not found'),
})
self.dn = founddn
def search(self):
'''
Efectuar búsqueda, retornar entrada(s) encontradas
'''
qfilter = '(objectClass=*)'
# armar el filtro de búsqueda agregando los filtros adicionales
if isinstance(self.search_filter, basestring):
qfilter = self.search_filter
else:
for f in self.search_filter:
qfilter = '(&{}{})'.format(qfilter,f)
# efectuar búsqueda mediante ldapsearch
cmd_args = '-s {} -b "{}" "{}"'.format(self.search_type, self.search_base, qfilter)
result = self._ldapsearch(cmd_args)
entries = []
if result['rc'] in (0,32):
# procesar la salida LDIF
ldif_out = io.StringIO(result['stdout']+'\n\n')
parser = MyLDIF(ldif_out)
parser.parse()
entries = parser.entries()
self.result['action_history'].append({
'function': 'search',
'outcome': ('found' if len(entries) else 'no results found'),
'arguments': cmd_args
})
else:
self.result['action_history'].append({
'function': 'search',
'outcome': 'failed',
'arguments': cmd_args
})
return entries
def add_entry(self):
'''
Agregar entrada al directorio llamando al módulo ldap_entry.
Los datos se obtienen de los argumentos pasados al módulo.
'''
attrs = dict(self.req_attrs)
for attr in attrs.keys():
if attr in self.ATTR_PASSWORD:
attrs[attr] = makeSecret(attrs[attr])
result = {}
failed = False
changed = False
args = dict( dn=self.req_dn,
objectClass=self.oc,
attributes=attrs,
params = self.ldap_params
)
try:
# llamar a ldap_entry con argumentos dn, objectClass, attributes
result = self._execute_module(
module_name='ldap_entry',
module_args=args
)
failed = result.get('failed',False)
changed = result.get('changed',False)
except Exception as e:
raise LDAPException("Error invoking ldap_entry: {}: {}".format(
type(e).__name__,to_text(e)))
if failed:
raise LDAPException("Error invoking ldap_entry, args: {}, message: {}".format(
args, result))
# registrar resultados
if changed:
self.result['changed'] = True
self.result['action_history'].append({
'function': 'add_entry',
'outcome': 'failed' if failed else 'success',
'result': result
})
# actualizar self.dn con el DN de la entrada recién creada
self._find_dn()
def findattr(self):
'''
Leer atributos de la entrada con ldapsearch.
'''
# efectuar búsqueda
cmd_args = '-s base -b "{}"'.format(self.dn)
result = self._ldapsearch(cmd_args)
# Leer LDIF resultante
ldif_out = io.StringIO(result['stdout'])
parser = MyLDIF(ldif_out)
parser.parse()
entries = parser.entries()
# validar que el resultado se una única entrada
if len(entries) != 1:
raise LDAPException("Unable to read attributes for DN={}, cmd={}, entries={}".format(self.dn,cmd,entries))
if entries[0][0] != self.dn:
raise LDAPException("Unexpected DN={} while reading entry attributes, expected {}".format(
entries[0][0], self.dn))
# registrar invocación y guardar self.attrs
self.result['action_history'].append({
'function': 'findattr',
'outcome': 'success',
'result': result
})
self.attrs = entries[0][1]
def updateattr(self, attribute, value):
'''Verificar/actualizar valor para un atributo'''
if attribute in self.ATTR_PASSWORD:
if not isinstance(value, basestring):
value = value[0]
if checkPassword(self.attrs.get(attribute,[None])[0], value):
# si la clave matchea, salir
self.result['action_history'].append({
'function': 'updateattr',
'arguments': [attribute, 'hidden password'],
'outcome': 'password not changed'
})
return
else:
# setear el valor al hash salteado
value = [makeSecret(value)]
# convertir el valor a una lista
if isinstance(value, basestring):
value = [value]
changed = False
failed = False
result = None
args = {
'dn': self.dn,
'name': attribute,
'values': value,
'state': 'present' if attribute in self.ATTR_FRAGILE else 'exact',
'params': self.ldap_params
}
if set(value) != set(self.attrs.get(attribute,[])):
# actualizar valor llamando a ldap_attr
try:
result = self._execute_module(
module_name='ldap_attr',
module_args=args
)
changed = result.get('changed',False)
failed = result.get('failed',False)
except Exception as e:
raise LDAPException("Error invoking ldap_attr: {}: {}, result = {}".format(
type(e).__name__,to_text(e),result))
# guardar resultados
if changed:
self.result['changed'] = True
if failed:
self.result['failed'] = True
self.result['action_history'].append({
'function': 'updateattr',
'arguments': [attribute, value],
'outcome': 'failed' if failed else ('changed' if changed else 'unchanged'),
'result': result
})
def run(self, tmp=None, task_vars={}):
'''Función principal, punto de entrada al plugin'''
# Llamar a la función run de la clase madre
result = super(ActionModule, self).run(tmp, task_vars)
# Verificar si debo salir o fallar
if result.get('skipped', False) or result.get('failed', False):
return result
# Asegurar que la importación LDAP no falló, salir con gracia
if not HAS_LDAP:
return merge_hash( self.result, {
'failed': True,
'msg': "Missing required 'ldap' module (pip install python-ldap)."
})
try:
# leer argumentos
self._read_args()
# si solo se requiere buscar, efectuar busqueda y salir
if self.state == 'search':
self.result.update(dict(entries=self.search()))
return self.result
# busqueda inicial del DN
self._find_dn()
if self.dn is None:
# el DN no existe, crear la entrada
self.add_entry()
else:
# el DN existe, verificar atributos
attrs = self.findattr()
# verificar/actualizar cada atributo
for attr in self.req_attrs.keys():
self.updateattr(attr, self.req_attrs[attr])
except LDAPException as e:
return merge_hash( self.result, {
'failed': True,
'msg': "Invalid arguments: {}".format(e.message)
})
self.result['dn'] = self.dn
self.result['attrs'] = self.attrs
return self.result

94
defaults/main.yml Normal file
View File

@ -0,0 +1,94 @@
---
# Indica si el host es provider en un esquema de replicación
openldap_provider: no
# Nombre del provider para un host consumer
openldap_provider_host: null
# Indica si el host es esclavo en un esquema de replicación
openldap_consumer: no
# Clave del usuario cn=admin,cn=config
openldap_admin_password: password
# DN del administrador de la base cn=accesslog (provider)
openldap_backup_dir: "/srv/backups/ldap"
openldap_backup_keep: 200
# DN del administrador de la base cn=accesslog (provider)
openldap_accesslog_admin_dn: cn=admin,dc=example,dc=com
# DN, clave y search base del replicador
openldap_replicator_dn: cn=replicator,dc=example,dc=com
openldap_replicator_password: password
openldap_replicator_base: dc=example,dc=com
# port in docker HOST to bind ldap service
openldap_bind_port: 10389
openldap_bind_host: "localhost"
# docker image name
openldap_image_name: "i-openldap"
# docker container name
openldap_container_name: "c-openldap"
# docker volume names
openldap_volume_config: "ldap_config"
openldap_volume_data: "ldap_data"
openldap_volume_backup: "ldap_backup"
# permisos de acceso por defecto
openldap_default_db_access:
- "{0}to attrs=userPassword by self write by anonymous auth by * none"
- "{1}to attrs=shadowLastChange by self write by * read"
- "{2}to * by * read"
# indices por defecto
openldap_default_db_index:
- "cn,uid eq"
- "member,memberUid eq"
- "objectClass eq"
- "uidNumber,gidNumber eq"
# limites por defecto: ninguno
openldap_default_db_limits: []
# OUs creadas por defecto dentro de cada dominio
openldap_default_domain_ous:
- Alias
- Group
- People
# Dominios a configurar:
# Cada dominio se especifica en un diccionario con las siguientes claves:
# - name: nombre del dominio (ejemplo.com) (obligatorio)
# a partir de este nombre se crea la organizacion dc=ejemplo,dc=com
# - admincn: nombre cn del administrador (defecto=admin)
# este cn deriva en un DN cn=admin,dc=ejemplo,dc=com
# - adminpw: clave del administrador de este dominio (defecto=password)
# SE RECOMIENDA CAMBIAR ESTE VALOR, O SETEAR ID RATTIC
# - access: lista con permisos de acceso. por defecto es la lista definida
# en la variable openldap_default_db_access
# - ou: unidades organizacionales del dominio, defecto openldap_default_domain_ous
openldap_domains: []
# - name: unl.edu.ar
# - name: rectorado.unl.edu.ar
# - name: servicios.unl.edu.ar
# Directorio donde crear la base cn=accesslog
openldap_accesslog_dir: "/var/lib/ldap/accesslog"
# esquemas a cargar
openldap_schemas:
- core
- cosine
- nis
- inetorgperson
- misc
# crear entradas en el DIT? (dominios, administradores, OUs)
openldap_create_dit_entries: yes
# habilitar modulo memberof?
openldap_enable_memberof: yes

40
files/Dockerfile Normal file
View File

@ -0,0 +1,40 @@
FROM debian:stable-slim
MAINTAINER Mauro Torrez <contact@mau.ro>
ENV OPENLDAP_ROOT_PASSWORD="root"
# space-separated list of schemas
ENV OPENLDAP_SCHEMAS="misc"
ENV OPENLDAP_BACKUP_MIN="0"
ENV OPENLDAP_BACKUP_HOUR="1"
ENV OPENLDAP_BACKUP_DOM="*"
ENV OPENLDAP_BACKUP_MON="*"
ENV OPENLDAP_BACKUP_DOW="*"
# TODO configurar Cron de backup
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
slapd \
ldap-utils && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY entrypoint.sh /entrypoint.sh
# TODO backup and restore
# ADD ldap_backup.sh /usr/local/sbin/ldap_backup.sh
# ADD ldap_restore.sh /usr/local/sbin/ldap_restore.sh
# add my_custom_schema: install by setting OPENLDAP_SCHEMAS=my_custom_schema
# COPY my_custom_schema.ldif /etc/ldap/schema/my_custom_schema.ldif
EXPOSE 389
VOLUME ["/etc/ldap/slapd.d", "/var/lib/ldap", "/var/backups/ldap"]
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
# log level info:
CMD ["slapd", "-d", "32768", "-u", "openldap", "-g", "openldap"]

85
files/entrypoint.sh Executable file
View File

@ -0,0 +1,85 @@
#!/bin/bash
msg(){ ${VERBOSE:-true} && echo ${@} ; }
assert(){ [[ $? -eq 0 ]] || { [[ -n ${1} ]] && msg ${@} ; exit 1 ; } }
# from https://github.com/dinkel/docker-openldap/blob/master/entrypoint.sh:
# When not limiting the open file descritors limit, the memory consumption of
# slapd is absurdly high. See https://github.com/docker/docker/issues/8231
ulimit -n 8192
msg "I: running slapd for initial setup..."
slapd -u openldap -g openldap -h ldapi:///
assert "E: openldap died unexpectedly!"
PIDFILE=$(ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b "cn=config" -s base \
"" olcPidFile | grep olcPidFile | awk "{print $2}")
msg "I: slapd running with PID ${PIDFILE}"
[[ -n "${OPENLDAP_ADMIN_PASSWORD}" ]]
assert "E: please set non-empty password in OPENLDAP_ADMIN_PASSWORD and retry."
HASHED_PW=$(slappasswd -h {SSHA} -s "${OPENLDAP_ADMIN_PASSWORD}")
[[ -n "${HASHED_PW}" ]]
assert "E: password hash unexpectedly empty!"
msg "I: Setting administrator password..."
ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: olcDatabase={0}config,cn=config
changetype: modify
replace: olcRootPW
olcRootPW: ${HASHED_PW}
EOF
assert "FATAL: failure setting administrator password!"
# find current schemas
eval "declare -A LOADED_SCHEMAS=( $(ldapsearch -LLL -Y EXTERNAL -H ldapi:/// \
-b "cn=schema,cn=config" -s one cn \
| sed -n 's/^cn:.*[{].*[}]\(.*\)$/[\1]=loaded/p') )"
msg "I: currently loaded schemas: ${!LOADED_SCHEMAS[@]}"
# load schemas
# built-in: core, cosine, nis, inetorgperson
# available: collective, corba, duaconf, dyngroup, java, misc, nis, openldap, pmi, ppolicy
for schema in ${OPENLDAP_SCHEMAS}
do
[[ -z "${LOADED_SCHEMAS[$schema]}" ]] || continue;
msg "I: loading schema ${schema}..."
[[ -f /etc/ldap/schema/${schema}.ldif ]]
assert "E: schema /etc/ldap/schema/${schema}.ldif not found!"
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/${schema}.ldif
assert "E: failure loading schema ${schema}!"
done
# enable memberof module
if ${OPENLDAP_ENABLE_MEMBEROF}
then
msg "I: enabling memberof module ..."
ldapmodify -LLL -Y EXTERNAL -H ldapi:/// <<EOF
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof
EOF
RES=$?
[[ $RES -eq 0 ]] || [[ $RES -eq 20 ]]
assert "E: failed loading memberof module (${RES})"
msg "I: module memberof enabled (${RES})"
unset RES
fi
# kill slapd after initial setup
msg "I: killing initial server..."
kill -INT $(cat ${PIDFILE})
# unset sensitive variables
unset OPENLDAP_ROOT_PASSWORD
unset HASHED_PW
unset LOADED_SCHEMAS
unset PIDFILE
# run Dockerfile CMD
msg "I: running CMD $@"
set -e
exec "$@"

304
tasks/domain.yml Normal file
View File

@ -0,0 +1,304 @@
---
# Configuración de un dominio en el DIT.
# Se distinguen 3 partes en este playbook:
# 1) Verificar si el dominio es parte de otro ya existente.
# 2) Si el dominio especificado NO es subdominio de otro, se agrega una entrada
# correspondiente en cn=config.
# 3) Se agrega el usuario admin para el dominio y las OUs
# respectivas, por defecto: People, Group, Alias
# parte 1: chequear si el dominio es subdominio de otro -----------------------
- name: "(aux) separar DN en componentes"
set_fact:
# componentes del dominio
dcs: "{{ domain.name.split('.') }}"
# dominio convertido a DN
ddn: "{{ domain.name.split('.')|map('regex_replace','^','dc=')|join(',') }}"
- name: "Buscar entradas en cn=config para {{ domain.name }} y superiores"
ldap:
state: "search"
dn: "cn=config"
objectClass: "olcDatabaseConfig"
filter: "(olcSuffix=*)"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
register: "sfxsearch"
- name: "(aux) matchear resultados de búsqueda"
set_fact:
sfxmatches: "[ {% for i in range(dcs|length -1, -1, -1) %}\
{% set sdn = dcs[i:]|map('regex_replace','^','dc=')|join(',') %}\
{% for e in sfxsearch.entries if e[1]['olcSuffix'][0] == sdn %}\
{{ { 'dn': sdn, 'name': dcs[i:]|join('.'), 'configdn': e[0] } }},\
{% endfor %}\
{% endfor %}\
]"
- name: "(aux) setear dominio superior y credenciales"
set_fact:
superd: "{{ sfxmatches | first | default({'name': domain.name, 'dn': ddn}) }}"
# DN del admin y clave
admindn: "cn={{ domain.admincn | default('admin') }},{{ ddn }}"
adminpw: "{{ domain.adminpw | default('password') }}"
- name: "(aux) mergear detalles de dominio superior"
set_fact:
super: "{{ openldap_domains | selectattr('name', 'equalto', superd.name) | \
first | combine(superd) }}"
# parte 2: agregar entrada en cn=config ---------------------------------------
- name: "Entrada en cn=config para {{ domain.name }}"
# Cuando el dominio no es hijo de otro ya existente, crear entrada en cn=config
when: "super.dn == ddn"
block:
- name: "(aux) setear propiedades"
set_fact:
olcDbIndex: "{{ domain.index | default(openldap_default_db_index) }}"
olcAccess: "{{ domain.access | default(openldap_default_db_access) }}"
backup_domains: "{{ backup_domains | default([]) | union([ddn]) }}"
- name: "(aux) validar que entryUUID esté en olcDbIndex (necesario para replicar)"
when:
- "openldap_provider == True"
- "olcDbIndex | map('regex_search','entryUUID.* eq$') | reject('equalto',[]) | list | length == 0"
set_fact:
olcDbIndex: "{{ olcDbIndex | union(['entryUUID eq']) }}"
- name: "(aux) propiedad olcAccess (provider)"
when:
- "openldap_provider == True"
- "domain.access_provider is defined"
set_fact:
olcAccess: "{{ domain.access_provider }}"
- name: "(aux) propiedad olcAccess (consumer)"
when:
- "openldap_consumer == True"
- "domain.access_consumer is defined"
set_fact:
olcAccess: "{{ domain.access_consumer }}"
- name: "Directorio para el dominio"
command: >-
docker exec -u openldap:openldap {{ openldap_container_name }}
mkdir "/var/lib/ldap/{{ ddn }}"
register: ret
failed_when: no
changed_when: "ret.rc == 0"
- name: "Entrada en cn=config para {{ ddn }}"
register: "entry_add"
ldap:
dn: "olcDatabase=mdb,cn=config"
dn_relative: yes
filter: "(olcSuffix=\"{{ ddn }}\")"
objectClass:
- "olcDatabaseConfig"
- "olcMdbConfig"
attributes:
olcDbMaxSize: "1073741824"
olcSuffix: "{{ ddn }}"
olcDbDirectory: "/var/lib/ldap/{{ ddn }}"
olcRootDN: "{{ admindn }}"
olcRootPW: "{{ adminpw }}"
olcAccess: "{{ olcAccess }}"
olcDbCheckpoint: "512 30"
olcLastMod: "TRUE"
olcDbIndex: "{{ olcDbIndex }}"
olcLimits: "{{ domain.limits | default(openldap_default_db_limits) }}"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Overlay memberof para {{ ddn }}"
when: "openldap_enable_memberof == True"
ldap:
dn: "olcOverlay=memberof,{{ entry_add.dn }}"
dn_relative: yes
objectClass:
- "olcOverlayConfig"
- "olcConfig"
- "olcMemberOf"
attributes:
olcMemberOfDangling: "ignore"
olcMemberOfRefInt: "FALSE"
olcMemberOfGroupOC: "groupOfNames"
olcMemberOfMemberAD: "member"
olcMemberOfMemberOfAD: "memberOf"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Overlay syncprov para {{ ddn }}"
when: "openldap_provider == True"
ldap:
dn: "olcOverlay=syncprov,{{ entry_add.dn }}"
dn_relative: yes
objectClass:
- "olcOverlayConfig"
- "olcSyncProvConfig"
attributes:
olcSpNoPresent: "TRUE"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Overlay accesslog para {{ ddn }}"
when: "openldap_provider == True"
ldap:
dn: "olcOverlay=accesslog,{{ entry_add.dn }}"
dn_relative: yes
objectClass:
- "olcOverlayConfig"
- "olcAccessLogConfig"
attributes:
olcAccessLogDB: "cn=accesslog"
olcAccessLogOps: "writes"
olcAccessLogPurge: "07+00:00 01+00:00"
olcAccessLogSuccess: "TRUE"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
# EL ORDEN EN LAS SIGUIENTES 2 es importante!!!
# solo se soporta UN provider
- name: "Configurar cliente de replicacion (1: entrada olcSyncRepl)"
when:
- "openldap_consumer == True"
- "openldap_provider_host | bool == True"
ldap_attr:
dn: "{{ entry_add.dn }}"
name: "olcSyncRepl"
state: "exact"
values: >-
{0}rid=3
provider=ldap://{{ openldap_provider_host }}
bindmethod=simple
binddn="{{ openldap_replicator_dn }}"
credentials="{{ openldap_replicator_password }}"
searchbase="{{ openldap_replicator_base }}"
logbase="cn=accesslog"
logfilter="(&(objectClass=auditWriteObject)(reqResult=0))"
schemachecking=on
type=refreshAndPersist
retry="60 10 300 +"
interval=00:00:01:00 syncdata=accesslog
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Configurar cliente de replicacion (2: entrada olcUpdateRef)"
when:
- "openldap_consumer == True"
- "openldap_provider_host | bool == True"
ldap_attr:
dn: "{{ entry_add.dn }}"
name: "olcUpdateRef"
state: "exact"
values: "ldap://{{ openldap_provider_host }}"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Desconfigurar cliente de replicacion (2: entrada olcUpdateRef)"
when: "openldap_consumer == False"
ldap_attr:
dn: "{{ entry_add.dn }}"
name: "olcUpdateRef"
state: "exact"
values: []
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Desconfigurar cliente de replicacion (1: entrada olcSyncRepl)"
when: "openldap_consumer == False"
ldap_attr:
dn: "{{ entry_add.dn }}"
name: "olcSyncRepl"
state: "exact"
values: []
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
# fin del bloque -------------------------------
# parte 3: agregar entrada en el DIT y crear usuario admin, replicator, OUs ---------------
- when:
- "openldap_create_dit_entries == True"
- "openldap_consumer == False"
block:
- name: "(aux) credenciales para modificar entradas en {{ super.dn }}"
set_fact:
rootdn: "cn={{ super.admincn | default('admin') }},{{ super.dn }}"
rootpw: "{{ super.adminpw | default(openldap_admmin_password|default('password')) }}"
- name: "Entrada para organización {{ ddn }}"
ldap:
dn: "{{ ddn }}"
objectClass:
- "dcObject"
- "organization"
- "top"
attributes:
o: "{{ domain.name }}"
bind_dn: "{{ rootdn }}"
bind_pw: "{{ rootpw }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Usuario admin para {{ ddn }}"
ldap:
dn: "{{ admindn }}"
objectClass:
- "organizationalRole"
- "simpleSecurityObject"
attributes:
description: "LDAP Administrator role for domain {{ domain.name }}"
userPassword: "{{ adminpw }}"
bind_dn: "{{ rootdn }}"
bind_pw: "{{ rootpw }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Usuario de replicacion para {{ ddn }}"
when:
- "super.dn == ddn"
- "ddn in openldap_replicator_dn"
ldap:
dn: "{{ openldap_replicator_dn }}"
objectClass:
- "organizationalRole"
- "simpleSecurityObject"
attributes:
description: "LDAP Replication role for domain {{ domain.name }}"
userPassword: "{{ openldap_replicator_password }}"
bind_dn: "{{ rootdn }}"
bind_pw: "{{ rootpw }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "OUs para {{ ddn }}"
ldap:
dn: "ou={{ item }},{{ ddn }}"
objectClass:
- "organizationalUnit"
- "top"
attributes:
ou: "{{ item }}"
bind_dn: "{{ rootdn }}"
bind_pw: "{{ rootpw }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
loop: "{{ domain.ou | default(openldap_default_domain_ous) }}"
- set_fact:
ldap_base_dn: "{{ lookup('vars', 'ldap_base_dn', default=ddn) }}"
ldap_admin_dn: "{{ lookup('vars', 'ldap_admin_dn', default=rootdn) }}"
ldap_admin_password: "{{ lookup('vars', 'ldap_admin_password', default=rootpw) }}"
# fin del bloque -------------------------------

52
tasks/main.yml Normal file
View File

@ -0,0 +1,52 @@
---
# Playbook for setting up a Docker container with openLDAP.
# A port binding to the Docker host is required for setting
# up domains and replication.
- name: "Create directory for building image"
file:
path: "/tmp/build.openldap-image"
state: "directory"
- name: "Copy required files"
copy:
src: "{{ item }}"
dest: "/tmp/build.openldap-image/"
loop:
- "Dockerfile"
- "entrypoint.sh"
- name: "Build openldap image"
docker_image:
path: "/tmp/build.openldap-image"
name: "{{ openldap_image_name }}"
- name: "Start openldap container"
docker_container:
image: "{{ openldap_image_name }}"
name: "{{ openldap_container_name }}"
volumes:
- "{{ openldap_volume_config }}:/etc/ldap"
- "{{ openldap_volume_data }}:/var/lib/ldap"
- "{{ openldap_volume_backup }}:/var/backups/ldap"
env:
OPENLDAP_ADMIN_PASSWORD: "{{ openldap_admin_password }}"
OPENLDAP_SCHEMAS: "{{ openldap_schemas | join (' ') }}"
OPENLDAP_ENABLE_MEMBEROF: "{{ 'true' if openldap_enable_module_memberof else 'false' }}"
networks:
- name: "{{ docker_network_name }}"
ports:
- "{{ openldap_bind_host }}:{{ openldap_bind_port }}:389"
- include_tasks: "provider.yml"
when: "openldap_provider == True"
- include_tasks: "domain.yml"
loop: "{{ openldap_domains }}"
loop_control:
loop_var: "domain"
- set_fact:
ldap_uri: "{{ lookup( 'vars', 'ldap_uri',
default='ldap://'+openldap_container_name+':389') }}"

82
tasks/provider.yml Normal file
View File

@ -0,0 +1,82 @@
---
# En este archivo se configura el provider en un esquema
# de replicación delta-syncrepl.
# Ver https://openldap.org/doc/admin24/replication.html#Delta-syncrepl
- name: "Habilitar módulos syncprov y accesslog"
ldap_attr:
dn: "cn=module{0},cn=config"
name: "olcModuleLoad"
values: >-
[ {% if openldap_enable_memberof %}
"{2}syncprov", "{3}accesslog" {% else %}
"{1}syncprov", "{2}accesslog" {% endif %} ]
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
- name: "Crear directorio para db accesslog"
file:
path: "{{ openldap_accesslog_dir }}"
state: "directory"
owner: "openldap"
group: "openldap"
- name: "Configurar base cn=accesslog"
ldap:
dn: "olcDatabase=mdb,cn=config"
dn_relative: yes
filter: "(olcSuffix=cn=accesslog)"
objectClass:
- "olcDatabaseConfig"
- "olcMdbConfig"
attributes:
olcRootDN: "{{ openldap_accesslog_admin_dn }}"
olcDbMaxSize: "8589934592"
olcSuffix: "cn=accesslog"
olcDbDirectory: "{{ openldap_accesslog_dir }}"
olcAccess:
- "{0}to * by dn=\"{{ openldap_replicator_dn }}\" read"
olcLimits:
- >-
{0}dn.exact="{{ openldap_replicator_dn }}"
time.soft=unlimited
time.hard=unlimited
size.soft=unlimited
size.hard=unlimited
- >-
{1}dn.exact="{{ openldap_accesslog_admin_dn }}"
time.soft=unlimited
time.hard=unlimited
size.soft=unlimited
size.hard=unlimited
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
register: accesslog_entry
- name: "Configurar base olcOverlay=syncprov,{{ accesslog_entry.dn }}"
ldap:
dn: "olcOverlay=syncprov,{{ accesslog_entry.dn }}"
dn_relative: yes
objectClass:
- "olcOverlayConfig"
- "olcSyncProvConfig"
attributes:
olcSpNoPresent: "TRUE"
olcSpReloadHint: "TRUE"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"
# Esto se debe agregar luego del overlay, por eso va aparte
- name: "Configurar propiedad olcDbIndex de {{ accesslog_entry.dn }}"
ldap_attr:
dn: "{{ accesslog_entry.dn }}"
name: "olcDbIndex"
values:
- "default eq"
- "entryCSN,objectClass,reqEnd,reqResult,reqStart"
bind_dn: "cn=admin,cn=config"
bind_pw: "{{ openldap_admin_password }}"
server_uri: "ldap://localhost:{{ openldap_bind_port }}"