瀏覽代碼

First commit

main
Bingen Eguzkitza 1 月之前
當前提交
a123bb28e8
共有 13 個文件被更改,包括 965 次插入0 次删除
  1. +1
    -0
      .gitignore
  2. +9
    -0
      .sops.yaml
  3. +9
    -0
      README.org
  4. +28
    -0
      etc_nixos_flake.nix
  5. +169
    -0
      flake.nix
  6. +35
    -0
      secrets/ogc.yaml
  7. +91
    -0
      services/gitea/flake.nix
  8. +44
    -0
      services/immich/flake.nix
  9. +84
    -0
      services/mail/flake.nix
  10. +64
    -0
      services/mariadb/flake.nix
  11. +79
    -0
      services/nextcloud/flake.nix
  12. +33
    -0
      services/nginx/flake.nix
  13. +319
    -0
      services/openldap/flake.nix

+ 1
- 0
.gitignore 查看文件

@@ -0,0 +1 @@
flake.lock

+ 9
- 0
.sops.yaml 查看文件

@@ -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

+ 9
- 0
README.org 查看文件

@@ -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

+ 28
- 0
etc_nixos_flake.nix 查看文件

@@ -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;
})
];
};
};
}

+ 169
- 0
flake.nix 查看文件

@@ -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;
};
}

+ 35
- 0
secrets/ogc.yaml 查看文件

@@ -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

+ 91
- 0
services/gitea/flake.nix 查看文件

@@ -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
'';
};
};
};
}

+ 44
- 0
services/immich/flake.nix 查看文件

@@ -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;
};
};
};
};
};
}

+ 84
- 0
services/mail/flake.nix 查看文件

@@ -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;
};
};
};
};
};
}

+ 64
- 0
services/mariadb/flake.nix 查看文件

@@ -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."
# '';
# };
};
};
};
}

+ 79
- 0
services/nextcloud/flake.nix 查看文件

@@ -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;
};
};
};
};
};
}

+ 33
- 0
services/nginx/flake.nix 查看文件

@@ -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;
};
};
};
};
}

+ 319
- 0
services/openldap/flake.nix 查看文件

@@ -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 ];
};
};
};
}

Loading…
取消
儲存