This commit is contained in:
Lennart J. Kurzweg (Nx2)
2026-04-24 19:58:21 +02:00
parent 5cc13c9b9a
commit 9ccabedbb2
12 changed files with 376 additions and 32 deletions

View File

@@ -49,9 +49,15 @@
./system-modules/hugo.nix
./system-modules/postgres.nix
./system-modules/nx2site/proxy.nix
# ./system-modules/nx2site/smtp.nix
# ./system-modules/nx2site/imap.nix
# ./system-modules/nx2site/vmail.nix
./system-modules/nx2site/maddy.nix
./system-modules/nx2site/audiobookshelf.nix
# ./system-modules/nx2site/baikal.nix
# ./system-modules/nx2site/nxcaldav.nix
./system-modules/nx2site/nxcaldav.nix
./system-modules/nx2site/copyparty.nix
./system-modules/nx2site/gitea.nix
./system-modules/nx2site/open-web-calendar.nix
@@ -61,7 +67,6 @@
./system-modules/calendar/publish.nix
./system-modules/calendar/lec.nix
# ./system-modules/calendar/lr.nix
./system-modules/calendar/dicos.nix
] else [ ]);
environment.systemPackages = import ./system-modules/base-packages.nix pkgs;

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,8 @@
443
8000
8080
(pkgs.lib.mkIf config.services.postfix.enable 587)
(pkgs.lib.mkIf config.services.dovecot2.enable 993)
];
};
}

View File

@@ -40,13 +40,15 @@ let dns-user = "cloudflare"; in
sub = "fc861353142bc05d5dbad1799178e6a1";
sub6 = "b8082b7afe9e80971fc9f9dda16ec284";
ssh = "c0f14f17f32d6595c202f041dd836eb3";
ssh6 = "f1ecb2d9d0522d4eec06437688ca76da";
ssh6 = "0067f396b3efb21e12f63e0c50643161";
dev = "80e76834acc9243696d9763759b22147";
mail = "d62a0dc01614b9f8f2b469219788fe0f";
mail6 = "f1ecb2d9d0522d4eec06437688ca76da";
};
passord-file-path = config.sops.secrets."nx2site/cloudflare/global-api-key".path;
in pkgs.writers.writePython3Bin "dyn_dns" {
libraries = with pkgs.python3Packages; [ requests ];
flakeIgnore = [ "E302" "E305" "E226" "E501" "E261" ];
flakeIgnore = [ "E302" "E305" "E226" "E501" "E261" "E241" ];
} /* python */ ''
import requests
import subprocess
@@ -55,7 +57,7 @@ from time import sleep
def get_public_ip(ipv6: bool = False) -> str:
return subprocess.run(['${pkgs.curl}/bin/curl', '-s', '-6' if ipv6 else '-4', 'https://ifconfig.me'], capture_output=True, text=True).stdout.strip()
def update_record(record_id: str, record_name: str, ip: str, type: str, proxied: bool, pw: str) -> None:
def update_record(record_id: str, record_name: str, ip: str, type: str, proxied: bool, pw: str) -> requests.Response:
sleep(5)
return requests.patch(
f'https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/{record_id}',
@@ -85,14 +87,16 @@ def main():
# Perform DNS updates
# https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-update-dns-record
print(f"${hyper.domain}: {update_record(record_id="${record_id.base}", record_name="${hyper.domain}", ip=my_ip, type="A", proxied=True, pw=pw).status_code}", end=", ")
print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub}", record_name="*.${hyper.domain}", ip=my_ip, type="A", proxied=True, pw=pw).status_code}", end=", ")
print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh}", record_name="ssh.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}", end=", ")
print(f"dev.${hyper.domain}: {update_record(record_id="${record_id.dev}", record_name="dev.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}", end=", ")
print(f"${hyper.domain}: {update_record(record_id="${record_id.base}", record_name="${hyper.domain}", ip=my_ip, type="A", proxied=True, pw=pw).status_code}")
print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub}", record_name="*.${hyper.domain}", ip=my_ip, type="A", proxied=True, pw=pw).status_code}")
print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh}", record_name="ssh.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}")
print(f"dev.${hyper.domain}: {update_record(record_id="${record_id.dev}", record_name="dev.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}")
print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.dev}", record_name="mail.${hyper.domain}", ip=my_ip, type="A", proxied=False, pw=pw).status_code}")
print(f"${hyper.domain}: {update_record(record_id="${record_id.base6}", record_name="${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}", end=", ")
print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub6}", record_name="*.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}", end=", ")
print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh6}", record_name="ssh.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}", end="")
print(f"${hyper.domain}: {update_record(record_id="${record_id.base6}", record_name="${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}")
print(f"*.${hyper.domain}: {update_record(record_id="${record_id.sub6}", record_name="*.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=True, pw=pw).status_code}")
print(f"ssh.${hyper.domain}: {update_record(record_id="${record_id.ssh6}", record_name="ssh.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}")
print(f"mail.${hyper.domain}: {update_record(record_id="${record_id.ssh6}", record_name="mail.${hyper.domain}", ip=my_ip6, type="AAAA", proxied=False, pw=pw).status_code}")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,5 @@
{ pkgs, ... }@all: with all;
{
# THIS IS NOT USED
{ pkgs, ... }@all: with all; {
sops.secrets = {
# "nx2site/namecheap.pw" = { };
# "nx2site/cloudflare/api-token-dns-edit" = { };
@@ -16,6 +16,8 @@
"${hyper.domain}"
"*.${hyper.domain}"
"ssh.${hyper.domain}"
"mail.${hyper.domain}"
"dev.${hyper.domain}"
];
proxied = true;
apiTokenFile = config.sops.secrets."nx2site/cloudflare/global-api-key-env".path;

View File

@@ -0,0 +1,68 @@
{ config, pkgs, ... }@all: with all; {
sops.secrets = {
"nx2site/dovecot" = {
owner = "dovecot2";
group = "dovecot2";
mode = "600";
};
};
services.dovecot2 = {
enable = true;
enablePAM = false; # independen from linux users
enableImap = true;
enableLmtp = true;
extraConfig = ''
# force to use full user name plus domain name
# for disambiguation
auth_username_format = %Lu
# Authentication configuration:
auth_mechanisms = plain
passdb {
driver = passwd-file
args = ${config.sops.secrets."nx2site/dovecot".path}
}
# for vitual users:
userdb {
driver = static
# the full e-mail address inside passwd-file is the username (%u)
# user@example.com
# %d for domain_name %n for user_name
args = uid=vmail gid=vmail username_format=%u home=/var/spool/mail/vmail/%d/%n
}
# for connecting with postfix
service lmtp {
unix_listener /var/spool/postfix/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/auth {
mode = 0600
user = postfix
group = postfix
}
}
'';
createMailUser = true;
mailUser = "vmail";
mailGroup = "vmail";
mailLocation = "maildir:~/Maildir";
mailboxes = { # RFC standart
All = { auto = "create"; autoexpunge = null; specialUse = "All"; };
Archive = { auto = "create"; autoexpunge = null; specialUse = "Archive"; };
Drafts = { auto = "create"; autoexpunge = null; specialUse = "Drafts"; };
Flagged = { auto = "create"; autoexpunge = null; specialUse = "Flagged"; };
Junk = { auto = "create"; autoexpunge = "60d"; specialUse = "Junk"; };
Sent = { auto = "create"; autoexpunge = null; specialUse = "Sent"; };
Trash = { auto = "create"; autoexpunge = "60d"; specialUse = "Trash"; };
};
sslServerCert = "/var/lib/acme/${hyper.domain}/fullchain.pem";
sslServerKey = "/var/lib/acme/${hyper.domain}/key.pem";
sslCACert = "/var/lib/acme/${hyper.domain}/chain.pem";
};
}

View File

@@ -0,0 +1,45 @@
{ config, pkgs, ... }@all: with all; {
sops.secrets = {
"nx2site/maddy/nxcaldav_password" = { owner = "maddy"; group = "maddy"; mode = "600"; };
"nx2site/maddy/lennart_password" = { owner = "maddy"; group = "maddy"; mode = "600"; };
"nx2site/maddy/daniel_password" = { owner = "maddy"; group = "maddy"; mode = "600"; };
};
users.users."maddy" = {
extraGroups = [ "acme" "nginx" ];
};
services.maddy = {
enable = true;
primaryDomain = hyper.domain;
user = "maddy";
group = "maddy";
hostname = "mail.${hyper.domain}";
ensureAccounts = [
"nxcaldav@${hyper.domain}"
"lennart@${hyper.domain}"
"daniel@${hyper.domain}"
];
ensureCredentials = {
"nxcaldav@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/nxcaldav_password".path;
"lennart@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/lennart_password".path;
"daniel@${hyper.domain}".passwordFile = config.sops.secrets."nx2site/maddy/daniel_password".path;
};
openFirewall = true;
tls = {
loader = "file";
certificates = [{
keyPath = "/var/lib/acme/nx2.site/key.pem";
certPath = "/var/lib/acme/nx2.site/cert.pem";
}];
};
# Enable TLS listeners. Configuring this via the module is not yet
# implemented, see https://github.com/NixOS/nixpkgs/pull/153372
config = builtins.replaceStrings [
"imap tcp://0.0.0.0:143"
"submission tcp://0.0.0.0:587"
] [
"imap tls://0.0.0.0:993 tcp://0.0.0.0:143"
"submission tls://0.0.0.0:465 tcp://0.0.0.0:587"
] options.services.maddy.config.default;
};
}

View File

@@ -1 +1,117 @@
{ pkgs, ... }@all: with all; { }
{ pkgs, ... }@all: with all; let
x = rec { version = "0.0.11";
user = "nxcaldav";
nxcsrc = pkgs.fetchFromGitea {
domain = "git.${hyper.domain}";
owner = "nx2";
repo = "NxCalDav";
rev = version;
hash = "sha256-Hk27BQCBtdRQ1aSHVEQ1EVjPrsC2jOUPDT4yuU9OCXQ=";
};
nxc = pkgs.buildGoModule {
pname = "NxCalDav";
inherit version;
src = nxcsrc;
vendorHash = "sha256-prstYDJuwS5E5uRwUkX0M+QdnIaQ0QewKe8HaoZ0Db4=";
};
nxc_helpers = pkgs.python3Packages.buildPythonApplication {
inherit version;
format = "other";
pname = "nxc_helpers";
src = nxcsrc;
propagatedBuildInputs = with pkgs.python313Packages; [ pyyaml psycopg2 ];
installPhase = ''
sed -i "15s|.*| parser.add_argument('--config', default='${cfg}', help='Path to config.yaml')|" ./export_events.py
sed -i "17s|.*| parser.add_argument('--config', default='${cfg}', help='Path to config.yaml')|" ./import_events.py
install -Dm755 "./export_events.py" "$out/bin/nxc_export"
install -Dm755 "./import_events.py" "$out/bin/nxc_import"
'';
};
cfg = (pkgs.formats.yaml { }).generate "nxcaldav-config.yaml" {
server = {
bind_address = "0.0.0.0:14243";
public_url = "http://nxc.${hyper.domain}/";
redaction_text = "[-]";
default_class = "CONFIDENTIAL";
};
database.url = "postgres://nxcaldav@localhost:5432/nxcaldav?sslmode=disable";
users = let dfu = name: {
name = name;
password_cmd = ''cat ${config.sops.secrets."nx2site/nxcaldav/${name}_password".path}'';
groups = [ "family" ];
}; in [
(dfu "lennart")
(dfu "daniel")
(dfu "diane")
(dfu "georg")
(dfu "tessa")
(dfu "shared")
];
calendars = [
{ owner = "lennart"; color = "#dddddd"; id = "preservation"; }
{ owner = "lennart"; color = "#dd2222"; id = "effort"; }
{ owner = "lennart"; color = "#2222dd"; id = "experience"; }
{ owner = "lennart"; color = "#22aa22"; id = "leisure"; }
{ id = "family";
owner = "shared";
color = "#dddd22";
access = [
{ group = "family"; mode = "read-write"; }
];
}
];
address_books = [
{ owner = "lennart"; id = "Others"; }
{ owner = "lennart"; id = "TUDa"; }
{ owner = "lennart"; id = "HSMW"; }
{ owner = "lennart"; id = "CWG"; }
{ owner = "lennart"; id = "Handball"; }
{ id = "Family & Freinds";
owner = "shared";
access = [
{ group = "family"; mode = "read-write"; }
];
}
];
aggregates = [
{ id = "lennart-aggregate";
owner = "shared";
sources = [ "preservation" "effort" "experience" "leisure" ];
access = [
{ group = "family" ; mode = "read-only"; }
{ ics = "future-only"; }
];
}
];
};
}; in with x; {
sops.secrets = let ss = { owner = user; group = user; mode = "600"; }; in {
"nx2site/nxcaldav/lennart_password" = ss;
"nx2site/nxcaldav/daniel_password" = ss;
"nx2site/nxcaldav/diane_password" = ss;
"nx2site/nxcaldav/georg_password" = ss;
"nx2site/nxcaldav/tessa_password" = ss;
"nx2site/nxcaldav/shared_password" = ss;
};
users = {
groups."${user}" = {};
users = {
"${hyper.user}".extraGroups = [ user ];
"${user}" = {
isSystemUser = true;
isNormalUser = false;
group = user;
};
};
};
environment.systemPackages = [ nxc_helpers ];
systemd.services."nxcaldav" = {
enable = true;
path = [ pkgs.bash pkgs.coreutils ];
serviceConfig = {
User = user;
Group = user;
ExecStart = ''${nxc}/bin/nxcaldav -c ${cfg}'';
};
};
}

View File

@@ -1,5 +1,4 @@
{ pkgs, ...}@all: with all;
{
{ pkgs, ...}@all: with all; {
sops.secrets = {
"nx2site/sslCertificate.pem" = { owner = config.services.nginx.user; };
"nx2site/sslCertificateKey.pem" = { owner = config.services.nginx.user; };
@@ -14,19 +13,25 @@
};
certs = {
"${hyper.domain}" = {
extraDomainNames = builtins.map (subd: "${subd}.${hyper.domain}") [ "sync" ];
extraDomainNames = builtins.map (subd: "${subd}.${hyper.domain}") [
"sync"
"mail"
];
};
};
};
users.users."nginx" = {
extraGroups = [ "nginx" "acme" "copyparty" ];
useDefaultShell = false;
linger = true;
home = "/var/nginx/";
homeMode = "770";
createHome = true;
isSystemUser = true;
isNormalUser = false;
users.users = {
"nginx" = {
extraGroups = [ "nginx" "acme" "copyparty" ];
useDefaultShell = false;
linger = true;
home = "/var/nginx/";
homeMode = "770";
createHome = true;
isSystemUser = true;
isNormalUser = false;
};
"acme".extraGroups = [ "nginx" "acme" "hugo" ];
};
systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
services.nginx = let
@@ -101,8 +106,16 @@
"~ ^(/gpg)$".return = "301 /cards/gpg";
"~ ^(/contact)$".return = "301 /cards/contact";
"~ ^(/ba)$".return = "301 /BA.pdf";
"~ ^(/schedule)$".return = "301 https://owc.${hyper.domain}/calendar.html?specification_url=https://${hyper.domain}/owc-schedule.json?";
};
};
"mail.${hyper.domain}" = {
listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::0]"; port = 80; }
];
locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenge";
};
"matrix.${hyper.domain}" = {
listen = dl;
locations."~.*".return = "502";
@@ -156,7 +169,7 @@
listen = dl;
locations = { "/" = { proxyPass = "http://127.0.0.1:5232"; }; };
});
"nxc.${hyper.domain}" = lib.mkIf config.services.radicale.enable (vh // {
"nxc.${hyper.domain}" = (vh // {
listen = dl;
locations = { "/" = { proxyPass = "http://127.0.0.1:14243"; }; };
});

View File

@@ -0,0 +1,60 @@
{ pkgs, ... }@all: with all; {
# Postfix: The SMTP server (MTA)
# Handles sending, receiving, and local delivery routing.
services.postfix = {
enable = true;
enableSubmission = true;
enableSubmissions = true;
# main.cf configuration
settings.main = {
hostname = "mail.${hyper.domain}";
domain = hyper.domain;
# Allow local services (like CalDAV) to send mail without authentication
networks = [ "127.0.0.0/8" "[::1]/128" ];
# TLS settings - using ACME certs from proxy.nix
smtpd_tls_security_level = "may";
smtpd_tls_auth_only = "yes";
smtpd_tls_cert_file = "/var/lib/acme/${hyper.domain}/fullchain.pem";
smtpd_tls_key_file = "/var/lib/acme/${hyper.domain}/key.pem";
# Use Dovecot for authentication (SASL)
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/var/spool/postfix/auth";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_security_options = "noanonymous";
# Use Dovecot for delivery (LMTP)
virtual_transport = "lmtp:unix:/var/spool/postfix/dovecot-lmtp";
virtual_mailbox_domains = [ hyper.domain ];
mailbox_transport = "lmtp:unix:/var/spool/postfix/dovecot-lmtp";
# Basic relay restrictions
smtpd_recipient_restrictions = [
"permit_mynetworks"
"permit_sasl_authenticated"
"reject_unauth_destination"
];
# master.cf configuration: Enable submission (port 587) for mail clients
# submission-options = {
# type = "inet";
# private = false;
# command = "smtpd";
# args = [
# "-o smtpd_tls_security_level=encrypt"
# "-o smtpd_sasl_auth_enable=yes"
# "-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
# "-o milter_macro_daemon_name=ORIGINATING"
# ];
# };
};
};
# create socket ala wiki
users.users."postfix" = {
createHome = true;
home = "/var/spool/postfix";
};
}

View File

@@ -0,0 +1,17 @@
{ ... }:{
users = {
groups."vmail" = {};
users = {
"vmail" = {
isSystemUser = true;
group = "vmail";
home = "/var/spool/mail/vmail";
createHome = true;
};
# Permissions to allow Postfix and Dovecot to read ACME certificates
"postfix".extraGroups = [ "acme" ];
"dovecot2".extraGroups = [ "acme" ];
};
};
}

View File

@@ -1,5 +1,5 @@
{ pkgs, ... }@all: with all; {
services.postgresql = {
services.postgresql = lib.mkIf (!hyper.isServer) {
enable = false;
ensureUsers = [{
name = "nxcaldav";