commit e8bc81e2018efb8358fb148b14c84791bedd08da parent fd38e2ab4f2c42b1ae6746936999e453704cdc84 Author: Silas Brack <silasbrack@gmail.com> Date: Tue, 14 Apr 2026 22:23:01 +0200 refactor: parametrize into services Diffstat:
| M | README.md | | | 27 | ++++----------------------- |
| M | hosts/gaia/configuration.nix | | | 186 | +++++++++++++++++++++++++++---------------------------------------------------- |
| D | hosts/gaia/vpn.nix | | | 41 | ----------------------------------------- |
| M | hosts/helios.nix | | | 311 | +++++++++++++------------------------------------------------------------------ |
| M | hosts/poseidon.nix | | | 186 | ++++++++++++++++++------------------------------------------------------------- |
| A | modules/forgejo.nix | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/freshrss.nix | | | 55 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/gitea-runner.nix | | | 101 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/mediawiki.nix | | | 88 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/mkv.nix | | | 150 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/nginx.nix | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/ollama.nix | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/paperless.nix | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/syncthing.nix | | | 80 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/wireguard-client.nix | | | 123 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | modules/wireguard-server.nix | | | 109 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
16 files changed, 1101 insertions(+), 590 deletions(-)
diff --git a/README.md b/README.md @@ -1,14 +1,7 @@ -# My Project +# Infrastructure Personal server infrastructure using NixOS declarative configuration. -## Infrastructure - -| Server | Provider | Location | Resources | -|--------|----------|----------|-----------| -| debian-2gb-hel1-1 | Hetzner | Helsinki | 2GB RAM, attached volume | -| nixos-s-1vcpu-512mb-10gb-ams3-01 | DigitalOcean | Amsterdam | 512MB RAM | - ## Stack - NixOS with Flakes - Nginx (reverse proxy) @@ -19,7 +12,7 @@ Personal server infrastructure using NixOS declarative configuration. ## Active Services -### Hetzner (Helsinki) +### Helios | Service | Description | Hostname | |---------|-------------|----------| | MediaWiki | Wiki platform | wiki.fnarglebeast.com | @@ -27,20 +20,8 @@ Personal server infrastructure using NixOS declarative configuration. | mkv | Distributed key-value store | VPN only (port 3000) | | WireGuard | VPN client | 10.100.0.7 | -### DigitalOcean (Amsterdam) +### Poseidon | Service | Description | Hostname | |---------|-------------|----------| | FreshRSS | RSS/Atom feed reader | freshrss.fnarglebeast.com | -| WireGuard | VPN server | 10.100.0.1 | -| dnsmasq | DNS for VPN clients | VPN only (port 53) | - -## VPN Peers -- Thinkpad (10.100.0.2) -- Pixel phone (10.100.0.3) -- Desktop (10.100.0.4) -- iPad (10.100.0.5) -- Lolo's Macbook Pro (10.100.0.6) -- Hetzner server (10.100.0.7) -- XPS 13 (10.100.0.8) -- Work Macbook Pro (10.100.0.9) - +| WireGuard + dnsmasq | VPN server | 10.100.0.1 | diff --git a/hosts/gaia/configuration.nix b/hosts/gaia/configuration.nix @@ -10,17 +10,24 @@ imports = [ ./hardware-configuration.nix ./home.nix - ./vpn.nix + ../../modules/wireguard-client.nix + ../../modules/gitea-runner.nix + ../../modules/mkv.nix + ../../modules/ollama.nix + ../../modules/paperless.nix + ../../modules/syncthing.nix ]; sops.defaultSopsFile = ../../secrets.yaml; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; - sops.secrets.gaia_wireguard_private_key = { - restartUnits = [ "wg-quick-wg0.service" ]; + # Wireguard client (VPN) + services.local.wireguard-client = { + enable = true; + address = "10.100.0.4/24"; + privateKeySecretName = "gaia_wireguard_private_key"; + routeAllTraffic = true; }; - sops.secrets.gaia_forgejo_runner_token = { }; - sops.secrets.gaia_gitea_runner_ssh_key = { }; nix.settings.experimental-features = [ "nix-command" @@ -368,77 +375,62 @@ EOF "github.com".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; }; - services.ollama = { + services.local.ollama = { enable = true; - package = pkgs-unstable.ollama-rocm; - acceleration = "rocm"; rocmOverrideGfx = "11.0.1"; - environmentVariables = { - HSA_OVERRIDE_GFX_VERSION = "11.0.1"; - # Use UUID to prefer discrete GPU over iGPU - ROCR_VISIBLE_DEVICES = "GPU-198836ed61e1b1cc"; - }; + visibleDevices = "GPU-198836ed61e1b1cc"; }; - services.paperless = { + services.local.paperless = { enable = true; - settings = { - PAPERLESS_DBHOST = "/run/postgresql"; - PAPERLESS_DATE_ORDER = "YMD"; - PAPERLESS_OCR_LANGUAGE = "deu+eng"; - PAPERLESS_OCR_USER_ARGS = { - # optimize = true; - invalidate_digital_signatures = true; - }; - }; + dateOrder = "YMD"; + ocrLanguages = "deu+eng"; }; - services.syncthing = { + services.local.syncthing = { enable = true; user = "silas"; dataDir = "/home/silas/.syncthing"; configDir = "/home/silas/.config/syncthing"; - settings = { - devices = { - "Lolo's Macbook Pro" = { - id = "AODRMP6-CEUBLIG-QFOJBXH-2S5DZ43-F2OSK7I-DDLXY3A-OSGZZEY-GKG4EQF"; - }; - "Pixel 6a" = { - id = "W3J66VB-KZYFQXI-UKL5XNO-D6W4KSA-2YAB7AZ-2BJ2BXA-T5ID4YA-NR22FQD"; - }; - "Work Macbook Pro" = { - id = "W3RN76W-PDTHRF7-QYT6LAV-ML5OR5D-IH4MTHC-IQRJT6C-R7JGYYS-ISMEKQ6"; - }; + devices = { + "Lolo's Macbook Pro" = { + id = "AODRMP6-CEUBLIG-QFOJBXH-2S5DZ43-F2OSK7I-DDLXY3A-OSGZZEY-GKG4EQF"; + }; + "Pixel 6a" = { + id = "W3J66VB-KZYFQXI-UKL5XNO-D6W4KSA-2YAB7AZ-2BJ2BXA-T5ID4YA-NR22FQD"; }; - folders = { - "Notes" = { - id = "7mruq-r5ufp"; - path = "/home/silas/notes"; - devices = [ - "Lolo's Macbook Pro" - "Pixel 6a" - "Work Macbook Pro" - ]; - }; - "Textbooks" = { - id = "z7wzr-eolsr"; - path = "/home/silas/docs/textbooks/"; - devices = [ - "Lolo's Macbook Pro" - "Pixel 6a" - "Work Macbook Pro" - ]; - }; - "Test" = { - id = "tkc8u-s54c3"; - path = "~/test"; - devices = [ "Lolo's Macbook Pro" ]; - }; - "Zotero" = { - id = "7xkgb-tuqgw"; - path = "/home/silas/Zotero/storage"; - devices = [ "Lolo's Macbook Pro" ]; - }; + "Work Macbook Pro" = { + id = "W3RN76W-PDTHRF7-QYT6LAV-ML5OR5D-IH4MTHC-IQRJT6C-R7JGYYS-ISMEKQ6"; + }; + }; + folders = { + "Notes" = { + id = "7mruq-r5ufp"; + path = "/home/silas/notes"; + devices = [ + "Lolo's Macbook Pro" + "Pixel 6a" + "Work Macbook Pro" + ]; + }; + "Textbooks" = { + id = "z7wzr-eolsr"; + path = "/home/silas/docs/textbooks/"; + devices = [ + "Lolo's Macbook Pro" + "Pixel 6a" + "Work Macbook Pro" + ]; + }; + "Test" = { + id = "tkc8u-s54c3"; + path = "~/test"; + devices = [ "Lolo's Macbook Pro" ]; + }; + "Zotero" = { + id = "7xkgb-tuqgw"; + path = "/home/silas/Zotero/storage"; + devices = [ "Lolo's Macbook Pro" ]; }; }; }; @@ -472,67 +464,17 @@ EOF ''; }; - services.nginx = { + services.local.mkv.volume = { enable = true; - virtualHosts."mkv-volume" = { - listen = [ - { addr = "10.100.0.4"; port = 8081; } - ]; - root = "/var/lib/mkv/volume"; - extraConfig = '' - autoindex on; - autoindex_format json; - dav_methods PUT DELETE; - create_full_put_path on; - client_max_body_size 256M; - ''; - }; - }; - systemd.tmpfiles.rules = [ - "d /var/lib/mkv/volume 0755 nginx nginx -" - ]; - networking.firewall.interfaces.wg0.allowedTCPPorts = [ 8081 ]; - systemd.services.nginx.serviceConfig = { - ReadWritePaths = [ "/var/lib/mkv/volume" ]; - ProtectSystem = lib.mkForce "full"; # downgrade from strict + listenAddress = "10.100.0.4"; + path = "/var/lib/mkv/volume"; }; - services.gitea-actions-runner = { - package = pkgs.forgejo-runner; - instances.default = { - enable = true; - name = "silas-desktop"; - url = "https://git.fnarglebeast.com"; - tokenFile = "/run/secrets/gaia_forgejo_runner_token"; - labels = [ - "docker:docker://alpine:3.23" - "native:host" - ]; - hostPackages = with pkgs; [ - nix - nodejs - git - bash - coreutils - openssh - curl - jq - ]; - }; - }; - systemd.services.gitea-runner-default = { - serviceConfig.LoadCredential = "ssh-key:/run/secrets/gaia_gitea_runner_ssh_key"; - preStart = lib.mkAfter '' - mkdir -p /var/lib/gitea-runner/default/.ssh - cp $CREDENTIALS_DIRECTORY/ssh-key /var/lib/gitea-runner/default/.ssh/id_ed25519 - chmod 600 /var/lib/gitea-runner/default/.ssh/id_ed25519 - echo "46.62.150.48 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEOKImx9KgmG+/ggpuS2z/hXkWu3TgT/xK9fI18tPOI1" > /var/lib/gitea-runner/default/.ssh/known_hosts - echo "188.166.127.72 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSAe7jj+yZ4iED+6u8oPX+UqEKzcZriLEXAVbjt3Cqo" >> /var/lib/gitea-runner/default/.ssh/known_hosts - echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" >> /var/lib/gitea-runner/default/.ssh/known_hosts - echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> /var/lib/gitea-runner/default/.ssh/known_hosts - echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" >> /var/lib/gitea-runner/default/.ssh/known_hosts - ''; - serviceConfig.ExecPaths = [ "/var/lib/gitea-runner" ]; + services.local.gitea-runner = { + enable = true; + name = "silas-desktop"; + tokenSecretName = "gaia_forgejo_runner_token"; + sshKeySecretName = "gaia_gitea_runner_ssh_key"; }; # This value determines the NixOS release from which the default diff --git a/hosts/gaia/vpn.nix b/hosts/gaia/vpn.nix @@ -1,41 +0,0 @@ -{ config, pkgs, ... }: - -{ - networking.firewall = { - # allowedTCPPorts = [ 28982 ]; - allowedUDPPorts = [ 10232 ]; - }; - # networking.firewall.interfaces.wg0.allowedTCPPorts = [ 28982 ]; - networking.extraHosts = '' - 10.100.0.1 poseidon - 10.100.0.2 thinkpad - 10.100.0.3 pixel - 10.100.0.4 gaia - 10.100.0.5 ipad - 10.100.0.6 macbook - 10.100.0.7 helios - 10.100.0.8 xps - ''; - # networking.interfaces.enp8s0.wakeOnLan = { - # enable = true; - # }; - networking.wg-quick.interfaces = { - wg0 = { - address = [ "10.100.0.4/24" ]; - dns = [ "10.100.0.1" ]; - listenPort = 10232; - - autostart = true; - - privateKeyFile = "/run/secrets/gaia_wireguard_private_key"; - peers = [ - { - publicKey = "2jWfr5UmACvuS+0HOfSNgEUYqoqVNnfVDoaatmgEykw="; - allowedIPs = [ "0.0.0.0/0" ]; - endpoint = "188.166.127.72:51820"; - persistentKeepalive = 25; - } - ]; - }; - }; -} diff --git a/hosts/helios.nix b/hosts/helios.nix @@ -8,8 +8,16 @@ ... }: { + imports = [ + ../modules/wireguard-client.nix + ../modules/nginx.nix + ../modules/mediawiki.nix + ../modules/forgejo.nix + ../modules/mkv.nix + ]; + # Generic - system.stateVersion = "25.05"; # Do not change lightly! + system.stateVersion = "25.05"; environment.systemPackages = with pkgs; [ vim wget @@ -24,19 +32,9 @@ "flakes" ]; services.openssh.enable = true; - networking.firewall = { - allowedTCPPorts = [ - 80 - 443 - ]; - # allowedUDPPorts = [ - # 10232 - # ]; - }; + networking.firewall.allowedTCPPorts = [ 80 443 ]; - boot.loader.grub = { - enable = false; - }; + boot.loader.grub.enable = false; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; @@ -56,164 +54,34 @@ networking.hostName = "helios"; time.timeZone = "Europe/Helsinki"; - imports = [ - # ../modules/simple-web-app.nix - - # (builtins.fetchTarball { - # # Pick a release version you are interested in and set its hash, e.g. - # url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.05/nixos-mailserver-nixos-25.05.tar.gz"; - # # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: - # # release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack - # sha256 = "0la8v8d9vzhwrnxmmyz3xnb6vm76kihccjyidhfg6qfi3143fiwq"; - # }) - ]; - sops.defaultSopsFile = ../secrets.yaml; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; - sops.secrets.hetzner_wireguard_key = { - restartUnits = [ "wg-quick-wg0.service" ]; - }; - sops.secrets.mediawiki_admin_password = { - owner = "mediawiki"; - mode = "0400"; - }; - - networking.wg-quick.interfaces = { - wg0 = { - address = [ "10.100.0.7/24" ]; - dns = [ "10.100.0.1" ]; - listenPort = 10232; - privateKeyFile = "/run/secrets/hetzner_wireguard_key"; - peers = [ - { - publicKey = "2jWfr5UmACvuS+0HOfSNgEUYqoqVNnfVDoaatmgEykw="; - allowedIPs = [ "10.100.0.0/24" ]; - endpoint = "188.166.127.72:51820"; - persistentKeepalive = 25; - } - ]; - }; - }; - - users.users = { - root.openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPGZOv4PvL/NvBiDFnCh8V6xm03k/C3b8O82z1+tmboI gitea-runner" - ]; - silas = { - isNormalUser = true; - description = "Silas Brack"; - home = "/home/silas"; - extraGroups = [ - "networkmanager" - "wheel" - ]; # "keys" - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ0gNM0od3EE/LL27kYQ/s5MOU7LBNc0pWRWROeINij silas@Lorenzas-MacBook-Pro.local" # Lolo's Macbook Pro - ]; - }; - }; - security.acme = { - acceptTerms = true; - defaults.email = "silasbrack@gmail.com"; + # VPN + services.local.wireguard-client = { + enable = true; + address = "10.100.0.7/24"; + privateKeySecretName = "hetzner_wireguard_key"; }; - services.nginx.enable = true; - services.nginx.recommendedGzipSettings = true; - services.nginx.recommendedBrotliSettings = true; # Better compression than gzip for modern browsers - services.nginx.recommendedOptimisation = true; # sendfile, tcp_nopush, tcp_nodelay, keepalive - services.nginx.recommendedTlsSettings = true; # Strong TLS defaults - services.nginx.recommendedProxySettings = true; # Good defaults for proxied services (Forgejo) - - # Rate limiting zone (10 requests/second per IP, 10MB zone) - services.nginx.commonHttpConfig = '' - limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=10r/s; - ''; - services.nginx.appendHttpConfig = '' - limit_req_status 429; - ''; - # Catch-all for unknown hostnames - services.nginx.virtualHosts."_" = { - default = true; - rejectSSL = true; - locations."/" = { - return = "444"; - }; + # Nginx + services.local.nginx-hardened = { + enable = true; + acmeEmail = "silasbrack@gmail.com"; }; # MediaWiki - services.mediawiki = { + services.local.mediawiki = { enable = true; name = "Silas' Wiki"; - webserver = "nginx"; - passwordFile = config.sops.secrets.mediawiki_admin_password.path; - database = { - type = "postgres"; - createLocally = true; - }; - nginx.hostName = "wiki.fnarglebeast.com"; - uploadsDir = "/var/lib/mediawiki/uploads"; - extensions = { - ParserFunctions = null; - }; - extraConfig = '' - $wgEnableUploads = true; - $wgUseImageMagick = true; - - # Security: disable reading by anonymous users - $wgGroupPermissions['*']['read'] = false; - - # Security: disable anonymous editing (require login) - $wgGroupPermissions['*']['edit'] = false; - $wgGroupPermissions['*']['createpage'] = false; - $wgGroupPermissions['*']['createtalk'] = false; - - # Security: disable account creation by anonymous users - $wgGroupPermissions['*']['createaccount'] = false; - - # Security: restrict upload file types - $wgFileExtensions = ['png', 'gif', 'jpg', 'jpeg', 'webp', 'pdf']; - ''; - }; - services.nginx.virtualHosts."wiki.fnarglebeast.com" = { - enableACME = true; - forceSSL = true; - extraConfig = '' - limit_req zone=ratelimit burst=20 nodelay; - ''; + domain = "wiki.fnarglebeast.com"; + passwordSecretName = "mediawiki_admin_password"; }; # Forgejo - services.forgejo = { + services.local.forgejo = { enable = true; - database.type = "postgres"; - lfs.enable = true; - settings = { - server = { - DOMAIN = "git.fnarglebeast.com"; - ROOT_URL = "https://git.fnarglebeast.com/"; - HTTP_PORT = 3001; - }; - service = { - DISABLE_REGISTRATION = true; - }; - session = { - COOKIE_SECURE = true; - }; - }; - }; - services.nginx.virtualHosts."git.fnarglebeast.com" = { - enableACME = true; - forceSSL = true; - extraConfig = '' - limit_req zone=ratelimit burst=20 nodelay; - ''; - locations."/" = { - proxyPass = "http://127.0.0.1:3001"; - proxyWebsockets = true; - }; + domain = "git.fnarglebeast.com"; }; # Personal website @@ -230,9 +98,6 @@ enableACME = true; forceSSL = true; root = staticSite; - extraConfig = '' - limit_req zone=ratelimit burst=20 nodelay; - ''; locations."/" = { index = "index.html"; extraConfig = '' @@ -240,114 +105,40 @@ ''; }; }; - # services.simple-web-app = { - # enable = true; - # package = simple-web-app.packages."x86_64-linux".default; - # port = 8032; - # }; - - # services.karakeep = { - # enable = true; - # extraEnvironment = { - # PORT = "8360"; - # DISABLE_SIGNUPS = "true"; - # }; - # }; - # services.nginx.virtualHosts."karakeep.fnarglebeast.com" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:8360"; - # }; - # }; - - # services.nginx.virtualHosts."mail.silasbrack.com" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = ""; - # }; - # }; - # mailserver = { - # enable = true; - # fqdn = "mail.silasbrack.com"; - # domains = [ "silasbrack.com" ]; - # - # # Use Let's Encrypt certificates. Note that this needs to set up a stripped - # # down nginx and opens port 80. - # certificateScheme = "acme-nginx"; - # - # # A list of all login accounts. To create the password hashes, use - # # nix-shell -p mkpasswd --run 'mkpasswd -s' - # loginAccounts = { - # "test@silasbrack.com" = { - # hashedPasswordFile = "/home/silas/.test.txt"; - # # aliases = [ "postmaster@example.com" ]; - # }; - # }; - # }; - - # Create mkv volume directory - systemd.tmpfiles.rules = [ - "d /mnt/volume-hel1-1/mkv 0755 nginx nginx -" - ]; - # mkv key-value store - systemd.services.mkv = { - description = "mkv distributed key-value store"; - wantedBy = [ "multi-user.target" ]; - after = [ - "network-online.target" - "wg-quick-wg0.service" - "nginx.service" + # MKV distributed key-value store + services.local.mkv = { + enable = true; + package = mkv.packages.${pkgs.stdenv.hostPlatform.system}.default; + volumes = [ + "http://10.100.0.7:8081" + "http://10.100.0.4:8081" ]; - wants = [ "network-online.target" ]; - requires = [ "wg-quick-wg0.service" ]; - environment = { - MKV_DB = "/var/lib/mkv/index.db"; - MKV_VOLUMES = "http://10.100.0.7:8081,http://10.100.0.4:8081"; - MKV_REPLICAS = "2"; - MKV_PORT = "3000"; - }; - serviceConfig = { - Type = "simple"; - DynamicUser = true; - StateDirectory = "mkv"; - ExecStart = "${mkv.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/mkv serve"; - Restart = "on-failure"; - RestartSec = 5; + volume = { + enable = true; + listenAddress = "10.100.0.7"; + path = "/mnt/volume-hel1-1/mkv"; }; }; - # nginx WebDAV volume server for mkv (VPN only) - services.nginx.virtualHosts."mkv-volume" = { - listen = [ - { - addr = "10.100.0.7"; - port = 8081; - } + # Users + users.users = { + root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPGZOv4PvL/NvBiDFnCh8V6xm03k/C3b8O82z1+tmboI gitea-runner" ]; - root = "/mnt/volume-hel1-1/mkv"; - extraConfig = '' - autoindex on; - autoindex_format json; - dav_methods PUT DELETE; - create_full_put_path on; - client_max_body_size 256M; - ''; - }; - - systemd.services.nginx.serviceConfig = { - ReadWritePaths = [ "/mnt/volume-hel1-1/mkv" ]; - ProtectSystem = lib.mkForce "full"; # downgrade from strict + silas = { + isNormalUser = true; + description = "Silas Brack"; + home = "/home/silas"; + extraGroups = [ "networkmanager" "wheel" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ0gNM0od3EE/LL27kYQ/s5MOU7LBNc0pWRWROeINij silas@Lorenzas-MacBook-Pro.local" + ]; + }; }; - # Allow mkv and volume ports on VPN interface only - networking.firewall.interfaces.wg0.allowedTCPPorts = [ - 3000 - 8081 - ]; - # Gitea runner SSH known hosts setup systemd.services.gitea-runner-known-hosts = { description = "Populate Gitea runner SSH known_hosts"; diff --git a/hosts/poseidon.nix b/hosts/poseidon.nix @@ -4,20 +4,22 @@ ... }: { - # Generic - system.stateVersion = "24.05"; # Do not change lightly! - environment.systemPackages = with pkgs; [ - vim + imports = [ + (modulesPath + "/virtualisation/digital-ocean-image.nix") + ../modules/wireguard-server.nix + ../modules/freshrss.nix ]; + + # Generic + system.stateVersion = "24.05"; + environment.systemPackages = with pkgs; [ vim ]; nix.settings.experimental-features = [ "nix-command" "flakes" ]; services.openssh.enable = true; - boot.loader.grub = { - enable = false; - }; + boot.loader.grub.enable = false; fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; @@ -33,164 +35,60 @@ networking.hostName = "poseidon"; time.timeZone = "Europe/Amsterdam"; - imports = [ - (modulesPath + "/virtualisation/digital-ocean-image.nix") - ]; - sops.defaultSopsFile = ../secrets.yaml; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; - sops.secrets.server_wireguard_key = { - restartUnits = [ "wg-quick-wg0.service" ]; - }; + # Firewall + networking.firewall.allowedTCPPorts = [ 80 443 ]; - # VPN - networking.nat = { + # VPN Server + services.local.wireguard-server = { enable = true; - externalInterface = "eth0"; - internalInterfaces = [ "wg0" ]; - }; - # Port 80/443 for HTTP(S), port 53 for DNS, port 51820 for WireGuard - networking.firewall = { - allowedTCPPorts = [ 80 443 53 ]; - allowedUDPPorts = [ 53 51820 ]; + privateKeySecretName = "server_wireguard_key"; + peers = [ + { name = "Thinkpad"; publicKey = "3owbBawNk7IEI8sHmJjr+Wna7QH5WxYuRGjU+OP4i2M="; allowedIPs = [ "10.100.0.2/32" ]; } + { name = "Pixel"; publicKey = "pTuUtRphmXYKhwInaCIQY7nXRgftNzgKJxleACutrFc="; allowedIPs = [ "10.100.0.3/32" ]; } + { name = "Desktop"; publicKey = "F/UODKJ2SQaRUOb7my2LtvRaN4p/khfW8bstO+09/zo="; allowedIPs = [ "10.100.0.4/32" ]; } + { name = "iPad"; publicKey = "KwQo8pMz0JYHaEV3sbezmpD8jwgbXjaumIKeqWYaeiE="; allowedIPs = [ "10.100.0.5/32" ]; } + { name = "Lolo's Macbook Pro"; publicKey = "qmch+NjlcVOxBcOsfwk2SJIcpN58i54131OS9J2yvmM="; allowedIPs = [ "10.100.0.6/32" ]; } + { name = "Hetzner server"; publicKey = "QE4UoW2vSQMX8o1liA4MSwe4fE5sUB0+Ari5a/CqkSc="; allowedIPs = [ "10.100.0.7/32" ]; } + { name = "XPS 13"; publicKey = "ZkRCc3EpJYl0EmRBEAjf9WXCW0JQvlzeq5h7fUn5aEs="; allowedIPs = [ "10.100.0.8/32" ]; } + { name = "Work Macbook Pro"; publicKey = "j+5aLHMTUGQyyjmU55ibA+JXC0pmEvQgQIOOfdMEQUA="; allowedIPs = [ "10.100.0.9/32" ]; } + ]; }; - services.dnsmasq = { - enable = true; - settings = { - interface = "wg0"; - }; + + # ACME + security.acme = { + acceptTerms = true; + defaults.email = "silasbrack@gmail.com"; }; - networking.wg-quick.interfaces = { - wg0 = { - address = [ "10.100.0.1/24" ]; - listenPort = 51820; - privateKeyFile = "/run/secrets/server_wireguard_key"; - postUp = '' - ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT - ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -i eth0 -j ACCEPT - ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s 10.100.0.1/24 -j MASQUERADE - ''; - preDown = '' - ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT - ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -i eth0 -j ACCEPT - ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s 10.100.0.1/24 -j MASQUERADE - ''; + # Nginx + services.nginx.enable = true; - peers = [ - { - # Thinkpad - publicKey = "3owbBawNk7IEI8sHmJjr+Wna7QH5WxYuRGjU+OP4i2M="; - allowedIPs = [ "10.100.0.2/32" ]; - } - { - # Pixel - publicKey = "pTuUtRphmXYKhwInaCIQY7nXRgftNzgKJxleACutrFc="; - allowedIPs = [ "10.100.0.3/32" ]; - } - { - # Desktop - publicKey = "F/UODKJ2SQaRUOb7my2LtvRaN4p/khfW8bstO+09/zo="; - allowedIPs = [ "10.100.0.4/32" ]; - } - { - # iPad - publicKey = "KwQo8pMz0JYHaEV3sbezmpD8jwgbXjaumIKeqWYaeiE="; - allowedIPs = [ "10.100.0.5/32" ]; - } - { - # Lolo's Macbook Pro - publicKey = "qmch+NjlcVOxBcOsfwk2SJIcpN58i54131OS9J2yvmM="; - allowedIPs = [ "10.100.0.6/32" ]; - } - { - # Hetzner server - publicKey = "QE4UoW2vSQMX8o1liA4MSwe4fE5sUB0+Ari5a/CqkSc="; - allowedIPs = [ "10.100.0.7/32" ]; - } - { - # XPS 13 - publicKey = "ZkRCc3EpJYl0EmRBEAjf9WXCW0JQvlzeq5h7fUn5aEs="; - allowedIPs = [ "10.100.0.8/32" ]; - } - { - # Work Macbook pro - publicKey = "j+5aLHMTUGQyyjmU55ibA+JXC0pmEvQgQIOOfdMEQUA="; - allowedIPs = [ "10.100.0.9/32" ]; - } - ]; - }; + # FreshRSS + services.local.freshrss = { + enable = true; + domain = "freshrss.fnarglebeast.com"; + defaultUser = "silas"; + passwordSecretName = "freshrss_password"; }; + # Users users.users = { root.openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPGZOv4PvL/NvBiDFnCh8V6xm03k/C3b8O82z1+tmboI gitea-runner" ]; silas = { isNormalUser = true; description = "Silas Brack"; home = "/home/silas"; - extraGroups = [ - "networkmanager" - "wheel" - ]; # "keys" + extraGroups = [ "networkmanager" "wheel" ]; openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" # Desktop - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ0gNM0od3EE/LL27kYQ/s5MOU7LBNc0pWRWROeINij silas@Lorenzas-MacBook-Pro.local" # Lolo's Macbook Pro + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqHJMc2SCzL50SO9Flfnhn012p1dmp4Iph8lPQ2aoe/ silasbrack@gmail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ0gNM0od3EE/LL27kYQ/s5MOU7LBNc0pWRWROeINij silas@Lorenzas-MacBook-Pro.local" ]; }; }; - - security.acme = { - acceptTerms = true; - defaults.email = "silasbrack@gmail.com"; - }; - services.nginx = { - enable = true; - virtualHosts."freshrss.fnarglebeast.com" = { - forceSSL = true; - enableACME = true; - }; - # virtualHosts."livebook.fnarglebeast.com" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:20123"; - # proxyWebsockets = true; # needed if you need to use WebSocket - # extraConfig = - # # required when the target is also TLS server with multiple hosts - # "proxy_ssl_server_name on;" - # + - # # required when the server wants to use HTTP Authentication - # "proxy_pass_header Authorization;"; - # }; - # }; - }; - - # sops.secrets.livebook_password_env_file = { }; - # services.livebook = { - # enableUserService = true; - # environment = { - # LIVEBOOK_PORT = 20123; - # LIVEBOOK_IFRAME_PORT = 20124; - # }; - # environmentFile = "/run/secrets/livebook_password_env_file"; - # extraPackages = with pkgs; [ - # gcc - # gnumake - # ]; - # }; - - sops.secrets.freshrss_password = { - owner = "freshrss"; - }; - services.freshrss = { - enable = true; - defaultUser = "silas"; - passwordFile = "/run/secrets/freshrss_password"; - baseUrl = "http://localhost:8080"; - virtualHost = "freshrss.fnarglebeast.com"; - }; } diff --git a/modules/forgejo.nix b/modules/forgejo.nix @@ -0,0 +1,67 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.local.forgejo; +in +{ + options.services.local.forgejo = { + enable = lib.mkEnableOption "Forgejo git server"; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain for Forgejo"; + }; + + httpPort = lib.mkOption { + type = lib.types.port; + default = 3001; + description = "HTTP port for Forgejo"; + }; + + lfsEnable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Git LFS"; + }; + + disableRegistration = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Disable public registration"; + }; + }; + + config = lib.mkIf cfg.enable { + services.forgejo = { + enable = true; + database.type = "postgres"; + lfs.enable = cfg.lfsEnable; + settings = { + server = { + DOMAIN = cfg.domain; + ROOT_URL = "https://${cfg.domain}/"; + HTTP_PORT = cfg.httpPort; + }; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + }; + session = { + COOKIE_SECURE = true; + }; + }; + }; + + services.nginx.virtualHosts.${cfg.domain} = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.httpPort}"; + proxyWebsockets = true; + }; + }; + }; +} diff --git a/modules/freshrss.nix b/modules/freshrss.nix @@ -0,0 +1,55 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.local.freshrss; +in +{ + options.services.local.freshrss = { + enable = lib.mkEnableOption "FreshRSS feed reader"; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain for FreshRSS"; + }; + + defaultUser = lib.mkOption { + type = lib.types.str; + default = "admin"; + description = "Default admin username"; + }; + + passwordSecretName = lib.mkOption { + type = lib.types.str; + description = "Name of the sops secret containing the password"; + }; + + baseUrl = lib.mkOption { + type = lib.types.str; + default = "http://localhost:8080"; + description = "Base URL for FreshRSS"; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets.${cfg.passwordSecretName} = { + owner = "freshrss"; + }; + + services.freshrss = { + enable = true; + defaultUser = cfg.defaultUser; + passwordFile = "/run/secrets/${cfg.passwordSecretName}"; + baseUrl = cfg.baseUrl; + virtualHost = cfg.domain; + }; + + services.nginx.virtualHosts.${cfg.domain} = { + forceSSL = true; + enableACME = true; + }; + }; +} diff --git a/modules/gitea-runner.nix b/modules/gitea-runner.nix @@ -0,0 +1,101 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.local.gitea-runner; +in +{ + options.services.local.gitea-runner = { + enable = lib.mkEnableOption "Forgejo/Gitea actions runner"; + + name = lib.mkOption { + type = lib.types.str; + description = "Runner instance name"; + }; + + url = lib.mkOption { + type = lib.types.str; + default = "https://git.fnarglebeast.com"; + description = "Forgejo/Gitea server URL"; + }; + + tokenSecretName = lib.mkOption { + type = lib.types.str; + description = "Name of the sops secret containing the runner token"; + }; + + sshKeySecretName = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Name of the sops secret containing the SSH key (optional)"; + }; + + labels = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "docker:docker://alpine:3.23" + "native:host" + ]; + description = "Runner labels"; + }; + + hostPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = with pkgs; [ + nix + nodejs + git + bash + coreutils + openssh + curl + jq + ]; + description = "Packages available to the runner"; + }; + + knownHosts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "46.62.150.48 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEOKImx9KgmG+/ggpuS2z/hXkWu3TgT/xK9fI18tPOI1" + "188.166.127.72 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILSAe7jj+yZ4iED+6u8oPX+UqEKzcZriLEXAVbjt3Cqo" + "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" + "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" + "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" + ]; + description = "SSH known hosts entries"; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets.${cfg.tokenSecretName} = { }; + sops.secrets.${cfg.sshKeySecretName} = lib.mkIf (cfg.sshKeySecretName != "") { }; + + services.gitea-actions-runner = { + package = pkgs.forgejo-runner; + instances.default = { + enable = true; + name = cfg.name; + url = cfg.url; + tokenFile = "/run/secrets/${cfg.tokenSecretName}"; + labels = cfg.labels; + hostPackages = cfg.hostPackages; + }; + }; + + systemd.services.gitea-runner-default = lib.mkIf (cfg.sshKeySecretName != "") { + serviceConfig.LoadCredential = "ssh-key:/run/secrets/${cfg.sshKeySecretName}"; + preStart = lib.mkAfter '' + mkdir -p /var/lib/gitea-runner/default/.ssh + cp $CREDENTIALS_DIRECTORY/ssh-key /var/lib/gitea-runner/default/.ssh/id_ed25519 + chmod 600 /var/lib/gitea-runner/default/.ssh/id_ed25519 + ${lib.concatMapStringsSep "\n" (host: ''echo "${host}" >> /var/lib/gitea-runner/default/.ssh/known_hosts'') cfg.knownHosts} + ''; + serviceConfig.ExecPaths = [ "/var/lib/gitea-runner" ]; + }; + }; +} diff --git a/modules/mediawiki.nix b/modules/mediawiki.nix @@ -0,0 +1,88 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.local.mediawiki; +in +{ + options.services.local.mediawiki = { + enable = lib.mkEnableOption "MediaWiki"; + + name = lib.mkOption { + type = lib.types.str; + default = "My Wiki"; + description = "Wiki name"; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain for MediaWiki"; + }; + + passwordSecretName = lib.mkOption { + type = lib.types.str; + description = "Name of the sops secret containing the admin password"; + }; + + uploadsDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/mediawiki/uploads"; + description = "Directory for uploads"; + }; + + privateWiki = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Restrict reading/editing to logged-in users"; + }; + + allowedFileExtensions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "png" "gif" "jpg" "jpeg" "webp" "pdf" ]; + description = "Allowed upload file extensions"; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets.${cfg.passwordSecretName} = { + owner = "mediawiki"; + mode = "0400"; + }; + + services.mediawiki = { + enable = true; + name = cfg.name; + webserver = "nginx"; + passwordFile = config.sops.secrets.${cfg.passwordSecretName}.path; + database = { + type = "postgres"; + createLocally = true; + }; + nginx.hostName = cfg.domain; + uploadsDir = cfg.uploadsDir; + extensions = { + ParserFunctions = null; + }; + extraConfig = '' + $wgEnableUploads = true; + $wgUseImageMagick = true; + '' + lib.optionalString cfg.privateWiki '' + $wgGroupPermissions['*']['read'] = false; + $wgGroupPermissions['*']['edit'] = false; + $wgGroupPermissions['*']['createpage'] = false; + $wgGroupPermissions['*']['createtalk'] = false; + $wgGroupPermissions['*']['createaccount'] = false; + '' + '' + $wgFileExtensions = [${lib.concatMapStringsSep ", " (ext: "'${ext}'") cfg.allowedFileExtensions}]; + ''; + }; + + services.nginx.virtualHosts.${cfg.domain} = { + enableACME = true; + forceSSL = true; + }; + }; +} diff --git a/modules/mkv.nix b/modules/mkv.nix @@ -0,0 +1,150 @@ +{ + config, + pkgs, + lib, + mkv, + ... +}: + +let + cfg = config.services.local.mkv; +in +{ + options.services.local.mkv = { + enable = lib.mkEnableOption "mkv distributed key-value store"; + + package = lib.mkOption { + type = lib.types.package; + default = mkv.packages.${pkgs.stdenv.hostPlatform.system}.default; + description = "mkv package to use"; + }; + + dbPath = lib.mkOption { + type = lib.types.str; + default = "/var/lib/mkv/index.db"; + description = "Path to the index database"; + }; + + volumes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "List of volume server URLs"; + }; + + replicas = lib.mkOption { + type = lib.types.int; + default = 2; + description = "Number of replicas"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "Port for the mkv service"; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open firewall port on wg0"; + }; + + # Volume server options + volume = { + enable = lib.mkEnableOption "MKV volume WebDAV server"; + + listenAddress = lib.mkOption { + type = lib.types.str; + description = "IP address to listen on (typically VPN address)"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8081; + description = "Port to listen on"; + }; + + path = lib.mkOption { + type = lib.types.path; + default = "/var/lib/mkv/volume"; + description = "Path to the volume directory"; + }; + + maxBodySize = lib.mkOption { + type = lib.types.str; + default = "256M"; + description = "Maximum upload size"; + }; + + firewallInterface = lib.mkOption { + type = lib.types.str; + default = "wg0"; + description = "Interface to allow traffic on"; + }; + }; + }; + + config = lib.mkMerge [ + # MKV server + (lib.mkIf cfg.enable { + systemd.services.mkv = { + description = "mkv distributed key-value store"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network-online.target" + "wg-quick-wg0.service" + "nginx.service" + ]; + wants = [ "network-online.target" ]; + requires = [ "wg-quick-wg0.service" ]; + environment = { + MKV_DB = cfg.dbPath; + MKV_VOLUMES = lib.concatStringsSep "," cfg.volumes; + MKV_REPLICAS = toString cfg.replicas; + MKV_PORT = toString cfg.port; + }; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + StateDirectory = "mkv"; + ExecStart = "${cfg.package}/bin/mkv serve"; + Restart = "on-failure"; + RestartSec = 5; + }; + }; + + networking.firewall.interfaces.wg0.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + }) + + # Volume server (nginx WebDAV) + (lib.mkIf cfg.volume.enable { + services.nginx = { + enable = true; + virtualHosts."mkv-volume" = { + listen = [ + { addr = cfg.volume.listenAddress; port = cfg.volume.port; } + ]; + root = cfg.volume.path; + extraConfig = '' + autoindex on; + autoindex_format json; + dav_methods PUT DELETE; + create_full_put_path on; + client_max_body_size ${cfg.volume.maxBodySize}; + ''; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.volume.path} 0755 nginx nginx -" + ]; + + networking.firewall.interfaces.${cfg.volume.firewallInterface}.allowedTCPPorts = [ cfg.volume.port ]; + + systemd.services.nginx.serviceConfig = { + ReadWritePaths = [ cfg.volume.path ]; + ProtectSystem = lib.mkForce "full"; + }; + }) + ]; +} diff --git a/modules/nginx.nix b/modules/nginx.nix @@ -0,0 +1,77 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.local.nginx-hardened; +in +{ + options.services.local.nginx-hardened = { + enable = lib.mkEnableOption "Hardened nginx configuration"; + + rateLimit = lib.mkOption { + type = lib.types.str; + default = "10r/s"; + description = "Rate limit (requests per second)"; + }; + + rateLimitZoneSize = lib.mkOption { + type = lib.types.str; + default = "10m"; + description = "Size of the rate limit zone"; + }; + + rateLimitBurst = lib.mkOption { + type = lib.types.int; + default = 20; + description = "Burst size for rate limiting"; + }; + + enableCatchAll = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable catch-all vhost that rejects unknown hostnames"; + }; + + acmeEmail = lib.mkOption { + type = lib.types.str; + description = "Email for ACME/Let's Encrypt"; + }; + }; + + config = lib.mkIf cfg.enable { + security.acme = { + acceptTerms = true; + defaults.email = cfg.acmeEmail; + }; + + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedBrotliSettings = true; + recommendedOptimisation = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + + commonHttpConfig = '' + limit_req_zone $binary_remote_addr zone=ratelimit:${cfg.rateLimitZoneSize} rate=${cfg.rateLimit}; + ''; + + appendHttpConfig = '' + limit_req_status 429; + ''; + + virtualHosts = lib.mkIf cfg.enableCatchAll { + "_" = { + default = true; + rejectSSL = true; + locations."/" = { + return = "444"; + }; + }; + }; + }; + }; +} diff --git a/modules/ollama.nix b/modules/ollama.nix @@ -0,0 +1,43 @@ +{ + config, + pkgs, + pkgs-unstable, + lib, + ... +}: + +let + cfg = config.services.local.ollama; +in +{ + options.services.local.ollama = { + enable = lib.mkEnableOption "Ollama with ROCm support"; + + rocmOverrideGfx = lib.mkOption { + type = lib.types.str; + default = "11.0.1"; + description = "ROCm GFX version override"; + }; + + visibleDevices = lib.mkOption { + type = lib.types.str; + default = ""; + description = "ROCR_VISIBLE_DEVICES value (GPU UUID)"; + example = "GPU-198836ed61e1b1cc"; + }; + }; + + config = lib.mkIf cfg.enable { + services.ollama = { + enable = true; + package = pkgs-unstable.ollama-rocm; + acceleration = "rocm"; + rocmOverrideGfx = cfg.rocmOverrideGfx; + environmentVariables = { + HSA_OVERRIDE_GFX_VERSION = cfg.rocmOverrideGfx; + } // lib.optionalAttrs (cfg.visibleDevices != "") { + ROCR_VISIBLE_DEVICES = cfg.visibleDevices; + }; + }; + }; +} diff --git a/modules/paperless.nix b/modules/paperless.nix @@ -0,0 +1,47 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.local.paperless; +in +{ + options.services.local.paperless = { + enable = lib.mkEnableOption "Paperless-ngx document management"; + + dateOrder = lib.mkOption { + type = lib.types.str; + default = "YMD"; + description = "Date parsing order"; + }; + + ocrLanguages = lib.mkOption { + type = lib.types.str; + default = "deu+eng"; + description = "OCR languages (tesseract format)"; + }; + + invalidateDigitalSignatures = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to invalidate digital signatures during OCR"; + }; + }; + + config = lib.mkIf cfg.enable { + services.paperless = { + enable = true; + settings = { + PAPERLESS_DBHOST = "/run/postgresql"; + PAPERLESS_DATE_ORDER = cfg.dateOrder; + PAPERLESS_OCR_LANGUAGE = cfg.ocrLanguages; + PAPERLESS_OCR_USER_ARGS = { + invalidate_digital_signatures = cfg.invalidateDigitalSignatures; + }; + }; + }; + }; +} diff --git a/modules/syncthing.nix b/modules/syncthing.nix @@ -0,0 +1,80 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.local.syncthing; +in +{ + options.services.local.syncthing = { + enable = lib.mkEnableOption "Syncthing file synchronization"; + + user = lib.mkOption { + type = lib.types.str; + description = "User to run syncthing as"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + description = "Data directory"; + example = "/home/silas/.syncthing"; + }; + + configDir = lib.mkOption { + type = lib.types.path; + description = "Config directory"; + example = "/home/silas/.config/syncthing"; + }; + + devices = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options.id = lib.mkOption { + type = lib.types.str; + description = "Device ID"; + }; + }); + default = { }; + description = "Syncthing devices"; + }; + + folders = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + id = lib.mkOption { + type = lib.types.str; + description = "Folder ID"; + }; + path = lib.mkOption { + type = lib.types.str; + description = "Local path"; + }; + devices = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Device names to sync with"; + }; + }; + }); + default = { }; + description = "Syncthing folders"; + }; + }; + + config = lib.mkIf cfg.enable { + services.syncthing = { + enable = true; + user = cfg.user; + dataDir = cfg.dataDir; + configDir = cfg.configDir; + settings = { + devices = cfg.devices; + folders = lib.mapAttrs (name: folder: { + inherit (folder) id path devices; + }) cfg.folders; + }; + }; + }; +} diff --git a/modules/wireguard-client.nix b/modules/wireguard-client.nix @@ -0,0 +1,123 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.local.wireguard-client; + + # Shared VPN host mappings + defaultExtraHosts = '' + 10.100.0.1 poseidon + 10.100.0.2 thinkpad + 10.100.0.3 pixel + 10.100.0.4 gaia + 10.100.0.5 ipad + 10.100.0.6 macbook + 10.100.0.7 helios + 10.100.0.8 xps + ''; +in +{ + options.services.local.wireguard-client = { + enable = lib.mkEnableOption "WireGuard VPN client"; + + address = lib.mkOption { + type = lib.types.str; + description = "VPN address with CIDR (e.g., 10.100.0.4/24)"; + }; + + privateKeySecretName = lib.mkOption { + type = lib.types.str; + description = "Name of the sops secret containing the private key"; + }; + + listenPort = lib.mkOption { + type = lib.types.port; + default = 10232; + description = "Local listen port"; + }; + + autostart = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Start VPN automatically"; + }; + + dns = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "10.100.0.1" ]; + description = "DNS servers"; + }; + + serverPublicKey = lib.mkOption { + type = lib.types.str; + default = "2jWfr5UmACvuS+0HOfSNgEUYqoqVNnfVDoaatmgEykw="; + description = "VPN server public key"; + }; + + serverEndpoint = lib.mkOption { + type = lib.types.str; + default = "188.166.127.72:51820"; + description = "VPN server endpoint"; + }; + + allowedIPs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "10.100.0.0/24" ]; + description = "Allowed IPs to route through VPN"; + }; + + routeAllTraffic = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Route all traffic through VPN (sets allowedIPs to 0.0.0.0/0)"; + }; + + persistentKeepalive = lib.mkOption { + type = lib.types.int; + default = 25; + description = "Keepalive interval in seconds"; + }; + + extraHosts = lib.mkOption { + type = lib.types.lines; + default = defaultExtraHosts; + description = "Extra /etc/hosts entries for VPN hosts"; + }; + + openFirewallPort = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open UDP port in firewall"; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets.${cfg.privateKeySecretName} = { + restartUnits = [ "wg-quick-wg0.service" ]; + }; + + networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewallPort [ cfg.listenPort ]; + + networking.extraHosts = cfg.extraHosts; + + networking.wg-quick.interfaces.wg0 = { + address = [ cfg.address ]; + dns = cfg.dns; + listenPort = cfg.listenPort; + autostart = cfg.autostart; + privateKeyFile = "/run/secrets/${cfg.privateKeySecretName}"; + peers = [ + { + publicKey = cfg.serverPublicKey; + allowedIPs = if cfg.routeAllTraffic then [ "0.0.0.0/0" ] else cfg.allowedIPs; + endpoint = cfg.serverEndpoint; + persistentKeepalive = cfg.persistentKeepalive; + } + ]; + }; + }; +} diff --git a/modules/wireguard-server.nix b/modules/wireguard-server.nix @@ -0,0 +1,109 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.local.wireguard-server; +in +{ + options.services.local.wireguard-server = { + enable = lib.mkEnableOption "WireGuard VPN server"; + + address = lib.mkOption { + type = lib.types.str; + default = "10.100.0.1/24"; + description = "VPN server address with CIDR"; + }; + + privateKeySecretName = lib.mkOption { + type = lib.types.str; + description = "Name of the sops secret containing the private key"; + }; + + listenPort = lib.mkOption { + type = lib.types.port; + default = 51820; + description = "WireGuard listen port"; + }; + + externalInterface = lib.mkOption { + type = lib.types.str; + default = "eth0"; + description = "External network interface for NAT"; + }; + + peers = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Peer name (for comments)"; + }; + publicKey = lib.mkOption { + type = lib.types.str; + description = "Peer's public key"; + }; + allowedIPs = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "Allowed IPs for this peer"; + }; + }; + }); + default = []; + description = "List of VPN peers"; + }; + + enableDnsmasq = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable dnsmasq for VPN DNS"; + }; + }; + + config = lib.mkIf cfg.enable { + sops.secrets.${cfg.privateKeySecretName} = { + restartUnits = [ "wg-quick-wg0.service" ]; + }; + + networking.nat = { + enable = true; + externalInterface = cfg.externalInterface; + internalInterfaces = [ "wg0" ]; + }; + + networking.firewall.allowedUDPPorts = [ cfg.listenPort ] ++ lib.optionals cfg.enableDnsmasq [ 53 ]; + networking.firewall.allowedTCPPorts = lib.optionals cfg.enableDnsmasq [ 53 ]; + + services.dnsmasq = lib.mkIf cfg.enableDnsmasq { + enable = true; + settings = { + interface = "wg0"; + }; + }; + + networking.wg-quick.interfaces.wg0 = { + address = [ cfg.address ]; + listenPort = cfg.listenPort; + privateKeyFile = "/run/secrets/${cfg.privateKeySecretName}"; + + postUp = '' + ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o ${cfg.externalInterface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -A FORWARD -o wg0 -i ${cfg.externalInterface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -s ${cfg.address} -j MASQUERADE + ''; + preDown = '' + ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o ${cfg.externalInterface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -D FORWARD -o wg0 -i ${cfg.externalInterface} -j ACCEPT + ${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -s ${cfg.address} -j MASQUERADE + ''; + + peers = map (peer: { + publicKey = peer.publicKey; + allowedIPs = peer.allowedIPs; + }) cfg.peers; + }; + }; +}