From d4e2805a875c2cced1b7b2509002402dc9beac57 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 14 Dec 2024 21:45:46 +0100 Subject: [PATCH] feat: nucnix --- config/support/server.nix | 55 ++++++ flake.lock | 8 +- hosts/elisabeth/default.nix | 54 +----- hosts/elisabeth/net.nix | 8 - hosts/nucnix/default.nix | 31 +++ hosts/nucnix/fs.nix | 91 +++++++++ hosts/nucnix/guests.nix | 182 ++++++++++++++++++ hosts/nucnix/net.nix | 54 ++++++ .../generated/initrd_host_ed25519_key.age | Bin 0 -> 1175 bytes hosts/nucnix/secrets/host.pub | 1 + hosts/nucnix/secrets/secrets.nix.age | Bin 0 -> 876 bytes modules-hm/hm-all.nix | 2 + modules/actual.nix | 154 --------------- secrets/secrets.nix.age | Bin 8336 -> 5233 bytes 14 files changed, 421 insertions(+), 219 deletions(-) create mode 100644 config/support/server.nix create mode 100644 hosts/nucnix/default.nix create mode 100644 hosts/nucnix/fs.nix create mode 100644 hosts/nucnix/guests.nix create mode 100644 hosts/nucnix/net.nix create mode 100644 hosts/nucnix/secrets/generated/initrd_host_ed25519_key.age create mode 100644 hosts/nucnix/secrets/host.pub create mode 100644 hosts/nucnix/secrets/secrets.nix.age delete mode 100644 modules/actual.nix diff --git a/config/support/server.nix b/config/support/server.nix new file mode 100644 index 0000000..4d3e51d --- /dev/null +++ b/config/support/server.nix @@ -0,0 +1,55 @@ +{ + environment = { + # Print the URL instead on servers + variables.BROWSER = "echo"; + # Don't install the /lib/ld-linux.so.2 and /lib64/ld-linux-x86-64.so.2 + # stubs. Server users should know what they are doing. + stub-ld.enable = false; + }; + # Given that our systems are headless, emergency mode is useless. + # We prefer the system to attempt to continue booting so + # that we can hopefully still access it remotely. + boot.initrd.systemd.suppressedUnits = [ + "emergency.service" + "emergency.target" + ]; + # Given that our systems are headless, emergency mode is useless. + # We prefer the system to attempt to continue booting so + # that we can hopefully still access it remotely. + systemd.enableEmergencyMode = false; + + documentation.nixos.enable = false; + + # No need for fonts on a server + fonts.fontconfig.enable = false; + + programs.command-not-found.enable = false; + + # freedesktop xdg files + xdg.autostart.enable = false; + xdg.icons.enable = false; + xdg.menus.enable = false; + xdg.mime.enable = false; + xdg.sounds.enable = false; + + systemd = { + + # For more detail, see: + # https://0pointer.de/blog/projects/watchdog.html + watchdog = { + # systemd will send a signal to the hardware watchdog at half + # the interval defined here, so every 7.5s. + # If the hardware watchdog does not get a signal for 15s, + # it will forcefully reboot the system. + runtimeTime = "15s"; + # Forcefully reboot if the final stage of the reboot + # hangs without progress for more than 30s. + # For more info, see: + # https://utcc.utoronto.ca/~cks/space/blog/linux/SystemdShutdownWatchdog + rebootTime = "30s"; + # Forcefully reboot when a host hangs after kexec. + # This may be the case when the firmware does not support kexec. + kexecTime = "1m"; + }; + }; +} diff --git a/flake.lock b/flake.lock index c0d0c06..f817007 100644 --- a/flake.lock +++ b/flake.lock @@ -1642,11 +1642,11 @@ "treefmt-nix": "treefmt-nix_3" }, "locked": { - "lastModified": 1733348844, - "narHash": "sha256-glufwHZDCoXjPrfvYSw8PrwQLyFVsg933gt/Gg4hlLE=", + "lastModified": 1734202825, + "narHash": "sha256-/9r2lRpVLG81uF7zxuk4LDnPZN0kk93tTclMA5KQK0E=", "ref": "refs/heads/main", - "rev": "3052ba7b255b8e3c333fcb318e79ce15c88dd2a7", - "revCount": 22, + "rev": "09fb938cb462681aaf6d7016e35a90d4995aad8c", + "revCount": 23, "type": "git", "url": "https://forge.lel.lol/patrick/nixp-meta.git" }, diff --git a/hosts/elisabeth/default.nix b/hosts/elisabeth/default.nix index 84fd7c7..457b5e6 100644 --- a/hosts/elisabeth/default.nix +++ b/hosts/elisabeth/default.nix @@ -16,6 +16,7 @@ ../../config/support/initrd-ssh.nix ../../config/support/physical.nix ../../config/support/secureboot.nix + ../../config/support/server.nix ../../config/support/zfs.nix ./net.nix @@ -28,58 +29,5 @@ }; nixpkgs.hostPlatform = "x86_64-linux"; - # Given that our systems are headless, emergency mode is useless. - # We prefer the system to attempt to continue booting so - # that we can hopefully still access it remotely. - boot.initrd.systemd.suppressedUnits = [ - "emergency.service" - "emergency.target" - ]; - environment = { - # Print the URL instead on servers - variables.BROWSER = "echo"; - # Don't install the /lib/ld-linux.so.2 and /lib64/ld-linux-x86-64.so.2 - # stubs. Server users should know what they are doing. - stub-ld.enable = false; - }; - # Given that our systems are headless, emergency mode is useless. - # We prefer the system to attempt to continue booting so - # that we can hopefully still access it remotely. - systemd.enableEmergencyMode = false; - - documentation.nixos.enable = false; - - # No need for fonts on a server - fonts.fontconfig.enable = false; - - programs.command-not-found.enable = false; - - # freedesktop xdg files - xdg.autostart.enable = false; - xdg.icons.enable = false; - xdg.menus.enable = false; - xdg.mime.enable = false; - xdg.sounds.enable = false; - - systemd = { - - # For more detail, see: - # https://0pointer.de/blog/projects/watchdog.html - watchdog = { - # systemd will send a signal to the hardware watchdog at half - # the interval defined here, so every 7.5s. - # If the hardware watchdog does not get a signal for 15s, - # it will forcefully reboot the system. - runtimeTime = "15s"; - # Forcefully reboot if the final stage of the reboot - # hangs without progress for more than 30s. - # For more info, see: - # https://utcc.utoronto.ca/~cks/space/blog/linux/SystemdShutdownWatchdog - rebootTime = "30s"; - # Forcefully reboot when a host hangs after kexec. - # This may be the case when the firmware does not support kexec. - kexecTime = "1m"; - }; - }; topology.self.interfaces.lan.network = "home"; } diff --git a/hosts/elisabeth/net.nix b/hosts/elisabeth/net.nix index 0197c49..ab03195 100644 --- a/hosts/elisabeth/net.nix +++ b/hosts/elisabeth/net.nix @@ -20,14 +20,6 @@ MulticastDNS = true; }; }; - "40-lan01" = { - dhcpV6Config.UseDNS = false; - dhcpV4Config.UseDNS = false; - ipv6AcceptRAConfig.UseDNS = false; - networkConfig = { - MulticastDNS = true; - }; - }; }; boot.initrd.systemd.network = { enable = true; diff --git a/hosts/nucnix/default.nix b/hosts/nucnix/default.nix new file mode 100644 index 0000000..c36938f --- /dev/null +++ b/hosts/nucnix/default.nix @@ -0,0 +1,31 @@ +{ + inputs, + minimal, + lib, + ... +}: +{ + imports = [ + inputs.nixos-hardware.nixosModules.common-pc + inputs.nixos-hardware.nixosModules.common-pc-ssd + inputs.nixos-hardware.nixosModules.common-cpu-intel + + ../../config/basic + + ../../config/support/initrd-ssh.nix + ../../config/support/physical.nix + ../../config/support/zfs.nix + ../../config/support/server.nix + + ./net.nix + ./fs.nix + ] ++ lib.lists.optionals (!minimal) [ ./guests.nix ]; + services.xserver = { + xkb = { + layout = "de"; + }; + }; + nixpkgs.hostPlatform = "x86_64-linux"; + + topology.self.interfaces.lan.network = "home"; +} diff --git a/hosts/nucnix/fs.nix b/hosts/nucnix/fs.nix new file mode 100644 index 0000000..670bf3a --- /dev/null +++ b/hosts/nucnix/fs.nix @@ -0,0 +1,91 @@ +{ + config, + lib, + ... +}: +{ + disko.devices = { + disk = { + ssd = rec { + type = "disk"; + device = "/dev/disk/by-id/${config.secrets.secrets.local.disko.nvme}"; + content = with lib.disko.gpt; { + type = "gpt"; + partitions = { + boot = (partEfi "1G") // { + device = "${device}-part1"; + }; + rpool = (partLuksZfs "ssd" "rpool" "100%") // { + device = "${device}-part2"; + }; + }; + }; + }; + }; + + zpool = with lib.disko.zfs; { + rpool = mkZpool { datasets = impermanenceZfsDatasets; }; + }; + }; + + boot.kernel.sysctl."fs.inotify.max_user_instances" = 1024; + + services.zrepl = { + enable = true; + settings = { + global = { + logging = [ + { + type = "syslog"; + level = "info"; + format = "human"; + } + ]; + # TODO Monitoring + }; + jobs = [ + #{ + # type = "push"; + # name = "push-to-remote"; + #} + { + type = "snap"; + name = "mach-schnipp-schusss"; + filesystems = { + "rpool/local/state<" = true; + "rpool/local/guests<" = true; + "rpool/safe<" = true; + }; + snapshotting = { + type = "periodic"; + prefix = "zrepl-"; + interval = "10m"; + timestamp_format = "iso-8601"; + }; + pruning = { + keep = [ + { + type = "regex"; + regex = "^zrepl-.*$"; + negate = true; + } + { + type = "grid"; + grid = lib.concatStringsSep " | " [ + "1x1d(keep=all)" + "142x1h(keep=2)" + "90x1d(keep=2)" + "500x7d" + ]; + regex = "^zrepl-.*$"; + } + ]; + }; + } + ]; + }; + }; + + fileSystems."/state".neededForBoot = true; + fileSystems."/persist".neededForBoot = true; +} diff --git a/hosts/nucnix/guests.nix b/hosts/nucnix/guests.nix new file mode 100644 index 0000000..51cec1f --- /dev/null +++ b/hosts/nucnix/guests.nix @@ -0,0 +1,182 @@ +{ + config, + stateVersion, + inputs, + lib, + minimal, + nodes, + ... +}: +let + domainOf = + hostName: + let + domains = + { + }; + in + "${domains.${hostName}}.${config.secrets.secrets.global.domains.web}"; + # TODO hard coded elisabeth nicht so schön + ipOf = hostName: nodes."elisabeth-${hostName}".config.wireguard.elisabeth.ipv4; +in +{ + services.nginx = + let + blockOf = + hostName: + { + virtualHostExtraConfig ? "", + maxBodySize ? "500M", + port ? 3000, + upstream ? hostName, + protocol ? "http", + }: + { + upstreams.${hostName} = { + servers."${ipOf upstream}:${toString port}" = { }; + extraConfig = '' + zone ${hostName} 64k ; + keepalive 5 ; + ''; + }; + virtualHosts.${domainOf hostName} = { + forceSSL = true; + useACMEHost = "web"; + locations."/" = { + proxyPass = "${protocol}://${hostName}"; + proxyWebsockets = true; + X-Frame-Options = "SAMEORIGIN"; + }; + extraConfig = + '' + client_max_body_size ${maxBodySize} ; + '' + + virtualHostExtraConfig; + }; + }; + proxyProtect = + hostName: cfg: allowedGroup: + lib.mkMerge [ + (blockOf hostName cfg) + { + virtualHosts.${domainOf hostName} = { + locations."/".extraConfig = '' + auth_request /oauth2/auth; + error_page 401 = /oauth2/sign_in; + + # pass information via X-User and X-Email headers to backend, + # requires running with --set-xauthrequest flag + auth_request_set $user $upstream_http_x_auth_request_preferred_username; + # Set the email to our own domain in case user change their mail + auth_request_set $email "''${upstream_http_x_auth_request_preferred_username}@${config.secrets.secrets.global.domains.web}"; + proxy_set_header X-User $user; + proxy_set_header X-Email $email; + + # if you enabled --cookie-refresh, this is needed for it to work with auth_request + auth_request_set $auth_cookie $upstream_http_set_cookie; + add_header Set-Cookie $auth_cookie; + ''; + locations."/oauth2/" = { + proxyPass = "http://oauth2-proxy"; + extraConfig = '' + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri; + ''; + }; + + locations."= /oauth2/auth" = { + proxyPass = + "http://oauth2-proxy/oauth2/auth" + + lib.optionalString allowedGroup "?allowed_groups=${hostName}_access"; + extraConfig = '' + internal; + + proxy_set_header X-Scheme $scheme; + # nginx auth_request includes headers but not body + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + ''; + }; + }; + } + ]; + in + lib.mkMerge [ + { + enable = false; + recommendedSetup = true; + } + ]; + + guests = + let + mkGuest = guestName: { + autostart = true; + zfs."/state" = { + pool = "rpool"; + dataset = "local/guests/${guestName}"; + }; + zfs."/persist" = { + pool = "rpool"; + dataset = "safe/guests/${guestName}"; + }; + modules = [ + ../../config/basic + ../../config/services/${guestName}.nix + { + node.secretsDir = config.node.secretsDir + "/${guestName}"; + networking.nftables.firewall.zones.untrusted.interfaces = [ + config.guests.${guestName}.networking.mainLinkName + ]; + systemd.network.networks."10-${config.guests.${guestName}.networking.mainLinkName}" = { + DHCP = lib.mkForce "no"; + address = [ + (lib.net.cidr.hostCidr + config.secrets.secrets.global.net.ips."${config.guests.${guestName}.nodeName}" + config.secrets.secrets.global.net.privateSubnetv4 + ) + (lib.net.cidr.hostCidr + config.secrets.secrets.global.net.ips."${config.guests.${guestName}.nodeName}" + config.secrets.secrets.global.net.privateSubnetv6 + ) + ]; + gateway = [ (lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnetv4) ]; + }; + } + ]; + }; + + mkMicrovm = guestName: cfg: { + ${guestName} = mkGuest guestName cfg // { + backend = "microvm"; + microvm = { + system = "x86_64-linux"; + macvtap = "lan"; + baseMac = config.secrets.secrets.local.networking.interfaces.lan01.mac; + }; + extraSpecialArgs = { + inherit (inputs.self) nodes; + inherit (inputs.self.pkgs.x86_64-linux) lib; + inherit inputs minimal stateVersion; + }; + }; + }; + + mkContainer = guestName: cfg: { + ${guestName} = mkGuest guestName cfg // { + backend = "container"; + container.macvlan = "lan"; + extraSpecialArgs = { + inherit + lib + nodes + inputs + minimal + stateVersion + ; + }; + }; + }; + in + { }; +} diff --git a/hosts/nucnix/net.nix b/hosts/nucnix/net.nix new file mode 100644 index 0000000..a8b7206 --- /dev/null +++ b/hosts/nucnix/net.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: +{ + networking = { + inherit (config.secrets.secrets.local.networking) hostId; + }; + systemd.network.networks = { + "10-lan01" = { + address = [ + (lib.net.cidr.hostCidr config.secrets.secrets.global.net.ips.${config.node.name} + config.secrets.secrets.global.net.privateSubnetv4 + ) + ]; + gateway = [ (lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnetv4) ]; + #matchConfig.MACAddress = config.secrets.secrets.local.networking.interfaces.lan01.mac; + matchConfig.Name = "lan"; + dhcpV6Config.UseDNS = false; + dhcpV4Config.UseDNS = false; + ipv6AcceptRAConfig.UseDNS = false; + networkConfig = { + MulticastDNS = true; + }; + }; + }; + boot.initrd.systemd.network = { + enable = true; + networks = { + # redo the network cause the livesystem has macvlans + "10-lan01" = { + address = [ + (lib.net.cidr.hostCidr config.secrets.secrets.global.net.ips.${config.node.name} + config.secrets.secrets.global.net.privateSubnetv4 + ) + ]; + gateway = [ (lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnetv4) ]; + matchConfig.MACAddress = config.secrets.secrets.local.networking.interfaces.lan01.mac; + dhcpV6Config.UseDNS = false; + dhcpV4Config.UseDNS = false; + ipv6AcceptRAConfig.UseDNS = false; + networkConfig = { + IPv6PrivacyExtensions = "yes"; + MulticastDNS = true; + }; + }; + }; + }; + networking.nftables.firewall.zones.untrusted.interfaces = [ "lan" ]; + + # To be able to ping containers from the host, it is necessary + # to create a macvlan on the host on the VLAN 1 network. + networking.macvlans.lan = { + interface = "lan01"; + mode = "bridge"; + }; +} diff --git a/hosts/nucnix/secrets/generated/initrd_host_ed25519_key.age b/hosts/nucnix/secrets/generated/initrd_host_ed25519_key.age new file mode 100644 index 0000000000000000000000000000000000000000..fe8fd27db88ef6806e858ed463b5a5a395631560 GIT binary patch literal 1175 zcmY+^oZ_S#+Ra!U3=~I z6`8LMCLoFt7D$xGFu+s>C<$>y5;HTHfJPxg7!a9@$WUNtFu=g^hkx+@FW*moagNop zA|qF_1zr;CC7II|0hM-%I%)txz@j!qB#Z?7UQhs>jE3>YN-@ylbz}?zlw&<6yX+Lm zGF-&`Rzek5=t?V`(C9+Bv|Y$KY^h)s3t%SRQ}Eb4K>}}*63%MQ@9^9230c0V%^Eb}{nt{@Vs=`Ngv8)(3NyWSru3A-;7hpjt)gIROIn@-;GR_#g$K3yjc&yxu_;SyhUNbAr3#i9&>w%edK)Rq^@?vLPoofve+ zFv43d=rK)Grfm1&5f|bjQ+!p$p+&k%!oEBWiWD01CTOJ;pi1eSbAr=q)dh{0w(>wy z%qN4aIXl6*an|Z0Gics|BYMf4azQScj3P!t)$&Zw*{P%F4^O&k`)$ATuS@;sprx1p z-uq>Gd{+I2rRRDkjSQ@NW%LV+*w9LUC0>2+`^n8yTIMc@KgfPK_xKP`b9YXyoW3HQ z_~fgj_!Htjp%S}>z+X>b^<-y&KeG{Vs5ryoLt+{zvcO_ zlPw+7=k)9v>SbPjGjp6SF`H+0 z3cZW<_pbxFF70qo;&cZh#}7Dq`|Q_d1p^0bvz?Q>l#!!9FvETSrjISDrHrTBR@X0b z^Yra!4=sAX{;_xWHC9;cT2?=}`Sy#yP#yj_ekpmXG&?^3NEO&td|`Rr zByH#g?k9E2)ii?Z;tc}X}ukN7> z^h=ws++sJpTBjZTE?Cp8$a?c&jlZF(?@sregNtv(|Af#RpMe_>jE%P+dB9gWXk5E@ r>qyJTYZe2^>kDsP8OSsT_a9nATxh0elovd@o*19@TFr)r<+c9-PX*4^ literal 0 HcmV?d00001 diff --git a/hosts/nucnix/secrets/host.pub b/hosts/nucnix/secrets/host.pub new file mode 100644 index 0000000..30e8790 --- /dev/null +++ b/hosts/nucnix/secrets/host.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDS0gxZD8aIAAKBtt7gyMHZ2KloQPlHxS+LsQY/62SzE diff --git a/hosts/nucnix/secrets/secrets.nix.age b/hosts/nucnix/secrets/secrets.nix.age new file mode 100644 index 0000000000000000000000000000000000000000..be0161d5727010d3d1c789752be7ce96fef801f7 GIT binary patch literal 876 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 ZLMX4eYYFn{T4Nk;>j!!=cdgp|odyQSk^TSx literal 8336 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 a98ARyoQEbUcNt@$NZJdOi3ZI2cD;X7)yR|p