Feature Specification: Component lldap

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/components/shared/lldap/default.nix, core/components/operator/operator-facing-options.csv, core/setups/hosts-common.nix, core/setups/groups/hosting-provider.nix, core/setups/groups/operator.nix, core/resources/ldap/, core/resources/oidc/), pull requests #1383, #1755, #1822, #1835, #1852 and #1874.

User Scenarios & Testing (mandatory)

User Story 1 - Keep the users of a group in one directory (Priority: P1)

Each group needs one user directory. The lldap component marks a node as the directory of its group. It declares provides = [ "ldap" ], so the node answers the ldap requests of the group (see ldap). The setup sets the daemon and the lldap-provision unit, which applies the seeded users and groups on this node with OpenTofu.

Why this priority: the SSO portal and the applications read users from this directory.

Independent Test: contracts-ldap-lldap, test-ldap-engine, test-lldap-seed-isolation and test-lldap-admin-release. nix-unit-setups-ldap-directory and nix-unit-setups-ldap-grant cover the wiring decisions.

Acceptance Scenarios:

  1. Given a group with an lldap machine, When the setups evaluate, Then the "directory".ldap request has domain = "ldap.<domain>", bindInstanceName = "ldap-bind-<group>" and host set to the lldap machine.
  2. Given a group with an authelia machine, When the setups evaluate, Then the lldap node gets a want for the bind group authelia-users.

User Story 2 - Let users manage their own account (Priority: P2)

A user changes a password or a display name in the lldap web UI. The component requests "lldap-ui".oidc with forward-auth on the subdomain users. Authelia protects the UI with one_factor and an empty allowedGroups, so every signed-in user reaches it. The upstream is 127.0.0.1:17170. Only the routes of the password reset skip the protection, so a user without a session can set a password from a reset link.

Why this priority: self-service is useful, but the directory works without it.

Independent Test: test-lldap-mail does a reset through the forward-auth vhost and checks that the other routes keep the protection. nix-unit-testers-oidc-routing covers the routing of oidc requests.

Acceptance Scenarios:

  1. Given a deployment with an lldap machine, When the setups evaluate, Then the "lldap-ui".oidc request has forwardAuth.enable = true, subdomain = "users" and adminUI = false.
  2. Given a user without a session, When the user requests a reset route on users.<domain>, Then nginx sends the request to lldap. When the user requests a login route, /auth/refresh or /api/, Then nginx sends the user to Authelia.

User Story 3 - Send password reset mail (Priority: P3)

lldap sends password reset mail. When the group has an smtp machine, the setup sets fediversity.lldap.smtp and adds a want for an lldap sender on the relay (see smtp).

Why this priority: reset mail is a convenience. An admin can reset a password without it.

Independent Test: test-lldap-mail and nix-unit-setups-lldap-mail.

Acceptance Scenarios:

  1. Given a group with lldap and smtp machines, When the setups evaluate, Then fediversity.lldap.smtp is set on the lldap node.

Edge Cases

  • A group without an lldap machine uses host = "ldap.<domain>" in its "directory".ldap request.
  • lldap gets publicUrl = https://users.<domain> when the node serves the forward-auth vhost of the UI. Else it gets http://<ldap host>:<httpPort>, the direct listener.
  • The admin user is the group name (adminUser = group). This keeps the name off the RestrictedNames list of pixelfed.
  • lldap derives the OPAQUE server key from a seed in OpenBao KV, not from a key file in /var/lib/lldap. Thus a node that is built again has the same key, and the passwords in a restored database work. lldap keeps a hash of the key in the database and does not start when the key changes. A database that has a key from a different source, for example a database from before the seed, starts once with LLDAP_FORCE_UPDATE_PRIVATE_KEY=true, and then each user resets the password.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare provides = [ "ldap" ], so the node answers the ldap requests of its group.
  • FR-002: The implementation MUST request "spiffe".spiffe.enable = true, so the node logs in to OpenBao with its SVID.
  • FR-003: The implementation MUST request "lldap-ui".oidc with enable = false and forwardAuth on subdomain users, upstream 127.0.0.1:17170, policy one_factor and allowedGroups = [ ].
  • FR-003a: The implementation MUST give the request the unauthenticatedLocations from reset-routes.nix, and MUST add the lldap-reset rate limit zone to the nginx config of the node.
  • FR-004: The lldap UI MUST NOT be an admin UI (adminUI = false), so deployAdminUIs does not gate it.
  • FR-005: The setup MUST set the daemon, the lldap-provision unit and the bind accounts from the ldap resource.
  • FR-006: The implementation MUST request postgresql with database, consumer and user lldap, and give the URL to the daemon in LLDAP_DATABASE_URL, because the daemon runs as a DynamicUser. When the result is present, the settings module MUST remove database_url from the configuration file. When the result is absent, the daemon MUST keep its local SQLite database.
  • FR-007: The implementation MUST request garage through mkBackupsGarageRequest with the bucket lldap-backups, and a streamingBackup of the postgres database to that bucket with the restic provider, as root.
  • FR-008: The ldap resource MUST generate the seed of the OPAQUE server key once, in the generateFiles instance keySeedInstanceName (the setup sets ldap-key-<group>), as an environment file that sets LLDAP_KEY_SEED. The daemon MUST read that file as an EnvironmentFile and set key_file = "".

Key Entities

  • "directory".ldap: the request of the setup. It carries domain, bindInstanceName, adminUser, email and host.
  • "lldap-ui".oidc: the forward-auth request for the self-service UI.
  • reset-routes.nix: the nginx locations of the reset that skip the forward-auth protection, and the zone that limits the rate of /auth/reset/.
  • fediversity.lldap.smtp: the mail account for password reset mail.
  • operator-facing-options.csv: rows with application = lldap. 34 options have the audience fediversity and 2 have the audience operator.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: contracts-ldap-lldap, test-ldap-engine, test-lldap-seed-isolation, test-lldap-admin-release and test-lldap-mail pass.
  • SC-002: nix-unit-setups-ldap-directory, nix-unit-setups-ldap-grant and nix-unit-setups-lldap-mail pass.

Assumptions

  • The hosting-provider and operator groups both declare an lldap machine.
  • The component has no icon. applications-catalog shows it without artwork.
  • The provider implementation (lldap-provider.nix, provision.nix) is in core/resources/ldap/, not in this component.

Known Deviations

None.