You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 satır
14KB

  1. {
  2. # nixosConfigurations.openldap-server example. To use this module in your own NixOS config, import it with:
  3. # {
  4. # imports = [ ogc.nixosModules.openldap ];
  5. # openldap = {
  6. # enable = true;
  7. # organization = "myorg";
  8. # extension = "com";
  9. # domain = "myorg.com";
  10. # adminPasswordFile = "/run/secrets/ldap-admin-password";
  11. # services.gitea.passwordFile = "/run/secrets/ldap-gitea-password";
  12. # # ... etc
  13. # };
  14. # }
  15. description = "NixOS OpenLDAP server with declarative directory content";
  16. inputs = {
  17. nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
  18. };
  19. outputs = { self, nixpkgs, ... }: {
  20. nixosModules.openldap = { config, lib, pkgs, ... }:
  21. let
  22. cfg = config.openldap;
  23. # Build the postfix-book schema as an LDIF file suitable for OLC (cn=config)
  24. postfixBookSchemaLdif = pkgs.writeText "postfix-book.ldif" ''
  25. dn: cn=postfix-book,cn=schema,cn=config
  26. objectClass: olcSchemaConfig
  27. cn: postfix-book
  28. 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 )
  29. 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} )
  30. 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 )
  31. 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 )
  32. 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 )
  33. 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 )
  34. 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 )
  35. 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 )
  36. 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 ) )
  37. 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 ) )
  38. '';
  39. # Shorthands
  40. baseDn = "dc=${cfg.organization},dc=${cfg.extension}";
  41. adminDn = "cn=admin,${baseDn}";
  42. servicesDn = "ou=services,${baseDn}";
  43. # Helper to build a service account LDIF entry
  44. mkServiceEntry = uid: ''
  45. dn: uid=${uid},${servicesDn}
  46. objectClass: simpleSecurityObject
  47. objectClass: account
  48. objectClass: top
  49. uid: ${uid}
  50. userPassword: {SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5
  51. '';
  52. in
  53. {
  54. options.openldap = {
  55. enable = lib.mkOption {type = lib.types.bool;};
  56. organization = lib.mkOption {
  57. type = lib.types.str;
  58. example = "myorg";
  59. description = "The LDAP organization component (first dc= part of the base DN).";
  60. };
  61. extension = lib.mkOption {
  62. type = lib.types.str;
  63. example = "com";
  64. description = "The LDAP extension component (second dc= part of the base DN).";
  65. };
  66. domain = lib.mkOption {
  67. type = lib.types.str;
  68. example = "myorg.com";
  69. description = "The domain used for email addresses (e.g. admin@domain).";
  70. };
  71. adminPasswordFile = lib.mkOption {
  72. type = lib.types.path;
  73. description = ''
  74. Path to a file containing the hashed admin password (e.g. output of slappasswd).
  75. The file must be readable by the openldap user.
  76. '';
  77. };
  78. services = {
  79. gitea = {
  80. uid = lib.mkOption {
  81. type = lib.types.str;
  82. default = "gitea";
  83. description = "UID for the Gitea LDAP service account.";
  84. };
  85. passwordFile = lib.mkOption {
  86. type = lib.types.path;
  87. description = "Path to file containing the hashed password for the Gitea service account.";
  88. };
  89. };
  90. hauk = {
  91. uid = lib.mkOption {
  92. type = lib.types.str;
  93. default = "hauk";
  94. description = "UID for the Hauk LDAP service account.";
  95. };
  96. passwordFile = lib.mkOption {
  97. type = lib.types.path;
  98. description = "Path to file containing the hashed password for the Hauk service account.";
  99. };
  100. };
  101. mail = {
  102. uid = lib.mkOption {
  103. type = lib.types.str;
  104. default = "mail";
  105. description = "UID for the mail (postfix/dovecot/roundcube) LDAP service account.";
  106. };
  107. passwordFile = lib.mkOption {
  108. type = lib.types.path;
  109. description = "Path to file containing the hashed password for the mail service account.";
  110. };
  111. };
  112. nextcloud = {
  113. uid = lib.mkOption {
  114. type = lib.types.str;
  115. default = "nextcloud";
  116. description = "UID for the Nextcloud LDAP service account.";
  117. };
  118. passwordFile = lib.mkOption {
  119. type = lib.types.path;
  120. description = "Path to file containing the hashed password for the Nextcloud service account.";
  121. };
  122. };
  123. };
  124. dataDir = lib.mkOption {
  125. type = lib.types.str;
  126. default = "/var/lib/openldap/data";
  127. description = "Directory for the MDB database files.";
  128. };
  129. };
  130. config = lib.mkIf cfg.enable {
  131. environment.systemPackages = with pkgs; [
  132. openldap
  133. ];
  134. services.openldap = {
  135. settings = {
  136. attrs = {
  137. olcLogLevel = "conns config";
  138. };
  139. children = {
  140. # ── Schema includes ──────────────────────────────────────────
  141. "cn=schema".includes = [
  142. "${pkgs.openldap}/etc/schema/core.ldif"
  143. "${pkgs.openldap}/etc/schema/cosine.ldif"
  144. "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
  145. "${pkgs.openldap}/etc/schema/nis.ldif"
  146. "${postfixBookSchemaLdif}"
  147. ];
  148. "olcDatabase={-1}frontend".attrs = {
  149. objectClass = [ "olcDatabaseConfig" "olcFrontendConfig" ];
  150. olcDatabase = "{-1}frontend";
  151. olcAccess = [
  152. ''{0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break''
  153. ''{1}to dn.exact="" by * read''
  154. ''{2}to dn.base="cn=Subschema" by * read''
  155. ];
  156. olcSizeLimit = "500";
  157. structuralObjectClass = "olcDatabaseConfig";
  158. };
  159. "olcDatabase={0}config".attrs = {
  160. #objectClass = [ "olcDatabaseConfig" "olcConfig" ];
  161. objectClass = "olcDatabaseConfig";
  162. olcDatabase = "{0}config";
  163. olcRootDN = "cn=admin,cn=config";
  164. olcAccess = [
  165. ''{0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break''
  166. ];
  167. };
  168. # ── MDB database ─────────────────────────────────────────────
  169. "olcDatabase={1}mdb".attrs = {
  170. objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
  171. olcDatabase = "{1}mdb";
  172. olcDbDirectory = cfg.dataDir;
  173. olcSuffix = baseDn;
  174. olcRootDN = adminDn;
  175. olcRootPW = "{SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5";
  176. olcDbCheckpoint = "512 30";
  177. olcDbIndex = [
  178. "objectClass eq"
  179. "cn,uid eq"
  180. "uidNumber,gidNumber eq"
  181. "member,memberUid eq"
  182. ];
  183. olcDbMaxSize = "1073741824";
  184. # ── ACLs (from _acl_add_0.ldif and _acl_add_1.ldif) ────────
  185. olcAccess = [
  186. # ACL 0: password access (from _acl_add_0.ldif)
  187. ''{0}to dn.subtree="${baseDn}" attrs=userPassword
  188. by self write
  189. by dn.base="${adminDn}" write
  190. by dn.children="${servicesDn}" read
  191. by anonymous auth
  192. by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
  193. by * none''
  194. # ACL 1: general subtree access (from _acl_add_1.ldif)
  195. ''{1}to dn.subtree="${baseDn}"
  196. by self read
  197. by dn.base="${adminDn}" write
  198. by dn.children="${servicesDn}" read
  199. by * none''
  200. # ACL 2: allow root via SASL EXTERNAL (ldapi socket peer)
  201. ''{2}to dn.subtree="${baseDn}"
  202. by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
  203. by * none''
  204. ];
  205. };
  206. };
  207. };
  208. # ── Declarative DIT content ──────────────────────────────────────
  209. # This populates the database on every start, ensuring the entries
  210. # from all the LDIF files in openldap-data/ are present.
  211. declarativeContents."${baseDn}" = ''
  212. dn: ${baseDn}
  213. objectClass: top
  214. objectClass: dcObject
  215. objectClass: organization
  216. o: ${cfg.organization}
  217. dc: ${cfg.organization}
  218. dn: ${adminDn}
  219. objectClass: organizationalRole
  220. objectClass: simpleSecurityObject
  221. objectClass: extensibleObject
  222. cn: admin
  223. description: LDAP administrator
  224. mail: admin@${cfg.domain}
  225. userPassword: {SSHA}kZBGx5nrimaj4UJr5KglO9tpOSsPWb/5
  226. dn: ou=people,${baseDn}
  227. objectClass: organizationalUnit
  228. objectClass: top
  229. ou: people
  230. dn: ou=services,${baseDn}
  231. objectClass: organizationalUnit
  232. objectClass: top
  233. ou: services
  234. ${mkServiceEntry cfg.services.gitea.uid}
  235. ${mkServiceEntry cfg.services.hauk.uid}
  236. ${mkServiceEntry cfg.services.mail.uid}
  237. ${mkServiceEntry cfg.services.nextcloud.uid}
  238. '';
  239. };
  240. # ── Set service account passwords from files at activation time ────
  241. # declarativeContents doesn't support reading passwords from files,
  242. # so we use a systemd service to set them after slapd starts.
  243. systemd.services.openldap-set-service-passwords = {
  244. description = "Set LDAP service account passwords from files";
  245. after = [ "openldap.service" ];
  246. requires = [ "openldap.service" ];
  247. wantedBy = [ "multi-user.target" ];
  248. serviceConfig = {
  249. Type = "oneshot";
  250. RemainAfterExit = true;
  251. };
  252. path = [ pkgs.openldap ];
  253. script = let
  254. mkPasswordScript = name: svcCfg: ''
  255. echo "Setting password for ${name} service account (uid=${svcCfg.uid})..."
  256. PASSWORD=$(cat "${svcCfg.passwordFile}")
  257. ldapmodify -Y EXTERNAL -H ldapi:/// -w aaa <<LDIF
  258. dn: uid=${svcCfg.uid},${servicesDn}
  259. changetype: modify
  260. replace: userPassword
  261. userPassword: $PASSWORD
  262. LDIF
  263. '';
  264. in ''
  265. set -euo pipefail
  266. ${mkPasswordScript "gitea" cfg.services.gitea}
  267. ${mkPasswordScript "hauk" cfg.services.hauk}
  268. ${mkPasswordScript "mail" cfg.services.mail}
  269. ${mkPasswordScript "nextcloud" cfg.services.nextcloud}
  270. # TODO: admin pwd
  271. echo "All service account passwords set."
  272. '';
  273. };
  274. # Ensure the data directory exists
  275. systemd.tmpfiles.rules = [
  276. "d ${cfg.dataDir} 0700 openldap openldap -"
  277. ];
  278. # Open LDAP port in firewall
  279. networking.firewall.allowedTCPPorts = [ 389 ];
  280. };
  281. };
  282. };
  283. }