| flake.lock |
| keys: | |||||
| - &local_vm age1tm9wt9qlf5rrr45cjphwx6lfh78ayedtkmcazxpuxuh7rpxcjgpsppsl29 | |||||
| - &local_me age1hm2wg56vf4hj7usgyvawwyednem789tvnnp4pvc5t457wafm3a4swtevuf | |||||
| creation_rules: | |||||
| - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ | |||||
| key_groups: | |||||
| - age: | |||||
| - *local_vm | |||||
| - *local_me |
| * Open Garden Cloud NixOS | |||||
| Open Garden Cloud NixOS config flake | |||||
| * Instructions | |||||
| #+BEGIN_SRC | |||||
| sudo cp etc_nixos_flake.nix /etc/nixos/flake.nix | |||||
| sudo nixos-rebuild switch --flake /etc/nixos#open-garden-cloud | |||||
| #+END_SRC |
| { | |||||
| description = "Open Garden Cloud NixOS config flake"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| #ogc.url = "github:youruser/ogc"; | |||||
| ogc.url = "/home/ogc/share/nixos-ogc"; | |||||
| ogc.inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ogc, ... }: { | |||||
| nixosConfigurations.open-garden-cloud = nixpkgs.lib.nixosSystem { | |||||
| system = "x86_64-linux"; | |||||
| modules = [ | |||||
| ./configuration.nix | |||||
| #./hardware-configuration.nix | |||||
| # Pull in all services with their defaults | |||||
| ogc.nixosModules.default | |||||
| # Machine-specific configuration + any overrides | |||||
| ({ config, pkgs, ... }: { | |||||
| # Override any service defaults if this machine is different: | |||||
| # openldap.domain = "other-domain.com"; | |||||
| # openldap.organization = "otherorg"; | |||||
| # Disable a service on this particular machine: | |||||
| # openldap.enable = false; | |||||
| }) | |||||
| ]; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "OpenGardenCloud"; | |||||
| inputs = { | |||||
| nixpkgs.url = "nixpkgs/nixos-25.11"; | |||||
| sops-nix = { | |||||
| url = "github:Mic92/sops-nix"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| openldap-server = { | |||||
| url = "./services/openldap/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| nginx-server = { | |||||
| url = "./services/nginx/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| nextcloud-server = { | |||||
| url = "./services/nextcloud/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| mail-server = { | |||||
| url = "./services/mail/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| gitea-server = { | |||||
| url = "./services/gitea/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| immich-server = { | |||||
| url = "./services/immich/"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| }; | |||||
| outputs = { | |||||
| self, nixpkgs, sops-nix, | |||||
| openldap-server, nginx-server, nextcloud-server, mail-server, gitea-server, immich-server, ... | |||||
| }: { | |||||
| # Re-export individual modules | |||||
| nixosModules = { | |||||
| openldap = openldap-server.nixosModules.openldap; | |||||
| nginx = nginx-server.nixosModules.nginx; | |||||
| nextcloud = nextcloud-server.nixosModules.nextcloud; | |||||
| mail = mail-server.nixosModules.mail; | |||||
| gitea = gitea-server.nixosModules.gitea; | |||||
| immich = immich-server.nixosModules.immich; | |||||
| }; | |||||
| # Convenience module: imports all service modules + sets default config | |||||
| nixosModules.ogc = {config, lib, ...}: | |||||
| let | |||||
| cfg = config.ogc; | |||||
| in { | |||||
| imports = [ | |||||
| openldap-server.nixosModules.openldap | |||||
| nginx-server.nixosModules.nextcloud | |||||
| nextcloud-server.nixosModules.nextcloud | |||||
| mail-server.nixosModules.mail | |||||
| gitea-server.nixosModules.gitea | |||||
| immich-server.nixosModules.immich | |||||
| sops-nix.nixosModules.sops | |||||
| ]; | |||||
| options.ogc = { | |||||
| organization = lib.mkOption {type = lib.types.str;}; | |||||
| extension = lib.mkOption {type = lib.types.str;}; | |||||
| domain = lib.mkOption {type = lib.types.str;}; | |||||
| }; | |||||
| config = { | |||||
| networking.firewall = { | |||||
| enable = true; | |||||
| allowedTCPPorts = [ 80 443 2022 ]; | |||||
| #allowedUDPPortRanges = [ | |||||
| #{ from = 4000; to = 4007; } | |||||
| #]; | |||||
| }; | |||||
| ogc = { | |||||
| organization = lib.mkDefault "opengardencloud"; | |||||
| extension = lib.mkDefault "com"; | |||||
| domain = lib.mkDefault "opengardencloud.com"; | |||||
| }; | |||||
| sops = { | |||||
| defaultSopsFile = ./secrets/ogc.yaml; | |||||
| # This will automatically import SSH keys as age keys | |||||
| age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; | |||||
| # This is using an age key that is expected to already be in the filesystem | |||||
| age.keyFile = "~/.config/sops/age/keys.txt"; | |||||
| # This will generate a new key if the key specified above does not exist | |||||
| age.generateKey = true; | |||||
| secrets."openldap/admin" = {}; | |||||
| secrets."openldap/nextcloud" = {}; | |||||
| secrets."openldap/mail" = {}; | |||||
| secrets."openldap/gitea" = {}; | |||||
| secrets."openldap/hauk" = {}; | |||||
| secrets."nextcloud/admin" = {}; | |||||
| }; | |||||
| # ── Default configuration for OpenLDAP ───────────────────── | |||||
| # All values use mkDefault so any machine flake can override them. | |||||
| openldap = { | |||||
| enable = lib.mkDefault true; | |||||
| #enable = false; | |||||
| organization = lib.mkDefault cfg.organization; | |||||
| extension = lib.mkDefault cfg.extension; | |||||
| domain = lib.mkDefault cfg.domain; | |||||
| urlList = lib.mkDefault [ "ldap:///" "ldapi:///" ]; | |||||
| adminPasswordFile = lib.mkDefault "/run/secrets/openldap/admin"; | |||||
| services = { | |||||
| nextcloud = { | |||||
| uid = lib.mkDefault "nextcloud"; | |||||
| passwordFile = lib.mkDefault "/run/secrets/openldap/nextcloud"; | |||||
| }; | |||||
| mail = { | |||||
| uid = lib.mkDefault "mail"; | |||||
| passwordFile = lib.mkDefault "/run/secrets/openldap/mail"; | |||||
| }; | |||||
| gitea = { | |||||
| uid = lib.mkDefault "gitea"; | |||||
| passwordFile = lib.mkDefault "/run/secrets/openldap/gitea"; | |||||
| }; | |||||
| hauk = { | |||||
| uid = lib.mkDefault "hauk"; | |||||
| passwordFile = lib.mkDefault "/run/secrets/openldap/hauk"; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| # Nginx | |||||
| nginx = { | |||||
| enable = lib.mkDefault true; | |||||
| }; | |||||
| # Nextcloud | |||||
| nextcloud = { | |||||
| enable = lib.mkDefault true; | |||||
| adminPasswordFile = lib.mkDefault "/run/secrets/nextcloud/admin"; | |||||
| domain = lib.mkDefault cfg.domain; | |||||
| hostName = lib.mkDefault "nextcloud"; | |||||
| port = lib.mkDefault 8080; | |||||
| }; | |||||
| mail = { | |||||
| enable = lib.mkDefault true; | |||||
| domain = lib.mkDefault cfg.domain; | |||||
| fqdn = lib.mkDefault "mail.${cfg.domain}"; | |||||
| }; | |||||
| gitea = { | |||||
| enable = lib.mkDefault true; | |||||
| hostName = lib.mkDefault "gitea"; | |||||
| sshPort = lib.mkDefault 2022; | |||||
| httpPort = lib.mkDefault 2080; | |||||
| }; | |||||
| # TODO: OpenLDAP | |||||
| immich = { | |||||
| enable = lib.mkDefault false; | |||||
| hostName = lib.mkDefault "immich"; | |||||
| port = lib.mkDefault 543; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| nixosModules.default = self.nixosModules.ogc; | |||||
| }; | |||||
| } |
| openldap: | |||||
| admin: ENC[AES256_GCM,data:+GEpZ9Z8Ig+rxxmp/1v10az2m2J0TWnNNxhPdFymxrcjY90ONCA=,iv:G+vNeHeYSvWur+YiX6qyO3MYxWbc/AwT2Xyv63K8yWs=,tag:JBx9CMXuyMy6vQcdOWKVFQ==,type:str] | |||||
| gitea: ENC[AES256_GCM,data:b/I329wc72uVT2o49jivU0zgjXgJVJX18yRyTK+O1HnNnRWynkY=,iv:ZG3d+Oqx9byM2wVtj7G8hOVmTw2gy8oERfthVoxvWxk=,tag:ifkuuLPEsf5MKqNO1cnYdQ==,type:str] | |||||
| hauk: ENC[AES256_GCM,data:C00Ias+RhWlooPbdEIFekQNZinFnRbiJylCUFVbWBnPYrQH0vZM=,iv:0PYtFLBEt8AJBD/z68P9Y0gXbygN4F/TbZ3zbFMK6hE=,tag:XcUUIQTaB8y3CBGWkIQz7g==,type:str] | |||||
| mail: ENC[AES256_GCM,data:3b5LXvT1lO1dNiwSp8AEDIiccqL5kW8sMwT4B7NsgTmOn6LBodg=,iv:4SiBIEOfU0kh6zB2OqvHC9Dr+lRMVnhaNOK29fetxnA=,tag:818j3d9wsNLGGtArNqxIDw==,type:str] | |||||
| nextcloud: ENC[AES256_GCM,data:Gn8KJbZ4hc5s7QhZDsOy1lZbvwKVguQQuNTZXM708yKYFFesujY=,iv:92usOKU/xFffUdNDwmNGfBo4TDPSzERhIZB5EkQL6UI=,tag:51HF5oa+8tk96pwIuNaf6w==,type:str] | |||||
| mariadb: | |||||
| root: ENC[AES256_GCM,data:94z0,iv:qCr0PpbFHqLhVjryVwS1lIavFNh5SvPGp4VStt+vbT4=,tag:Y4mtJ6zZwxEbWUljup+Uaw==,type:str] | |||||
| nextcloud: ENC[AES256_GCM,data:k6emclI=,iv:TKbFbPBP3txE+78UleE6/i41uHFIO+B712qXdg++sEo=,tag:KAYbdSF4KwVN7kp5GTxGOg==,type:str] | |||||
| nextcloud: | |||||
| admin: ENC[AES256_GCM,data:IXXN,iv:YSog2jCSIsgSlyLhDPY79CqcOmgdHW2bi3evanF/6JQ=,tag:HmzdmJC2GT2kWjMhz6wzlA==,type:str] | |||||
| sops: | |||||
| age: | |||||
| - recipient: age1tm9wt9qlf5rrr45cjphwx6lfh78ayedtkmcazxpuxuh7rpxcjgpsppsl29 | |||||
| enc: | | |||||
| -----BEGIN AGE ENCRYPTED FILE----- | |||||
| YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZDRKUzFWaVNFSFROYnpr | |||||
| Rm5mejhEMnRYOFhlb1A5RStUamV5NThyUUVBClloanYyS1RIUVpjQ3V1K1lBOXF6 | |||||
| elVlMTA4QTNLNW9HWWx0YXI2dGlnMW8KLS0tIGQ0aUM5U1ZWdVEyVlN1VXJwSERG | |||||
| cVVJQmpFMXY4eEhZOVFDWE9CYzBvbXcKYYjee6eu5H05E6/ws33guazKR6vuWkNc | |||||
| b83MYoILiDXxmblJRykcHj01heNBBzhZt4GneCWq7jcE7lY/v0AYhw== | |||||
| -----END AGE ENCRYPTED FILE----- | |||||
| - recipient: age1hm2wg56vf4hj7usgyvawwyednem789tvnnp4pvc5t457wafm3a4swtevuf | |||||
| enc: | | |||||
| -----BEGIN AGE ENCRYPTED FILE----- | |||||
| YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwcjRrY05qbXBBUmJDdHha | |||||
| OHd1NEZ2WThRZUdTQU5LdnRmWkVNM1lYbXlvCk5hSXNmS01oM3J6anFOdFRPTkpL | |||||
| NW03UFUzaEIzam5Zd1VtOFovc01jWVEKLS0tIGVQQjVnOUlsT1JPckcvVjVYeU5F | |||||
| KzQ4Mk9xMmovdEkrQlBzUklkc0FzN1kKi9GaGupb0Jhrw366O62FXzvt8Vl7jCcu | |||||
| ieavPYeD5AxTfnbGFqI7oXNby31owCCXeTcMiItnD4uM8wR6mupa2Q== | |||||
| -----END AGE ENCRYPTED FILE----- | |||||
| lastmodified: "2026-03-10T19:03:43Z" | |||||
| mac: ENC[AES256_GCM,data:B2FdqinfzrdkmZPN/0AUcKI5iu//bxRX2i35aNXQRBiJRasqq4L06L+SrPhRSU4SCIoHPXJgR//HUi1AwnQe6W4IhSgIJ7xUSfS+j5vAU0gJBSzRqR+4mH3u94JMVZ/LgzcXmSjU7h1niiz9FHAoo96/m6lQhfFAo+uD6qttHe8=,iv:FhDPs6gyVzqcwe8Akq4EveFbmsD1vgCE8Ny+5ASr3OM=,tag:GT/HfXUUY7VKOEGEBstujA==,type:str] | |||||
| unencrypted_suffix: _unencrypted | |||||
| version: 3.11.0 |
| { | |||||
| description = "NixOS Gitea server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ... }: { | |||||
| nixosModules.gitea = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.gitea; | |||||
| # https://github.com/majewsky/nixos-modules/blob/master/gitea.nix | |||||
| ldapOptions = let | |||||
| inherit (config.services) openldap; | |||||
| in { | |||||
| name = "ldap"; | |||||
| security-protocol = "LDAPS"; | |||||
| host = "localhost"; | |||||
| port = "389"; | |||||
| bind-dn = "uid=${openldap.services.gitea.uid},ou=services,dc=${openldap.organization},dc=${openldap.extension}"; | |||||
| bind-password = openldap.services.gitea.passwordFile; | |||||
| user-search-base = "ou=people,dc=${openldap.organization},dc=${openldap.extension}"; | |||||
| user-filter = "(&(objectclass=*)(|(uniqueIdentifier=%[1]s)(mail=%[1]s)))"; | |||||
| #admin-filter = "(isMemberOf=cn=gitea-admins,ou=groups,${ldap.suffix})"; | |||||
| username-attribute = "uniqueIdentifier"; | |||||
| firstname-attribute = "givenName"; | |||||
| surname-attribute = "sn"; | |||||
| email-attribute = "mail"; | |||||
| }; | |||||
| ldapFlags = "--attributes-in-bind --synchronize-users"; | |||||
| in | |||||
| { | |||||
| options.gitea = { | |||||
| enable = lib.mkOption {type = lib.types.bool;}; | |||||
| hostName = lib.mkOption {type = lib.types.str;}; | |||||
| sshPort = lib.mkOption {type = lib.types.ints.unsigned;}; | |||||
| httpPort = lib.mkOption {type = lib.types.ints.unsigned;}; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| services.gitea = { | |||||
| enable = true; | |||||
| database = { | |||||
| type = "sqlite3"; | |||||
| createDatabase = true; | |||||
| }; | |||||
| stateDir = "/var/lib/gitea"; | |||||
| settings = { | |||||
| server = { | |||||
| SSH_PORT = cfg.sshPort; | |||||
| HTTP_PORT = cfg.httpPort; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| # nginx virtual host | |||||
| services.nginx.virtualHosts.${cfg.hostName} = { | |||||
| enableACME = true; | |||||
| acmeRoot = null; | |||||
| addSSL = true; | |||||
| # directs traffic to the appropriate port | |||||
| locations."/" = { | |||||
| proxyPass = "http://localhost:${cfg.httpPort}"; | |||||
| proxyWebsockets = true; | |||||
| }; | |||||
| }; | |||||
| # LDAP authentication cannot be set up declaratively, so we have to do it | |||||
| # at the end of the preStart script | |||||
| # | |||||
| # WARNING: This assumes that the LDAP auth source has the internal ID 1. | |||||
| systemd.services.gitea.preStart = let | |||||
| giteaBin = "${pkgs.gitea}/bin/gitea"; | |||||
| formatOption = key: value: "--${key} ${lib.strings.escapeShellArg value}"; | |||||
| ldapOptionsStrs = lib.mapAttrsToList formatOption ldapOptions; | |||||
| ldapOptionsStr = lib.concatStringsSep " " ldapOptionsStrs; | |||||
| in lib.mkAfter '' | |||||
| if ${giteaBin} admin auth list | grep -q ${ldapOptions.name}; then | |||||
| ${giteaBin} admin auth update-ldap --id 1 ${ldapOptionsStr} ${ldapFlags} | |||||
| else | |||||
| ${giteaBin} admin auth add-ldap ${ldapOptionsStr} ${ldapFlags} | |||||
| fi | |||||
| ''; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "NixOS Immich server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ... }: { | |||||
| nixosModules.immich = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.immich; | |||||
| in | |||||
| { | |||||
| options.immich = { | |||||
| enable = lib.mkOption {type = lib.types.bool;}; | |||||
| port = lib.mkOption {type = lib.types.ints.unsigned;}; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| # https://medium.com/@piyushkumarsingh.nmims/self-hosting-your-photos-with-immich-on-nixos-its-easier-than-you-think-c3d14fcabad1 | |||||
| services.immich = { | |||||
| enable = true; | |||||
| port = cfg.port; | |||||
| host = "0.0.0.0"; # Makes it accessible on your network | |||||
| mediaLocation = "/var/lib/immich"; # Ensure this has enough space | |||||
| openFirewall = true; # Auto-opens the port | |||||
| }; | |||||
| # nginx virtual host | |||||
| services.nginx.virtualHosts.${cfg.hostName} = { | |||||
| enableACME = true; | |||||
| acmeRoot = null; | |||||
| addSSL = true; | |||||
| # directs traffic to the appropriate port | |||||
| locations."/" = { | |||||
| proxyPass = "http://localhost:${cfg.port}"; | |||||
| proxyWebsockets = true; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "NixOS Mail server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| # https://nixos-mailserver.readthedocs.io/en/latest/flakes.html | |||||
| simple-nixos-mailserver = { | |||||
| url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-25.11"; | |||||
| inputs.nixpkgs.follows = "nixpkgs"; | |||||
| }; | |||||
| }; | |||||
| outputs = { self, nixpkgs, simple-nixos-mailserver, ... }: { | |||||
| nixosModules.mail = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.mail; | |||||
| ldapOptions = let | |||||
| inherit (config.services) openldap; | |||||
| in { | |||||
| name = "ldap"; | |||||
| security-protocol = "LDAPS"; | |||||
| host = "localhost"; | |||||
| port = "389"; | |||||
| bind-dn = "uid=${openldap.services.mail.uid},ou=services,dc=${openldap.organization},dc=${openldap.extension}"; | |||||
| bind-password = openldap.services.mail.passwordFile; | |||||
| user-search-base = "ou=people,dc=${openldap.organization},dc=${openldap.extension}"; | |||||
| user-filter = "(&(objectclass=*)(|(uniqueIdentifier=%[1]s)(mail=%[1]s)))"; | |||||
| #admin-filter = "(isMemberOf=cn=mail-admins,ou=groups,${ldap.suffix})"; | |||||
| username-attribute = "uniqueIdentifier"; | |||||
| firstname-attribute = "givenName"; | |||||
| surname-attribute = "sn"; | |||||
| email-attribute = "mail"; | |||||
| }; | |||||
| in | |||||
| { | |||||
| options.mail = { | |||||
| enable = lib.mkOption {type = lib.types.bool;}; | |||||
| domain = lib.mkOption {type = lib.types.str;}; | |||||
| fqdn = lib.mkOption {type = lib.types.str;}; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| mailserver = { | |||||
| enable = true; | |||||
| stateVersion = 4; | |||||
| fqdn = cfg.fqdn; | |||||
| domains = [ cfg.domain ]; | |||||
| # Reference the existing ACME configuration created by nginx | |||||
| x509.useACMEHost = cfg.fqdn; | |||||
| # LDAP | |||||
| # https://nixos-mailserver.readthedocs.io/en/latest/ldap.html | |||||
| ldap = { | |||||
| enable = true; | |||||
| uris = [ | |||||
| "ldaps://localhost:389" | |||||
| ]; | |||||
| bind = { | |||||
| dn = ldapOptions.bind-dn; | |||||
| passwordFile = ldapOptions.bind-password; | |||||
| }; | |||||
| base = ldapOptions.user-search-base; | |||||
| scope = "one"; | |||||
| }; | |||||
| }; | |||||
| # nginx virtual host | |||||
| services.nginx.virtualHosts.${cfg.hostName} = { | |||||
| enableACME = true; | |||||
| acmeRoot = null; | |||||
| addSSL = true; | |||||
| # directs traffic to the appropriate port | |||||
| locations."/" = { | |||||
| proxyPass = "http://localhost:${cfg.port}"; | |||||
| proxyWebsockets = true; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "NixOS MariaDB server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ... }: { | |||||
| nixosModules.mariadb = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.mariadb; | |||||
| in | |||||
| { | |||||
| options.mariadb = { | |||||
| enable = lib.mkOption { | |||||
| type = lib.types.bool; | |||||
| }; | |||||
| rootPasswordFile = lib.mkOption { | |||||
| type = lib.types.path; | |||||
| description = "Path to file containing the root password."; | |||||
| }; | |||||
| nextcloudPasswordFile = lib.mkOption { | |||||
| type = lib.types.path; | |||||
| description = "Path to file containing the nextcloud user password."; | |||||
| }; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| services.mysql = { | |||||
| #enable = true; | |||||
| enable = false; | |||||
| package = pkgs.mariadb; | |||||
| #ensureDatabases = [ "nextcloud" ]; | |||||
| #ensureUsers = [ | |||||
| # { | |||||
| # name = "nextcloud"; | |||||
| # ensurePermissions = { "nextcloud.*" = "ALL PRIVILEGES"; }; | |||||
| # } | |||||
| # ]; | |||||
| }; | |||||
| # systemd script to set up users passwords | |||||
| # systemd.services.mariadb-set-nextcloud-password = { | |||||
| # description = "Set MariaDB user passwords from file"; | |||||
| # after = [ "mysql.service" ]; | |||||
| # requires = [ "mysql.service" ]; | |||||
| # wantedBy = [ "multi-user.target" ]; | |||||
| # serviceConfig = { | |||||
| # Type = "oneshot"; | |||||
| # RemainAfterExit = true; | |||||
| # }; | |||||
| # script = '' | |||||
| # set -euo pipefail | |||||
| # echo "Setting nextcloud user password..." | |||||
| # PASSWORD=$(cat "${cfg.nextcloudPasswordFile}") | |||||
| # ${pkgs.mariadb}/bin/mysql -u root -p"$(cat ${cfg.rootPasswordFile})" -e "ALTER USER 'nextcloud'@'localhost' IDENTIFIED BY '$PASSWORD';" | |||||
| # echo "Nextcloud user password set." | |||||
| # ''; | |||||
| # }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "NixOS Nextcloud server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ... }: { | |||||
| nixosModules.nextcloud = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.nextcloud; | |||||
| in | |||||
| { | |||||
| options.nextcloud = { | |||||
| enable = lib.mkOption {type = lib.types.bool;}; | |||||
| adminPasswordFile = lib.mkOption { | |||||
| type = lib.types.path; | |||||
| description = "Path to file containing the root password."; | |||||
| }; | |||||
| dbPasswordFile = lib.mkOption { | |||||
| type = lib.types.path; | |||||
| description = "Path to file containing the DB password."; | |||||
| }; | |||||
| domain = lib.mkOption {type = lib.types.str;}; | |||||
| hostName = lib.mkOption {type = lib.types.str;}; | |||||
| port = lib.mkOption {type = lib.types.ints.unsigned;}; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| services.nextcloud = { | |||||
| enable = true; | |||||
| package = pkgs.nextcloud32; | |||||
| hostName = cfg.hostName; | |||||
| database.createLocally = true; | |||||
| https = true; | |||||
| port = cfg.port; | |||||
| caching.redis = true; | |||||
| config = { | |||||
| adminuser = "admin"; | |||||
| adminpassFile = cfg.adminPasswordFile; | |||||
| dbtype = "mysql"; | |||||
| dbuser = "nextcloud"; | |||||
| #dbhost = "localhost"; | |||||
| #dbpassFile = cfg.dbPasswordFile; | |||||
| }; | |||||
| settings = { | |||||
| trusted_domains = [cfg.domain]; | |||||
| }; | |||||
| extraApps = with config.services.nextcloud.package.packages.apps; { | |||||
| inherit calendar tasks contacts news; | |||||
| }; | |||||
| extraAppsEnable = true; | |||||
| # redis caching | |||||
| extraOptions = { | |||||
| redis = { | |||||
| host = "127.0.0.1"; | |||||
| port = 31638; | |||||
| dbindex = 0; | |||||
| timeout = 1.5; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| # nginx virtual host | |||||
| services.nginx.virtualHosts.${cfg.hostName} = { | |||||
| enableACME = true; | |||||
| acmeRoot = null; | |||||
| addSSL = true; | |||||
| # directs traffic to the appropriate port | |||||
| locations."/" = { | |||||
| proxyPass = "http://localhost:${cfg.port}"; | |||||
| proxyWebsockets = true; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| description = "NixOS Nginx server"; | |||||
| inputs = { | |||||
| nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; | |||||
| }; | |||||
| outputs = { self, nixpkgs, ... }: { | |||||
| nixosModules.nginx = { config, lib, pkgs, ... }: | |||||
| let | |||||
| cfg = config.nginx; | |||||
| in | |||||
| { | |||||
| options.nginx = { | |||||
| enable = lib.mkOption {type = lib.types.bool;}; | |||||
| }; | |||||
| config = lib.mkIf cfg.enable { | |||||
| # https://letsencrypt.org/repository/#let-s-encrypt-subscriber-agreement | |||||
| security.acme.acceptTerms = true; | |||||
| services.nginx = { | |||||
| enable = true; | |||||
| recommendedGzipSettings = true; | |||||
| recommendedOptimisation = true; | |||||
| recommendedProxySettings = true; | |||||
| recommendedTlsSettings = true; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |
| { | |||||
| # 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 ]; | |||||
| }; | |||||
| }; | |||||
| }; | |||||
| } |