| @@ -0,0 +1 @@ | |||
| flake.lock | |||
| @@ -0,0 +1,9 @@ | |||
| keys: | |||
| - &local_vm age1tm9wt9qlf5rrr45cjphwx6lfh78ayedtkmcazxpuxuh7rpxcjgpsppsl29 | |||
| - &local_me age1hm2wg56vf4hj7usgyvawwyednem789tvnnp4pvc5t457wafm3a4swtevuf | |||
| creation_rules: | |||
| - path_regex: secrets/[^/]+\.(yaml|json|env|ini)$ | |||
| key_groups: | |||
| - age: | |||
| - *local_vm | |||
| - *local_me | |||
| @@ -0,0 +1,9 @@ | |||
| * 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 | |||
| @@ -0,0 +1,28 @@ | |||
| { | |||
| 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; | |||
| }) | |||
| ]; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,169 @@ | |||
| { | |||
| 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; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| 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 | |||
| @@ -0,0 +1,91 @@ | |||
| { | |||
| 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 | |||
| ''; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| { | |||
| 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; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,84 @@ | |||
| { | |||
| 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; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| { | |||
| 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." | |||
| # ''; | |||
| # }; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,79 @@ | |||
| { | |||
| 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; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| { | |||
| 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; | |||
| }; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,319 @@ | |||
| { | |||
| # 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 ]; | |||
| }; | |||
| }; | |||
| }; | |||
| } | |||