diff --git a/ix-dev/community/lldap/README.md b/ix-dev/community/lldap/README.md new file mode 100644 index 00000000000..041e3fabf6b --- /dev/null +++ b/ix-dev/community/lldap/README.md @@ -0,0 +1,32 @@ +# LLDAP + +LLDAP is a lightweight LDAP directory server with a built-in web interface for managing users, groups, and application credentials. + +## Endpoints + +- Web UI: `http://:17170` +- LDAP: `ldap://:3890` +- LDAPS (optional): `ldaps://:6360` + +## Default Access + +- Admin username: `admin` +- Admin password: generated during install (displayed in install dialog) + +## Required Configuration + +- LDAP domain (e.g. `example.com`) +- Data directory (`/data` is persisted to the host) + +## Optional Configuration + +- Custom ports / bind addresses +- External PostgreSQL / MySQL database URL +- SMTP settings for email-based password reset +- LDAPS certificate/key paths + +## Reference + +- Project: https://github.com/lldap/lldap +- Documentation: https://github.com/lldap/lldap/tree/main/docs +- Docker image: https://hub.docker.com/r/lldap/lldap diff --git a/ix-dev/community/lldap/app.yaml b/ix-dev/community/lldap/app.yaml new file mode 100644 index 00000000000..9a67946b990 --- /dev/null +++ b/ix-dev/community/lldap/app.yaml @@ -0,0 +1,34 @@ +app_version: 0.6.2 +capabilities: [] +categories: + - identity + - authentication +changelog_url: https://github.com/lldap/lldap/releases +date_added: '2025-12-25' +description: Lightweight LDAP server with a built-in web interface for identity management. +home: https://lldap.io +host_mounts: [] +icon: https://media.sys.truenas.net/apps/lldap/icons/icon.png +keywords: + - ldap + - identity + - authentication +lib_version: 2.1.74 +maintainers: + - email: dev@truenas.com + name: TrueNAS + url: https://www.truenas.com/ +name: lldap +run_as_context: + user_context: + - description: LLDAP runs as an unprivileged user inside the container. + gid: 568 + group_name: lldap + uid: 568 + user_name: lldap +sources: + - https://github.com/lldap/lldap + - https://hub.docker.com/r/lldap/lldap +title: LLDAP +train: community +version: 1.0.0 diff --git a/ix-dev/community/lldap/item.yaml b/ix-dev/community/lldap/item.yaml new file mode 100644 index 00000000000..c84ac1c1192 --- /dev/null +++ b/ix-dev/community/lldap/item.yaml @@ -0,0 +1,10 @@ +categories: + - identity + - authentication +icon_url: https://media.sys.truenas.net/apps/lldap/icons/icon.png +screenshots: [] +short_description: Lightweight LDAP server with a built-in web UI for credentials management. +tags: + - ldap + - identity + - authentication diff --git a/ix-dev/community/lldap/ix_values.yaml b/ix-dev/community/lldap/ix_values.yaml new file mode 100644 index 00000000000..2747d55c74e --- /dev/null +++ b/ix-dev/community/lldap/ix_values.yaml @@ -0,0 +1,107 @@ +images: + image: + repository: lldap/lldap + tag: stable +consts: + main_container_name: lldap + data_mount_path: /data + ldap_port: 3890 + ldaps_port: 6360 + http_port: 17170 + default_domain: example.com + jwt_secret_length: 32 + admin_password_length: 16 +values: + TZ: Etc/UTC + settings: + domain: example.com + admin_user: admin + admin_email: admin@example.com + admin_password_reset: never + run_as: + user: 0 + group: 0 + supplementary_groups: [] + networking: + http_port: + bind_mode: published + port_number: 17170 + host_ips: + - 0.0.0.0 + - '::' + ldap_port: + bind_mode: published + port_number: 3890 + host_ips: + - 0.0.0.0 + - '::' + ldaps_port: + bind_mode: '' + port_number: 6360 + host_ips: + - 0.0.0.0 + - '::' + resources: + limits: + cpus: 2.0 + memory: 2048 + requests: + cpus: 0.2 + memory: 256 + storage: + data: + type: ix_volume + ix_volume_config: + dataset_name: lldap-data + acl_enable: false + create_host_path: true + auto_permissions: true + additional_storage: [] + additional_storage_defaults: [] + database: + type: sqlite + external_url: '' + smtp: + enabled: false + server: smtp.example.com + port: 587 + encryption: STARTTLS + username: user@example.com + password: '' + from: LLDAP Admin + reply_to: LLDAP Admin + tls: + ldaps_enabled: false + certificate_path: /certs/ldaps.crt + key_path: /certs/ldaps.key + advanced: + environment: [] +secrets: + admin_password: + random: false + value: ChangeMe! + jwt_secret: + random: true + length: 32 + key_seed: + random: true + length: 32 +notes: +- title: Credentials + level: INFO + message: 'The default admin account is `admin` with password `ChangeMe!`. Please + sign in and change it immediately from the web interface or via `lldap admin setPassword`. + + ' +- title: LDAP Recommendations + level: WARNING + message: 'Do not expose LDAP/LDAPS ports directly to the internet. Place them behind + a VPN or reverse proxy when federating external services. + + ' +storage_defaults: + data: + type: ix_volume + ix_volume_config: + dataset_name: lldap-data + acl_enable: false diff --git a/ix-dev/community/lldap/questions.yaml b/ix-dev/community/lldap/questions.yaml new file mode 100644 index 00000000000..14dd43c31be --- /dev/null +++ b/ix-dev/community/lldap/questions.yaml @@ -0,0 +1,298 @@ + +groups: + - name: LLDAP Configuration + description: Configure directory settings + - name: User and Group Configuration + description: Configure container user and group + - name: Network Configuration + description: Configure external access + - name: Storage Configuration + description: Configure persistent storage + - name: Optional Services + description: Configure SMTP, TLS, and database settings + - name: Advanced Configuration + description: Additional options + +questions: + - variable: settings + group: LLDAP Configuration + label: Directory Settings + schema: + type: dict + attrs: + - variable: domain + label: LDAP Domain + description: Base domain used to construct the LDAP suffix (e.g. example.com). + schema: + type: string + default: example.com + required: true + - variable: admin_user + label: Admin Username + description: Username for the built-in admin account. + schema: + type: string + default: admin + required: true + - variable: admin_email + label: Admin Email + description: Initial email assigned to the admin account. + schema: + type: string + default: admin@example.com + - variable: admin_password_reset + label: Force Password Reset + description: Force the admin password to be reset on next start. + schema: + type: string + default: "never" + enum: + - value: "never" + description: Do not reset automatically + - value: "once" + description: Reset password on next start only + - value: "always" + description: Reset password on every start + + - variable: run_as + group: User and Group Configuration + label: Container User + schema: + type: dict + attrs: + - variable: user + label: User ID + description: User ID that owns data inside the container. Defaults to 0 so the entrypoint can adjust permissions on first start. + schema: + type: int + min: 0 + default: 0 + required: true + - variable: group + label: Group ID + description: Group ID that owns data inside the container. Defaults to 0 to match the container user. + schema: + type: int + min: 0 + default: 0 + required: true + - variable: supplementary_groups + label: Supplementary Groups + description: Additional group IDs granted to the container user. + schema: + type: list + default: [] + items: + - variable: group_id + label: Group ID + schema: + type: int + min: 568 + + - variable: networking + group: Network Configuration + label: Networking + schema: + type: dict + attrs: + - variable: http_port + label: Web UI Port + schema: + $ref: + - definitions/networkPortConfig + attrs: + - variable: port_number + label: Port Number + schema: + type: int + default: 17170 + - variable: ldap_port + label: LDAP Port + schema: + $ref: + - definitions/networkPortConfig + attrs: + - variable: port_number + label: Port Number + schema: + type: int + default: 3890 + - variable: ldaps_port + label: LDAPS Port + schema: + $ref: + - definitions/networkPortConfig + attrs: + - variable: bind_mode + schema: + default: "" + - variable: port_number + label: Port Number + schema: + type: int + default: 6360 + + - variable: storage + group: Storage Configuration + label: Storage + schema: + type: dict + attrs: + - variable: data + label: Data Storage + description: Persistent storage for configuration and database files. + schema: + $ref: + - definitions/storage + attrs: + - variable: type + schema: + default: ix_volume + - variable: ix_volume_config + attrs: + - variable: dataset_name + default: lldap-data + - variable: additional_storage + label: Additional Storage + schema: + $ref: + - definitions/additionalStorage + + - variable: smtp + group: Optional Services + label: SMTP + schema: + type: dict + attrs: + - variable: enabled + label: Enable SMTP + schema: + type: boolean + default: false + - variable: server + label: SMTP Server + schema: + type: string + default: smtp.example.com + show_if: [["enabled", "=", true]] + - variable: port + label: SMTP Port + schema: + type: int + default: 587 + show_if: [["enabled", "=", true]] + - variable: encryption + label: SMTP Encryption + schema: + type: string + default: STARTTLS + show_if: [["enabled", "=", true]] + enum: + - value: NONE + description: None + - value: STARTTLS + description: STARTTLS + - value: TLS + description: TLS + - variable: username + label: SMTP Username + schema: + type: string + default: user@example.com + show_if: [["enabled", "=", true]] + - variable: password + label: SMTP Password + schema: + type: password + show_if: [["enabled", "=", true]] + - variable: from + label: From Address + schema: + type: string + default: "LLDAP Admin " + show_if: [["enabled", "=", true]] + - variable: reply_to + label: Reply-To Address + schema: + type: string + default: "LLDAP Admin " + show_if: [["enabled", "=", true]] + + - variable: tls + group: Optional Services + label: LDAPS + schema: + type: dict + attrs: + - variable: ldaps_enabled + label: Enable LDAPS + schema: + type: boolean + default: false + - variable: certificate_path + label: Certificate Path + description: Path inside the container to the certificate file. + schema: + type: string + default: /certs/ldaps.crt + show_if: [["ldaps_enabled", "=", true]] + - variable: key_path + label: Key Path + description: Path inside the container to the key file. + schema: + type: string + default: /certs/ldaps.key + show_if: [["ldaps_enabled", "=", true]] + + - variable: database + group: Optional Services + label: External Database + schema: + type: dict + attrs: + - variable: type + label: Database Type + schema: + type: string + default: sqlite + enum: + - value: sqlite + description: Embedded SQLite (default) + - value: postgres + description: External PostgreSQL + - value: mysql + description: External MySQL / MariaDB + - variable: external_url + label: Database URL + description: Connection string when using an external database. + schema: + type: string + default: "" + show_if: [["type", "!=", "sqlite"]] + + - variable: advanced + group: Advanced Configuration + label: Advanced Options + schema: + type: dict + attrs: + - variable: environment + label: Additional Environment Variables + schema: + type: list + default: [] + items: + - variable: env + label: Environment Variable + schema: + type: dict + attrs: + - variable: name + label: Name + schema: + type: string + required: true + - variable: value + label: Value + schema: + type: string diff --git a/ix-dev/community/lldap/templates/docker-compose.yaml b/ix-dev/community/lldap/templates/docker-compose.yaml new file mode 100644 index 00000000000..f6f91c203ee --- /dev/null +++ b/ix-dev/community/lldap/templates/docker-compose.yaml @@ -0,0 +1,107 @@ +{% set tpl = ix_lib.base.render.Render(values) %} + +{% set container = tpl.add_container(values.consts.main_container_name, "image") %} +{% set base_dn = "dc=" + values.settings.domain.replace('.', ',dc=') %} +{% set jwt_secret = tpl.funcs.or_default(values.secrets.jwt_secret.value, values.secrets.jwt_secret) %} +{% set key_seed = tpl.funcs.or_default(values.secrets.key_seed.value, values.secrets.key_seed) %} +{% do container.environment.add_env("LLDAP_JWT_SECRET", jwt_secret) %} +{% do container.environment.add_env("JWT_SECRET", jwt_secret) %} +{% do container.environment.add_env("LLDAP_KEY_SEED", key_seed) %} +{% do container.environment.add_env("KEY_SEED", key_seed) %} +{% do container.environment.add_env("LLDAP_KEY_FILE", "") %} +{% do container.environment.add_env("ADMIN_USER", values.settings.admin_user) %} +{% do container.environment.add_env("ADMIN_EMAIL", values.settings.admin_email) %} +{% do container.environment.add_env("USERS__DEFAULT_EMAIL", values.settings.admin_email) %} +{% do container.environment.add_env("USERS__DEFAULT_USERNAME", values.settings.admin_user) %} +{% set admin_pass = tpl.funcs.or_default(values.secrets.admin_password.value, values.secrets.admin_password) %} +{% do container.environment.add_env("LLDAP_LDAP_USER_PASS", admin_pass) %} +{% do container.environment.add_env("USERS__DEFAULT_PASSWORD", admin_pass) %} +{% do container.environment.add_env("USERS__DEFAULT_GROUPS__0", "lldap_admin") %} +{% if values.settings.admin_password_reset != "never" %} + {% do container.environment.add_env("FORCE_LDAP_USER_PASS_RESET", values.settings.admin_password_reset) %} +{% endif %} +{% do container.environment.add_env("LDAP_BASE_DN", base_dn) %} +{% do container.environment.add_env("LDAP_USER_BASE_DN", "ou=people," + base_dn) %} +{% do container.environment.add_env("LDAP_GROUP_BASE_DN", "ou=groups," + base_dn) %} +{% do container.environment.add_env("HTTP_HOST", "0.0.0.0") %} +{% do container.environment.add_env("HTTP_PORT", values.networking.http_port.port_number) %} +{% do container.environment.add_env("LDAP_HOST", "0.0.0.0") %} +{% do container.environment.add_env("LDAP_PORT", values.networking.ldap_port.port_number) %} + +{% if values.tls.ldaps_enabled %} + {% do container.environment.add_env("LDAPS_OPTIONS__ENABLED", true) %} + {% do container.environment.add_env("LDAPS_OPTIONS__CERT_FILE", values.tls.certificate_path) %} + {% do container.environment.add_env("LDAPS_OPTIONS__KEY_FILE", values.tls.key_path) %} + {% do container.add_port(values.networking.ldaps_port) %} +{% else %} + {% do container.environment.add_env("LDAPS_OPTIONS__ENABLED", false) %} +{% endif %} + +{% if values.smtp.enabled %} + {% do container.environment.add_env("SMTP_OPTIONS__ENABLE_PASSWORD_RESET", true) %} + {% do container.environment.add_env("SMTP_OPTIONS__SERVER", values.smtp.server) %} + {% do container.environment.add_env("SMTP_OPTIONS__PORT", values.smtp.port) %} + {% do container.environment.add_env("SMTP_OPTIONS__SMTP_ENCRYPTION", values.smtp.encryption) %} + {% do container.environment.add_env("SMTP_OPTIONS__USER", values.smtp.username) %} + {% if values.smtp.password %} + {% do container.environment.add_env("SMTP_OPTIONS__PASSWORD", values.smtp.password) %} + {% endif %} + {% if values.smtp.from %} + {% do container.environment.add_env("SMTP_OPTIONS__FROM", values.smtp.from) %} + {% endif %} + {% if values.smtp.reply_to %} + {% do container.environment.add_env("SMTP_OPTIONS__REPLY_TO", values.smtp.reply_to) %} + {% endif %} +{% endif %} + +{% if values.database.type != "sqlite" %} + {% do container.environment.add_env("DATABASE_URL", values.database.external_url) %} +{% else %} + {% do container.environment.add_env("DATABASE_URL", "sqlite:///data/users.db?mode=rwc") %} +{% endif %} + +{% do container.environment.add_user_envs(values.advanced.environment) %} + +{% do container.healthcheck.set_custom_test(["CMD", "/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]) %} + +{% if values.run_as.user is not none and values.run_as.group is not none %} + {% do container.set_user(values.run_as.user, values.run_as.group) %} + {% do container.environment.add_env("PUID", values.run_as.user) %} + {% do container.environment.add_env("UID", values.run_as.user) %} + {% do container.environment.add_env("USER_ID", values.run_as.user) %} + {% do container.environment.add_env("PGID", values.run_as.group) %} + {% do container.environment.add_env("GID", values.run_as.group) %} + {% do container.environment.add_env("GROUP_ID", values.run_as.group) %} +{% endif %} + +{# Minimal capabilities so the entrypoint can adjust ownership and drop privileges #} +{% do container.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE", "SETUID", "SETGID"]) %} + +{% do container.add_port(values.networking.http_port) %} +{% do container.add_port(values.networking.ldap_port) %} + +{% set data_storage = values.storage.data.copy() %} +{% set _ = data_storage.setdefault("create_host_path", True) %} +{% do container.add_storage(values.consts.data_mount_path, data_storage) %} +{% for store in values.storage.additional_storage %} + {% do container.add_storage(store.mount_path, store) %} +{% endfor %} + +{% do tpl.portals.add(values.networking.http_port, {"description": "LLDAP Web UI"}) %} + +{% do container.deploy.resources.remove_cpus_and_memory() %} + +{% set compose = tpl.render() %} +{# docker compose v3 schema lacks start_interval; strip it from generated healthchecks #} +{% for service in compose["services"].values() %} + {% set healthcheck = service.get("healthcheck") %} + {% if healthcheck %} + {% set _ = healthcheck.pop("start_interval", None) %} + {% endif %} + {% if service.get("group_add") %} + {# docker compose expects group_add entries as strings #} + {% set _ = service.update({"group_add": service["group_add"] | map('string') | list}) %} + {% endif %} +{% endfor %} + +{{ compose | tojson }} diff --git a/ix-dev/community/lldap/templates/test_values/basic-values.yaml b/ix-dev/community/lldap/templates/test_values/basic-values.yaml new file mode 100644 index 00000000000..eba48f506a5 --- /dev/null +++ b/ix-dev/community/lldap/templates/test_values/basic-values.yaml @@ -0,0 +1,52 @@ +settings: + domain: example.com + admin_user: admin + admin_email: admin@example.com + admin_password_reset: never +run_as: + user: 0 + group: 0 + supplementary_groups: [] +networking: + http_port: + bind_mode: published + port_number: 17170 + host_ips: + - 0.0.0.0 + ldap_port: + bind_mode: published + port_number: 3890 + host_ips: + - 0.0.0.0 +storage: + data: + type: ix_volume + ix_volume_config: + dataset_name: lldap-test + create_host_path: true + +ix_volumes: + lldap-test: /opt/tests/mnt/lldap + +smtp: + enabled: false + +tls: + ldaps_enabled: false + +database: + type: sqlite + +advanced: + environment: [] + +secrets: + admin_password: + random: true + length: 16 + jwt_secret: + random: true + length: 32 + key_seed: + random: true + length: 24