|
- {
- # nixosConfigurations.openldap-server example. To use this module in your own NixOS config, import it with:
- # {
- # imports = [ ogc.nixosModules.openldap ];
- # openldap = {
- # enable = true;
- # organization = "myorg";
- # extension = "com";
- # domain = "myorg.com";
- # adminPasswordFile = "/run/secrets/ldap-admin-password";
- # services.gitea.passwordFile = "/run/secrets/ldap-gitea-password";
- # # ... etc
- # };
- # }
-
- description = "NixOS OpenLDAP server with declarative directory content";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
- };
-
- outputs = { self, nixpkgs, ... }: {
-
- nixosModules.openldap = { config, lib, pkgs, ... }:
- let
- cfg = config.openldap;
-
- # Build the postfix-book schema as an LDIF file suitable for OLC (cn=config)
- postfixBookSchemaLdif = pkgs.writeText "postfix-book.ldif" ''
- dn: cn=postfix-book,cn=schema,cn=config
- objectClass: olcSchemaConfig
- cn: postfix-book
- olcAttributeTypes: {0}( 1.3.6.1.4.1.29426.1.10.1 NAME 'mailHomeDirectory' DESC 'The absolute path to the mail user home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
- olcAttributeTypes: {1}( 1.3.6.1.4.1.29426.1.10.2 NAME 'mailAlias' DESC 'RFC822 Mailbox - mail alias' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
- olcAttributeTypes: {2}( 1.3.6.1.4.1.29426.1.10.3 NAME 'mailUidNumber' DESC 'UID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
- olcAttributeTypes: {3}( 1.3.6.1.4.1.29426.1.10.4 NAME 'mailGidNumber' DESC 'GID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
- olcAttributeTypes: {4}( 1.3.6.1.4.1.29426.1.10.5 NAME 'mailEnabled' DESC 'TRUE to enable, FALSE to disable account' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
- olcAttributeTypes: {5}( 1.3.6.1.4.1.29426.1.10.6 NAME 'mailGroupMember' DESC 'Name of a mail distribution list' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
- olcAttributeTypes: {6}( 1.3.6.1.4.1.29426.1.10.7 NAME 'mailQuota' DESC 'Mail quota limit in kilobytes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
- olcAttributeTypes: {7}( 1.3.6.1.4.1.29426.1.10.8 NAME 'mailStorageDirectory' DESC 'The absolute path to the mail users mailbox' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
- olcObjectClasses: {0}( 1.3.6.1.4.1.29426.1.2.2.1 NAME 'PostfixBookMailAccount' DESC 'Mail account used in Postfix Book' SUP top AUXILIARY MUST mail MAY ( mailHomeDirectory $ mailAlias $ mailGroupMember $ mailUidNumber $ mailGidNumber $ mailEnabled $ mailQuota $ mailStorageDirectory ) )
- olcObjectClasses: {1}( 1.3.6.1.4.1.29426.1.2.2.2 NAME 'PostfixBookMailForward' DESC 'Mail forward used in Postfix Book' SUP top AUXILIARY MUST ( mail $ mailAlias ) )
- '';
-
- # Shorthands
- baseDn = "dc=${cfg.organization},dc=${cfg.extension}";
- adminDn = "cn=admin,${baseDn}";
- servicesDn = "ou=services,${baseDn}";
-
- # Helper to build a service account LDIF entry
- mkServiceEntry = uid: ''
- dn: uid=${uid},${servicesDn}
- objectClass: simpleSecurityObject
- objectClass: account
- objectClass: top
- uid: ${uid}
- userPassword: {SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5
- '';
-
- in
- {
- options.openldap = {
- enable = lib.mkOption {type = lib.types.bool;};
-
- organization = lib.mkOption {
- type = lib.types.str;
- example = "myorg";
- description = "The LDAP organization component (first dc= part of the base DN).";
- };
-
- extension = lib.mkOption {
- type = lib.types.str;
- example = "com";
- description = "The LDAP extension component (second dc= part of the base DN).";
- };
-
- domain = lib.mkOption {
- type = lib.types.str;
- example = "myorg.com";
- description = "The domain used for email addresses (e.g. admin@domain).";
- };
-
- adminPasswordFile = lib.mkOption {
- type = lib.types.path;
- description = ''
- Path to a file containing the hashed admin password (e.g. output of slappasswd).
- The file must be readable by the openldap user.
- '';
- };
-
- services = {
- gitea = {
- uid = lib.mkOption {
- type = lib.types.str;
- default = "gitea";
- description = "UID for the Gitea LDAP service account.";
- };
- passwordFile = lib.mkOption {
- type = lib.types.path;
- description = "Path to file containing the hashed password for the Gitea service account.";
- };
- };
-
- hauk = {
- uid = lib.mkOption {
- type = lib.types.str;
- default = "hauk";
- description = "UID for the Hauk LDAP service account.";
- };
- passwordFile = lib.mkOption {
- type = lib.types.path;
- description = "Path to file containing the hashed password for the Hauk service account.";
- };
- };
-
- mail = {
- uid = lib.mkOption {
- type = lib.types.str;
- default = "mail";
- description = "UID for the mail (postfix/dovecot/roundcube) LDAP service account.";
- };
- passwordFile = lib.mkOption {
- type = lib.types.path;
- description = "Path to file containing the hashed password for the mail service account.";
- };
- };
-
- nextcloud = {
- uid = lib.mkOption {
- type = lib.types.str;
- default = "nextcloud";
- description = "UID for the Nextcloud LDAP service account.";
- };
- passwordFile = lib.mkOption {
- type = lib.types.path;
- description = "Path to file containing the hashed password for the Nextcloud service account.";
- };
- };
- };
-
- dataDir = lib.mkOption {
- type = lib.types.str;
- default = "/var/lib/openldap/data";
- description = "Directory for the MDB database files.";
- };
- };
-
- config = lib.mkIf cfg.enable {
- environment.systemPackages = with pkgs; [
- openldap
- ];
-
- services.openldap = {
- settings = {
- attrs = {
- olcLogLevel = "conns config";
- };
-
- children = {
- # ── Schema includes ──────────────────────────────────────────
- "cn=schema".includes = [
- "${pkgs.openldap}/etc/schema/core.ldif"
- "${pkgs.openldap}/etc/schema/cosine.ldif"
- "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
- "${pkgs.openldap}/etc/schema/nis.ldif"
- "${postfixBookSchemaLdif}"
- ];
-
- "olcDatabase={-1}frontend".attrs = {
- objectClass = [ "olcDatabaseConfig" "olcFrontendConfig" ];
- olcDatabase = "{-1}frontend";
- olcAccess = [
- ''{0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break''
- ''{1}to dn.exact="" by * read''
- ''{2}to dn.base="cn=Subschema" by * read''
- ];
- olcSizeLimit = "500";
- structuralObjectClass = "olcDatabaseConfig";
- };
-
- "olcDatabase={0}config".attrs = {
- #objectClass = [ "olcDatabaseConfig" "olcConfig" ];
- objectClass = "olcDatabaseConfig";
- olcDatabase = "{0}config";
- olcRootDN = "cn=admin,cn=config";
- olcAccess = [
- ''{0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break''
- ];
- };
-
- # ── MDB database ─────────────────────────────────────────────
- "olcDatabase={1}mdb".attrs = {
- objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
- olcDatabase = "{1}mdb";
- olcDbDirectory = cfg.dataDir;
- olcSuffix = baseDn;
- olcRootDN = adminDn;
- olcRootPW = "{SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5";
- olcDbCheckpoint = "512 30";
-
- olcDbIndex = [
- "objectClass eq"
- "cn,uid eq"
- "uidNumber,gidNumber eq"
- "member,memberUid eq"
- ];
-
- olcDbMaxSize = "1073741824";
-
- # ── ACLs (from _acl_add_0.ldif and _acl_add_1.ldif) ────────
- olcAccess = [
- # ACL 0: password access (from _acl_add_0.ldif)
- ''{0}to dn.subtree="${baseDn}" attrs=userPassword
- by self write
- by dn.base="${adminDn}" write
- by dn.children="${servicesDn}" read
- by anonymous auth
- by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
- by * none''
-
- # ACL 1: general subtree access (from _acl_add_1.ldif)
- ''{1}to dn.subtree="${baseDn}"
- by self read
- by dn.base="${adminDn}" write
- by dn.children="${servicesDn}" read
- by * none''
-
- # ACL 2: allow root via SASL EXTERNAL (ldapi socket peer)
- ''{2}to dn.subtree="${baseDn}"
- by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
- by * none''
- ];
- };
- };
- };
-
- # ── Declarative DIT content ──────────────────────────────────────
- # This populates the database on every start, ensuring the entries
- # from all the LDIF files in openldap-data/ are present.
- declarativeContents."${baseDn}" = ''
- dn: ${baseDn}
- objectClass: top
- objectClass: dcObject
- objectClass: organization
- o: ${cfg.organization}
- dc: ${cfg.organization}
-
- dn: ${adminDn}
- objectClass: organizationalRole
- objectClass: simpleSecurityObject
- objectClass: extensibleObject
- cn: admin
- description: LDAP administrator
- mail: admin@${cfg.domain}
- userPassword: {SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5
-
- dn: ou=people,${baseDn}
- objectClass: organizationalUnit
- objectClass: top
- ou: people
-
- dn: ou=services,${baseDn}
- objectClass: organizationalUnit
- objectClass: top
- ou: services
-
- ${mkServiceEntry cfg.services.gitea.uid}
- ${mkServiceEntry cfg.services.hauk.uid}
- ${mkServiceEntry cfg.services.mail.uid}
- ${mkServiceEntry cfg.services.nextcloud.uid}
- '';
- };
-
- # ── Set service account passwords from files at activation time ────
- # declarativeContents doesn't support reading passwords from files,
- # so we use a systemd service to set them after slapd starts.
- systemd.services.openldap-set-service-passwords = {
- description = "Set LDAP service account passwords from files";
- after = [ "openldap.service" ];
- requires = [ "openldap.service" ];
- wantedBy = [ "multi-user.target" ];
- serviceConfig = {
- Type = "oneshot";
- RemainAfterExit = true;
- };
- path = [ pkgs.openldap ];
- script = let
- mkPasswordScript = name: svcCfg: ''
- echo "Setting password for ${name} service account (uid=${svcCfg.uid})..."
- PASSWORD=$(cat "${svcCfg.passwordFile}")
- ldapmodify -Y EXTERNAL -H ldapi:/// -w aaa <<LDIF
- dn: uid=${svcCfg.uid},${servicesDn}
- changetype: modify
- replace: userPassword
- userPassword: $PASSWORD
- LDIF
- '';
- in ''
- set -euo pipefail
- ${mkPasswordScript "gitea" cfg.services.gitea}
- ${mkPasswordScript "hauk" cfg.services.hauk}
- ${mkPasswordScript "mail" cfg.services.mail}
- ${mkPasswordScript "nextcloud" cfg.services.nextcloud}
- # TODO: admin pwd
- echo "All service account passwords set."
- '';
- };
-
- # Ensure the data directory exists
- systemd.tmpfiles.rules = [
- "d ${cfg.dataDir} 0700 openldap openldap -"
- ];
-
- # Open LDAP port in firewall
- networking.firewall.allowedTCPPorts = [ 389 ];
- };
- };
- };
- }
|