diff --git a/acme-copy-certs/.gitignore b/acme-copy-certs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..38ce4278b6f53ca9c84b17a805fab69689273e86
--- /dev/null
+++ b/acme-copy-certs/.gitignore
@@ -0,0 +1,123 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don’t work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/acme-copy-certs/Dockerfile b/acme-copy-certs/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..337bea601f7c452381cd37e7716684c74f521567
--- /dev/null
+++ b/acme-copy-certs/Dockerfile
@@ -0,0 +1,10 @@
+FROM alpine:3.9
+
+WORKDIR /usr/src/app
+COPY requirements.txt ./
+
+RUN apk add --no-cache python3 && \
+    python3 -m ensurepip && \
+    rm -r /usr/lib/python*/ensurepip && \
+    pip3 install --upgrade pip setuptools -r requirements.txt && \
+    rm -r /root/.cache
diff --git a/acme-copy-certs/README.md b/acme-copy-certs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..fa57959406502189a96761b5395d9aa1c50b3d32
--- /dev/null
+++ b/acme-copy-certs/README.md
@@ -0,0 +1,17 @@
+# ACME Copy certs 
+
+Ce service permet l'exploitation des certificats TLS générés par Traefik 
+par d'autres serveurs pour leur propres besoins de chiffrement de connexions.
+
+Traefik a la capacité d'automatiser la demande et le renouvellement de certificats
+TLS auprès de services en ligne tels que [Let's Encrypt](https://letsencrypt.org)
+pour assurer le chiffrement des connexions HTTPS aux sites web dont il a la charge.
+
+L'idée est de déléguer la gestion de certificats utilisés par d'autres services dans
+d'autres conteneurs à Traefik, ce qui présente les avantages suivants :
+  * Les certificats générés sont signés par une autorité de certification publique dont les 
+    certificats sont déjà présents sur la plupart des systèmes, ce qui facilite la 
+    vérification de l'authenticité des certificats par rapport à des certificats auto-signés (pas 
+    de certificats de signature à déployer)
+  * A leur expiration, les certificats sont automatiquement renouvelés, ce qui élimine au tâche d'administration
+    et garantit la disponibilité continue des services
\ No newline at end of file
diff --git a/acme-copy-certs/app/cert_updater.py b/acme-copy-certs/app/cert_updater.py
new file mode 100644
index 0000000000000000000000000000000000000000..da5a24ae43c134d08d4945726a7e72d0e2c430ef
--- /dev/null
+++ b/acme-copy-certs/app/cert_updater.py
@@ -0,0 +1,200 @@
+""" Traefik Certificate extraction and update module
+
+Provides tools to extract TLS certificates from Treafik acme.json files.
+
+Classes
+-------
+    
+    CertUpdater:
+        This class handles acme files decoding and stores or updates the resulting
+        certificates in separate folders.
+"""
+
+import os
+import errno
+import json
+from base64 import b64decode
+
+class CertUpdater:
+    """Decodes acme files and extact the resulting certificates if needed.
+
+    This class keeps flat certificates for specified domain names 
+    in sync with the contents of an acme.json file. The certificates are stored in separated
+    subdirectories named after the main domain name they are valid for.
+    If the certificates specify x509 SAN records, only the main CN is used (a single 
+    directory is created, regardless of the number of alternate names.)
+
+    Attributes
+    ----------
+        acme_file : str
+            Name of the acme.json file containing the certificates
+        names : list 
+            List of domain names to keep updated
+        certs_root_path : str
+            Directory where the certificates will be stored. The certificates are
+            stored in subdirectories of this directory
+
+    Methods
+    -------
+        add(name)
+            Adds a domain name to the updater
+
+        remove(name)
+            Removes a domain name from the updater
+
+        update()
+            Decodes the acme.json file and updates the certicates that need to be updated.
+
+
+
+    """
+    def __init__(self, acme_file, certs_root_path):
+        """
+        Parameters
+        ----------
+            acme_file : str
+                path of the acme file containing the certificates
+            certs_root_path : str 
+                root directory where certificates will be extracted. 
+                They are extracted in subdirectories named after the CN stored in
+                the certificate.
+        """
+        self.acme_file = acme_file
+        self.names = []
+        self.certs_root_path = certs_root_path + '/'
+
+    def add(self, name):
+        """Adds a domain name to the updater.
+
+        Parameters
+        ----------
+            name : str
+                domain name to add
+        """
+        
+        self.names.append(name)
+
+        # Create directories etc if they do not exist
+        try:
+            os.makedirs(self.certs_root_path + name + '/')
+        except OSError as error:
+            if error.errno != errno.EEXIST:
+                raise
+        
+    def remove(self, name):
+        """Removes a domain name from the updater.
+
+        Parameters
+        ----------
+            name : str
+                domain name to remove
+        """
+
+        try:
+            self.names.remove(name)
+        except ValueError:
+            pass
+
+    def update(self):
+        """Decodes the acme.json file and updates the certicates that need to be updated.
+
+        Returns
+        -------
+            list
+                a list of domain names that have been updated. An empty list if no certificate
+                has been updated.
+        """
+
+        # Open and decode the acme file
+        try:
+            with open(self.acme_file) as acme:
+                data = json.loads(acme.read())
+        except FileNotFoundError:
+            print('ACME file {0} not found when trying to decode.'.format(self.acme_file))
+            return []
+        except json.JSONDecodeError:
+            print('File {0} does not look like an ACME file.'.format(self.acme_file))
+            return []
+
+        # Get the acme file version
+        try:
+            acme_ver = 2 if 'acme-v02' in data['Account']['Registration']['uri'] else 1
+        except TypeError:
+            if 'DomainsCertificate' in data:
+                acme_ver = 1
+            else:
+                acme_ver = 2
+
+        # Get the certificates
+        if acme_ver == 1:
+            certs = data['DomainsCertificate']['Certs']
+        elif acme_ver == 2:
+            certs = data['Certificates']
+
+        # Iterate over certificates. If a certificate has been updated,
+        # add its name to the updated_names.
+        updated_names = []
+        
+        for c in certs:
+            if acme_ver == 1:
+                name = c['Certificate']['Domain']
+                key = c['Certificate']['PrivateKey']
+                fullchain = c['Certificate']['Certificate']
+            elif acme_ver == 2:
+                name = c['Domain']['Main']
+                key = c['Key']
+                fullchain = c['Certificate']
+
+            if name in self.names:
+                key = b64decode(key).decode('utf-8')
+                fullchain = b64decode(fullchain).decode('utf-8')
+                chain_start = fullchain.find('-----BEGIN CERTIFICATE-----', 1)
+                cert = fullchain[0:chain_start]
+                chain = fullchain[chain_start:] 
+            
+                print('Updating certificates for {0}'.format(name))
+                if self._needs_updating(name, fullchain):
+                    path = self.certs_root_path + name + '/'
+                    with open(path + 'privkey.pem', 'w') as f:
+                        f.write(key)
+
+                    with open(path + 'cert.pem', 'w') as f:
+                        f.write(cert)
+
+                    with open(path + 'chain.pem', 'w') as f:
+                        f.write(chain)
+
+                    with open(path + 'fullchain.pem', 'w') as f:
+                        f.write(fullchain)
+                    
+                    print('Certificates updated')
+
+                    updated_names.append(name)
+                else:
+                    print('Cetificates are already up-to-date')
+    
+        return updated_names
+
+    def _needs_updating(self, name, fullchain):
+        """Checks if a certificate has changed
+
+        Parameters
+        ----------
+            name : str
+                Name of the directory containing the certificates
+            fullchain : str
+                Full certificates extracted from the acme.json file
+
+        Returns
+        -------
+            bool
+                True if the contents of the current certificates are
+                different from the fullchain. False otherwise.
+        """
+
+        path = self.certs_root_path + name + '/fullchain.pem'
+        try:
+            with open(path, 'r') as f:
+                return f.read() != fullchain
+        except FileNotFoundError:
+            return True        
\ No newline at end of file
diff --git a/acme-copy-certs/app/docker_actions.py b/acme-copy-certs/app/docker_actions.py
new file mode 100644
index 0000000000000000000000000000000000000000..06735e64313217ba528c50ac63f2954004ff9a08
--- /dev/null
+++ b/acme-copy-certs/app/docker_actions.py
@@ -0,0 +1,39 @@
+import docker
+
+class DockerAction:
+    def __init__(self, docker_client, container_id):
+        self.client = docker_client
+        self.container_id = container_id
+        pass
+
+    def exec(self, id):
+        c = self.client.container.get(self.container_id)
+
+        if c.status == 'running':
+            self.action(c)
+
+    def action(self, container):
+        pass
+
+class RestartDockerAction(DockerAction):
+    def __init__(self, docker_client, container_id):
+        super().__init__(docker_client, container_id)
+
+    def action(self, container):
+        try:
+            container.restart()
+        except docker.errors.APIError:
+            print('Unable to restart container {0}({1})'.format(container.name,
+                container.id))
+
+class KillDockerAction(DockerAction):
+    def __init__(self, docker_client, container_id, signal='SIGKILL'):
+        super().__init__(docker_client, container_id)
+        self.signal = signal
+
+    def action(self, container):
+        try:
+            container.kill(self.signal)
+        except docker.errors.APIError:
+            print('Unable to kill container {0}({1}) with signal {2}'
+                .format(container.name, self.container_id, self.signal))
\ No newline at end of file
diff --git a/acme-copy-certs/app/docker_monitor.py b/acme-copy-certs/app/docker_monitor.py
new file mode 100644
index 0000000000000000000000000000000000000000..13e41ca600173877ee9c12570b7d4a329584dd62
--- /dev/null
+++ b/acme-copy-certs/app/docker_monitor.py
@@ -0,0 +1,120 @@
+"""Docker event monitoring
+
+Provides a monitor for docker events that add/remove services when
+they occur according to the configuration provided in the labels of these
+services.
+
+Classes
+-------
+    DockerMonitor
+        A monitor for docker events.
+"""
+
+import docker
+import threading
+import requests
+
+from service_manager import Service
+from docker_actions import RestartDockerAction
+from docker_actions import KillDockerAction
+
+class DockerMonitor(threading.Thread):
+    """Implements a docker events monitoring thread.
+
+    This class implements a thread that listen to the docker socket to intercept
+    events on specific containers to launch actions on these when start and stop events occur.
+    Containers that should be monitored are identified by labels.
+
+    """
+    def __init__(self, services_mgr):
+        self.docker_client = docker.from_env()
+        self.events = None
+        self.services_mgr = services_mgr
+        super().__init__()
+
+    def run(self):
+        print('Starting monitor')
+        # Identify all running containers and get dependent services 
+        # at startup
+        try:
+            containers = self.docker_client.containers.list()
+
+            for c in containers:
+                if c.status == 'running':
+                    self._add_service(c)
+        
+            # Then, wait for docker events and identify new dependent services
+            # or services that exit the system. Do an update each time a new 
+            # service is added / restarted etc. or a service is changed.
+            self.events = self.docker_client.events(decode = True)
+            for event in self.events:
+                if 'status' not in event:
+                    continue
+
+                try:
+                    c = self.docker_client.containers.get(event['id'])
+                    if event['status'] == 'stop':
+                        self._remove_service(c)
+                    elif event['status'] == 'start':
+                        self._add_service(c)
+
+                except docker.errors.NotFound:
+                    pass
+                except docker.errors.APIError as error:
+                    print('Docker error while looking up container {0}: {1} '.format(event.id, error.strerror))
+        except requests.exceptions.ConnectionError as error:
+            print('Connection to docker socket refused.')
+        
+        print('No more events to handle: stopping monitor')
+            
+    def stop(self):
+        if self.events is not None:
+            self.events.close()
+            self.join()
+    
+    def _get_host_from_traefik_rule(self, container):
+        if 'traefik.frontend.rule' in container.labels:
+            try:
+                return container.labels['traefik.frontend.rule'].split('Host:')[1].split(',')[0].strip()
+            except IndexError:
+                return ''
+
+    def _get_action_from_label(self, container):
+        if 'acme_copy_certs.action' not in container.labels:
+            return None
+
+        try:
+            action = container.label('acme_copy_certs.action')
+            if action[0].strip() == 'kill':
+                if len(action) == 1:
+                    return KillDockerAction(self.docker_client, container.id, 'SIGHUP')
+                else:
+                    return KillDockerAction(self.docker_client, container.id, action[1].strip())
+             elif action[0].strip() == 'restart':
+                return RestartDockerAction(self.docker_client, container.id)
+            else:
+                return None
+
+        except IndexError:
+            print('Invalid action')
+            return None
+
+    def _add_service(self, container):
+        if 'acme_copy_certs.enable' in container.labels:
+            if container.labels['acme_copy_certs.enable'] == 'true':
+                host = self._get_host_from_traefik_rule(container)
+            elif container.labels['acme_copy_certs.enable'] == 'false':
+                pass
+            else:
+                print('Invalid acme_copy_certs.enable value for {0}'
+                      .format(container.name))
+            
+            if host:
+                print('Handling container {0}({1})'.format(container.name, container.id))
+                s = Service(container.id, host, self._get_action_from_label(container))
+                self.services_mgr.add(s)
+        else:
+            print('Not handling container {0}({1})'.format(container.name, container.id))
+
+    def _remove_service(self, container):
+        self.services_mgr.remove(container.id)
\ No newline at end of file
diff --git a/acme-copy-certs/app/file_watcher.py b/acme-copy-certs/app/file_watcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..580041be3b6193eada13aaf8013e6745c4cfc47f
--- /dev/null
+++ b/acme-copy-certs/app/file_watcher.py
@@ -0,0 +1,33 @@
+import os
+from watchdog.observers import Observer
+from watchdog.events import PatternMatchingEventHandler
+
+class FileWatcher:
+    def __init__(self, file_path, handler):
+        self.observer = Observer()
+        abs_path, file_name = os.path.split(os.path.abspath(file_path))
+        self.observed_path = abs_path
+        self.observed_file = file_name
+        self.handler = handler
+
+        self.event_handler = PatternMatchingEventHandler(
+            patterns=['*/' + file_name],
+            ignore_patterns=[],
+            ignore_directories=True)
+
+        self.event_handler.on_any_event = self._on_any_event
+
+        self.observer.schedule(self.event_handler,
+            self.observed_path,
+            recursive=False)
+        
+    def _on_any_event(self, event):
+        self.handler()
+
+    def start(self):
+        print('Starting observing file {0} in directory {1}'.format(self.observed_file, self.observed_path))
+        self.observer.start()
+
+    def stop(self):
+        self.observer.stop()
+        self.observer.join()
\ No newline at end of file
diff --git a/acme-copy-certs/app/service_manager.py b/acme-copy-certs/app/service_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..b20e84dcdba18b7d44df4cbe20143c8f4ad60448
--- /dev/null
+++ b/acme-copy-certs/app/service_manager.py
@@ -0,0 +1,139 @@
+"""Handling of docker services
+
+This module provides a ServicesManager class implementing a collection of
+services whose certificates are to be updated by the ssytem.
+
+Classes
+-------
+    Service
+        A class representing a single service
+    
+    ServiceManager
+        A class handling a collection of services
+"""
+
+from threading import Lock
+
+class Service:
+    """
+    A service wrapper This object represents a docker service
+
+    Attributes
+    ----------
+
+    id : int
+        The id of the container running the service
+    host : str
+        The hostname associated with the service used for CN in TLS certs
+    action : DockerAction
+        An action to perform on the service when the run_action method is
+        invoked.
+
+    Methods
+    -------
+
+    run_action()
+        Runs the action bound to this service
+    """
+
+    def __init__(self, id, host, action):
+        """
+        Parameters
+        ----------
+        id : int
+            The id of the container running the service
+        host : str
+            The hostname associated with the service used for CN in TLS certs
+        action : DockerAction
+            An action to perform on the service when the run_action method is invoked. 
+        """   
+
+        self.id = id
+        self.host = host
+        self.action = action
+
+    def run_action(self):
+        """Executes the action on the container associated to the service."""
+        if self.action is not None:
+            self.action.exec(self.id)
+
+class ServicesManager:
+    """
+    A ServicesManager handles a collection of services and forwards certicate update
+    requests to a CertUpdater. If a service certificate is updated (either after adding a
+    service or after an explicit update request), the docker action associated
+    with the service is executed.
+
+    Attribures
+    ----------
+        services_by_id : dict
+            A dictionnary of the managed services identified by their container id.
+        services_by_host : dict
+            A dictionnary of the managed services identified by the CN of their
+            certificates.
+        updater : CertUpdater object
+            The certificates updater in charge of handling the certificates for the managed
+            services 
+    
+    Methods
+    -------
+        add(service)
+            Adds a new service to the collection
+
+        remove(id)
+            Removes a service from the collection based on its container id
+
+        update()
+            Checks if any certificate for a managed service needs to be updated. If
+            so, performs the docker action associated with the service on its 
+            container
+    """
+
+    def __init__(self, updater):
+        """
+        Parameters
+        ----------
+            updater : CertUpdater
+                The CertUpdater used to handle certificates for the services
+        """
+        self.services_by_id = {}
+        self.services_by_host = {}
+        self.updater = updater
+        self.lock = Lock()
+
+    def add(self, service):
+        """ Adds a service to the collection and enables handling of its 
+        certificates
+        """
+        with self.lock:
+            self.services_by_id[service.id] = service
+            if service.host not in self.services_by_host:
+                self.services_by_host[service.host] = [service]
+            else:
+                self.services_by_host[service.host].append(service)
+        
+            self.updater.add(service.host)
+        self.update()
+
+    def remove(self, id):
+        """ Removes a service by its id from the collection and disables handling
+        of its certificates
+        """
+        with self.lock:
+            if id in self.services_by_id:
+                service = self.services_by_id[id] 
+                host = service.host
+                self.services_by_host[host].remove(service)
+                del self.services_by_id[id]
+                if len(self.services_by_host[host]) == 0:
+                    del self.services_by_host[host]
+                    self.updater.remove(host)
+
+    def update(self):
+        """ Updates the certificates of the services if needed. If a certificate
+        has changed, exectute the associated docker action on the service container.
+        """
+        with self.lock:
+            for h in self.updater.update():
+                for s in self.services_by_host[h]:
+                    s.run_action()
\ No newline at end of file
diff --git a/acme-copy-certs/requirements.txt b/acme-copy-certs/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c546145a62c9e8123dbbe26981510ce150404eea
--- /dev/null
+++ b/acme-copy-certs/requirements.txt
@@ -0,0 +1,2 @@
+docker
+watchdog
diff --git a/acme-copy-certs/tests/test.py b/acme-copy-certs/tests/test.py
new file mode 100755
index 0000000000000000000000000000000000000000..9772682d64e5dcc9a973f26b76e156d22378581b
--- /dev/null
+++ b/acme-copy-certs/tests/test.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+
+import sys, os
+sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../app')
+
+import time
+import cert_updater
+import file_watcher
+import docker_monitor
+import service_manager
+
+
+if __name__ == "__main__":
+    u = cert_updater.CertUpdater('./acme.json', '.')
+
+
+    sm = service_manager.ServicesManager(u)
+
+    f = file_watcher.FileWatcher('./acme.json', sm.update)
+    dm = docker_monitor.DockerMonitor(sm)
+    
+    f.start()
+    dm.start()
+    
+    try:
+        while True:
+           time.sleep(1) 
+    except KeyboardInterrupt:
+        dm.stop()
+        f.stop()
diff --git a/pica-openldap/Dockerfile b/pica-openldap/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..0f0dbe8d3a5ff424d986872cd992ccdf78d0f7d7
--- /dev/null
+++ b/pica-openldap/Dockerfile
@@ -0,0 +1,7 @@
+FROM osixia/openldap:1.4.0
+LABEL maintainer="quentinduchemin@tuta.io,bonnest@utc.fr"
+
+ADD bootstrap /container/service/slapd/assets/config/bootstrap
+ADD environment /container/environment/01-custom
+
+CMD [ "--copy-service" ]
diff --git a/pica-openldap/README.md b/pica-openldap/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4442785d85a90e803f87da8af18cba8d0fb0a
--- /dev/null
+++ b/pica-openldap/README.md
@@ -0,0 +1,145 @@
+# Serveur LDAP
+
+<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
+
+- [Démarrage](#dmarrage)
+- [Configuration](#configuration)
+	- [Schémas additionnels](#schmas-additionnels)
+	- [Entrée initiales de l'annuaire](#entre-initiales-de-lannuaire)
+	- [Configuration par défaut](#configuration-par-dfaut)
+	- [Certificats](#certificats)
+	- [Secrets](#secrets)
+	- [Plus de logs](#plus-de-logs)
+- [Mise à jour de l'image](#mise-jour-de-limage)
+- [Démarrage du conteneur](#dmarrage-du-conteneur)
+- [Test de l'image sur un client](#test-de-limage-sur-un-client)
+
+<!-- /TOC -->
+
+Cette image est basée sur [osixia/openldap](https://github.com/osixia/docker-openldap). Elle est spécialisée pour les besoins de l'infrastructure de Picasoft.
+
+Cette image **doit être lancée** aux côtés de [TLS Certs Monitor](../pica-tls-certs-monitor) pour fonctionner.
+
+## Démarrage
+
+Copier `pica-openldap.secrets.example` en `pica-openldap.secrets`, puis lancer :
+
+```bash
+docker-compose up -d && docker-compose logs -f
+```
+
+Notez que l'ensemble de la configuration pour préparer l'instance n'est utilisée que lors du premier lancement : elle n'aura aucun effet sur les instances existantes.
+
+## Configuration
+
+Par rapport à la configuration de base, cette version :
+* Spécifie l'organisation Picasoft (variables d'environnement dans le Docker Compose),
+* Configure un utilisateur `readonly` appellé `nss`,
+* Active et force la connexion TLS au serveur LDAP,
+* Ajoute les schémas additionnels (permettant d'utiliser `host` et `authorizedService`, par exemple),
+* Crée la structure du base du LDAP (Group, People, Service),
+* Ajoute des entrées d'exemple pour les comptes.
+
+La configuration par défaut de l'image est dans les répertoires `bootstrap`, `environment` et `secrets`.
+
+La table suivante donne le rôle de ces répertoires :
+
+| Répertoire | Contenu | À quoi ça sert |
+|------------|---------|----------------|
+| [bootstrap/schema](./bootstap/schema) | Schémas additionnels (format ldif) | Schémas ajoutés automatiquement à l'initialisation de la configuration |
+| [bootstrap/ldif](./bootstrap/ldif) | Entrées initiales de l'annuaire (format ldif) | Entrées automatiquement ajoutées à l'initialisation de l'annuaire |
+| [environment](./environment) | Configuration par défaut | Définition des valeurs par défaut des variables d'environnement utilisables dans le Docker Compose |
+| [secrets](./secrets) | Mots de passe | Mots de passe utilisés pour les trois utilisateurs par défaut (admin, config et nss)
+
+Le contenu de ces répertoire est copié dans l'image à la construction. Cette copie
+est supprimée par défaut une fois l'image configurée, après la première exécution du
+conteneur.
+
+### Schémas additionnels
+
+Le fichier [ldapns.schema](./bootstrap/schema/ldapns.schema) permet le support des attributs `host` et `authorizedServices`.
+
+### Entrée initiales de l'annuaire
+
+Le fichier [init.ldif](./bootstrap/ldfi/init.ldif) crée la structure de base de l'annuaire :
+* Une OU (Organizational Unit) `People`, pour les comptes POSIX personnels,
+* Une OU `Groups`, pour les groupes POSIX,
+* Une OU `Services`, pour les comptes POSIX "virtuels", destinés aux services comme Mattermost.
+
+Il crée aussi des entrées `example` donnant un exemple pour chacun de ces types.
+
+### Configuration par défaut
+
+* Le fichier [pica.startup.yaml](./environment/pica.startup.yaml) contient toutes les
+valeurs par défaut des paramètres de configuration. Ces valeurs sont utilisées à la
+création de le configuration du serveur ldap **à la première exécution du conteneur**. Le fichier est supprimé ensuite.
+
+* Le fichier [pica.yaml](./environment/pica.yaml) contient les paramètres de
+de configuration utilisés à chaque démarrage du conteneur. Pour l'instant il est utilisé pour définir le niveau de messages de débogage de `slapd` et limiter le nombre
+de descripteurs de fichiers utilisés (1024). On peut l'augmenter si on estime qu'il y aura plus de connexions simultanées sur le serveur.
+
+### Certificats
+
+La configuration par défaut spécifie les fichiers suivants :
+
+| Fichier | Rôle |
+|---------|------|
+| cert.pem | Certificat serveur |
+| chain.pem | Certificat CA |
+| privkey.pem | Clé privée serveur |
+
+### Secrets
+
+Le repertoire [secrets](./secrets) doit contenir le fichier `pica-openldap.secrets`
+avec les mots de passe en clair qui seront utilisés pour l'administrateur de
+l'annuaire, l'accès à la configuration et l'accès en lecture seule. Un modèle est
+fourni dans le fichier [pica-ldap.secrets.examples](./secrets/
+pica-ldap.secrets.example).
+
+| Variable | Usage |
+|----------|-------|
+| LDAP_ADMIN_PASSWORD | Mot de passe de l'adminsitrateur de l'annuaire (cn=admin,dc=picasoft,dc=net)|
+| LDAP_CONFIG_PASSWORD | Mot de passe de configuration (cn=admin,cn=config) |
+| LDAP_READONLY_USER_PASSWORD | Mot de passe de l'utilisateur *read only* (cn=nss,dc=picasoft,dc=net) |
+
+> Attention : les mots de passe apparaîtront **en clair** dans l'environnement du conteneur.
+
+### Plus de logs
+
+Pour obtenir plus de logs, il suffit d'ajouter la ligne suivante au service `ldap-host` dans le fichier
+[docker-compose.yml](./docker-compose.yml) :
+
+```yaml
+command: --loglevel debug
+```
+
+## Mise à jour de l'image
+
+Il suffit de modifier la version de osixia/openldap dans le [Dockerfile](./Dockerfile) :
+
+```Dockerfile
+FROM osixia/openldap:XXX
+```
+
+Ne pas oublier de mettre à jour la version dans le [docker-compose.yml](./docker-compose.yml) :
+
+```
+image: registry.picasoft.net/pica-openldap:XXX
+```
+
+## Test de l'image sur un client
+
+* Accès à la configuration (utiliser le mot de passe défini par `LDAP_CONFIG_PASSWORD`)
+
+```bash
+ldapsearch -Z -W -x -D cn=admin,cn=config -b cn=config
+```
+
+* Accès à l'annuaire (utiliser le mot de passe défini par `LDAP_ADMIN_PASSWORD`)
+
+```bash
+ldapsearch -Z -W -x -D cn=admin,dc=picasoft,dc=net -b dc=picasoft,dc=net
+```
+
+Ces deux commandes doivent fonctionner **dans le conteneur**. Si ce n'est pas le cas, il y
+a une erreur de configuration.
diff --git a/pica-openldap/bootstrap/ldif/init.ldif b/pica-openldap/bootstrap/ldif/init.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..23e37b2de745eade0680d464d66edfe6a4539d19
--- /dev/null
+++ b/pica-openldap/bootstrap/ldif/init.ldif
@@ -0,0 +1,85 @@
+dn: ou=Services,{{ LDAP_BASE_DN }}
+objectClass: top
+objectClass: organizationalUnit
+ou: Services
+description: Comptes LDAP pour les services, utiles par exemple pour envoyer des mails en leur nom
+
+dn: ou=People,{{ LDAP_BASE_DN }}
+objectClass: top
+objectClass: organizationalUnit
+ou: People
+description: Comptes LDAP pour les humains, permettant de se connecter sur les machines par SSH
+
+dn: ou=Groups,{{ LDAP_BASE_DN }}
+objectClass: top
+objectClass: organizationalUnit
+ou: Groups
+description: Espace pour gérer les groupes (POSIX), permettant de configurer les droits des humains
+
+dn: cn=tech,ou=Groups,{{ LDAP_BASE_DN }}
+gidNumber: 500
+cn: tech
+objectClass: posixGroup
+objectClass: top
+description: Groupe primaire. Les personnes dans ce groupe ont accès à toutes les commandes Docker sur les machines autorisées.
+
+dn: cn=admin,ou=Groups,{{ LDAP_BASE_DN }}
+gidNumber: 501
+cn: admin
+objectClass: posixGroup
+objectClass: top
+description: Groupe primaire. Les personnes dans ce groupe ont un accès root sur les machines autorisées.
+
+dn: cn=member,ou=Groups,{{ LDAP_BASE_DN }}
+gidNumber: 502
+objectClass: posixGroup
+objectClass: top
+cn: representant
+description: Groupe secondaire. Les personnes dans ce groupes peuvent accéder aux services restreints aux membres (exemple : Cloud...)
+memberUid:
+
+dn: cn=representant,ou=Groups,{{ LDAP_BASE_DN }}
+gidNumber: 503
+objectClass: posixGroup
+objectClass: top
+cn: representant
+description: Groupe secondaire. Les personnes dans ce groupes sont les représentants de l'association (ou par transfert), il est utile pour les informations privées, d'ordre administratif... (exemple : Wiki)
+memberUid:
+
+dn: cn=example,ou=People,{{ LDAP_BASE_DN }}
+gidNumber: -1
+cn: example
+objectClass: inetOrgPerson
+objectClass: top
+objectClass: posixAccount
+objectClass: shadowAccount
+objectClass: ldapPublicKey
+objectClass: hostObject
+objectClass: authorizedServiceObject
+loginShell: /bin/bash
+uidNumber:
+givenName: Prénom
+sn: Nom
+uid: example
+sshPublicKey:
+homeDirectory: /home/users/example
+userPassword::
+authorizedService: example
+host: example
+shadowExpire:
+description: Ce type de compte est réservé aux utilisateurs physiques, pouvant avoir un accès aux machines.
+
+dn: cn=example,ou=Services,{{ LDAP_BASE_DN }}
+objectClass: organizationalRole
+objectClass: top
+objectClass: simpleSecurityObject
+objectClass: posixAccount
+objectClass: authorizedServiceObject
+homeDirectory: /dev/null
+uidNumber:
+gidNumber:
+uid: example
+cn: example
+authorizedService:
+userPassword::
+description: Ce type de compte est réservé aux services, ne peuvent pas se connecter aux machines.
diff --git a/pica-openldap/bootstrap/schema/ldapns.schema b/pica-openldap/bootstrap/schema/ldapns.schema
new file mode 100644
index 0000000000000000000000000000000000000000..0c3fbf4272a1c3c624a7564d8aa6089720183bf4
--- /dev/null
+++ b/pica-openldap/bootstrap/schema/ldapns.schema
@@ -0,0 +1,22 @@
+# $Id$
+
+# LDAP Name Service Additional Schema
+
+# http://www.iana.org/assignments/gssapi-service-names
+
+attributetype ( 1.3.6.1.4.1.5322.17.2.1 NAME 'authorizedService'
+	DESC 'IANA GSS-API authorized service name'
+	EQUALITY caseIgnoreMatch
+	SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
+
+objectclass ( 1.3.6.1.4.1.5322.17.1.1 NAME 'authorizedServiceObject'
+	DESC 'Auxiliary object class for adding authorizedService attribute'
+	SUP top
+	AUXILIARY
+	MAY authorizedService )
+
+objectclass ( 1.3.6.1.4.1.5322.17.1.2 NAME 'hostObject'
+	DESC 'Auxiliary object class for adding host attribute'
+	SUP top
+	AUXILIARY
+	MAY host )
diff --git a/pica-openldap/docker-compose.yml b/pica-openldap/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1882ea54aedd24d57b0b8e85eec58ca888293759
--- /dev/null
+++ b/pica-openldap/docker-compose.yml
@@ -0,0 +1,27 @@
+version: "3.7"
+
+volumes:
+  ldap_db:
+    name: ldap_db
+  ldap_config:
+    name: ldap_config
+
+services:
+  ldap:
+    image: registry.picasoft.net/pica-openldap:1.4.0
+    build: .
+    container_name: ldap
+    ports:
+      - "636:636"
+    env_file:
+      - ./secrets/pica-openldap.secrets
+    labels:
+      traefik.frontend.rule: "Host:ldap.picasoft.net"
+      traefik.enable: true
+      tls-certs-monitor.enable: true
+      tls-certs-monitor.action: "restart"
+    volumes:
+      - ldap_db:/var/lib/ldap
+      - ldap_config:/etc/ldap/slapd.d
+      - /DATA/docker/certs/ldap.picasoft.net:/container/service/slapd/assets/certs
+    restart: unless-stopped
diff --git a/pica-openldap/environment/pica.startup.yaml b/pica-openldap/environment/pica.startup.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a5081d817e4040db526c1023f268bd39f1222821
--- /dev/null
+++ b/pica-openldap/environment/pica.startup.yaml
@@ -0,0 +1,24 @@
+# See this page to know what variables can be used : https://github.com/osixia/docker-openldap#defaultstartupyaml
+# Here we only let the modified default variables
+
+# Required and used for new ldap server only
+LDAP_ORGANISATION: Picasoft
+LDAP_DOMAIN: picasoft.net
+LDAP_BASE_DN: #if empty automatically set from LDAP_DOMAIN
+
+LDAP_READONLY_USER: true
+LDAP_READONLY_USER_USERNAME: nss
+
+# TLS
+LDAP_TLS: true
+LDAP_TLS_CRT_FILENAME: cert.pem
+LDAP_TLS_KEY_FILENAME: privkey.pem
+LDAP_TLS_CA_CRT_FILENAME: chain.pem
+# Note 25/04 : This sets ssf to 128. Maybe it should set minssf to 128 instead to
+# reject any non-encryption connexion on port 389. Testing required.
+# (Indeed, when this variable is true, this file is executed :
+# https://github.com/osixia/docker-openldap/blob/stable/image/service/slapd/assets/config/tls/tls-enforce-enable.ldif)
+LDAP_TLS_ENFORCE: true
+LDAP_TLS_VERIFY_CLIENT: never
+
+HOSTNAME: ldap.picasoft.net
diff --git a/pica-openldap/environment/pica.yaml b/pica-openldap/environment/pica.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f578909dde989be5d86ef4357d6d59758f81ff9f
--- /dev/null
+++ b/pica-openldap/environment/pica.yaml
@@ -0,0 +1,13 @@
+# This is the default image configuration file
+# These values will persists in container environment.
+
+# All environment variables used after the container first start
+# must be defined here.
+# more information : https://github.com/osixia/docker-light-baseimage
+
+# General container configuration
+# see table 5.1 in http://www.openldap.org/doc/admin24/slapdconf2.html for the available log levels.
+LDAP_LOG_LEVEL: 0
+
+# Ulimit
+LDAP_NOFILE: 1024
diff --git a/pica-openldap/secrets/pica-openldap.secrets.example b/pica-openldap/secrets/pica-openldap.secrets.example
new file mode 100644
index 0000000000000000000000000000000000000000..51b5638118a6e20304f9da6f499314de4bd1c90b
--- /dev/null
+++ b/pica-openldap/secrets/pica-openldap.secrets.example
@@ -0,0 +1,3 @@
+LDAP_ADMIN_PASSWORD=admin
+LDAP_CONFIG_PASSWORD=config
+LDAP_READONLY_USER_PASSWORD=nss