diff --git a/flake.lock b/flake.lock index 532ec50..14ff73e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,28 @@ { "nodes": { + "agenix": { + "inputs": { + "darwin": "darwin", + "home-manager": "home-manager", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1770165109, + "narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=", + "owner": "ryantm", + "repo": "agenix", + "rev": "b027ee29d959fda4b60b57566d64c98a202e0feb", + "type": "github" + }, + "original": { + "owner": "ryantm", + "repo": "agenix", + "type": "github" + } + }, "apple-fonts": { "inputs": { "ci": "ci", @@ -108,6 +131,28 @@ "type": "github" } }, + "darwin": { + "inputs": { + "nixpkgs": [ + "agenix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1744478979, + "narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=", + "owner": "lnl7", + "repo": "nix-darwin", + "rev": "43975d782b418ebf4969e9ccba82466728c2851b", + "type": "github" + }, + "original": { + "owner": "lnl7", + "ref": "master", + "repo": "nix-darwin", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -150,15 +195,36 @@ "home-manager": { "inputs": { "nixpkgs": [ + "agenix", "nixpkgs" ] }, "locked": { - "lastModified": 1786031233, - "narHash": "sha256-TIDlLTLI1/pB7IqgjzcKQjpODQsZE2oII4XGG9B6KjI=", + "lastModified": 1745494811, + "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", "owner": "nix-community", "repo": "home-manager", - "rev": "7834e82588860aaf780cec1366524456a70898d7", + "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1786356609, + "narHash": "sha256-ou8fYz5w9yhXC8YbvvExybFIa19MyW5G/8dEPqa90hM=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "c30c7955cec30d664a9baced6bc0112e263d4647", "type": "github" }, "original": { @@ -172,15 +238,15 @@ "bun2nix": "bun2nix", "flake-parts": "flake-parts", "nixpkgs": "nixpkgs_2", - "systems": "systems", + "systems": "systems_2", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1786236470, - "narHash": "sha256-eIRMdjSQKSUf5aug00ycVSsO+8kIVQy5bH+K28U9LfE=", + "lastModified": 1786389530, + "narHash": "sha256-YTLO0pwBI3eAxT6tssly2MY1lhntcNOV3Xg1agIL5x4=", "owner": "numtide", "repo": "llm-agents.nix", - "rev": "bd89b8dd8837cc8d3098524c7cfe08c884ad5569", + "rev": "5b6721acbc198851658de3f93444d2be308ffe77", "type": "github" }, "original": { @@ -210,11 +276,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1786241478, - "narHash": "sha256-K/jbo7rTV0jRcDdHKL9VxGURqH61OJcCat9al7q9oMg=", + "lastModified": 1786328173, + "narHash": "sha256-ghZYtP08BstfBD3keiUHM5KupIRY3+YS+naBsFPtGzc=", "owner": "nix-community", "repo": "nix-vscode-extensions", - "rev": "c7e586ad09d79ab867b055b818d4122334ff6dfe", + "rev": "f5911382d78f7d739197702509b83d790d592cea", "type": "github" }, "original": { @@ -339,8 +405,9 @@ }, "root": { "inputs": { + "agenix": "agenix", "apple-fonts": "apple-fonts", - "home-manager": "home-manager", + "home-manager": "home-manager_2", "llm-agents": "llm-agents", "nix-flatpak": "nix-flatpak", "nix-vscode-extensions": "nix-vscode-extensions", @@ -469,6 +536,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index f412f94..4a6a7bd 100644 --- a/flake.nix +++ b/flake.nix @@ -23,9 +23,13 @@ vscode-server.url = "github:nix-community/nixos-vscode-server"; llm-agents.url = "github:numtide/llm-agents.nix"; nix-vscode-extensions.url = "github:nix-community/nix-vscode-extensions"; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, apple-fonts, home-manager, plasma-manager, sidra, vscode-server, nix-vscode-extensions, ... }@inputs: { + outputs = { self, nixpkgs, apple-fonts, home-manager, plasma-manager, sidra, vscode-server, nix-vscode-extensions, agenix, ... }@inputs: { nixosConfigurations = { miku-homelab = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; @@ -36,6 +40,7 @@ home-manager.nixosModules.home-manager inputs.nix-flatpak.nixosModules.nix-flatpak vscode-server.nixosModules.default + agenix.nixosModules.default { nixpkgs.overlays = [ inputs.nix-vscode-extensions.overlays.default @@ -53,6 +58,17 @@ } ]; }; + + vps = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + specialArgs = { inherit inputs; }; + modules = [ + ./hosts/vps + vscode-server.nixosModules.default + agenix.nixosModules.default + ]; + }; }; }; nixConfig = { diff --git a/hosts/miku-homelab/default.nix b/hosts/miku-homelab/default.nix index aaa2852..115fbdf 100644 --- a/hosts/miku-homelab/default.nix +++ b/hosts/miku-homelab/default.nix @@ -1,26 +1,83 @@ -{ pkgs, ... }: +{ config, pkgs, ... }: { - imports = [ + imports = [ ./hardware-configuration.nix - ../../modules/security/secureboot.nix - ../../modules/services/maintenance.nix - ../../modules/services/flatpak.nix - ../../modules/system/audio.nix - ../../modules/system/fonts.nix - ../../modules/system/gaming.nix - ../../modules/system/graphics.nix + ../../modules/profiles/desktop.nix + ../../modules/profiles/server.nix + ../../modules/services/samba.nix ../../modules/system/nix-ld.nix ../../modules/system/packages.nix - ../../modules/system/shells.nix - ../../modules/system/users.nix - ../../modules/system/networking.nix - ../../modules/desktop/firefox.nix - ../../modules/desktop/plasma.nix - ../../modules/services/containers.nix - ../../modules/services/vscode-server.nix ]; + age.secrets.miku-homelab-wg.file = ../../secrets/miku-homelab-wg.age; + + myNetworking = { + hostName = "miku-homelab"; + useNetworkManager = true; + staticIp = { + interface = "enp6s0"; + address = "10.1.1.21"; + prefixLength = 24; + gateway = "10.1.1.1"; + nameservers = [ "1.1.1.1" "8.8.8.8" ]; + }; + extraUdpPorts = [ 80 443 4242 49983 24800 26900 60977 ]; + extraTcpPorts = [ 80 443 4242 49983 24800 26900 60977 ]; + sshAllowUsers = [ "emmatherock" ]; + wireguard = { + enable = true; + ips = [ "10.20.0.2/24" ]; + privateKeyFile = config.age.secrets.miku-homelab-wg.path; + peers = [ + { + publicKey = "zERcSEQhan+xtmPOIjuVSkQaBynTjH96SgZZF9CZNV8="; + allowedIPs = [ "10.20.0.1/32" ]; + endpoint = "vps.external.mikufanclub.lat:51822"; + persistentKeepalive = 25; + } + ]; + }; + }; + + myUsers = { + admins.emmatherock = { + description = "EmmaTheRock"; + shell = pkgs.fish; + extraGroups = [ "plugdev" "networkmanager" "wheel" "video" "mikushare-group" "render" ]; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA8vfwM5g9RJXqHtqTgNqsYg9SxSm+UMvFqTjBoAsLJ6 emmatherock@MAIN-PC" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpTslcK0yQ6k+h8foNl17wVRyJUfEGzq7f1h3014WNB s21 plus" + ]; + }; + serviceUsers.mikushare = { + description = "Acceso remoto Mikufanclub"; + group = "mikushare-group"; + createHome = false; + }; + extraGroups = [ "mikushare-group" "plugdev" ]; + }; + + myContainers = { + enable = true; + storageDriver = "btrfs"; + globalNetwork = "homelab_net"; + composeStacks = { + tools = { + path = "/mnt/containers/tools"; + after = [ "network-online.target" "tailscaled.service" ]; + }; + arrs = { + path = "/mnt/containers/arrs"; + after = [ "docker-compose-tools.service" "local-fs.target" ]; + }; + gameservers = { + path = "/mnt/containers/gameserver"; + after = [ "docker-compose-tools.service" ]; + }; + }; + }; + boot = { loader = { efi.canTouchEfiVariables = true; @@ -58,11 +115,22 @@ # Elgato 4K S (all speed modes) SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="00af", MODE="0666", GROUP="plugdev" SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="00ae", MODE="0666", GROUP="plugdev" - + # nvtop SUBSYSTEM=="drm", KERNEL=="card*", SUBSYSTEMS=="pci", DRIVERS=="amdgpu", RUN+="/bin/sh -c 'chmod -R g+r /sys/class/drm/%k/device/'" ''; - + + systemd.services.tailscale-udp-gro = { + description = "Configurar UDP GRO para Tailscale"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.ethtool}/bin/ethtool -K enp6s0 rx-udp-gro-forwarding on rx-gro-list on"; + RemainAfterExit = true; + }; + }; + nixpkgs.config.allowUnfree = true; nix.settings = { experimental-features = [ "nix-command" "flakes" ]; diff --git a/hosts/vps/default.nix b/hosts/vps/default.nix new file mode 100644 index 0000000..e3d17a3 --- /dev/null +++ b/hosts/vps/default.nix @@ -0,0 +1,73 @@ +{ config, ... }: + +{ + imports = [ + ./hardware-configuration.nix + ../../modules/profiles/server.nix + ]; + + # PLACEHOLDER: secrets/vps-wg.age does not exist yet. It has to be created + # (same way as secrets/miku-homelab-wg.age) once this host is installed and + # its own WireGuard private key has been generated. + age.secrets.vps-wg.file = ../../secrets/vps-wg.age; + + myNetworking = { + hostName = "vps"; + useNetworkManager = false; + staticIp = { + interface = "eth0"; + address = "23.175.41.196"; + prefixLength = 27; + gateway = "23.175.41.225"; + nameservers = [ "1.1.1.1" "1.0.0.1" ]; + }; + extraTcpPorts = [ 80 443 ]; + extraUdpPorts = [ 51822 ]; + sshAllowUsers = [ "emmatherock" ]; + wireguard = { + enable = true; + ips = [ "10.20.0.1/24" ]; + privateKeyFile = config.age.secrets.vps-wg.path; + listenPort = 51822; + peers = [ + { + publicKey = "bqxDHQNxOSFpTo3We9ujC2WljtaRBgiNEewW+Rlu10k="; + allowedIPs = [ "10.20.0.2/32" ]; + } + ]; + }; + }; + + myUsers = { + admins.emmatherock = { + description = "EmmaTheRock"; + extraGroups = [ "wheel" ]; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA8vfwM5g9RJXqHtqTgNqsYg9SxSm+UMvFqTjBoAsLJ6 emmatherock@MAIN-PC" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpTslcK0yQ6k+h8foNl17wVRyJUfEGzq7f1h3014WNB s21 plus" + ]; + }; + }; + + myContainers = { + enable = true; + composeStacks.core = { + path = "/opt/containers/core"; + after = [ "network-online.target" ]; + }; + }; + + # PLACEHOLDER: adjust once the real VPS image/provider is known (BIOS vs + # UEFI, actual boot device). Assumes a generic BIOS-booted image for now. + boot.loader.grub = { + enable = true; + device = "/dev/sda"; + }; + + time.timeZone = "UTC"; + i18n.defaultLocale = "en_US.UTF-8"; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + system.stateVersion = "25.11"; +} diff --git a/hosts/vps/hardware-configuration.nix b/hosts/vps/hardware-configuration.nix new file mode 100644 index 0000000..f5cbd09 --- /dev/null +++ b/hosts/vps/hardware-configuration.nix @@ -0,0 +1,23 @@ +{ lib, ... }: + +# PLACEHOLDER: this host has not been installed yet. Replace this whole file +# with the real hardware-configuration.nix generated by `nixos-generate-config` +# during the actual installation (it will pick up the real disk UUIDs, +# filesystem types, and kernel modules for the VPS provider's image). +{ + imports = [ ]; + + boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000"; + fsType = "ext4"; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/modules/profiles/desktop.nix b/modules/profiles/desktop.nix new file mode 100644 index 0000000..08d7dc3 --- /dev/null +++ b/modules/profiles/desktop.nix @@ -0,0 +1,12 @@ +{ + imports = [ + ../desktop/firefox.nix + ../desktop/plasma.nix + ../system/gaming.nix + ../system/audio.nix + ../system/graphics.nix + ../system/fonts.nix + ../security/secureboot.nix + ../services/flatpak.nix + ]; +} diff --git a/modules/profiles/server.nix b/modules/profiles/server.nix new file mode 100644 index 0000000..511bb05 --- /dev/null +++ b/modules/profiles/server.nix @@ -0,0 +1,10 @@ +{ + imports = [ + ../system/networking.nix + ../system/users.nix + ../system/shells.nix + ../services/containers.nix + ../services/maintenance.nix + ../services/vscode-server.nix + ]; +} diff --git a/modules/services/containers.nix b/modules/services/containers.nix index 8e7ee30..b5feee8 100644 --- a/modules/services/containers.nix +++ b/modules/services/containers.nix @@ -1,70 +1,92 @@ -{ pkgs, ... }: -{ - virtualisation = { - containers.enable = true; - podman = { - enable = true; - defaultNetwork.settings.dns_enabled = true; - }; - docker = { - enable = true; - storageDriver = "btrfs"; - daemon.settings = { - dns = [ "1.1.1.1" "8.8.8.8" ]; +{ lib, pkgs, config, ... }: + +let + cfg = config.myContainers; + + composeStackOpts = { ... }: { + options = { + path = lib.mkOption { + type = lib.types.path; + description = "Directory containing the docker-compose.yml for this stack."; + }; + after = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra systemd units this stack's compose service should start after and want, beyond docker.service."; }; }; }; - systemd.services = { - init-docker-networks = { - description = "Crear redes globales de Docker si no existen"; - after = [ "docker.service" ]; - requires = [ "docker.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.docker}/bin/docker network inspect homelab_net >/dev/null 2>&1 || ${pkgs.docker}/bin/docker network create homelab_net'"; + mkComposeService = name: stack: { + name = "docker-compose-${name}"; + value = { + description = "Docker Compose stack: ${name}"; + after = [ "docker.service" ] ++ lib.optional (cfg.globalNetwork != null) "init-docker-networks.service" ++ stack.after; + wants = [ "docker.service" ] ++ lib.optional (cfg.globalNetwork != null) "init-docker-networks.service" ++ stack.after; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + WorkingDirectory = stack.path; + ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; + ExecStop = "${pkgs.docker}/bin/docker compose down"; + }; }; }; - docker-compose-tools = { - description = "Stack de Docker Compose: Tools"; - after = [ "docker.service" "network-online.target" "tailscaled.service" ]; - wants = [ "docker.service" "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - WorkingDirectory = "/mnt/containers/tools"; - ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; - ExecStop = "${pkgs.docker}/bin/docker compose down"; +in +{ + options.myContainers = { + enable = lib.mkEnableOption "Docker/Podman container support and Docker Compose stacks"; + + storageDriver = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ "aufs" "btrfs" "devicemapper" "overlay" "overlay2" "zfs" ]); + default = null; + description = "Docker storage driver to use. Leave null to let Docker choose."; + }; + + globalNetwork = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Name of a shared Docker network to create on boot, for compose stacks to join. Leave null to skip."; + }; + + composeStacks = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule composeStackOpts); + default = { }; + description = "Docker Compose stacks to run as systemd services, each producing a docker-compose- unit."; }; }; - docker-compose-arrs = { - description = "Stack de Docker Compose: Arrs"; - after = [ "docker.service" "docker-compose-tools.service" "local-fs.target" ]; - wants = [ "docker.service" "docker-compose-tools.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - WorkingDirectory = "/mnt/containers/arrs"; - ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; - ExecStop = "${pkgs.docker}/bin/docker compose down"; + + config = lib.mkIf cfg.enable { + virtualisation = { + containers.enable = true; + podman = { + enable = true; + defaultNetwork.settings.dns_enabled = true; + }; + docker = { + enable = true; + storageDriver = cfg.storageDriver; + daemon.settings = { + dns = [ "1.1.1.1" "8.8.8.8" ]; + }; + }; }; + + systemd.services = + lib.optionalAttrs (cfg.globalNetwork != null) { + init-docker-networks = { + description = "Create the ${cfg.globalNetwork} Docker network if it doesn't exist"; + after = [ "docker.service" ]; + requires = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.docker}/bin/docker network inspect ${cfg.globalNetwork} >/dev/null 2>&1 || ${pkgs.docker}/bin/docker network create ${cfg.globalNetwork}'"; + }; + }; + } + // lib.mapAttrs' mkComposeService cfg.composeStacks; }; - docker-compose-gameservers = { - description = "Stack de Docker Compose: Gameservers"; - after = [ "docker.service" "docker-compose-tools.service" ]; - wants = [ "docker.service" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - WorkingDirectory = "/mnt/containers/gameserver"; - ExecStart = "${pkgs.docker}/bin/docker compose up -d --remove-orphans"; - ExecStop = "${pkgs.docker}/bin/docker compose down"; - }; - }; - }; } diff --git a/modules/services/samba.nix b/modules/services/samba.nix new file mode 100644 index 0000000..fb23a00 --- /dev/null +++ b/modules/services/samba.nix @@ -0,0 +1,27 @@ +{ + services.samba = { + enable = true; + openFirewall = true; + settings = { + global = { + "vfs objects" = "acl_xattr"; + "map acl inherit" = "yes"; + "store dos attributes" = "yes"; + }; + mikufanclub = { + path = "/mnt/data/mikufanclub"; + writable = "yes"; + "valid users" = "mikushare emmatherock"; + "force group" = "mikushare-group"; + "create mask" = "0660"; + "directory mask" = "0770"; + }; + data-private = { + path = "/mnt/data"; + writable = "yes"; + "valid users" = "emmatherock"; + "browseable" = "yes"; + }; + }; + }; +} diff --git a/modules/system/networking.nix b/modules/system/networking.nix index 1ff32fb..0e5fc83 100644 --- a/modules/system/networking.nix +++ b/modules/system/networking.nix @@ -1,81 +1,167 @@ -{ config, pkgs, ... }: { - networking = { - hostName = "miku-homelab"; - networkmanager.enable = true; - firewall = { +{ config, lib, ... }: + +let + cfg = config.myNetworking; + + wireguardPeerOpts = { ... }: { + options = { + publicKey = lib.mkOption { + type = lib.types.str; + description = "Public key of the remote WireGuard peer."; + }; + allowedIPs = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "IP ranges routed through this peer."; + }; + endpoint = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Endpoint (host:port) of the remote peer, if it has a stable address."; + }; + persistentKeepalive = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Interval in seconds for sending keepalive packets to this peer."; + }; + }; + }; +in +{ + options.myNetworking = { + hostName = lib.mkOption { + type = lib.types.str; + description = "The system hostname."; + }; + + useNetworkManager = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable NetworkManager to manage network interfaces."; + }; + + staticIp = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + interface = lib.mkOption { + type = lib.types.str; + description = "Network interface to assign the static address to."; + }; + address = lib.mkOption { + type = lib.types.str; + description = "Static IPv4 address for the interface."; + }; + prefixLength = lib.mkOption { + type = lib.types.int; + description = "IPv4 prefix length for the static address."; + }; + gateway = lib.mkOption { + type = lib.types.str; + description = "Default gateway address."; + }; + nameservers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "1.1.1.1" "8.8.8.8" ]; + description = "DNS nameservers to use."; + }; + }; + }); + default = null; + description = "Static IPv4 configuration for this host. Leave null to rely on DHCP or NetworkManager."; + }; + + extraUdpPorts = lib.mkOption { + type = lib.types.listOf lib.types.port; + default = [ ]; + description = "Additional UDP ports to open in the firewall, beyond the Tailscale port."; + }; + + extraTcpPorts = lib.mkOption { + type = lib.types.listOf lib.types.port; + default = [ ]; + description = "Additional TCP ports to open in the firewall, beyond SSH."; + }; + + sshAllowUsers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Usernames allowed to log in over SSH. Empty means no restriction is applied."; + }; + + wireguard = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to configure a WireGuard interface named wg- on this host."; + }; + ips = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "IP addresses (with prefix length) assigned to the local WireGuard interface."; + }; + privateKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to the file containing this host's WireGuard private key."; + }; + listenPort = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = null; + description = "UDP port for the WireGuard interface to listen on. Leave null to auto-select, which is fine for hosts that only initiate connections outward."; + }; + peers = lib.mkOption { + type = lib.types.listOf (lib.types.submodule wireguardPeerOpts); + default = [ ]; + description = "WireGuard peers this host connects to."; + }; + }; + }; + + config = { + networking = lib.mkMerge [ + { + hostName = cfg.hostName; + networkmanager.enable = cfg.useNetworkManager; + firewall = { + enable = true; + allowedUDPPorts = [ config.services.tailscale.port ] ++ cfg.extraUdpPorts; + allowedTCPPorts = [ 22 ] ++ cfg.extraTcpPorts; + }; + } + (lib.mkIf (cfg.staticIp != null) { + useDHCP = false; + interfaces.${cfg.staticIp.interface}.ipv4.addresses = [{ + address = cfg.staticIp.address; + prefixLength = cfg.staticIp.prefixLength; + }]; + defaultGateway = cfg.staticIp.gateway; + nameservers = cfg.staticIp.nameservers; + }) + (lib.mkIf cfg.wireguard.enable { + wireguard.interfaces."wg-${cfg.hostName}" = { + ips = cfg.wireguard.ips; + privateKeyFile = cfg.wireguard.privateKeyFile; + listenPort = cfg.wireguard.listenPort; + peers = map (peer: { + inherit (peer) publicKey allowedIPs; + } // lib.optionalAttrs (peer.endpoint != null) { + inherit (peer) endpoint; + } // lib.optionalAttrs (peer.persistentKeepalive != null) { + inherit (peer) persistentKeepalive; + }) cfg.wireguard.peers; + }; + }) + ]; + + services.tailscale.enable = true; + + services.openssh = { enable = true; - allowedUDPPorts = [ config.services.tailscale.port 80 443 4242 49983 24800 26900 60977]; - allowedTCPPorts = [ 22 80 443 4242 49983 24800 26900 60977]; - }; - useDHCP = false; - interfaces.enp6s0.ipv4.addresses = [{ - address = "10.1.1.21"; - prefixLength = 24; - }]; - defaultGateway = "10.1.1.1"; - nameservers = [ "1.1.1.1" "8.8.8.8" ]; - - wireguard.interfaces = { - wg-miku-homelab = { - ips = [ "10.20.0.2/24" ]; - privateKeyFile = "/etc/wireguard/miku-homelab_private.key"; - - peers = [ - { - publicKey = "zERcSEQhan+xtmPOIjuVSkQaBynTjH96SgZZF9CZNV8="; - allowedIPs = [ "10.20.0.1/32" ]; - endpoint = "vps.external.mikufanclub.lat:51822"; - persistentKeepalive = 25; - } - ]; + settings = { + PermitRootLogin = "no"; + PasswordAuthentication = false; + } // lib.optionalAttrs (cfg.sshAllowUsers != [ ]) { + AllowUsers = cfg.sshAllowUsers; }; }; }; - - services.tailscale.enable = true; - services.openssh = { - enable = true; - settings = { - PermitRootLogin = "no"; - PasswordAuthentication = false; - AllowUsers = [ "emmatherock" ]; - }; - }; - - services.samba = { - enable = true; - openFirewall = true; - settings = { - global = { - "vfs objects" = "acl_xattr"; - "map acl inherit" = "yes"; - "store dos attributes" = "yes"; - }; - mikufanclub = { - path = "/mnt/data/mikufanclub"; - writable = "yes"; - "valid users" = "mikushare emmatherock"; - "force group" = "mikushare-group"; - "create mask" = "0660"; - "directory mask" = "0770"; - }; - data-private = { - path = "/mnt/data"; - writable = "yes"; - "valid users" = "emmatherock"; - "browseable" = "yes"; - }; - }; - }; - - systemd.services.tailscale-udp-gro = { - description = "Configurar UDP GRO para Tailscale"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${pkgs.ethtool}/bin/ethtool -K enp6s0 rx-udp-gro-forwarding on rx-gro-list on"; - RemainAfterExit = true; - }; - }; } diff --git a/modules/system/users.nix b/modules/system/users.nix index 4430b40..5929dc3 100644 --- a/modules/system/users.nix +++ b/modules/system/users.nix @@ -1,25 +1,88 @@ -{ pkgs, ... }: { - environment.localBinInPath = true; +{ lib, pkgs, config, ... }: - users.groups.mikushare-group = {}; - - users.groups.plugdev = {}; +let + cfg = config.myUsers; - users.users.emmatherock = { - isNormalUser = true; - description = "EmmaTheRock"; - shell = pkgs.fish; - extraGroups = [ "plugdev" "networkmanager" "wheel" "video" "mikushare-group" "render" ]; - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA8vfwM5g9RJXqHtqTgNqsYg9SxSm+UMvFqTjBoAsLJ6 emmatherock@MAIN-PC" - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpTslcK0yQ6k+h8foNl17wVRyJUfEGzq7f1h3014WNB s21 plus" - ]; + adminOpts = { ... }: { + options = { + description = lib.mkOption { + type = lib.types.str; + description = "Human-readable description (full name) for this user."; + }; + shell = lib.mkOption { + type = lib.types.package; + default = pkgs.bash; + description = "Login shell package for this user."; + }; + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra groups this user belongs to."; + }; + authorizedKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "OpenSSH public keys authorized to log in as this user."; + }; + }; }; - users.users.mikushare = { - isNormalUser = true; - description = "Acceso remoto Mikufanclub"; - group = "mikushare-group"; - createHome = false; + serviceUserOpts = { ... }: { + options = { + description = lib.mkOption { + type = lib.types.str; + description = "Human-readable description for this service user."; + }; + group = lib.mkOption { + type = lib.types.str; + description = "Primary group for this service user."; + }; + createHome = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to create a home directory for this service user."; + }; + }; + }; +in +{ + options.myUsers = { + admins = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule adminOpts); + default = { }; + description = "Admin users with interactive shell access."; + }; + + serviceUsers = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule serviceUserOpts); + default = { }; + description = "Service users without administrative privileges, used for things like file shares."; + }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra system groups to create, beyond the ones implied by admins/serviceUsers."; + }; + }; + + config = { + environment.localBinInPath = true; + + users.groups = lib.genAttrs cfg.extraGroups (_: { }); + + users.users = lib.mapAttrs + (_: admin: { + isNormalUser = true; + inherit (admin) description shell extraGroups; + openssh.authorizedKeys.keys = admin.authorizedKeys; + }) + cfg.admins + // lib.mapAttrs + (_: svc: { + isSystemUser = true; + inherit (svc) description group createHome; + }) + cfg.serviceUsers; }; } diff --git a/secrets/miku-homelab-wg.age b/secrets/miku-homelab-wg.age new file mode 100644 index 0000000..412b7f7 --- /dev/null +++ b/secrets/miku-homelab-wg.age @@ -0,0 +1,9 @@ +age-encryption.org/v1 +-> ssh-ed25519 IW6ZLQ fGdkbwV5VE8c5ueKCr/q3Agl9cKnqzmpPUGQsOTMqVU +ZvXNujDfCWxuprM/2k7FR7aeNcpwU+cAd9H+ZByYOkE +-> ssh-ed25519 h0IYxQ 3PzwNp8Sn8eeYBNMBKOJ+hoH9uxVlgV0RtqsPdZNtnM +WDLCfjfmKsj3u0GL+luYLwwXit98RLbgwMBNzp+x24U +-> ssh-ed25519 4D7N4w 3ndiKKCZSJoZNpn1HElFnyPOx8DeFJPm9iAMQdtm4SM +KqX/oScwxOhOqHM1Z+uoXWMRZrCpl5ax6BuHiy6puwc +--- CEVnIJUTA60ffvNCDrodud14KIfucXHub+tl7tXX/N4 +ÅШb³ú_zj(Ãö…s>îãF¼x£UßãFعÝÓÛñS·ˆ`†ŸOdÙ…N‰lÛFã%š LfÇ—F fÞ‡#ê6ò‘ðÎ \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..326e9f1 --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,20 @@ +let + # Emma's personal SSH keys (same ones authorized in modules/system/users.nix), + # so secrets can be edited/re-keyed from her own workstations. + emma-main-pc = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA8vfwM5g9RJXqHtqTgNqsYg9SxSm+UMvFqTjBoAsLJ6 emmatherock@MAIN-PC"; + emma-s21-plus = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIpTslcK0yQ6k+h8foNl17wVRyJUfEGzq7f1h3014WNB s21 plus"; + + # Host SSH host keys, used by agenix at runtime to decrypt (via the default + # age.identityPaths, which points at services.openssh host keys). + miku-homelab = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH7zsQdzX7RHRd2plwrTKJR89uwR2YfzBm1n+HkcYEbb root@NixOS"; + + # TODO: replace with the real vps SSH host key once the host is installed + # (e.g. via `ssh-keyscan` or by reading /etc/ssh/ssh_host_ed25519_key.pub on + # the vps itself), then re-key any vps secrets that were encrypted without it. + vps = "ssh-ed25519 REPLACE_ME_AFTER_VPS_INSTALL"; + + admins = [ emma-main-pc emma-s21-plus ]; +in +{ + "miku-homelab-wg.age".publicKeys = admins ++ [ miku-homelab ]; +}