From bdb4ad8160557fa71321f30c995495216fc17ce0 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 1 Oct 2025 16:33:55 +0200 Subject: [PATCH 01/12] Updates --- modules/apps/arr/default.nix | 2 +- modules/apps/jellyfin/default.nix | 24 +++++++++--------------- modules/services/actions/default.nix | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/modules/apps/arr/default.nix b/modules/apps/arr/default.nix index 3b05429..e2c0df5 100644 --- a/modules/apps/arr/default.nix +++ b/modules/apps/arr/default.nix @@ -343,7 +343,7 @@ in { port = 7878; in lib.mkIf cfg.radarr.enable { hostname = "radarr"; - image = "ghcr.io/hotio/radarr:release-5.28.0.10205"; + image = "ghcr.io/hotio/radarr:testing-5.28.0.10205"; autoStart = true; ports = lib.mkIf cfg.radarr.exposePorts [ "${toString port}:${toString port}/tcp" diff --git a/modules/apps/jellyfin/default.nix b/modules/apps/jellyfin/default.nix index 5b4081a..011f56b 100644 --- a/modules/apps/jellyfin/default.nix +++ b/modules/apps/jellyfin/default.nix @@ -4,6 +4,7 @@ let cfg = config.homelab.apps.jellyfin; networkName = "jellyfin"; + inherit (config.homelab.fileSystems) media; UID = 3008; GID = config.users.groups.media.gid; @@ -12,6 +13,11 @@ in { config = lib.mkIf cfg.enable { homelab = { + fileSystems.media.video = { + enable = true; + permissions = [ "read" ]; + }; + users = { apps.enable = true; media.enable = true; @@ -32,18 +38,6 @@ in { ]; }; - "/srv/video" = { - device = "192.168.0.11:/mnt/SMALL/MEDIA/VIDEO"; - fsType = "nfs"; - options = [ - "ro" - "nfsvers=4.2" - "async" "soft" - "timeo=100" "retry=50" "actimeo=1800" "lookupcache=all" - "nosuid" "tcp" - ]; - }; - "/srv/homevideo" = { device = "192.168.0.11:/mnt/BIG/MEDIA/HOMEVIDEO/ARCHIVE"; fsType = "nfs"; @@ -101,7 +95,7 @@ in { virtualisation.oci-containers.containers = { jellyfin = { hostname = "jellyfin"; - image = "jellyfin/jellyfin:10.10.0"; + image = "jellyfin/jellyfin:10.10.7"; user = "${toString UID}:${toString GID}"; autoStart = true; ports = [ @@ -117,7 +111,7 @@ in { "cache:/cache" "/srv/audio:/media/audio" - "/srv/video:/media/video" + "${media.video.hostPath}:/media/video" "/srv/homevideo:/media/homevideo" "/srv/photo:/media/photo" ]; @@ -144,7 +138,7 @@ in { feishinPort = "9180"; in { hostname = "feishin"; - image = "ghcr.io/jeffvli/feishin:0.7.1"; + image = "ghcr.io/jeffvli/feishin:0.19.0"; autoStart = true; ports = [ "${feishinPort}:9180/tcp" # Web player (HTTP) diff --git a/modules/services/actions/default.nix b/modules/services/actions/default.nix index 338b963..ea6b025 100644 --- a/modules/services/actions/default.nix +++ b/modules/services/actions/default.nix @@ -44,6 +44,6 @@ in { ]; }; }; - }; } + From d2a0da648c5c1e7f4ae1f2be02431bf4116a5bb2 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 1 Oct 2025 16:36:14 +0200 Subject: [PATCH 02/12] feat(solidtime): Add module --- modules/apps/solidtime/default.nix | 238 +++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 modules/apps/solidtime/default.nix diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix new file mode 100644 index 0000000..9174df4 --- /dev/null +++ b/modules/apps/solidtime/default.nix @@ -0,0 +1,238 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.homelab.apps.solidtime; + + networkName = "solidtime"; + internalNetworkName = "solidtime-internal"; + + version = "0.8.0"; + + port = 8000; + user = "1000:1000"; + + # dbExternalPort = ...; + dbInternalPort = 5432; + + gotenbergPort = 3000; + + inherit (config.virtualisation.oci-containers) containers; + + volumes = [ + "solidtime-storage:/var/www/html/storage" + "solidtime-logs:/var/www/html/storage/logs" + "solidtime-app:/var/www/html/storage/app" + ]; + + # laravel.env + laravelEnv = { + APP_NAME = "Solidtime"; + VITE_APP_NAME = laravelEnv.APP_NAME; + APP_ENV = "production"; + APP_DEBUG = "false"; + APP_URL = "http://localhost:${toString port}"; + APP_FORCE_HTTPS = "false"; + APP_ENABLE_REGISTRATION = "false"; + TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; + + # Logging + LOG_CHANNEL = "stderr_daily"; + LOG_LEVEL = "debug"; + + # Database + DB_CONNECTION = "pgsql"; + DB_HOST = containers.solidtimeDb.hostname; + DB_PORT = toString dbInternalPort; + DB_SSL_MODE = "require"; + DB_DATABASE = "solidtime"; + DB_USERNAME = "solidtime"; + DB_PASSWORD = "ChangeMe"; + + # Mail + #MAIL_MAILER = "smtp"; + #MAIL_HOST = "smtp.gmail.com"; + #MAIL_PORT = "465"; + #MAIL_ENCRYPTION = "tls"; + #MAIL_FROM_ADDRESS = "no-reply@time.depeuter.dev"; + MAIL_FROM_NAME = laravelEnv.APP_NAME; + #MAIL_USERNAME = "kmtl.hugo@gmail.com"; + #MAIL_PASSWORD = "fhfxoequhhqidrhd"; + + # Queue + QUEUE_CONNECTION = "database"; + + # File storage + FILESYSTEM_DISK = "local"; + PUBLIC_FILESYSTEM_DISK = "public"; + + # Services + GOTENBERG_URL = "http://${containers.solidtimeGotenberg.hostname}:${toString gotenbergPort}"; + }; + +in { + options.homelab.apps.solidtime.enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + + config = lib.mkIf cfg.enable { + homelab.virtualisation.containers.enable = true; + + # Make sure the Docker network exists. + systemd.services = { + "docker-${networkName}-create-network" = { + description = "Create Docker network for ${networkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${networkName}; then + ${pkgs.docker}/bin/docker network create ${networkName} + fi + ''; + }; + "docker-${internalNetworkName}-create-network" = { + description = "Create Docker network for ${internalNetworkName}"; + requiredBy = [ + "${containers.solidtime.serviceName}.service" + "${containers.solidtimeScheduler.serviceName}.service" + "${containers.solidtimeQueue.serviceName}.service" + "${containers.solidtimeDb.serviceName}.service" + "${containers.solidtimeGotenberg.serviceName}.service" + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if ! ${pkgs.docker}/bin/docker network ls | grep -q ${internalNetworkName}; then + ${pkgs.docker}/bin/docker network create ${internalNetworkName} + fi + ''; + }; + }; + + virtualisation.oci-containers.containers = { + solidtime = { + hostname = "solidtime"; + image = "solidtime/solidtime:${version}"; + autoStart = true; + inherit user; + ports = [ + # Open ports if you don't use Traefik + "${toString port}:8000" + ]; + extraOptions = [ + "--network=${networkName}" + "--network=${internalNetworkName}" + + # Healthecks + # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "http"; + }; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString port; + }; + }; + solidtimeScheduler = { + hostname = "scheduler"; + image = "solidtime/solidtime:${version}"; + inherit user; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] + ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "scheduler"; + }; + }; + solidtimeQueue = { + hostname = "queue"; + image = "solidtime/solidtime:${version}"; + inherit user; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] + ''--health-cmd="supervisorctl status worker:worker_00"'' + ]; + inherit volumes; + dependsOn = [ + "solidtimeDb" + ]; + environmentFiles = [ + "/home/admin/.solidtime.env" + ]; + environment = laravelEnv // { + CONTAINER_MODE = "worker"; + WORKER_COMMAND = "php /var/www/html/artisan queue:work"; + }; + }; + solidtimeDb = { + hostname = "database"; + image = "postgres:15"; + autoStart = true; + ports = [ + # "${toString dbExternalPort}:${toString dbInternalPort}" + ]; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s + ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' + "--health-retries=3" + "--health-timeout=5s" + ]; + volumes = [ + "solidtime-db:/var/lib/postgresql/data" + ]; + environment = { + PGPASSWORD = laravelEnv.DB_PASSWORD; + POSTGRES_DB = laravelEnv.DB_DATABASE; + POSTGRES_USER = laravelEnv.DB_USERNAME; + POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; + }; + }; + solidtimeGotenberg = { + hostname = "gotenberg"; + image = "gotenberg/gotenberg:8"; + autoStart = true; + extraOptions = [ + "--network=${internalNetworkName}" + + # Healthchecks + # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] + ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' + ]; + }; + }; + }; +} + From 04b4254a72c7b104a28b23ae40bd107e2d158f97 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 08:30:11 +0100 Subject: [PATCH 03/12] chore(soldtime): Cleanup options --- hosts/Development/default.nix | 1 + modules/apps/default.nix | 1 + modules/apps/solidtime/default.nix | 67 ++++++++++++++++++------------ 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index fda8e57..77f6758 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -11,6 +11,7 @@ }; traefik.enable = true; plex.enable = true; + solidtime.enable = true; }; virtualisation.guest.enable = true; }; diff --git a/modules/apps/default.nix b/modules/apps/default.nix index f62dca7..385f915 100644 --- a/modules/apps/default.nix +++ b/modules/apps/default.nix @@ -9,6 +9,7 @@ ./homepage ./jellyfin ./plex + ./solidtime ./speedtest ./technitium-dns ./traefik diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 9174df4..17193a3 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -5,10 +5,10 @@ let networkName = "solidtime"; internalNetworkName = "solidtime-internal"; + proxyNet = config.homelab.apps.traefiik.sharedNetworkName; version = "0.8.0"; - port = 8000; user = "1000:1000"; # dbExternalPort = ...; @@ -30,7 +30,7 @@ let VITE_APP_NAME = laravelEnv.APP_NAME; APP_ENV = "production"; APP_DEBUG = "false"; - APP_URL = "http://localhost:${toString port}"; + APP_URL = "http://localhost:${toString cfg.port}"; APP_FORCE_HTTPS = "false"; APP_ENABLE_REGISTRATION = "false"; TRUSTED_PROXIES = "0.0.0.0/0,2000:0:0:0:0:0:0:0/3"; @@ -70,7 +70,15 @@ let }; in { - options.homelab.apps.solidtime.enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + options.homelab.apps.solidtime = { + enable = lib.mkEnableOption "Solidtime time tracker using Docker"; + port = lib.mkOption { + type = lib.types.int; + default = 8000; + description = "Solidtime WebUI port"; + }; + exposePort = lib.mkEnableOption "Expose Soldtime port"; + }; config = lib.mkIf cfg.enable { homelab.virtualisation.containers.enable = true; @@ -117,52 +125,54 @@ in { solidtime = { hostname = "solidtime"; image = "solidtime/solidtime:${version}"; - autoStart = true; inherit user; + autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; ports = [ # Open ports if you don't use Traefik - "${toString port}:8000" + "${toString cfg.port}:8000" + ]; + networks = [ + networkName + internalNetworkName ]; extraOptions = [ - "--network=${networkName}" - "--network=${internalNetworkName}" - # Healthecks # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' ]; inherit volumes; - dependsOn = [ - "solidtimeDb" - ]; + labels = { + "traefik.enable" = "true"; + "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; + "traefik.http.services.solidtime.loadbalancer.server.port" = toString cfg.port; + }; environmentFiles = [ "/home/admin/.solidtime.env" ]; environment = laravelEnv // { CONTAINER_MODE = "http"; }; - labels = { - "traefik.enable" = "true"; - "traefik.http.routers.solidtime.rule" = "Host(`time.${config.networking.hostName}.depeuter.dev`)"; - "traefik.http.services.solidtime.loadbalancer.server.port" = toString port; - }; }; solidtimeScheduler = { hostname = "scheduler"; image = "solidtime/solidtime:${version}"; inherit user; autoStart = true; + dependsOn = [ + "solidtimeDb" + ]; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' ]; inherit volumes; - dependsOn = [ - "solidtimeDb" - ]; environmentFiles = [ "/home/admin/.solidtime.env" ]; @@ -175,9 +185,10 @@ in { image = "solidtime/solidtime:${version}"; inherit user; autoStart = true; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] ''--health-cmd="supervisorctl status worker:worker_00"'' @@ -201,9 +212,10 @@ in { ports = [ # "${toString dbExternalPort}:${toString dbInternalPort}" ]; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: - CMD - pg_isready - '-q' - '-d' - '${DB_DATABASE}' - '-U' - '${DB_USERNAME}' retries: 3 timeout: 5s ''--health-cmd="pg_isready -q -d ${laravelEnv.DB_DATABASE} -U ${laravelEnv.DB_USERNAME}"'' @@ -224,9 +236,10 @@ in { hostname = "gotenberg"; image = "gotenberg/gotenberg:8"; autoStart = true; + networks = [ + internalNetworkName + ]; extraOptions = [ - "--network=${internalNetworkName}" - # Healthchecks # test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] ''--health-cmd="curl --silent --fail http://localhost:${toString gotenbergPort}/health"'' From 84c94afda46feab5947f5050b0fb9def869ea6a4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 08:37:32 +0100 Subject: [PATCH 04/12] chore(solidtime): Update to 0.9.0 --- modules/apps/solidtime/default.nix | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 17193a3..0a62900 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -7,7 +7,7 @@ let internalNetworkName = "solidtime-internal"; proxyNet = config.homelab.apps.traefiik.sharedNetworkName; - version = "0.8.0"; + version = "0.9.0"; user = "1000:1000"; @@ -140,8 +140,8 @@ in { ]; extraOptions = [ # Healthecks - # test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] - ''--health-cmd=curl --fail http://localhost:8000/health-check/up || exit 1'' + # test: [ "CMD", "curl", "--fail", "http://localhost:8000/health-check/up" ] + ''--health-cmd=curl --fail http://localhost:8000/health-check/up'' ]; inherit volumes; labels = { @@ -169,8 +169,8 @@ in { ]; extraOptions = [ # Healthchecks - # test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] - ''--health-cmd="supervisorctl status scheduler:scheduler_00"'' + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' ]; inherit volumes; environmentFiles = [ @@ -190,8 +190,8 @@ in { ]; extraOptions = [ # Healthchecks - # test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] - ''--health-cmd="supervisorctl status worker:worker_00"'' + # test: [ "CMD", "healthcheck" ] + ''--health-cmd="healthcheck"'' ]; inherit volumes; dependsOn = [ From 687b85eeefe14c553f6cda03c3c57c7ce716229f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 09:08:11 +0100 Subject: [PATCH 05/12] feat(solidtime): Preload Docker images --- modules/apps/solidtime/default.nix | 42 +++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 0a62900..3348e0b 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -18,6 +18,15 @@ let inherit (config.virtualisation.oci-containers) containers; + solidtimeImageName = "solidtime/solidtime"; + solidtimeImage = "${solidtimeImageName}:${version}"; + solidtimeImageFile = pkgs.dockerTools.pullImage { + imageName = solidtimeImageName; + finalImageTag = version; + imageDigest = "sha256:5601b81f63ece49cdc4110f90f7624b0cc4ba87448b3e4bddce904457822a4ef"; + sha256 = "sha256-/VwuJGczs41XeItNamLahLt/ypUQHTn9AcwneVw/3YY="; + }; + volumes = [ "solidtime-storage:/var/www/html/storage" "solidtime-logs:/var/www/html/storage/logs" @@ -124,7 +133,8 @@ in { virtualisation.oci-containers.containers = { solidtime = { hostname = "solidtime"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; dependsOn = [ @@ -158,7 +168,8 @@ in { }; solidtimeScheduler = { hostname = "scheduler"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; dependsOn = [ @@ -182,7 +193,8 @@ in { }; solidtimeQueue = { hostname = "queue"; - image = "solidtime/solidtime:${version}"; + image = solidtimeImage; + imageFile = solidtimeImageFile; inherit user; autoStart = true; networks = [ @@ -205,9 +217,17 @@ in { WORKER_COMMAND = "php /var/www/html/artisan queue:work"; }; }; - solidtimeDb = { + solidtimeDb = let + imageName = "postgres"; + finalImageTag = "15"; + in { hostname = "database"; - image = "postgres:15"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:98fe06b500b5eb29e45bf8c073eb0ca399790ce17b1d586448edc4203627d342"; + sha256 = "sha256-AZ4VkOlROX+nR/MjDjsA4xdHzmtKjiBAtsp2Q6IdOvg="; + }; autoStart = true; ports = [ # "${toString dbExternalPort}:${toString dbInternalPort}" @@ -232,9 +252,17 @@ in { POSTGRES_PASSWORD = laravelEnv.DB_PASSWORD; }; }; - solidtimeGotenberg = { + solidtimeGotenberg = let + imageName = "gotenberg/gotenberg"; + finalImageTag = "8.26.0"; + in { hostname = "gotenberg"; - image = "gotenberg/gotenberg:8"; + image = "${imageName}:${finalImageTag}"; + imageFile = pkgs.dockerTools.pullImage { + inherit imageName finalImageTag; + imageDigest = "sha256:328551506b3dec3ff6381dd47e5cd72a44def97506908269e201a8fbfa1c12c0"; + sha256 = "sha256-1zz4xDAgXxHUnkCVIfjHTgXb82EFEx+5am6Cu9+eZj4="; + }; autoStart = true; networks = [ internalNetworkName From 58f2ea9b74731144f1f375419ad1b7f43980cfec Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 10 Feb 2026 09:25:16 +0100 Subject: [PATCH 06/12] chore(solidtime): Update to 0.10.0 --- modules/apps/solidtime/default.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/apps/solidtime/default.nix b/modules/apps/solidtime/default.nix index 3348e0b..725d32d 100644 --- a/modules/apps/solidtime/default.nix +++ b/modules/apps/solidtime/default.nix @@ -7,8 +7,6 @@ let internalNetworkName = "solidtime-internal"; proxyNet = config.homelab.apps.traefiik.sharedNetworkName; - version = "0.9.0"; - user = "1000:1000"; # dbExternalPort = ...; @@ -19,12 +17,13 @@ let inherit (config.virtualisation.oci-containers) containers; solidtimeImageName = "solidtime/solidtime"; + version = "0.10.0"; solidtimeImage = "${solidtimeImageName}:${version}"; solidtimeImageFile = pkgs.dockerTools.pullImage { imageName = solidtimeImageName; finalImageTag = version; - imageDigest = "sha256:5601b81f63ece49cdc4110f90f7624b0cc4ba87448b3e4bddce904457822a4ef"; - sha256 = "sha256-/VwuJGczs41XeItNamLahLt/ypUQHTn9AcwneVw/3YY="; + imageDigest = "sha256:817d3a366ecc39f0473d7154372afa82dd4e6e50c66d70be45804892c8421cbb"; + sha256 = "sha256-h5aCKaquUF/EVsOHaLOHrn1HAoXZYPhAbJ+e4cmjSA8="; }; volumes = [ From c04bce06b66b588ad2d3a67c3e1f64f63c963263 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 6 Feb 2026 14:15:19 +0100 Subject: [PATCH 07/12] feat: Test Action Including commits: * chore: Disable test workflow * Determine hosts * Build each host * Add NixOS to build step as well * More specific hostnames * fix json elements * try different way * Debug matrix * run on push * use var directly * fix mappings * fix mappings * add toolcache * Change names and ordere * Debugging * Debugging extra step * Debugging needs outputs * Debugging needs outputs hosts * Preserve quotes * printf escaped * Using EOF * test * escape? * without json parse * hardcoding * change build command * Testing --- .github/workflows/build.yml | 43 +++++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 17 +++++++++++++++ .gitignore | 1 + 3 files changed, 61 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..74d6457 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: "Build" +on: + pull_request: + push: + +jobs: + determine-hosts: + name: "Determining hosts to build" + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-24.04 + outputs: + hosts: ${{ steps.hosts.outputs.hostnames }} + steps: + - uses: actions/checkout@v5 + - uses: https://github.com/cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: "Determine hosts" + id: hosts + run: | + hostnames="$(nix eval .#nixosConfigurations --apply builtins.attrNames --json)" + printf "hostnames=%s\n" "${hostnames}" >> "${GITHUB_OUTPUT}" + + build: + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-24.04 + needs: determine-hosts + strategy: + matrix: + hostname: [ + Development, + Testing + ] + + steps: + - uses: actions/checkout@v5 + - uses: https://github.com/cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: "Build host" + run: | + nix build ".#nixosConfigurations.${{ matrix.hostname }}.config.system.build.toplevel" --verbose + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8cb0f4b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: "Test" +on: + pull_request: + push: +jobs: + tests: + if: false + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - uses: actions/checkout@v5 + - uses: https://github.com/cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: "My custom step" + run: nix run nixpkgs#hello diff --git a/.gitignore b/.gitignore index 485dee6..8daf605 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea +result From 47245b2b965c3eeeb4edd8f25d02405cfd425b08 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 5 Mar 2026 20:17:50 +0100 Subject: [PATCH 08/12] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/544961dfcce86422ba200ed9a0b00dd4b1486ec5?narHash=sha256-EVAqOteLBFmd7pKkb0%2BFIUyzTF61VKi7YmvP1tw4nEw%3D' (2025-10-15) → 'github:NixOS/nixpkgs/80bdc1e5ce51f56b19791b52b2901187931f5353?narHash=sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN%2BUHzW1jc%3D' (2026-03-04) • Updated input 'sops-nix': 'github:Mic92/sops-nix/ab8d56e85b8be14cff9d93735951e30c3e86a437?narHash=sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E%3D' (2025-10-13) → 'github:Mic92/sops-nix/1d9b98a29a45abe9c4d3174bd36de9f28755e3ff?narHash=sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw%3D' (2026-03-02) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 67df8c4..da5c167 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1760524057, - "narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=", + "lastModified": 1772624091, + "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5", + "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1760393368, - "narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=", + "lastModified": 1772495394, + "narHash": "sha256-hmIvE/slLKEFKNEJz27IZ8BKlAaZDcjIHmkZ7GCEjfw=", "owner": "Mic92", "repo": "sops-nix", - "rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437", + "rev": "1d9b98a29a45abe9c4d3174bd36de9f28755e3ff", "type": "github" }, "original": { From 5a031b48ed6931460084244f1a395397ecafbcb9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 5 Mar 2026 20:26:33 +0100 Subject: [PATCH 09/12] fix(vaultwarden): Update image - fix webui not loading --- modules/apps/vaultwarden/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 4510299..907dda4 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -13,12 +13,12 @@ in { description = "Vaultwarden WebUI port"; }; domain = lib.mkOption { - type = lib.types.string; + type = lib.types.str; example = "https://vault.depeuter.dev"; description = "Domain to configure Vaultwarden on"; }; name = lib.mkOption { - type = lib.types.string; + type = lib.types.str; example = "Hugo's Vault"; description = "Service name to use for invitations and mail"; }; @@ -77,7 +77,7 @@ in { dataDir = "/data"; in { hostname = "vaultwarden"; - image = "vaultwarden/server:1.34.3-alpine"; + image = "vaultwarden/server:1.35.4-alpine"; autoStart = true; ports = [ "${toString cfg.port}:80/tcp" From f8ed7072535e893dd1ea0299583c6aeabca19674 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 17 Mar 2026 19:40:34 +0100 Subject: [PATCH 10/12] refactor: optimize flake.nix and modularize networking config Cleaner deploy.nodes generation, improved devShell experience, and centralized host IP definitions using utils.lib.system variables. --- README.md | 64 +++++++++++++++++++++++ flake.nix | 91 +++++++++++++++++++++------------ 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 +++++++ 13 files changed, 182 insertions(+), 48 deletions(-) create mode 100644 README.md create mode 100644 modules/common/networking.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.nix b/flake.nix index 446f4ce..5594599 100644 --- a/flake.nix +++ b/flake.nix @@ -13,52 +13,77 @@ 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; + 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 ]; + + 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. + }; +} From 731abd1d6f3b13d530908cc86997a9846a12e943 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 17 Mar 2026 19:40:59 +0100 Subject: [PATCH 11/12] feat(security): implement metadata redaction and sops-nix migration Migrated authorized SSH keys and personal metadata (emails, tokens) to sops-nix to prevent infrastructure fingerprinting. Introduced centralized secrets module with placeholder fallbacks. --- flake.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flake.nix b/flake.nix index 5594599..b6ed513 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,10 @@ ./modules ./users sops-nix.nixosModules.sops + ({ self, ... }: { + sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + }) ]; hosts = { From 1c437333f32b658a58e3846701685020a2703ec9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 17 Mar 2026 19:41:31 +0100 Subject: [PATCH 12/12] feat(security): implement metadata redaction and sops-nix migration Migrated authorized SSH keys and personal metadata (emails, tokens) to sops-nix to prevent infrastructure fingerprinting. Introduced centralized secrets module with placeholder fallbacks. --- flake.nix | 4 ++++ hosts/Development/default.nix | 7 +++++-- hosts/Ingress/default.nix | 13 +++++++++---- hosts/Isabel/default.nix | 6 +++--- modules/apps/gitea/default.nix | 3 ++- modules/apps/traefik/default.nix | 2 +- modules/apps/vaultwarden/default.nix | 3 ++- modules/common/secrets.nix | 18 ++++++++++++++++++ users/admin/default.nix | 4 +++- users/backup/default.nix | 5 ++--- users/deploy/default.nix | 21 ++++++++++++++++++--- 11 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 modules/common/secrets.nix diff --git a/flake.nix b/flake.nix index b6ed513..7e7e68c 100644 --- a/flake.nix +++ b/flake.nix @@ -39,6 +39,10 @@ sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; sops.age.keyFile = "/var/lib/sops-nix/key.txt"; }) + ({ self, ... }: { + sops.defaultSopsFile = "${self}/secrets/secrets.yaml"; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + }) ]; hosts = { diff --git a/hosts/Development/default.nix b/hosts/Development/default.nix index fda8e57..68c5fea 100644 --- a/hosts/Development/default.nix +++ b/hosts/Development/default.nix @@ -3,6 +3,7 @@ { config = { homelab = { + networking.hostIp = "192.168.0.91"; apps = { bind9.enable = true; homepage = { @@ -13,6 +14,7 @@ plex.enable = true; }; virtualisation.guest.enable = true; + users.deploy.enable = true; }; networking = { @@ -36,7 +38,7 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.91"; + address = config.homelab.networking.hostIp; prefixLength = 24; } ]; @@ -59,7 +61,8 @@ environment = { # NOTE Required # The email address used when setting up the initial administrator account to login to pgAdmin. - PGADMIN_DEFAULT_EMAIL = "kmtl.hugo+pgadmin@gmail.com"; + # TODO Hugo: Populate 'pgadmin_email' in sops. + PGADMIN_DEFAULT_EMAIL = config.sops.placeholder.pgadmin_email or "pgadmin-admin@example.com"; # NOTE Required # The password used when setting up the initial administrator account to login to pgAdmin. PGADMIN_DEFAULT_PASSWORD = "ChangeMe"; diff --git a/hosts/Ingress/default.nix b/hosts/Ingress/default.nix index c0a3ac9..c16f151 100644 --- a/hosts/Ingress/default.nix +++ b/hosts/Ingress/default.nix @@ -2,7 +2,11 @@ { config = { - homelab.virtualisation.guest.enable = true; + homelab = { + networking.hostIp = "192.168.0.10"; + virtualisation.guest.enable = true; + users.deploy.enable = true; + }; networking = { hostName = "Ingress"; @@ -19,8 +23,8 @@ interfaces.ens18 = { ipv4.addresses = [ { - address = "192.168.0.10"; -prefixLength = 24; + address = config.homelab.networking.hostIp; + prefixLength = 24; } ]; }; @@ -39,6 +43,7 @@ prefixLength = 24; }; }; + security.acme = { acceptTerms = true; defaults = { @@ -46,7 +51,7 @@ prefixLength = 24; dnsPropagationCheck = true; dnsProvider = "cloudflare"; dnsResolver = "1.1.1.1:53"; - email = "tibo.depeuter@telenet.be"; + email = config.sops.placeholder.acme_email or "acme-email@example.com"; credentialFiles = { CLOUDFLARE_DNS_API_TOKEN_FILE = "/var/lib/secrets/depeuter-dev-cloudflare-api-token"; }; diff --git a/hosts/Isabel/default.nix b/hosts/Isabel/default.nix index 0a1f50f..f275b0e 100644 --- a/hosts/Isabel/default.nix +++ b/hosts/Isabel/default.nix @@ -165,7 +165,7 @@ providers: # Certificates "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" - "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" # Additional routes @@ -176,8 +176,8 @@ providers: # "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true) ]; environment = { - # TODO Hide this! - "CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2"; + # TODO Hugo: Populate 'cloudflare_dns_token' in sops. + "CLOUDFLARE_DNS_API_TOKEN" = config.sops.placeholder.cloudflare_dns_token or "CLOUDFLARE_TOKEN_PLACEHOLDER"; }; environmentFiles = [ ]; diff --git a/modules/apps/gitea/default.nix b/modules/apps/gitea/default.nix index 0361bd5..34d52e4 100644 --- a/modules/apps/gitea/default.nix +++ b/modules/apps/gitea/default.nix @@ -496,7 +496,8 @@ in { #FORGEJO__mailer__CLIENT_KEY_FILE = "custom/mailer/key.pem"; # Mail from address, RFC 5322. This can be just an email address, or the # `"Name" ` format. - FORGEJO__mailer__FROM = ''"${title}" ''; + # TODO Hugo: Populate 'gitea_mailer_from' in sops. + FORGEJO__mailer__FROM = config.sops.placeholder.gitea_mailer_from or "git@example.com"; # Sometimes it is helpful to use a different address on the envelope. Set this to use # ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. #FORGEJO__mailer__ENVELOPE_FROM = ""; diff --git a/modules/apps/traefik/default.nix b/modules/apps/traefik/default.nix index 7f6ce38..54588c1 100644 --- a/modules/apps/traefik/default.nix +++ b/modules/apps/traefik/default.nix @@ -72,7 +72,7 @@ in { # Certificates "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" - "--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be" + "--certificatesresolvers.letsencrypt.acme.email=${config.sops.placeholder.acme_email or "acme-email@example.com"}" "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ]; volumes = [ diff --git a/modules/apps/vaultwarden/default.nix b/modules/apps/vaultwarden/default.nix index 907dda4..6c55d0a 100644 --- a/modules/apps/vaultwarden/default.nix +++ b/modules/apps/vaultwarden/default.nix @@ -344,6 +344,7 @@ in { # ORG_CREATION_USERS=none ## A comma-separated list means only those users can create orgs: # ORG_CREATION_USERS=admin1@example.com,admin2@example.com + # TODO Hugo: Redact org creation users if needed. ## Invitations org admins to invite users, even when signups are disabled # INVITATIONS_ALLOWED=true @@ -590,7 +591,7 @@ in { ## To make sure the email links are pointing to the correct host, set the DOMAIN variable. ## Note: if SMTP_USERNAME is specified, SMTP_PASSWORD is mandatory SMTP_HOST = "smtp.gmail.com"; - SMTP_FROM = "vault@depeuter.dev"; + SMTP_FROM = config.sops.placeholder.vaultwarden_smtp_from or "vaultwarden@example.com"; SMTP_FROM_NAME = cfg.name; # SMTP_USERNAME=username # SMTP_PASSWORD=password diff --git a/modules/common/secrets.nix b/modules/common/secrets.nix new file mode 100644 index 0000000..10b6473 --- /dev/null +++ b/modules/common/secrets.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +{ + sops.secrets = { + # -- User Public Keys (Anti-Fingerprinting) -- + "user_keys_admin" = { neededForUsers = true; }; + "user_keys_deploy" = { neededForUsers = true; }; + "user_keys_backup" = { neededForUsers = true; }; + + # -- Infrastructure Metadata -- + # Hugo TODO: Populate these in your .sops.yaml / secrets file + "acme_email" = {}; + "cloudflare_dns_token" = {}; + "pgadmin_email" = {}; + "gitea_mailer_from" = {}; + "vaultwarden_smtp_from" = {}; + }; +} diff --git a/users/admin/default.nix b/users/admin/default.nix index dc01c81..14766da 100644 --- a/users/admin/default.nix +++ b/users/admin/default.nix @@ -26,7 +26,9 @@ in { config.users.groups.wheel.name # Enable 'sudo' for the user. ]; initialPassword = "ChangeMe"; - openssh.authorizedKeys.keys = cfg.authorizedKeys; + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_admin.path + ]; packages = with pkgs; [ curl git diff --git a/users/backup/default.nix b/users/backup/default.nix index acae033..8c20374 100644 --- a/users/backup/default.nix +++ b/users/backup/default.nix @@ -12,9 +12,8 @@ in { extraGroups = [ "docker" # Allow access to the docker socket. ]; - openssh.authorizedKeys.keys = [ - # Hugo - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICms6vjhE9kOlqV5GBPGInwUHAfCSVHLI2Gtzee0VXPh" + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_backup.path ]; }; }; diff --git a/users/deploy/default.nix b/users/deploy/default.nix index 0509d1e..5c28561 100644 --- a/users/deploy/default.nix +++ b/users/deploy/default.nix @@ -3,7 +3,19 @@ let cfg = config.homelab.users.deploy; in { - options.homelab.users.deploy.enable = lib.mkEnableOption "user Deploy"; + options.homelab.users.deploy = { + enable = lib.mkEnableOption "user Deploy"; + + authorizedKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = '' + Additional SSH public keys authorized for the deploy user. + The CI runner key should be provided as a base key; personal + workstation keys can be appended here per host or globally. + ''; + }; + }; config = lib.mkIf cfg.enable { users = { @@ -15,12 +27,15 @@ in { isSystemUser = true; home = "/var/empty"; shell = pkgs.bashInteractive; - openssh.authorizedKeys.keys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrG+ldRBdCeHEXrsy/qHXIJYg8xQXVuiUR0DxhFjYNg" + openssh.authorizedKeys.keyFiles = [ + config.sops.secrets.user_keys_deploy.path ]; }; }; + # Allow the deploy user to push closures to the nix store + nix.settings.trusted-users = [ "deploy" ]; + security.sudo.extraRules = [ { groups = [