diff --git a/.editorconfig b/.editorconfig index edb4648..9dd02f4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,21 +3,18 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_size = 4 +indent_size = 2 indent_style = space insert_final_newline = true -max_line_length = 120 +max_line_length = 80 trim_trailing_whitespace = true [*.md] max_line_length = off trim_trailing_whitespace = false -[*.{ini, cfg}] -indent_size = 2 - [*.{yaml, yml}] -indent_size = 2 +max_line_length = 120 [Makefile] indent_size = 1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7acc17c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +--- +name: Lint + +'on': + push: + branches: ['*'] + workflow_dispatch: + +jobs: + hadolint: + name: Hadolint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker://pipelinecomponents/hadolint:latest + with: + args: hadolint Dockerfile + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker://pipelinecomponents/shellcheck:latest + with: + args: shellcheck docker/docker-entrypoint.sh + + yamllint: + name: Yamllint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: docker://pipelinecomponents/yamllint:latest + with: + args: yamllint . diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..65ae83c --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,4 @@ +--- + +ignored: + - DL3018 diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..b1c62c1 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,4 @@ +color=auto +enable=all +severity=warning +shell=bash diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ace8c20..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -language: python -os: linux -python: 3.9 - -cache: - pip: true - -install: - - pip install -r requirements-dev.txt - -env: - - CHECK="yamllint" - -script: make $CHECK diff --git a/Dockerfile b/Dockerfile index e69de29..dac06ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,53 @@ +FROM php:7.4-fpm-alpine + +RUN apk add --no-cache \ + multirun \ + # docker-entrypoint.sh + bash gettext openldap-clients patch \ + # Web server + nginx phpldapadmin \ + # OpenLDAP + openldap openldap-back-mdb \ + # ldap.h for PHP 'ldap' package + openldap-dev \ + # bindtextdomain() for PHP 'gettext' + musl-libintl \ + && docker-php-ext-install -j "$(nproc)" gettext ldap \ + && rm -rf /var/cache/apk/* + + +WORKDIR /docker-entrypoint.d/ +COPY ./docker/ / + + +# OpenLDAP +ENV DOMAIN_NAME="local" +ENV LDAPCONF=/etc/openldap/slapd.conf +ENV LDAP_CONF_DIR=/etc/openldap/slapd.d +ENV LDAP_INIT_DIR=/var/lib/openldap/openldap-init +ENV LDAP_LOG_LEVEL="1024" +ENV LDAP_ROOT_PASSWORD="changeme" +ENV LDAP_ROOT_USERNAME="root" + +RUN mkdir -p "$LDAP_CONF_DIR" \ + && mv /var/lib/openldap/openldap-data/DB_CONFIG.example /docker-entrypoint.d/openldap/DB_CONFIG \ + && rm -f "$LDAPCONF" + +VOLUME /var/lib/openldap/openldap-data +VOLUME /var/lib/openldap/openldap-init + +EXPOSE 389/tcp + + +# Nginx, PHP, phpLDAPadmin +RUN rm -rf /etc/nginx/conf.d/default.conf /usr/local/etc/php-fpm.d/* + +EXPOSE 80/udp + + +# Entrypoint +ENV DISABLE_PHPLDAPADMIN="" + +WORKDIR / + +CMD ["/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index fba62a6..bbe95c7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,32 @@ +NAME = homelab-ldap + +# Docker + +build: + @docker build -t $(NAME) . + +build-nocache: + @docker build -t $(NAME) --no-cache . + +run: + @docker rm -f $(NAME) || true + docker run --rm --name $(NAME) -p 8088:80 -p 389:389 $(NAME) + +# QA + +hadolint: + @hadolint --version + @hadolint Dockerfile + +shellcheck: + @shellcheck --version + @shellcheck docker/docker-entrypoint.sh yamllint: @yamllint --version @yamllint --strict . -lint-all: yamllint +lint-all: hadolint shellcheck yamllint + + +.PHONY: build build-nocache hadolint run shellcheck yamllint diff --git a/README.md b/README.md index 5f99ec2..0b9217c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,82 @@ -[![QA Build Status](https://travis-ci.com/danie1k/homelab-???.svg?branch=master)](https://travis-ci.com/danie1k/homelab-???) -[![Docker Hub Build Status](https://img.shields.io/docker/cloud/build/danie1k/homelab-???)](https://hub.docker.com/repository/docker/danie1k/homelab-???) -[![Docker Image Version](https://img.shields.io/docker/v/danie1k/homelab-???)](https://hub.docker.com/repository/docker/danie1k/homelab-???) -[![MIT License](https://img.shields.io/github/license/danie1k/homelab-???)](https://github.com/danie1k/homelab-???/blob/master/LICENSE) +[![QA Build Status](https://github.com/danie1k/homelab-ldap/workflows/Lint/badge.svg)](https://github.com/danie1k/homelab-ldap/actions?query=workflow%3ALint) +[![Docker Hub Build Status](https://img.shields.io/docker/cloud/build/danie1k/homelab-ldap)](https://hub.docker.com/repository/docker/danie1k/homelab-ldap) +[![Docker Image Version](https://img.shields.io/docker/v/danie1k/homelab-ldap)](https://hub.docker.com/repository/docker/danie1k/homelab-ldap) +[![MIT License](https://img.shields.io/github/license/danie1k/homelab-ldap)](https://github.com/danie1k/homelab-ldap/blob/master/LICENSE) -# +# OpenLDAP server with built-in phpLDAPadmin +This container is far from perfect and set only the minimum needed settings (especially when it comes to [OpenLDAP] server), +but does its job and can be a great base for building much more complex solution. + +Based on: +- https://github.com/docker-library/php/blob/master/7.4/alpine3.13/fpm/Dockerfile +- https://github.com/nextcloud/docker/blob/master/20.0/apache/Dockerfile + +## Included services +- [nginx] +- [OpenLDAP] +- [phpLDAPadmin] + + +## Environment Variables you should set + +- `DOMAIN_NAME` -- Domain name for LDAP suffix +- `LDAP_ROOT_USERNAME` -- root/admin user name for [OpenLDAP] +- `LDAP_ROOT_PASSWORD` -- password for [OpenLDAP] root/admin user \* + +\* Plain-text password is possible, but not recommended! To generate password hash, + use the [`slappasswd`] command and set this environment variable to value returned by [`slappasswd`]. + If you don't want to install this command, use: + + ```shell + $ docker run --rm -it alpine:latest sh -c 'apk add openldap 2>/dev/null; slappasswd' + ``` + +*nginx, php & phpLDAPadmin can be disabled altogether by setting `DISABLE_PHPLDAPADMIN="1"` environment variable.* + + +## Exposed Ports + +- `80` (tcp) -- [phpLDAPadmin] via [nginx] +- `389` (tcp) -- [OpenLDAP] + + +## Volumes + +- `/var/lib/openldap/openldap-data` -- [OpenLDAP] database +- `/var/lib/openldap/openldap-init` -- custom [LDIF] config files for [OpenLDAP] + + +## Useful commands + +- Test LDAP root login: + ```shell + ldapsearch -D 'cn=root,dc=example,dc=com' -W '(objectclass=*)' -b 'dc=example,dc=com' + ``` + + +## Useful links + +### LDAP/OpenLDAP (`slapd`) documentation + +- https://wiki.archlinux.org/index.php/OpenLDAP +- https://linux.die.net/man/5/slapd.conf +- https://ldapwiki.com/wiki/ +- [log levels](ttp://www.openldap.org/doc/admin24/slapdconf2.html) + +### phpLDAPadmin documentation + +- https://wiki.archlinux.org/index.php/PhpLDAPadmin +- http://phpldapadmin.sourceforge.net/wiki/index.php/LDAP_server_definitions + + +## License + +MIT + + +[LDIF]: https://www.openldap.org/software//man.cgi?query=LDIF&sektion=5&apropos=0&manpath=OpenLDAP+2.4-Release +[OpenLDAP]: https://www.openldap.org/ +[nginx]: https://www.nginx.com/ +[phpLDAPadmin]: http://phpldapadmin.sourceforge.net/ +[`slappasswd`]: https://command-not-found.com/slappasswd diff --git a/docker/docker-entrypoint.d/nginx/default.conf b/docker/docker-entrypoint.d/nginx/default.conf new file mode 100644 index 0000000..b5deda1 --- /dev/null +++ b/docker/docker-entrypoint.d/nginx/default.conf @@ -0,0 +1,21 @@ +server { + listen 80 default_server; + + root /usr/share/webapps/phpldapadmin/htdocs; + index index.php index.html; + + location = /favicon.ico { + return 404; + log_not_found off; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_pass 127.0.0.1:9000; + # Mitigate https://httpoxy.org/ vulnerabilities + fastcgi_param HTTP_PROXY ""; + } +} diff --git a/docker/docker-entrypoint.d/nginx/nginx.conf b/docker/docker-entrypoint.d/nginx/nginx.conf new file mode 100644 index 0000000..8ba5f06 --- /dev/null +++ b/docker/docker-entrypoint.d/nginx/nginx.conf @@ -0,0 +1,20 @@ +pid /var/run/nginx.pid; +user www-data www-data; +worker_processes 2; + +events { + worker_connections 1024; +} + +http { + access_log off; + aio on; + default_type application/octet-stream; + directio 8m; + error_log /proc/self/fd/2 warn; + include mime.types; + sendfile on; + tcp_nopush on; + + include /etc/nginx/http.d/*.conf; +} diff --git a/docker/docker-entrypoint.d/openldap/base-init.template.ldif b/docker/docker-entrypoint.d/openldap/base-init.template.ldif new file mode 100644 index 0000000..32c37bd --- /dev/null +++ b/docker/docker-entrypoint.d/openldap/base-init.template.ldif @@ -0,0 +1,6 @@ +dn: ${LDAP_DN} +objectClass: dcObject +objectClass: organization +${LDAP_DC_VERTICAL} +o: ${DOMAIN_NAME} +description: Example directory diff --git a/docker/docker-entrypoint.d/openldap/base-root-user.template.ldif b/docker/docker-entrypoint.d/openldap/base-root-user.template.ldif new file mode 100644 index 0000000..9e61f07 --- /dev/null +++ b/docker/docker-entrypoint.d/openldap/base-root-user.template.ldif @@ -0,0 +1,4 @@ +dn: ${LDAP_ROOTDN} +objectClass: organizationalRole +cn: ${LDAP_ROOT_USERNAME} +description: Directory Manager diff --git a/docker/docker-entrypoint.d/openldap/slapd.template.conf b/docker/docker-entrypoint.d/openldap/slapd.template.conf new file mode 100644 index 0000000..b8bda45 --- /dev/null +++ b/docker/docker-entrypoint.d/openldap/slapd.template.conf @@ -0,0 +1,68 @@ +include /etc/openldap/schema/core.schema +include /etc/openldap/schema/cosine.schema +include /etc/openldap/schema/inetorgperson.schema +include /etc/openldap/schema/nis.schema + +# Define global ACLs to disable default read access. + +# Do not enable referrals until AFTER you have a working directory +# service AND an understanding of referrals. +#referral ldap://root.openldap.org + +pidfile /var/run/slapd.pid +argsfile /var/run/slapd.args + +# Load dynamic backend modules: +modulepath /usr/lib/openldap +moduleload back_mdb.so + +# Sample security restrictions +# Require integrity protection (prevent hijacking) +# Require 112-bit (3DES or better) encryption for updates +# Require 63-bit encryption for simple bind +# security ssf=1 update_ssf=112 simple_bind=64 + +# Sample 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 +# Directives needed to implement policy: +# access to dn.base="" by * read +# access to dn.base="cn=Subschema" by * read +# access 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! + +####################################################################### +# MDB database definitions +####################################################################### + +database mdb +maxsize 1073741824 +suffix "${LDAP_DN}" + +rootdn "${LDAP_ROOTDN}" +rootpw ${LDAP_ROOTPW} + +# The database directory MUST exist prior to running slapd AND +# should only be accessible by the slapd and slap tools. +# Mode 700 recommended. +directory /var/lib/openldap/openldap-data + +# Indices to maintain +index objectClass eq +index uid pres,eq +index mail pres,sub,eq +index cn pres,sub,eq +index sn pres,sub,eq +index dc eq diff --git a/docker/docker-entrypoint.d/php/docker.conf b/docker/docker-entrypoint.d/php/docker.conf new file mode 100644 index 0000000..20b60d5 --- /dev/null +++ b/docker/docker-entrypoint.d/php/docker.conf @@ -0,0 +1,18 @@ +[global] +error_log = /proc/self/fd/2 +; https://github.com/docker-library/php/pull/725#issuecomment-443540114 +log_limit = 8192 +daemonize = no + +[www] +catch_workers_output = yes +clear_env = no +decorate_workers_output = no +group = www-data +listen = 127.0.0.1:9000 +pm = dynamic +pm.max_children = 5 +pm.max_spare_servers = 5 +pm.min_spare_servers = 1 +pm.start_servers = 1 +user = www-data diff --git a/docker/docker-entrypoint.d/phpldapadmin/config.template.php b/docker/docker-entrypoint.d/phpldapadmin/config.template.php new file mode 100644 index 0000000..ab487fe --- /dev/null +++ b/docker/docker-entrypoint.d/phpldapadmin/config.template.php @@ -0,0 +1,8 @@ +newServer('ldap_pla'); +$servers->setValue('server', 'name', 'OpenLDAP'); +$servers->setValue('server', 'host', '127.0.0.1'); +$servers->setValue('server', 'base', ['${LDAP_DN}']); +$servers->setValue('login', 'bind_id', '${LDAP_ROOTDN}'); diff --git a/docker/docker-entrypoint.d/phpldapadmin/login.patch b/docker/docker-entrypoint.d/phpldapadmin/login.patch new file mode 100644 index 0000000..a61fe3b --- /dev/null +++ b/docker/docker-entrypoint.d/phpldapadmin/login.patch @@ -0,0 +1,10 @@ +--- a/login.php ++++ b/login.php +@@ -20,6 +20,7 @@ + $user = array(); + $user['login'] = get_request('login'); + $user['password'] = get_request('login_pass'); ++ $user['password'] = str_replace('&', '&', get_request('login_pass')); + + if ($user['login'] && !strlen($user['password'])) { + system_message(array( diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..a9edd09 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -eu + +init_nginx() { + local _config_file + _config_file='/etc/nginx/conf.d/default.conf' + + if [[ -f "${_config_file}" ]]; then + return + fi + + echo "[INIT] Configuring nginx" + + echo "[INIT] /etc/nginx/nginx.conf" + cp -f /docker-entrypoint.d/nginx/nginx.conf /etc/nginx/nginx.conf + + echo "[INIT] ${_config_file}" + cp /docker-entrypoint.d/nginx/default.conf "${_config_file}" +} + +init_openldap() { + local _openldap_db + _openldap_db="$(ls -A /var/lib/openldap/openldap-data)" + + # shellcheck disable=SC2154 + if [[ -n "${_openldap_db}" ]] && [[ -f "${LDAPCONF}" ]]; then + return + fi + + echo "[INIT] Configuring OpenLDAP" + + if [[ ! -f "${LDAPCONF}" ]]; then + echo "[INIT] Creating ${LDAPCONF}" + envsubst <"/docker-entrypoint.d/openldap/slapd.template.conf" >"${LDAPCONF}" + fi + + if [[ -z "${_openldap_db}" ]]; then + echo "[INIT] OpenLDAP database not found, initializing" + + trap 'rm -rf /var/lib/openldap/openldap-data/*' EXIT + + echo "[INIT] Creating DB_CONFIG" + cp /docker-entrypoint.d/openldap/DB_CONFIG /var/lib/openldap/openldap-data/DB_CONFIG + + echo "[INIT] Initializing empty OpenLDAP database" + # shellcheck disable=SC2154 + slapadd -f "${LDAPCONF}" -F "${LDAP_CONF_DIR}" -l /dev/null + + echo "[INIT] Generating initial LDIF files" + for ldif_file in /docker-entrypoint.d/openldap/*.template.ldif; do + _target="$(basename "${ldif_file}" | sed 's/\.template//')" + echo "[INIT] + ${_target}" + # shellcheck disable=SC2154 + envsubst <"${ldif_file}" >"${LDAP_INIT_DIR}/${_target}" + done + + echo "[INIT] Applying LDIF files" + for ldif_file in "${LDAP_INIT_DIR}/"*".ldif"; do + echo "[INIT] + $(basename "${ldif_file}")" + slapadd -f "${LDAPCONF}" -F "${LDAP_CONF_DIR}" -l "${ldif_file}" + done + + echo "[INIT] Verifying OpenLDAP server configuration" + slaptest -f "${LDAPCONF}" -F "${LDAP_CONF_DIR}" -d 256 + + trap - EXIT + fi + +} + +init_php_fpm() { + local _config_file + _config_file='/usr/local/etc/php-fpm.d/docker.conf' + + if [[ -f "${_config_file}" ]]; then + return + fi + + echo "[INIT] Configuring PHP-FPM" + + echo "[INIT] ${_config_file}" + cp /docker-entrypoint.d/php/docker.conf "${_config_file}" +} + +init_phpldapadmin() { + local _config_file + _config_file='/etc/phpldapadmin/config.php' + + if [[ -f "${_config_file}" ]]; then + return + fi + + echo "[INIT] Configuring phpLDAPadmin" + + trap 'rm -f "${_config_file}"' EXIT + + echo "[INIT] ${_config_file}" + # shellcheck disable=SC2016 + export servers='$servers' + envsubst <"/docker-entrypoint.d/phpldapadmin/config.template.php" >"${_config_file}" + unset servers + + echo "[INIT] Fix the issue with ampersand in passwords" + # https://github.com/leenooks/phpLDAPadmin/issues/104 + patch /usr/share/webapps/phpldapadmin/htdocs/login.php /docker-entrypoint.d/phpldapadmin/login.patch + + trap - EXIT +} + + +# shellcheck disable=SC2154 +readonly LDAP_DN="dc=${DOMAIN_NAME//\./,dc=}" +# shellcheck disable=SC2154 +readonly LDAP_ROOTDN="cn=${LDAP_ROOT_USERNAME},${LDAP_DN}" +# shellcheck disable=SC2154 +readonly LDAP_ROOTPW="${LDAP_ROOT_PASSWORD}" +# shellcheck disable=SC2001 +readonly LDAP_DC_VERTICAL=$(echo "${LDAP_DN//=/: }" | sed 's/,/\n/g') +export LDAP_DN +export LDAP_ROOTDN +export LDAP_ROOTPW +export LDAP_DC_VERTICAL + +init_nginx +init_openldap +init_php_fpm +init_phpldapadmin + + +_COMMAND=( + "slapd -f \"${LDAPCONF}\" -F \"${LDAP_CONF_DIR}\" -u root -g root -d ${LDAP_LOG_LEVEL}" +) + +if [[ -z "${DISABLE_PHPLDAPADMIN:-""}" ]]; then + _COMMAND+=("nginx -g 'daemon off;'") + _COMMAND+=('php-fpm') +fi + +exec multirun "${_COMMAND[@]}" diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..1ca1517 --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,18 @@ +--- +version: '3.7' + +services: + ldap: + image: homelab-ldap:latest + environment: + DOMAIN_NAME: "example.com" + LDAP_ROOT_USERNAME: "root" + LDAP_ROOT_PASSWORD: '{SSHA}20wM3aP+sZydN3Zrocz9WDZ+mRLxjMtv' + ports: + - 8088:80 + - 389:389 + volumes: + - ldap-data:/var/lib/openldap/openldap-data/ + +volumes: + ldap-data: diff --git a/requirements-dev.in b/requirements-dev.in index b2c729c..dc5ec30 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1 +1,2 @@ +pip-tools yamllint diff --git a/requirements-dev.txt b/requirements-dev.txt index d9d0a0e..deb7cb0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,12 +4,17 @@ # # pip-compile requirements-dev.in # +click==7.1.2 + # via pip-tools pathspec==0.8.1 # via yamllint +pip-tools==5.5.0 + # via -r requirements-dev.in pyyaml==5.4.1 # via yamllint yamllint==1.25.0 # via -r requirements-dev.in # The following packages are considered to be unsafe in a requirements file: +# pip # setuptools