feat(ldap): add role for managing openLDAP in a container

This commit is contained in:
transcaffeine 2021-05-03 08:43:34 +02:00
parent ff4a212d1e
commit e473eb415b
No known key found for this signature in database
GPG key ID: 03624C433676E465
12 changed files with 529 additions and 0 deletions

View file

@ -1 +1,2 @@
* @jcgruenhage @Ratzupaltuff * @jcgruenhage @Ratzupaltuff
/roles/ldap/ @jcgruenhage @transcaffeine

52
roles/ldap/README.md Normal file
View file

@ -0,0 +1,52 @@
# openLDAP role
## Description
Deploys [`famedly/containers/openldap`](https://gitlab.com/famedly/containers/openldap),
which is openldap running in an alpine linux-based docker container.
The `core.schema`, `cosine.schema` and `inetOrgPerson.schema` are loaded by default,
and an MDB database is configured for the `ldap_domain`.
Access control lists (ACLs) can be specified in `ldap_acls` and are applied to the MDB database.
A root user can be specified and has full access on the database,
full access to the config (`cn=config`) is given to local root
(`gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth`).
An `organizationalUnit` for users and groups is created per default,
the users should be created as `uid=$userName,ou=users,$ldap_dn` with `objectClass=inetOrgPerson`,
and groups would be `cn=$groupName,ou=groups,$ldap_dn` with `objectClass=groupOfNames`.
## Requirements
Needs `python-ldap` installed for the `ldap_entry`/`ldap_attr` modules to be able to connect.
Docker daemon also needs to run and be accessable from the `ansible_user`.
The role uses privilege escalation to become host-root to be able
to set the ACLs in the container (which needs root there).
## Usage
- `ldap_domain`: Where the LDAP server runs, e.g. 'example.org'.
The root node and it's DN are constructed from this value.
- `ldap_root_user`/`ldap_root_pass`/`ldap_root_pass_hash`: The root user
of the database and the password in cleartext and the hashed form of the password
which gets written into the config. The rootDN is constructed from
`ldap_root_user`+`ldap_dn` (`ldap_dn` is constructed from `ldap_domain`).
- `ldap_org`: Name of the organization. A root node in the DIT is automatically
created and the Organization name can be set here.
- `ldap_org_units`: Additional `organizationalUnit`s the role creates at the top
level of the DIT. Defaults to `[ groups, users ]`.
- `ldap_additional_schemas`: Can be populated with dicts of the form
`{name: "example.ldif", content: "schema_definition_here"}` to load those schemas
into the ldap config tree during initial setup.
- `ldap_additional_indices`: Can be used to declare additional indices on the `mdb`
database, like `uid eq` (maintains an equality index on the `uid` attribute).
See the [test playbook](tests/test.yml) for an example of how to use the role.
You can use `sudo ANSIBLE_ROLES_PATH="$(pwd)/roles" ansible-playbook -i roles/ldap/tests/inventory roles/ldap/tests/test.yml`
from the collection-directory to run the tests.

View file

@ -0,0 +1,58 @@
---
ldap_base_path: /opt/ldap
ldap_sock_path: "{{ ldap_base_path }}/sock"
ldap_data_path: "{{ ldap_base_path }}/data"
ldap_config_path: "{{ ldap_base_path }}/config"
ldap_container_version: "v2.4.50-r0"
ldap_container_image: "registry.gitlab.com/famedly/containers/openldap:{{ ldap_container_version }}"
ldap_container_name: ldap
ldap_container_ports: "389:389"
ldap_server_uri: "ldap:///"
ldap_container_labels: {}
ldap_container_networks: []
ldap_container_etc_hosts: {}
ldap_container_pull: true
ldap_container_recreate: false
ldap_container_fd_soft_limit: "8192"
ldap_container_fd_hard_limit: "8192"
ldap_container_ulimits: ["nofile:{{ ldap_container_fd_soft_limit }}:{{ ldap_container_fd_hard_limit }}"]
ldap_container_memory_reservation: "256M"
ldap_container_memory: "512M"
ldap_init_container_additional_volumes:
- "{{ ldap_base_path }}/slapd.ldif:{{ ldap_container_slapd_file }}:z"
# phpLDAPadmin config
ldap_management_container_enabled: False
ldap_management_container_name: "ldap_management"
ldap_management_version: "0.9.0"
ldap_management_container_image: "docker.io/osixia/phpldapadmin:{{ ldap_management_version }}"
ldap_management_container_ports: []
ldap_management_container_env: {}
ldap_management_container_labels: {}
ldap_management_container_pull: true
ldap_management_container_recreate: false
# LDAP specific config
ldap_domain: ~
ldap_dn: "dc={{ ldap_domain | regex_replace('\\.', ',dc=') }}"
ldap_org: ~
ldap_root_user: "admin"
ldap_root_pass: ~
ldap_root_pass_hash: ~
ldap_rootdn: "cn={{ ldap_root_user }},{{ ldap_dn }}"
ldap_config_db: "olcDatabase={1}mdb,cn=config"
# Expects {name: "costumSchema.ldif", content: $fileContent}
ldap_additional_schemas: []
ldap_additional_indices: []
ldap_org_units:
- groups
- users
# Default ACLs
ldap_acls:
- "{0} to dn.subtree=\"{{ ldap_dn }}\" by dn.exact=\"{{ ldap_rootdn }}\" manage by * break"
- "{1} to attrs=userPassword by anonymous auth by self =w by * none"
- "{2} to * by users read"

View file

@ -0,0 +1,13 @@
# Maintenance
## Updating
When the ldap container image is updated, one needs to make
sure the config template is still up-to-date.
To do this, run `docker run --rm registry.gitlab.com/famedly/containers/openldap:$VERSION cat /etc/openldap/slapd.ldif > templates/slapd_$VERSION.ldif`
and use `diff templates/slapd_$VERSION templates/slapd.ldif.j2`.
When you integrated potential config changes, make sure that the
header in `templates/slapd.ldif.j2` is up-to-date AND both the
version bump and the config change are done in a SINGLE commit.

View file

@ -0,0 +1,87 @@
---
# Configures the ACL via root on LDAP-IPC-Socket
- name: Configure ACL
become: true
ldap_attr:
dn: "{{ ldap_config_db }}"
name: olcAccess
values: "{{ ldap_acls }}"
state: exact
server_uri: "ldapi://{{ (ldap_sock_path + '/slapd.sock') | urlencode | replace('/', '%2F') }}"
retries: 3
delay: 3
register: acl_res
until: acl_res is succeeded
tags:
- ldap-sync
- ldap-sync-acl
- name: Ensure rootDN credentials up-to-date
become: true
ldap_attr:
dn: "{{ ldap_config_db }}"
name: "{{ item.key }}"
values: "{{ item.value }}"
state: exact
server_uri: "ldapi://{{ (ldap_sock_path + '/slapd.sock') | urlencode | replace('/', '%2F') }}"
no_log: "{{ item.log is defined and item.log == false }}"
loop:
- key: olcRootDN
value: "{{ ldap_rootdn }}"
- key: olcRootPW
value: "{{ ldap_root_pass }}"
log: false
tags:
- ldap-sync
# Root node can be created with normal bind via LDAP
- name: Create root node
become: true
ldap_entry:
dn: "{{ ldap_dn }}"
objectClass:
- top
- dcObject
- organization
attributes:
dc: "{{ ldap_domain|regex_replace('\\..+', '') }}"
o: "{{ ldap_org }}"
bind_dn: "{{ ldap_rootdn }}"
bind_pw: "{{ ldap_root_pass }}"
server_uri: "{{ ldap_server_uri }}"
tags:
- ldap-sync
- name: Ensure root node is correctly configured
become: true
ldap_attr:
dn: "{{ ldap_dn }}"
name: "{{ item.key }}"
values: "{{ item.value }}"
state: exact
bind_dn: "{{ ldap_rootdn }}"
bind_pw: "{{ ldap_root_pass }}"
server_uri: "{{ ldap_server_uri }}"
no_log: "{{ item.log is defined and item.log == false }}"
loop:
- key: o
value: "{{ ldap_org }}"
- key: dc
value: "{{ ldap_domain|regex_replace('\\..+', '') }}"
tags:
- ldap-sync
- name: Create organizational units
ldap_entry:
dn: "ou={{ ou_name }},{{ ldap_dn }}"
objectClass: organizationalUnit
state: present
bind_dn: "{{ ldap_rootdn }}"
bind_pw: "{{ ldap_root_pass }}"
server_uri: "{{ ldap_server_uri }}"
loop: "{{ ldap_org_units }}"
loop_control:
loop_var: ou_name
tags:
- ldap-sync

View file

@ -0,0 +1,64 @@
---
- name: Stat the LDAP OLC config directory
stat:
path: "{{ ldap_config_path }}/cn=config"
register: stat_result
- name: Determine if the container needs to be initialized
set_fact:
ldap_needs_init: "{{ stat_result.stat.exists|bool == False }}"
- name: (init) Template initial slapd.ldif
template:
src: slapd.ldif.j2
dest: "{{ ldap_base_path }}/slapd.ldif"
mode: 0644
when: ldap_needs_init|bool
- name: (init) Copy additional schema
copy:
content: "{{ schema.content }}"
dest: "{{ ldap_base_path }}/{{ schema.name }}"
mode: 0644
when: ldap_needs_init|bool
loop: "{{ ldap_additional_schemas }}"
loop_control:
loop_var: schema
label: "{{ schema.name }}"
- name: (init) Map additional schemas into container
set_fact:
ldap_init_container_additional_volumes: >-
{{ ldap_init_container_additional_volumes }}
+ {{ volume_mount }}
vars:
schema_file: "{{ ldap_base_path }}/{{ schema.name }}"
volume_mount:
- "{{ schema_file }}:{{ ldap_container_schema_path }}/{{ schema.name }}:ro"
when: ldap_needs_init|bool
loop: "{{ ldap_additional_schemas }}"
loop_control:
loop_var: schema
label: "{{ schema.name }}"
# The detach and cleanup options force the task to
# stall until slapadd is done. Else, an unconfigured
# container is started which will not work
- name: (init) Run init script in container
docker_container:
name: "{{ ldap_container_name }}"
image: "{{ ldap_container_image }}"
command: "slapadd -v -F {{ ldap_container_conf_dir }} -n 0 -l {{ ldap_container_slapd_file }}"
cleanup: yes
detach: no
container_default_behavior: no_defaults
pull: "{{ ldap_container_pull }}"
volumes: "{{ ldap_container_volumes + ldap_init_container_additional_volumes }}"
when: ldap_needs_init|bool
- name: (init) Remove files needed for bootstrapping
file:
path: "{{ ldap_base_path }}/{{ item }}"
state: absent
loop: "{{ [ { name: 'slapd.ldif'} ] + ldap_additional_schemas }}"

View file

@ -0,0 +1,26 @@
---
- name: Ensure LDAP management container is started
docker_container:
name: "{{ ldap_management_container_name }}"
image: "{{ ldap_management_container_image }}"
ports: "{{ ldap_management_container_ports }}"
labels: "{{ ldap_management_container_labels_complete }}"
env: "{{ ldap_management_container_env }}"
links:
- "{{ ldap_container_name }}:ldap"
restart_policy: unless-stopped
recreate: "{{ ldap_management_container_recreate }}"
pull: "{{ ldap_management_container_pull }}"
healthcheck:
test: >-
[ $(pgrep -u www-data -c -f /usr/sbin/apache2) -gt 0 ]
|| exit 1
when: ldap_management_container_enabled|bool
- name: Ensure LDAP management container is absent
docker_container:
name: "{{ ldap_management_container_name }}"
state: absent
when: not ldap_management_container_enabled|bool

47
roles/ldap/tasks/main.yml Normal file
View file

@ -0,0 +1,47 @@
---
- name: Ensure base path exists
file:
path: "{{ ldap_base_path }}"
state: directory
mode: 0700
- name: Create volume paths
file:
path: "{{ item }}"
state: directory
mode: 0700
loop:
- "{{ ldap_data_path }}"
- "{{ ldap_config_path }}"
- "{{ ldap_base_path }}/sock"
- name: Provide intial container configuration
include_tasks: initialize.yml
- name: Ensure LDAP container is started
docker_container:
name: "{{ ldap_container_name }}"
image: "{{ ldap_container_image }}"
ports: "{{ ldap_container_ports }}"
volumes: "{{ ldap_container_volumes }}"
labels: "{{ ldap_container_labels_complete }}"
networks: "{{ ldap_container_networks }}"
etc_hosts: "{{ ldap_container_etc_hosts }}"
ulimits: "{{ ldap_container_ulimits }}"
memory_reservation: "{{ ldap_container_memory_reservation }}"
memory: "{{ ldap_container_memory }}"
restart_policy: unless-stopped
recreate: "{{ ldap_container_recreate }}"
pull: "{{ ldap_container_pull }}"
healthcheck:
test: >-
[[ $(netstat -plnte | grep slapd | wc -l) -ge 1 ]]
&& [[ $(ps aux | grep slapd | wc -l) -ge 1 ]]
|| exit 1
- name: Configure LDAP DIT
import_tasks: configure.yml
- name: Set up phpLDAPAdmin container
import_tasks: ldap-web-ui.yml

View file

@ -0,0 +1,115 @@
# This config template is based on the slapd.ldif which is shipped in
# https://gitlab.com/famedly/containers/openldap:v2.4.50-r1
# For updating, see docs/MAINTENANCE.md
#
# See slapd-config(5) for details on configuration options.
# This file should NOT be world readable.
#
dn: cn=config
objectClass: olcGlobal
cn: config
#
#
# Define global ACLs to disable default read access.
#
# If you change this, set pidfile variable in /etc/conf.d/slapd!
olcPidFile: /run/openldap/slapd.pid
olcArgsFile: /run/openldap/slapd.args
#
# Do not enable referrals until AFTER you have a working directory
# service AND an understanding of referrals.
#olcReferral: ldap://root.openldap.org
#
# Sample security restrictions
# Require integrity protection (prevent hijacking)
# Require 112-bit (3DES or better) encryption for updates
# Require 64-bit encryption for simple bind
#olcSecurity: ssf=1 update_ssf=112 simple_bind=64
#
# Load dynamic backend modules:
#
dn: cn=module,cn=config
objectClass: olcModuleList
cn: module
olcModulepath: /usr/lib/openldap
#olcModuleload: back_bdb.so
#olcModuleload: back_hdb.so
#olcModuleload: back_ldap.so
olcModuleload: back_mdb.so
#olcModuleload: back_passwd.so
#olcModuleload: back_shell.so
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
include: file:///etc/openldap/schema/core.ldif
{% for schema in ldap_schemas_to_load %}
include: file://{{ ldap_container_schema_path }}/{{ schema.name }}
{% endfor %}
# Frontend settings
#
dn: olcDatabase=frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: frontend
dn: olcDatabase=config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: config
olcAccess: to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage by * none
#
# Sample global access control policy:
# Root DSE: allow anyone to read it
# Subschema (sub)entry DSE: allow anyone to read it
# Other DSEs:
# Allow self write access
# Allow authenticated users read access
# Allow anonymous users to authenticate
#
#olcAccess: to dn.base="" by * read
#olcAccess: to dn.base="cn=Subschema" by * read
#olcAccess: to *
# by self write
# by users read
# by anonymous auth
#
# if no access controls are present, the default policy
# allows anyone and everyone to read anything but restricts
# updates to rootdn. (e.g., "access to * by * read")
#
# rootdn can always read and write EVERYTHING!
#
#######################################################################
# LMDB database definitions
#######################################################################
#
dn: olcDatabase=mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: mdb
olcSuffix: {{ ldap_dn }}
olcRootDN: {{ ldap_rootdn }}
#
# Cleartext passwords, especially for the rootdn, should
# be avoided. See slappasswd(8) and slapd-config(5) for details.
# Use of strong authentication encouraged.
olcRootPW: {{ ldap_root_pass_hash }}
#
# The database directory MUST exist prior to running slapd AND
# should only be accessible by the slapd and slap tools.
# Mode 700 recommended.
olcDbDirectory: {{ ldap_container_data_dir }}
#
# Indices to maintain
olcDbIndex: objectClass eq
{% for index in ldap_additional_indices %}
olcDbIndex {{ index }}
{% endfor %}

View file

@ -0,0 +1,2 @@
localhost ansible_connection=local

34
roles/ldap/tests/test.yml Normal file
View file

@ -0,0 +1,34 @@
---
- hosts: localhost
remote_user: root
roles:
- ldap
vars:
ldap_domain: 'dep-b.example.org'
ldap_org: "Department B of the Example Organization"
ldap_root_pass: admin
ldap_root_pass_hash: admin
ldap_base_path: /tmp/ldap
post_tasks:
- name: Create dummy users in org
ldap_entry:
dn: "uid={{ item }},ou=users,{{ ldap_dn }}"
objectClass:
- inetOrgPerson
attributes:
uid: "{{ item }}"
givenName: "firstname"
sn: "surname"
cn: "firstname lastname"
mail: "{{ item }}@mail.{{ ldap_domain }}"
userPassword: "{SSHA}NdjwrLbBHcs9JfWRoz//91CSDRYpmKvx" #password
state: present
bind_dn: "{{ ldap_rootdn }}"
bind_pw: "{{ ldap_root_pass }}"
server_uri: "ldap:///"
loop:
- testUserA
- testUserB
- testUserC
- testManagerA
- testManagerB

30
roles/ldap/vars/main.yml Normal file
View file

@ -0,0 +1,30 @@
---
ldap_container_socket_dir: "/var/run/sockets"
ldap_container_conf_dir: "/etc/openldap/slapd.d"
ldap_container_data_dir: "/var/lib/openldap/openldap-data"
ldap_container_slapd_file: "/etc/openldap/slapd.ldif"
ldap_container_schema_path: "/etc/openldap/schema"
ldap_base_schemas_to_load:
- name: cosine.ldif
- name: inetorgperson.ldif
ldap_schemas_to_load: >-
{{ ldap_base_schemas_to_load }}
+ {{ ldap_additional_schemas }}
ldap_container_volumes:
- "{{ ldap_config_path }}:{{ ldap_container_conf_dir }}:z"
- "{{ ldap_data_path }}:{{ ldap_container_data_dir}}:z"
- "{{ ldap_sock_path }}:{{ ldap_container_socket_dir }}:z"
ldap_container_labels_base:
version: "{{ ldap_container_version }}"
ldap_container_labels_complete: "{{ ldap_container_labels_base | combine(ldap_container_labels) }}"
# phpLDAPadmin container labels
ldap_management_container_labels_base:
version: "{{ ldap_management_version }}"
ldap_management_container_labels_complete: "{{ ldap_management_container_labels_base | combine(ldap_management_container_labels) }}"