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