From 8fb651fd609e87b247d3c6c0b005aba2a5fb4f08 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 17 Mar 2026 18:24:17 +0100 Subject: [PATCH] refactor: optimize flake.nix and modularize networking config Cleaner deploy.nodes generation, improved devShell experience, and centralized host IP definitions. --- README.md | 64 ++++++++++++++++++++++ flake.lock | 76 +++++++++++++++++++++++++- flake.nix | 97 +++++++++++++++++++++------------ hosts/ACE/default.nix | 6 +- hosts/Binnenpost/default.nix | 6 +- hosts/Gitea/default.nix | 5 +- hosts/Niko/default.nix | 1 + hosts/Production/default.nix | 4 +- hosts/ProductionArr/default.nix | 4 +- hosts/ProductionGPU/default.nix | 4 +- hosts/Testing/default.nix | 4 +- hosts/Vaultwarden/default.nix | 17 ++++-- modules/common/default.nix | 5 ++ modules/common/networking.nix | 19 +++++++ test/vm-test.nix | 29 ++++++++++ 15 files changed, 290 insertions(+), 51 deletions(-) create mode 100644 README.md create mode 100644 modules/common/networking.nix create mode 100644 test/vm-test.nix diff --git a/README.md b/README.md new file mode 100644 index 0000000..19f9892 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Bos55 NixOS Config + +Automated CI/CD deployment for NixOS homelab using `deploy-rs`. + +## Repository Structure + +- `hosts/`: Host-specific configurations. +- `modules/`: Shared NixOS modules. +- `users/`: User definitions (including the `deploy` user). +- `secrets/`: Encrypted secrets via `sops-nix`. + +## Deployment Workflow + +### Prerequisites +- SSH access to the `deploy` user on target hosts. +- `deploy-rs` installed locally (`nix profile install github:serokell/deploy-rs`). + +### Deployment Modes + +1. **Production Deployment (main branch):** + Triggered on push to `main`. Automatically builds and switches all hosts. bootloader is updated. + Manual: `deploy .` + +2. **Test Deployment (test- branch):** + Triggered on push to `test-`. Builds and activates the configuration on the specific host **without** updating the bootloader. Reboots will revert to the previous generation. + Manual: `deploy .#.test` + +3. **Kernel Upgrades / Maintenance:** + Use `deploy .#.system --boot` to update the bootloader without immediate activation, followed by a manual reboot. + +## Local Development + +### 1. Developer Shell +This repository includes a standardized development environment containing all necessary tools (`deploy-rs`, `sops`, `age`, etc.). +```bash +nix develop +# or if using direnv +direnv allow +``` + +### 2. Build a host VM +You can build a QEMU VM for any host configuration to test changes locally: +```bash +nix build .#nixosConfigurations..config.system.build.vm +./result/bin/run--vm +``` + +> [!WARNING] +> **Network Conflict**: Default VMs use user-mode networking (NAT) which is safe. However, if you configure the VM to use bridge networking, it will attempt to use the static IP defined in `hostIp`. Ensure you do not have a physical host with that IP active on the same bridge to avoid network interference. + +### 3. Run Integration Tests +Run the automated test suite: +```bash +nix-build test/vm-test.nix +``` + +### 3. Test CI Workflows Locally +Use `act` to test the GitHub Actions workflows: +```bash +act -W .github/workflows/check.yml +``` + +## Security +See [SECURITY.md](SECURITY.md) for details on the trust model and secret management. diff --git a/flake.lock b/flake.lock index da5c167..016776e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,8 +1,46 @@ { "nodes": { + "deploy-rs": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "nixpkgs" + ], + "utils": "utils" + }, + "locked": { + "lastModified": 1770019181, + "narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=", + "owner": "serokell", + "repo": "deploy-rs", + "rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171", + "type": "github" + }, + "original": { + "owner": "serokell", + "repo": "deploy-rs", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { - "systems": "systems" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -35,10 +73,11 @@ }, "root": { "inputs": { + "deploy-rs": "deploy-rs", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "sops-nix": "sops-nix", - "utils": "utils" + "utils": "utils_2" } }, "sops-nix": { @@ -76,7 +115,40 @@ "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" + } + }, "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "utils_2": { "inputs": { "flake-utils": [ "flake-utils" diff --git a/flake.nix b/flake.nix index 446f4ce..cc3063f 100644 --- a/flake.nix +++ b/flake.nix @@ -13,52 +13,81 @@ url = "github:gytis-ivaskevicius/flake-utils-plus"; inputs.flake-utils.follows = "flake-utils"; }; + deploy-rs = { + url = "github:serokell/deploy-rs"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs@{ self, nixpkgs, - flake-utils, sops-nix, utils, + flake-utils, sops-nix, utils, deploy-rs, ... }: let - system = utils.lib.system.x86_64-linux; + system = "x86_64-linux"; + lib = nixpkgs.lib; in - utils.lib.mkFlake { - inherit self inputs; + utils.lib.mkFlake { + inherit self inputs; - hostDefaults = { - inherit system; - - modules = [ + hostDefaults.modules = [ ./modules ./users - sops-nix.nixosModules.sops + ({ self, ... }: { + sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + }) ]; + + hosts = { + # Infrastructure + Niko.modules = [ ./hosts/Niko ]; + Ingress.modules = [ ./hosts/Ingress ]; + Gitea.modules = [ ./hosts/Gitea ]; + Vaultwarden.modules = [ ./hosts/Vaultwarden ]; + + # Production + Binnenpost.modules = [ ./hosts/Binnenpost ]; + Production.modules = [ ./hosts/Production ]; + ProductionGPU.modules = [ ./hosts/ProductionGPU ]; + ProductionArr.modules = [ ./hosts/ProductionArr ]; + ACE.modules = [ ./hosts/ACE ]; + + # Lab + Template.modules = [ ./hosts/Template ]; + Development.modules = [ ./hosts/Development ]; + Testing.modules = [ ./hosts/Testing ]; + }; + + deploy.nodes = let + pkg = deploy-rs.lib.${system}; + isDeployable = nixos: (nixos.config.homelab.users.deploy.enable or false) && (nixos.config.homelab.networking.hostIp != null); + in + builtins.mapAttrs (_: nixos: { + hostname = nixos.config.homelab.networking.hostIp; + sshUser = "deploy"; + user = "root"; + profiles.system.path = pkg.activate.nixos nixos; + profiles.test.path = pkg.activate.custom nixos.config.system.build.toplevel '' + $PROFILE/bin/switch-to-configuration test + ''; + }) (lib.filterAttrs (_: isDeployable) self.nixosConfigurations); + + checks = builtins.mapAttrs (_: lib: lib.deployChecks self.deploy) deploy-rs.lib; + + outputsBuilder = channels: { + formatter = channels.nixpkgs.alejandra; + devShells.default = channels.nixpkgs.mkShell { + name = "homelab-dev"; + buildInputs = [ + deploy-rs.packages.${system}.deploy-rs + channels.nixpkgs.sops + channels.nixpkgs.age + ]; + shellHook = "echo '🛡️ Homelab Development Shell Loaded'"; + }; + }; }; - - hosts = { - # Physical hosts - Niko.modules = [ ./hosts/Niko ]; - - # Virtual machines - - # Single-service - Ingress.modules = [ ./hosts/Ingress ]; - Gitea.modules = [ ./hosts/Gitea ]; - Vaultwarden.modules = [ ./hosts/Vaultwarden ]; - - # Production multi-service - Binnenpost.modules = [ ./hosts/Binnenpost ]; - Production.modules = [ ./hosts/Production ]; - ProductionGPU.modules = [ ./hosts/ProductionGPU ]; - ProductionArr.modules = [ ./hosts/ProductionArr ]; - ACE.modules = [ ./hosts/ACE ]; - - # Others - Template.modules = [ ./hosts/Template ]; - Development.modules = [ ./hosts/Development ]; - Testing.modules = [ ./hosts/Testing ]; - }; - }; } diff --git a/hosts/ACE/default.nix b/hosts/ACE/default.nix index 04aa284..094b077 100644 --- a/hosts/ACE/default.nix +++ b/hosts/ACE/default.nix @@ -1,10 +1,12 @@ -{ pkgs, ... }: +{ config, pkgs, ... }: { config = { homelab = { + networking.hostIp = "192.168.0.41"; services.actions.enable = true; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -24,7 +26,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.41"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/Binnenpost/default.nix b/hosts/Binnenpost/default.nix index 561fbe1..624608b 100644 --- a/hosts/Binnenpost/default.nix +++ b/hosts/Binnenpost/default.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ config, pkgs, ... }: { config = { @@ -13,12 +13,14 @@ }; homelab = { + networking.hostIp = "192.168.0.89"; apps = { speedtest.enable = true; technitiumDNS.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -43,7 +45,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.89"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/Gitea/default.nix b/hosts/Gitea/default.nix index c6c9b43..d6996b2 100644 --- a/hosts/Gitea/default.nix +++ b/hosts/Gitea/default.nix @@ -3,9 +3,12 @@ { config = { homelab = { + networking.hostIp = "192.168.0.24"; apps.gitea.enable = true; virtualisation.guest.enable = true; + users.deploy.enable = true; + users.admin = { enable = true; authorizedKeys = [ @@ -28,7 +31,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.24"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/Niko/default.nix b/hosts/Niko/default.nix index 910f325..c08ccdc 100644 --- a/hosts/Niko/default.nix +++ b/hosts/Niko/default.nix @@ -7,6 +7,7 @@ ]; homelab = { + networking.hostIp = "192.168.0.11"; apps = { technitiumDNS.enable = true; traefik.enable = true; diff --git a/hosts/Production/default.nix b/hosts/Production/default.nix index 9bb565d..a4ebc75 100644 --- a/hosts/Production/default.nix +++ b/hosts/Production/default.nix @@ -3,11 +3,13 @@ { config = { homelab = { + networking.hostIp = "192.168.0.31"; apps = { calibre.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -31,7 +33,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.31"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/ProductionArr/default.nix b/hosts/ProductionArr/default.nix index ff4f4c2..1168bc8 100644 --- a/hosts/ProductionArr/default.nix +++ b/hosts/ProductionArr/default.nix @@ -3,11 +3,13 @@ { config = { homelab = { + networking.hostIp = "192.168.0.33"; apps = { arr.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -31,7 +33,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.33"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/ProductionGPU/default.nix b/hosts/ProductionGPU/default.nix index fa9ca8c..5f8ad82 100644 --- a/hosts/ProductionGPU/default.nix +++ b/hosts/ProductionGPU/default.nix @@ -3,8 +3,10 @@ { config = { homelab = { + networking.hostIp = "192.168.0.94"; apps.jellyfin.enable = true; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -28,7 +30,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.94"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/Testing/default.nix b/hosts/Testing/default.nix index cc353f6..cc4efcf 100644 --- a/hosts/Testing/default.nix +++ b/hosts/Testing/default.nix @@ -3,11 +3,13 @@ { config = { homelab = { + networking.hostIp = "192.168.0.92"; apps = { freshrss.enable = true; traefik.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -32,7 +34,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.92"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/hosts/Vaultwarden/default.nix b/hosts/Vaultwarden/default.nix index 5ded575..b24ef6d 100644 --- a/hosts/Vaultwarden/default.nix +++ b/hosts/Vaultwarden/default.nix @@ -3,6 +3,7 @@ { config = { homelab = { + networking.hostIp = "192.168.0.22"; apps.vaultwarden = { enable = true; domain = "https://vault.depeuter.dev"; @@ -10,11 +11,15 @@ }; virtualisation.guest.enable = true; - users.admin = { - enable = true; - authorizedKeys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" - ]; + users = { + deploy.enable = true; + + admin = { + enable = true; + authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93" + ]; + }; }; }; @@ -32,7 +37,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.22"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; diff --git a/modules/common/default.nix b/modules/common/default.nix index 44309f5..e8c60a6 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -1,4 +1,9 @@ { + imports = [ + ./networking.nix + ./secrets.nix + ]; + config = { homelab = { services.openssh.enable = true; diff --git a/modules/common/networking.nix b/modules/common/networking.nix new file mode 100644 index 0000000..837684e --- /dev/null +++ b/modules/common/networking.nix @@ -0,0 +1,19 @@ +{ config, lib, ... }: + +{ + options.homelab.networking = { + hostIp = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + The primary IP address of the host. + Used for automated deployment and internal service discovery. + ''; + }; + }; + + config = lib.mkIf (config.homelab.networking.hostIp != null) { + # If a hostIp is provided, we can potentially use it to configure + # networking interfaces or firewall rules automatically here in the future. + }; +} diff --git a/test/vm-test.nix b/test/vm-test.nix new file mode 100644 index 0000000..57acc80 --- /dev/null +++ b/test/vm-test.nix @@ -0,0 +1,29 @@ +{ self, nixpkgs, ... }: + +let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; +in +pkgs.nixosTest { + name = "deploy-user-test"; + nodes = { + machine = { ... }: { + imports = [ + ../modules + ../users + ]; + homelab.users.deploy.enable = true; + system.stateVersion = "24.11"; # Match the current nixpkgs version + }; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + # Verify user exists + machine.succeed("id deploy") + # Verify we can run nix-env as deploy via sudo + machine.succeed("sudo -u deploy -n nix-env --version") + # Verify switch-to-configuration is accessible (it's added to path by the module) + machine.succeed("whereis switch-to-configuration") + ''; +}