From 181e6f681e110ac6ae3a7c3ad5743f89ec2eb10d Mon Sep 17 00:00:00 2001 From: Luka Dekanozishvili Date: Mon, 19 Jan 2026 21:47:18 +0000 Subject: [PATCH] init commit --- configuration.nix | 46 +++ flake.lock | 44 +++ flake.nix | 45 +++ hardware-configuration.nix | 39 +++ pkgs/aliases.nix | 34 +++ pkgs/backend.nix | 3 + pkgs/common-packages.nix | 24 ++ pkgs/extra.nix | 50 ++++ pkgs/neovim.nix | 36 +++ pkgs/nginx.nix | 41 +++ pkgs/powerdns-podman.nix | 71 +++++ pkgs/powerdns.nix | 25 ++ pkgs/server-ssh.nix | 30 ++ pkgs/stalwart.nix | 272 ++++++++++++++++++ pkgs/tailscale.nix | 25 ++ pkgs/uptime-kuma.nix | 15 + pkgs/virtualisation.nix | 13 + .../virtualisation/restart-netbird-relay.nix | 18 ++ scripts/virtualisation/restart-pihole.nix | 18 ++ scripts/virtualisation/update-containers.nix | 24 ++ scripts/zfs-healthcheck/service.nix | 38 +++ scripts/zfs-healthcheck/uptime-kuma.sh | 29 ++ 22 files changed, 940 insertions(+) create mode 100644 configuration.nix create mode 100755 flake.lock create mode 100755 flake.nix create mode 100644 hardware-configuration.nix create mode 100644 pkgs/aliases.nix create mode 100644 pkgs/backend.nix create mode 100644 pkgs/common-packages.nix create mode 100644 pkgs/extra.nix create mode 100644 pkgs/neovim.nix create mode 100644 pkgs/nginx.nix create mode 100644 pkgs/powerdns-podman.nix create mode 100644 pkgs/powerdns.nix create mode 100644 pkgs/server-ssh.nix create mode 100644 pkgs/stalwart.nix create mode 100644 pkgs/tailscale.nix create mode 100644 pkgs/uptime-kuma.nix create mode 100644 pkgs/virtualisation.nix create mode 100644 scripts/virtualisation/restart-netbird-relay.nix create mode 100644 scripts/virtualisation/restart-pihole.nix create mode 100644 scripts/virtualisation/update-containers.nix create mode 100755 scripts/zfs-healthcheck/service.nix create mode 100755 scripts/zfs-healthcheck/uptime-kuma.sh diff --git a/configuration.nix b/configuration.nix new file mode 100644 index 0000000..23e6df4 --- /dev/null +++ b/configuration.nix @@ -0,0 +1,46 @@ +{ pkgs, config, modulesPath, ... }: + +{ + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + networking.hostName = "hexname-ns1"; + + users.users = { + luka = { + isNormalUser = true; + linger = true; # Keep user services running + extraGroups = [ "networkmanager" "wheel" "podman" ]; + hashedPassword = "$y$j9T$6bEHYFO.AGCC2bnKxC3xB/$6/1zmuzaSvDSHID6ZTgnrHiWRS8ayEXhNBp48ugR4z7"; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/4F45h/xkq+MIRDzhHqDm5uWM4KTpYi3Tv/DtSo28t luka@gram" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7OvW6MffYFshZyarEaWvWjEmhodn/P+NLcnqbbMpma luka@conway" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIT+vMzh2ngUeqnVJS8Zl1m1HQMBkDOqoGdoARPyJgDM u0_a380@localhost" # s + ]; + }; + root = { + isSystemUser = true; + hashedPassword = "$y$j9T$6bEHYFO.AGCC2bnKxC3xB/$6/1zmuzaSvDSHID6ZTgnrHiWRS8ayEXhNBp48ugR4z7"; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM/4F45h/xkq+MIRDzhHqDm5uWM4KTpYi3Tv/DtSo28t luka@gram" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7OvW6MffYFshZyarEaWvWjEmhodn/P+NLcnqbbMpma luka@conway" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIT+vMzh2ngUeqnVJS8Zl1m1HQMBkDOqoGdoARPyJgDM u0_a380@localhost" # s + ]; + }; + }; + + nix.settings.trusted-users = [ "luka" ]; # TODO: remove this line + + boot.loader.grub = { + enable = true; + efiSupport = true; + efiInstallAsRemovable = true; + device = "nodev"; + }; + boot.supportedFilesystems = [ "zfs" ]; + networking.hostId = "11111111"; # $ head -c 8 /etc/machine-id + + system.stateVersion = "25.11"; +} + diff --git a/flake.lock b/flake.lock new file mode 100755 index 0000000..d42ace6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1768621446, + "narHash": "sha256-6YwHV1cjv6arXdF/PQc365h1j+Qje3Pydk501Rm4Q+4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "72ac591e737060deab2b86d6952babd1f896d7c5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nixpkgs-stable": "nixpkgs-stable" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100755 index 0000000..d34eaaf --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + description = "Epic"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.11"; + }; + + outputs = { nixpkgs, nixpkgs-stable, ... } @ inputs: { + nixosConfigurations = { + hexname-ns1 = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs; }; + modules = [ + ######## Boilerplate ######## + ./configuration.nix + ./hardware-configuration.nix + + ./pkgs/extra.nix + + ######## HexName configuration ######## + #./pkgs/hexname/powerdns-podman.nix + + ######## Networking ######## + ./pkgs/server-ssh.nix + ./pkgs/nginx.nix + ./pkgs/virtualisation.nix + # ./pkgs/stalwart.nix + + ######## Sysadmin ######## + ./pkgs/neovim.nix + ./pkgs/common-packages.nix + ./pkgs/aliases.nix + + ######## etc. ######## + + ######## Scripts ######## + #./scripts/virtualisation/update-containers.nix # Runs podman pull weekly + #./scripts/virtualisation/restart-pihole.nix + ]; + }; + }; + }; +} + diff --git a/hardware-configuration.nix b/hardware-configuration.nix new file mode 100644 index 0000000..5f2b9dd --- /dev/null +++ b/hardware-configuration.nix @@ -0,0 +1,39 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "zroot/nixos"; + fsType = "zfs"; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/3979-106D"; + fsType = "vfat"; + options = [ "fmask=0022" "dmask=0022" ]; + }; + + swapDevices = + [ { device = "/dev/disk/by-uuid/e383cf2c-0d45-4e13-8bc8-7151023fb881"; } + ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.enp1s0.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/pkgs/aliases.nix b/pkgs/aliases.nix new file mode 100644 index 0000000..5c5e5eb --- /dev/null +++ b/pkgs/aliases.nix @@ -0,0 +1,34 @@ +{ config, pkgs, ... }: + +{ + programs.bash = { + shellAliases = { + ll = "ls -Ahlv --time-style=iso --group-directories-first"; + l = "ls -hgov --time-style=iso --group-directories-first"; + + gs = "git status"; + ga = "git add ."; + gd = "git diff"; + gp = "git push"; + gr = "git restore"; + + du = "du -sh"; + + ".." = "cd .."; + + n = "cd /etc/nixos"; + f = "vim /etc/nixos/flake.nix"; + c = "vim /etc/nixos/configuration.nix"; + }; + shellInit = '' + export COLORTERM=truecolor; + s() { + cd /etc/nixos; + git add . && + sudo nixos-rebuild switch --flake /etc/nixos; + cd - &> /dev/null; + } + ''; + }; +} + diff --git a/pkgs/backend.nix b/pkgs/backend.nix new file mode 100644 index 0000000..4285edd --- /dev/null +++ b/pkgs/backend.nix @@ -0,0 +1,3 @@ +{}: + + diff --git a/pkgs/common-packages.nix b/pkgs/common-packages.nix new file mode 100644 index 0000000..ea98feb --- /dev/null +++ b/pkgs/common-packages.nix @@ -0,0 +1,24 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ + vim + + tmux + + fastfetch + btop + iotop + dool + ncdu + dig + wget + unzip + nettools + pciutils + + iptables + openssl # Generate secure passwords with: $ openssl rand -base64 48 + ]; +} + diff --git a/pkgs/extra.nix b/pkgs/extra.nix new file mode 100644 index 0000000..ed3bfba --- /dev/null +++ b/pkgs/extra.nix @@ -0,0 +1,50 @@ +{ pkgs, lib, ... }: + +{ + programs.git = { + enable = true; + config = { + user.name = "Luka Dekanozishvili"; + user.email = "me@lukadeka.com"; + }; + }; + + networking.networkmanager.enable = true; + + security.sudo.wheelNeedsPassword = false; + + # Select internationalisation properties. + i18n.defaultLocale = "en_US.UTF-8"; + i18n.extraLocaleSettings = let + locale = "en_US.UTF-8"; + in { + LC_ADDRESS = locale; + LC_IDENTIFICATION = locale; + LC_MEASUREMENT = locale; + LC_MONETARY = locale; + LC_NAME = locale; + LC_NUMERIC = locale; + LC_PAPER = locale; + LC_TELEPHONE = locale; + LC_TIME = locale; + }; + + nix = { + settings.experimental-features = [ "nix-command" "flakes" ]; + optimise.automatic = true; + + package = pkgs.nixVersions.latest; + + # Remove warning each rebuild that files aren't commited to git + extraOptions = '' + warn-dirty = false + ''; + + gc = { + automatic = true; + dates = "Mon *-*-* 16:00:00"; + options = "--delete-older-than 90d"; + }; + }; +} + diff --git a/pkgs/neovim.nix b/pkgs/neovim.nix new file mode 100644 index 0000000..4cfb4ed --- /dev/null +++ b/pkgs/neovim.nix @@ -0,0 +1,36 @@ +{ config, pkgs, programs, ... }: + +{ + programs.neovim = { + enable = true; + defaultEditor = true; + + # viAlias = true; + vimAlias = true; + + configure = { + customRC = '' + set cc=80 + set list + set listchars=tab:→\ ,trail:•,precedes:«,extends:» + colorscheme habamax + ''; + packages.myVimPackage = with pkgs.vimPlugins; { + start = [ + (nvim-treesitter.withPlugins ( + plugins: with plugins; [ + nix + python + bash + c + ] + )) + telescope-nvim + vim-commentary # gcc + vim-startify + ]; + }; + }; + }; +} + diff --git a/pkgs/nginx.nix b/pkgs/nginx.nix new file mode 100644 index 0000000..32cd696 --- /dev/null +++ b/pkgs/nginx.nix @@ -0,0 +1,41 @@ +{ config, pkgs, ... }: + +{ + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedTlsSettings = true; + # recommendedProxySettings = true; + + # Only allow PFS-enabled ciphers with AES256 + sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; + + appendHttpConfig = '' + # Minimize information leaked to other domains + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + + # Disable embedding as a frame + add_header X-Frame-Options SAMEORIGIN; + + # Prevent injection of code in other mime types (XSS Attacks) + add_header X-Content-Type-Options nosniff; + + # error_log /var/log/nginx/error.log debug; + # error_log stderr; + # access_log syslog:server=unix:/dev/log combined; + ''; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "me@lukadeka.com"; + }; + + networking.firewall = { + enable = true; + allowedTCPPorts = [ 80 443 ]; + }; +} + diff --git a/pkgs/powerdns-podman.nix b/pkgs/powerdns-podman.nix new file mode 100644 index 0000000..4667f91 --- /dev/null +++ b/pkgs/powerdns-podman.nix @@ -0,0 +1,71 @@ +{ config, pkgs, lib, ... }: + +let + domain = "hexname.com"; +in +{ + virtualisation.oci-containers.containers = { + hexname-powerdns-postgres = { + hostname = "pgsql"; + image = "postgres:18-alpine"; + # ports = [ + # "127.0.0.1:5432:5432" + # ]; + volumes = [ + "/etc/localtime:/etc/localtime:ro" + "pgsql:/var/lib/postgresql/data:Z" + ]; + networks = [ "hexname-powerdns-net" ]; + environmentFiles = [ "/etc/env/hexname/postgres.env" ]; # POSTGRES_PASSWORD=... + }; + + hexname-powerdns = { + image = "pschiffe/pdns-pgsql:latest"; + hostname = "ns1.${domain}"; + ports = [ + "127.0.0.2:53:53/tcp" # TODO: remove localhost + "127.0.0.2:53:53/udp" + "127.0.0.2:8081:8081/tcp" + ]; + networks = [ "hexname-powerdns-net" ]; + + volumes = [ + "/etc/localtime:/etc/localtime:ro" + ]; + + environmentFiles = [ "/etc/env/hexname/powerdns.env" ]; + environment = { + PDNS_primary = "yes"; + PDNS_api = "yes"; + #PDNS_webserver = "yes"; + PDNS_webserver_address = "0.0.0.0"; + PDNS_webserver_port = "8081"; + PDNS_local_address = "0.0.0.0:53"; + PDNS_webserver_allow_from = "10.0.0.0/8"; + PDNS_version_string = "anonymous"; + PDNS_default_ttl = "1500"; + # PDNS_allow_axfr_ips = "172.5.0.21"; + + # PDNS_gpgsql_password=... + # PDNS_api_key=... + }; + dependsOn = [ "hexname-powerdns-postgres" ]; + }; + }; + + systemd.services.podman-network-hexname = { + description = "Podman network for HexName/PowerDNS"; + after = [ "podman.service" ]; + wantedBy = [ "multi-user.target" "podman-hexname-powerdns.target" "podman-hexname-powerdns-postgres.target" ]; + serviceConfig.Type = "oneshot"; + path = [ pkgs.podman] ; + script = '' + podman network inspect hexname-powerdns-net >/dev/null 2>&1 || \ + podman network create hexname-powerdns-net + ''; + }; + + networking.firewall.allowedTCPPorts = [ 53 ]; + networking.firewall.allowedUDPPorts = [ 53 ]; +} + diff --git a/pkgs/powerdns.nix b/pkgs/powerdns.nix new file mode 100644 index 0000000..b558fc3 --- /dev/null +++ b/pkgs/powerdns.nix @@ -0,0 +1,25 @@ +{ ... }: + +let + domain = "hexname.com"; +in +{ + services.powerdns = { + enable = true + + # To hash the api-key, use: + # $ pdnsutil hash-password + extraConfig = '' + api=true + api-key= + primary=yes + webserver-address=127.0.0.1 + webserver-port=8081 + local-address=0.0.0.0:53 + webserver-allow-from=127.0.0.1/32 + version-string=anonymous + default-ttl=1500 + ''; + }; +} + diff --git a/pkgs/server-ssh.nix b/pkgs/server-ssh.nix new file mode 100644 index 0000000..81318f2 --- /dev/null +++ b/pkgs/server-ssh.nix @@ -0,0 +1,30 @@ +{ pkgs, config, ... }: + +{ + services.openssh = { + enable = true; + ports = [ 6968 ]; + openFirewall = true; + settings = { + PasswordAuthentication = false; + AllowUsers = [ "luka" ]; + UseDns = false; # Disable checking of rDNS records to speed up login + X11Forwarding = false; + PermitRootLogin = "prohibit-password"; + }; + }; + + services.fail2ban = { + enable = false; + bantime = "24h"; # Ban IPs for one day on the first ban + # ignoreIP = [ ]; + }; + + networking.firewall = { + enable = true; + + # allowedTCPPorts = [ ]; + # allowedUDPPorts = []; + }; +} + diff --git a/pkgs/stalwart.nix b/pkgs/stalwart.nix new file mode 100644 index 0000000..e652fac --- /dev/null +++ b/pkgs/stalwart.nix @@ -0,0 +1,272 @@ +{ config, inputs, lib, pkgs, ... }: + +let + domain = "hexname.com"; + stalwartDomain = "mail.${domain}"; + roundcubeDomain = "email.${domain}"; + + dataDir = "/var/lib/stalwart-mail"; + credPath = "/run/credentials/stalwart-mail.service"; +in +{ + services.stalwart-mail = { + enable = true; + package = pkgs.stalwart-mail; + openFirewall = true; + + settings = { + server = { + hostname = stalwartDomain; + tls = { + enable = true; + implicit = true; + }; + listener = { + smtp = { + bind = [ "[::]:25" ]; + protocol = "smtp"; + }; + submission = { + bind = [ "[::]:587" ]; + protocol = "smtp"; + }; + submissions = { + bind = [ "[::]:465" ]; + protocol = "smtp"; + tls.implicit = true; + }; + imap = { + bind = [ "[::]:143" ]; + protocol = "imap"; + }; + imaps = { + bind = [ "[::]:993" ]; + protocol = "imap"; + tls.implicit = true; + }; + http = { + bind = [ "[::]:51020" ]; + protocol = "http"; + url = "https://${stalwartDomain}"; + }; + }; + }; + + directory."in-memory" = { + type = "memory"; + # Generate hashes with: + # $ openssl passwd -6 + principals = [ + { + name = "contact-us@${domain}"; + email = [ "contact-us@${domain}" "@${domain}" ]; + secret = "$6$E8AhTdIdgl2ag6/x$reYvoPByjvkPK/Uwm3/481BBBkuBKQxd3rgSgQw3PawJ4G8TOt0jlIXdOo5xuDv1DQAdn52lUAgx0U9GSVoc7/"; + class = "individual"; + } + { + name = "no-reply@${domain}"; + email = [ "no-reply@${domain}" ]; + secret = "$6$V/u1LImVZAyynuLO$l/mMaLWQ5t0jWz6XWNlHcha8nuTQbjQfES.Nj73mNS0xQjv3vu5z03fLMAt3hxAb5BwE3jgtfmh.PknBjM1M//"; + class = "individual"; + } + ]; + }; + + authentication.fallback-admin = { + user = "superdupermegaadmin"; + secret = "$6$LPDx0LFqtpAVJO2s$GPR/4Rguhmspy8OLLKI2oZxVgvWrlHRckd6WN2RZNMxkSN9YMiPJ/pfq.XD/VTKsqCu2GCnzerQOv5bivBCph."; + }; + + email.folders = let + mkFolder = name: { + inherit name; + create = true; + subscribe = true; + }; + in { + inbox = mkFolder "Inbox"; + sent = mkFolder "Sent"; + drafts = mkFolder "Drafts"; + archive = mkFolder "Archive"; + junk = mkFolder "Spam"; + trash = mkFolder "Trash"; + }; + + session.rcpt = { + catch-all = true; + script = "'reject-addresses'"; + }; + + sieve.trusted.scripts.reject-addresses.contents = '' + require ["envelope", "reject"]; + + if anyof ( + envelope :is "to" "no-reply@${domain}" + envelope :is "to" "info@${domain}", + envelope :is "to" "contact@${domain}", + envelope :is "to" "support@${domain}" + envelope :is "to" "marketing@${domain}", + envelope :is "to" "sales@${domain}" + ) { + reject "403 This address does not accept incoming mails."; + } + + redirect "contact-us@${domain}"; + ''; + + # Change the DNS records manually to these addresses to + # keep postmaster free for non-automated emails + # https://github.com/stalwartlabs/mail-server/discussions/877 + report.analysis = { + addresses = [ + "dmarc-reports@*" + "tls-reports@*" + "spf-reports@*" + ]; + forward = false; + }; + + # Stop warnings about what's managed where + config.local-keys = [ + "authentication.fallback-admin.*" + "certificate.*" + "cluster.node-id" + "directory.*" + "email.folders.*" + "lookup.default.domain" + "lookup.default.hostname" + "report.analysis.*" + "resolver.*" + "server.*" + "!server.blocked-ip.*" + "session.mta-sts.*" + "session.rcpt.catch-all" + "session.rcpt.script" + "sieve.trusted.scripts.*" + "spam-filter.resource" + "storage.blob" + "storage.data" + "storage.directory" + "storage.fts" + "storage.lookup" + "store.*" + "tracer.*" + "webadmin.*" + ]; + + # Store blobs in the file system for easier backups. + # Since the database is backed up to /tmp, it would not fit in RAM + # with all the blobs. + store.fs = { + type = "fs"; + path = "${dataDir}/blobs"; + }; + storage.blob = "fs"; + + # We have DANE and don't want a certificate for each domain + # session.mta-sts.mode = "none"; + + certificate.default = { + cert = "%{file:${credPath}/cert.pem}%"; + private-key = "%{file:${credPath}/key.pem}%"; + default = true; + }; + + lookup.default = { + inherit domain; + hostname = stalwartDomain; + }; + + tracer.stdout.level = "info"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 25 143 465 587 993 ]; + + systemd.services.stalwart-mail = { + wants = [ "acme-${stalwartDomain}.service" ]; + after = [ "acme-${stalwartDomain}.service" ]; + preStart = '' + mkdir -p ${dataDir}/db + ''; + serviceConfig = { + LogsDirectory = "stalwart-mail"; + LoadCredential = [ + "cert.pem:${config.security.acme.certs.${stalwartDomain}.directory}/cert.pem" + "key.pem:${config.security.acme.certs.${stalwartDomain}.directory}/key.pem" + ]; + }; + }; + + services.roundcube = { + enable = true; + package = pkgs.roundcube; + dicts = with pkgs.aspellDicts; [ en de ]; + hostName = roundcubeDomain; + plugins = [ + "archive" + "zipdownload" + "acl" + ]; + extraConfig = '' + $config['imap_host'] = 'ssl://${stalwartDomain}:993'; + $config['smtp_host'] = 'ssl://%h:465'; + $config['mail_domain'] = '%z'; + ''; + }; + + services.nginx.virtualHosts = let + proxy = "http://localhost:51020"; + in { + ${stalwartDomain} = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = proxy; + }; + ${roundcubeDomain}.locations."/".extraConfig = '' + add_header Cache-Control "public, max-age=604800, must-revalidate" always; + add_header Referrer-Policy "origin-when-cross-origin" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + ''; + "mta-sts.${domain}" = { + enableACME = true; + forceSSL = true; + locations."/".root = pkgs.writeTextFile { + name = "mta-sts.txt"; + text = '' + version: STSv1 + mode: enforce + mx: ${stalwartDomain} + max_age: 86400 + ''; + destination = "/.well-known/mta-sts.txt"; + }; + }; + }; + + security.acme.certs.${stalwartDomain} = { + # Keep a stable private key for TLSA records (DANE) + # https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022/14 + # extraLegoRenewFlags = [ "--reuse-key" ]; + # Restart Stalwart to apply new certificates + reloadServices = [ "stalwart-mail.service" ]; + }; + + # services.restic = { + # backupPrepareCommand = '' + # ${pkgs.coreutils}/bin/install -b -m 700 -d /tmp/stalwart-db-secondary /tmp/stalwart-db-backup + # ${lib.getExe' rocksdb.tools "ldb"} --db=${dataDir}/db --secondary_path=/tmp/stalwart-db-secondary backup --backup_dir=/tmp/stalwart-db-backup + # ''; + # backupCleanupCommand = '' + # rm -rf /tmp/stalwart-db-secondary + # rm -rf /tmp/stalwart-db-backup + # ''; + # paths = [ + # "/tmp/stalwart-db-backup" + # "${dataDir}/blobs" + # ]; + # }; +} + diff --git a/pkgs/tailscale.nix b/pkgs/tailscale.nix new file mode 100644 index 0000000..827a218 --- /dev/null +++ b/pkgs/tailscale.nix @@ -0,0 +1,25 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ tailscale ]; + + services.tailscale = { + enable = true; + openFirewall = true; + useRoutingFeatures = "both"; # Act as a client and a server (exit node) + + # extraSetFlags = [ + # "--advertise-exit-node" + # ]; + + disableUpstreamLogging = true; + disableTaildrop = true; + }; + + # Enable IP forwarding for subnet routers + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + + # TODO: this fixes MagicDNS but breaks DNS resolution on LAN (Pihole) + # services.resolved.enable = true; +} + diff --git a/pkgs/uptime-kuma.nix b/pkgs/uptime-kuma.nix new file mode 100644 index 0000000..54eb7ea --- /dev/null +++ b/pkgs/uptime-kuma.nix @@ -0,0 +1,15 @@ +{ config, pkgs, ... }: + +{ + services.uptime-kuma = { + enable = true; + settings = { + # NODE_EXTRA_CA_CERTS = "/etc/env/ssl/${domain}.pem"; + PORT = "4000"; + HOST = "127.0.0.1"; + }; + }; + + # networking.firewall.allowedTCPPorts = [ 4000 ]; +} + diff --git a/pkgs/virtualisation.nix b/pkgs/virtualisation.nix new file mode 100644 index 0000000..07dadbd --- /dev/null +++ b/pkgs/virtualisation.nix @@ -0,0 +1,13 @@ +{ ... }: + +{ + # Auto-prune old containers + virtualisation.podman = { + enable = true; + autoPrune = { + enable = true; + flags = [ "--all" ]; + }; + }; +} + diff --git a/scripts/virtualisation/restart-netbird-relay.nix b/scripts/virtualisation/restart-netbird-relay.nix new file mode 100644 index 0000000..3e6dd46 --- /dev/null +++ b/scripts/virtualisation/restart-netbird-relay.nix @@ -0,0 +1,18 @@ +{ pkgs, ... }: + +{ + systemd.timers.restart-netbird-relay = { + timerConfig = { + Unit = "update-containers.service"; + OnCalendar = "Tue 02:40"; # 10 mins after podman pull + }; + wantedBy = [ "timers.target" ]; + }; + systemd.services.restart-netbird-relay = { + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.systemd}/bin/systemctl try-restart podman-netbird-relay.service"; + }; + }; +} + diff --git a/scripts/virtualisation/restart-pihole.nix b/scripts/virtualisation/restart-pihole.nix new file mode 100644 index 0000000..060c4c5 --- /dev/null +++ b/scripts/virtualisation/restart-pihole.nix @@ -0,0 +1,18 @@ +{ pkgs, ... }: + +{ + systemd.timers.restart-pihole = { + timerConfig = { + Unit = "update-containers.service"; + OnCalendar = "Tue 02:40"; # 10 mins after podman pull + }; + wantedBy = [ "timers.target" ]; + }; + systemd.services.restart-pihole = { + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.systemd}/bin/systemctl try-restart podman-pihole.service"; + }; + }; +} + diff --git a/scripts/virtualisation/update-containers.nix b/scripts/virtualisation/update-containers.nix new file mode 100644 index 0000000..d5a2d94 --- /dev/null +++ b/scripts/virtualisation/update-containers.nix @@ -0,0 +1,24 @@ +{ pkgs, lib, ... }: + +{ + systemd.timers.update-containers = { + timerConfig = { + Unit = "update-containers.service"; + OnCalendar = "Mon 02:30"; + }; + wantedBy = [ "timers.target" ]; + }; + systemd.services.update-containers = { + serviceConfig = { + Type = "oneshot"; + ExecStart = lib.getExe (pkgs.writeShellScriptBin "update-containers" '' + images=$(${pkgs.podman}/bin/podman ps -a --format="{{.Image}}" | sort -u) + + for image in $images; do + ${pkgs.podman}/bin/podman pull "$image" + done + ''); + }; + }; +} + diff --git a/scripts/zfs-healthcheck/service.nix b/scripts/zfs-healthcheck/service.nix new file mode 100755 index 0000000..d852c39 --- /dev/null +++ b/scripts/zfs-healthcheck/service.nix @@ -0,0 +1,38 @@ +{ config, pkgs, ... }: + +let + scriptPath = "${config.vars.homeDir}/nixos/scripts"; + after = [ "network.target" "NetworkManager.service" "uptime-kuma.service" ]; + environment = { + VAR_IP = config.vars.privateIp; + }; +in +{ + systemd.services = { + "zfs-uptime-kuma" = { + inherit environment after; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "root"; + }; + path = with pkgs; [ bash curl zfs jq ]; + script = '' + bash ${scriptPath}/zfs-healthcheck/uptime-kuma.sh + ''; + }; + }; + + systemd.timers = { + "zfs-uptime-kuma" = { + wantedBy = [ "timers.target" ]; + partOf = [ "zfs-uptime-kuma.service" ]; + timerConfig = { + Persistent = true; # Execute immediately if missed + OnUnitActiveSec = "7m"; # Run every x minutes + Unit = "zfs-uptime-kuma.service"; + }; + }; + }; +} + diff --git a/scripts/zfs-healthcheck/uptime-kuma.sh b/scripts/zfs-healthcheck/uptime-kuma.sh new file mode 100755 index 0000000..cf3ddb2 --- /dev/null +++ b/scripts/zfs-healthcheck/uptime-kuma.sh @@ -0,0 +1,29 @@ +#! /bin/sh +set -euo pipefail +set -x + +push_token=$(< /etc/env/zfs/push-token); + +start_time=$(date -u +%s%3N) +health=$(zpool list -H -o health) + +status="up" + +echo "$health" | while IFS= read -r line; do + if [ "$line" != "ONLINE" ]; then + status="down" + break + fi +done + +end_time=$(date -u +%s%3N) +duration=$(("$end_time" - "$start_time")) + +msg=$(printf '%s' "$health" | tr '\n' ',' | tr -d "'" | jq -sRr @uri) +url="http://$VAR_IP:4000/api/push/$push_token?ping=$duration&status=$status&msg='$msg'" + +output=$(curl --fail --no-progress-meter --retry 1 $url 2>&1) +if [ $? -ne 0 ]; then + echo "Ping failed: $output" >&2 +fi +