feat(gitops): implement native custom gitops solution with webhooks

This commit is contained in:
Tibo De Peuter 2026-07-22 21:20:16 +02:00
parent 501cda6402
commit 78a20fbe00
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
25 changed files with 1804 additions and 18 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.idea

View file

@ -0,0 +1,138 @@
# **Infrastructure Specification: Automated NixOS GitOps Environment**
## **1\. Architecture Overview**
This specification outlines the migration from a manually deployed NixOS environment to a fully automated, pull-based GitOps architecture.
* **Source of Truth:** Forgejo repository (v2 branch).
* **Infrastructure Provisioning:** Terraform (via bpg/proxmox provider) executed by Forgejo Actions.
* **Configuration Management:** NixOS Flakes, pulled autonomously by individual nodes via comin.
* **Secret Management:** sops-nix using Age keys (SSH host keys for VM decryption).
* **Storage:** TrueNAS serving persistent data via NFS/iSCSI, utilizing ZFS snapshot cloning for the staging environment.
* **Dependency Management:** RenovateBot running via Forgejo Actions.
## **2\. Phase 1: Foundation & State Preparation**
### **2.1 Git Branching Strategy**
* Initialize a new orphan branch named v2 (or a fresh repository) to ensure a clean slate for the flake lockfile and SOPS configuration.
* Cherry-pick existing .nix service modules from the legacy main branch as they are migrated.
### **2.2 Secret Management (Bankruptcy & Reset)**
* Delete all legacy .sops.yaml configurations.
* Generate a new master age key. This key will be stored securely offline (e.g., Bitwarden) and **never** placed on the Forgejo runner or target VMs.
* Initialize a new .sops.yaml. Target VMs will be added via their public SSH host keys as they are provisioned.
### **2.3 Storage Strategy (TrueNAS & Staging)**
To provide the Staging VM with production-accurate data without risking corruption:
1. **Production State:** TrueNAS maintains the primary ZFS datasets.
2. **Staging State (On-Demand):** When the staging environment spins up, a script (via Proxmox/Forgejo Action or TrueNAS API) takes a temporary ZFS snapshot of the production dataset.
3. **Clone & Mount:** The snapshot is cloned and exported via NFS/SMB specifically for the Staging VM.
4. **Teardown:** When Staging is spun down, the ZFS clone is destroyed.
## **3\. Phase 2: Proxmox Configuration & Least Privilege**
Terraform requires API access to Proxmox. To adhere to the principle of least privilege, Proxmox RBAC (Role-Based Access Control) will be utilized.
### **3.1 Proxmox Static Configuration (RBAC)**
The root user (root@pam) must **never** be used for automation. Instead, a dedicated API user with a highly restricted role must be created. This can be done via the Proxmox Web UI (Datacenter \-\> Permissions) or via the Proxmox shell (pveum).
**1\. Create the Restricted Role (TerraformProv):**
This role grants only the permissions needed to clone templates, configure hardware, and manage power states.
pveum role add TerraformProv \-privs "VM.Allocate VM.Audit VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit SDN.Use"
**2\. Create the Terraform User:**
Create a user in the Proxmox VE authentication realm (@pve).
pveum user add terraform@pve
**3\. Assign Permissions to Paths:**
Bind the role to the user, strictly limiting access to the VMs path and the specific storage path where VMs will reside (e.g., local-zfs).
pveum acl modify /vms \-user terraform@pve \-role TerraformProv
pveum acl modify /storage/local-zfs \-user terraform@pve \-role TerraformProv
**4\. Generate API Token:**
Generate a token for the user. Proxmox will output a Secret ID. This ID is saved into Forgejo Secrets and passed to the Terraform provider.
pveum user token add terraform@pve tf-automation \--privsep 0
*(Note: \--privsep 0 ensures the token inherits the user's permissions, rather than requiring separate ACL definitions).*
## **4\. Phase 3: Infrastructure as Code (Terraform)**
### **4.1 The Golden Image (.qcow2)**
A minimal NixOS image is built locally using nixos-generators and uploaded to Proxmox as a template (e.g., ID 9000). This template contains:
* QEMU Guest Agent.
* Cloud-Init support.
* comin installed.
### **4.2 Terraform Provisioning**
Terraform maintains the hardware state. Its responsibilities are strictly limited to:
* Cloning Template 9000\.
* Allocating vCPU, RAM, and Virtual Disks.
* Using Cloud-Init to inject network configurations (IP, Gateway) and the Forgejo runner's public SSH key for health check access.
## **5\. Phase 4: CI/CD & Automation**
### **5.1 Forgejo Runner & Terraform Pipeline**
* The newly provisioned Forgejo Runner VM registers with Forgejo.
* A workflow (terraform.yml) runs terraform plan on Pull Requests and terraform apply on merges to the main branch.
### **5.2 GitOps Pull Agent (comin and Alternatives)**
Once provisioned by Terraform, VMs boot and pull their OS configuration autonomously.
* **Chosen Tool (comin):** Polling agent designed specifically for NixOS GitOps. Runs as a systemd service, supports multiple git remotes, branch tracking, and local cryptographic verification.
* **Alternative 1 (system.autoUpgrade):** The native NixOS approach utilizing systemd timers. *Rejected* because it lacks advanced Git authentication and webhooks for immediate triggering.
* **Alternative 2 (deploy-rs / colmena):** Excellent push-based deployment tools. *Rejected* because they require the CI runner to maintain SSH root access to the entire fleet, violating our zero-trust/pull-only goals.
### **5.3 RenovateBot Integration**
* Renovate is deployed via a Forgejo Action running on a cron schedule.
* Nix flake support is explicitly enabled in renovate.json.
* Renovate parses flake.lock and Docker tags, automatically opening PRs for updates.
## **6\. Phase 5: Staging Lifecycle & Healthchecks**
### **6.1 Lifecycle Automation**
1. **Trigger:** A PR is opened by Renovate or a developer.
2. **Wake-up:** A Forgejo Action calls the Proxmox API to start the nixos-staging VM and triggers the TrueNAS ZFS snapshot clone.
3. **Pull:** The Staging VM boots, connects to the network, and comin pulls the PR's commit hash.
### **6.2 Automated Testing**
The Forgejo Action will verify the deployment was successful:
* **Systemd Checks:** systemctl is-system-running \--wait and systemctl is-active \<service\>.
* **Docker Healthchecks:** docker inspect \--format='{{json .State.Health.Status}}' \<container\>.
* **HTTP Probes:** Execute curl commands against the Staging VM to verify HTTP 200 OK.
### **6.3 Spin Down**
Upon PR merge or closure, the Forgejo Action shuts down the VM via the Proxmox API and destroys the temporary TrueNAS ZFS clone.
## **7\. Phase 6: Rollbacks & Disaster Recovery**
Because infrastructure involves both *stateless* configurations (NixOS) and *stateful* data (TrueNAS), a unified rollback strategy is critical.
### **7.1 Differentiating Failures (Nix vs. TrueNAS)**
If a deployment fails, the root cause must be identified to determine the rollback path:
* **Stateless Failure (NixOS/Code):** The service fails to start immediately. System logs (journalctl \-u my-service) show syntax errors, missing binaries, or bad systemd unit definitions.
* **Stateful Failure (TrueNAS/Data):** The service starts but crashes with a database schema version mismatch, "permission denied" on the NFS mount, or missing user data. This means a service (like a Docker container) attempted a one-way database migration on the persistent TrueNAS dataset and failed halfway.
### **7.2 The Unified Rollback Procedure**
When a production rollout fails, **do not** attempt to fix it live.
1. **Revert the State (TrueNAS):** If the failure was stateful (e.g., a bad database migration), immediately log into TrueNAS and rollback the primary dataset to the automated ZFS snapshot taken right before the deployment.
2. **Revert the Code (Forgejo):** Use the Forgejo UI to click "Revert" on the problematic Pull Request. This creates a new commit restoring the previous flake.nix state.
3. **Autonomous Recovery:** Within 60 seconds, the production VM's comin agent will detect the new commit on main, pull the reverted code, apply the old configuration, and reconnect to the restored TrueNAS dataset.

102
flake.lock generated Normal file
View file

@ -0,0 +1,102 @@
{
"nodes": {
"flake-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"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1760524057,
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",
"utils": "utils"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1760393368,
"narHash": "sha256-8mN3kqyqa2PKY0wwZ2UmMEYMcxvNTwLaOrrDsw6Qi4E=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "ab8d56e85b8be14cff9d93735951e30c3e86a437",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"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": {
"flake-utils": [
"flake-utils"
]
},
"locked": {
"lastModified": 1738591040,
"narHash": "sha256-4WNeriUToshQ/L5J+dTSWC5OJIwT39SEP7V7oylndi8=",
"owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus",
"rev": "afcb15b845e74ac5e998358709b2b5fe42a948d1",
"type": "github"
},
"original": {
"owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -16,11 +16,6 @@
inputs.flake-utils.follows = "flake-utils";
};
comin = {
url = "github:nlewo/comin";
inputs.nixpkgs.follows = "nixpkgs";
};
pre-commit-hooks = {
url = "github:cachix/pre-commit-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
@ -29,7 +24,7 @@
outputs = inputs@{
self, nixpkgs,
flake-utils, sops-nix, utils, comin, pre-commit-hooks,
flake-utils, sops-nix, utils, pre-commit-hooks,
...
}:
let
@ -67,19 +62,9 @@
./users
sops-nix.nixosModules.sops
comin.nixosModules.comin
# Base comin configuration for all nodes
({ config, pkgs, ... }: {
services.comin = {
enable = true;
remotes = [{
name = "origin";
# Replace with actual internal forgejo URL once available
url = "https://github.com/example/nix-config.git";
branches.main.name = "v2";
}];
};
# Inject git revision for our custom GitOps module to query
system.configurationRevision = self.rev or self.dirtyRev or "unknown";
})
];
};

41
hosts/ACE/default.nix Normal file
View file

@ -0,0 +1,41 @@
{ pkgs, ... }:
{
config = {
homelab = {
services.actions.enable = true;
virtualisation.guest.enable = true;
};
networking = {
hostName = "ACE";
hostId = "aaaa4100";
domain = "depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.41";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,90 @@
{ pkgs, ... }:
{
config = {
environment = {
etc."nixos/tailscale-authkey".text = ''
tskey-auth-k1tfJLTnGB11CNTRL-HhnegtfNzQ3G8h71SC2DR38PFXwseQiu
'';
systemPackages = with pkgs; [
ethtool
];
};
homelab = {
apps = {
speedtest.enable = true;
technitiumDNS.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostName = "Binnenpost";
hostId = "aaaa1001";
domain = "depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.89";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
services = {
networkd-dispatcher = {
enable = true;
rules."50-tailscale" = {
onState = ["routable"];
script = ''
${pkgs.ethtool}/bin/ethtool -K ens18 rx-udp-gro-forwarding on rx-gro-list off
'';
};
};
tailscale = {
enable = true;
useRoutingFeatures = "server";
authKeyFile = "/etc/nixos/tailscale-authkey";
extraUpFlags = [
"--advertise-routes=192.168.0.0/24"
"--exit-node"
];
};
};
virtualisation.oci-containers.containers.traefik.labels = {
"traefik.http.routers.roxanne.rule" = "Host(`roxanne.depeuter.dev`)";
"traefik.http.services.roxanne.loadbalancer.server.url" = "https://192.168.0.13:8006";
"traefik.http.routers.hugo.rule" = "Host(`hugo.depeuter.dev`)";
"traefik.http.services.hugo.loadbalancer.server.url" = "https://192.168.0.11:444";
};
system.stateVersion = "24.05";
};
}

50
hosts/Builder/default.nix Normal file
View file

@ -0,0 +1,50 @@
{ pkgs, ... }:
{
config = {
networking = {
hostName = "Builder";
hostId = "aaaa4200";
domain = "depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.42";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1"
"1.0.0.1"
];
};
# Dedicated builder user
users.users.builder = {
isNormalUser = true;
description = "Nix Remote Builder";
# You must add the public key corresponding to `builder-ssh-key` here
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1... TODO: replace with build-farm-key.pub"
];
};
# Optimize nix settings for a build farm
nix.settings.trusted-users = [ "builder" ];
nix.settings.cores = 0; # Use all cores
nix.settings.max-jobs = "auto";
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,72 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
bind9.enable = true;
homepage = {
enable = true;
exposePort = true;
};
traefik.enable = true;
plex.enable = true;
solidtime.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa9100";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.91";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
virtualisation.oci-containers.containers = {
pgadmin = {
image = "dpage/pgadmin4:8.11.0";
ports = [
"30056:80/tcp"
];
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";
# NOTE Required
# The password used when setting up the initial administrator account to login to pgAdmin.
PGADMIN_DEFAULT_PASSWORD = "ChangeMe";
};
autoStart = true;
};
};
};
}

45
hosts/Gitea/default.nix Normal file
View file

@ -0,0 +1,45 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps.gitea.enable = true;
virtualisation.guest.enable = true;
users.admin = {
enable = true;
authorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFrp6aM62Bf7bj1YM5AlAWuNrANU3N5e8+LtbbpmZPKS"
];
};
};
networking = {
hostId = "aaaa1500";
domain = "depeuter.dev";
enableIPv6 = true;
useDHCP = false;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.24";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

263
hosts/Ingress/default.nix Normal file
View file

@ -0,0 +1,263 @@
{ config, pkgs, modulesPath, lib, system, ... }:
{
config = {
homelab.virtualisation.guest.enable = true;
networking = {
hostName = "Ingress";
hostId = "aaaa1000";
domain = "depeuter.dev";
enableIPv6 = true;
useDHCP = false;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.10";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
firewall = {
enable = true;
allowedTCPPorts = [
80 # HTTP
443 # HTTPS
];
};
};
security.acme = {
acceptTerms = true;
defaults = {
inherit (config.services.nginx) group;
dnsPropagationCheck = true;
dnsProvider = "cloudflare";
dnsResolver = "1.1.1.1:53";
email = "tibo.depeuter@telenet.be";
credentialFiles = {
CLOUDFLARE_DNS_API_TOKEN_FILE = "/var/lib/secrets/depeuter-dev-cloudflare-api-token";
};
reloadServices = [ "nginx" ];
};
certs = {
"depeuter.dev" = {
domain = "depeuter.dev";
extraDomainNames = [ "*.depeuter.dev" ];
};
"cloud.depeuter.dev" = { };
"git.depeuter.dev" = { };
"home.depeuter.dev" = { };
"jelly.depeuter.dev" = { };
"vault.depeuter.dev" = { };
};
};
# List services that you want to enable.
services = {
# Enable Nginx as a reverse proxy
nginx = let
nextcloud = {
host = "192.168.0.23";
officePort = 8080;
};
in {
enable = true;
# Use recommended settings
# recommendedGzipSettings = true;
# recommendedOptimisation = true;
# recommendedProxySettings = true;
# recommendedTlsSettings = true;
# Only allow PFS-enabled ciphers with AES256
sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
upstreams.docservice.servers."${nextcloud.host}:${toString nextcloud.officePort}" = {};
appendHttpConfig = ''
map $http_x_forwarded_proto $the_scheme {
default $http_x_forwarded_proto;
"" $scheme;
}
map $http_x_forwarded_host $the_host {
default $http_x_forwarded_host;
"" $host;
}
map $http_upgrade $proxy_connection {
default upgrade;
"" close;
}
'';
# Define hosts
virtualHosts = {
# Disable automatic routing.
"default" = {
locations."/".return = "301 https://youtu.be/dQw4w9WgXcQ";
default = true;
};
"cloud.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations = {
"/" = {
proxyPass = "http://${nextcloud.host}";
extraConfig = ''
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
fastcgi_request_buffering off;
'';
};
"/office/" = {
proxyPass = "http://${nextcloud.host}:${toString nextcloud.officePort}/";
priority = 500;
recommendedProxySettings = false;
extraConfig = ''
proxy_http_version 1.1;
'';
};
};
extraConfig = ''
client_max_body_size 10G;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Forwarded-Host $the_host/office;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
"home.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://192.168.0.21:8123";
extraConfig = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
'';
};
};
"jelly.depeuter.dev" = let
jellyfin = {
host = "192.168.0.94";
port = 8096;
};
in {
enableACME = true;
forceSSL = true;
locations = {
"/" = {
proxyPass = "http://${jellyfin.host}:${toString jellyfin.port}";
extraConfig = ''
# Proxy main Jellyfin traffic
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
proxy_buffering off;
'';
};
"/socket" = {
proxyPass = "http://${jellyfin.host}:${toString jellyfin.port}";
extraConfig = ''
# Proxy Jellyfin Websockets traffic
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
'';
};
};
extraConfig = ''
client_max_body_size 512M;
# Security / XSS Mitigation Headers
# NOTE: X-Frame-Options may cause issues with the webOS app
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
# Permissions policy. May cause issues with some clients
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always;
# Content Security Policy
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
# Enforces https content and restricts JS/CSS to origin
# External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.
# NOTE: The default CSP headers may cause issues with the webOS app
add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'";
'';
};
"git.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://192.168.0.24:3000";
extraConfig = ''
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10G;
keepalive_timeout 600s;
proxy_buffers 4 256k; # Number and size of buffers for reading response
proxy_buffer_size 256k; # Buffer for the first part of the response
proxy_busy_buffers_size 256k; # Max size of busy buffers
proxy_http_version 1.1;
proxy_read_timeout 600s;
proxy_temp_file_write_size 256k; # Size of temp file for large responses
'';
};
"vault.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations = {
"/" = {
proxyPass = "http://192.168.0.22:10102";
proxyWebsockets = true;
};
"~ ^/admin".return = 403;
};
};
"rss.depeuter.dev" = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://192.168.92:${toString config.homelab.apps.freshrss.port}";
};
};
};
};
system.stateVersion = "24.05";
};
}

0
hosts/Isabel/.keep Normal file
View file

View file

@ -0,0 +1,32 @@
- Office:
- Zoho Mail:
- icon: zohomail
href: https://mail.zoho.eu
- Network:
- Cloudlfare:
- icon: cloudflare
href: https://dash.cloudflare.com
- Pulsetic:
- href: https://status.depeuter.dev
icon: https://pulsetic.com/favicon-196x196.png
- Telenet Internet usage:
- icon: https://static.telenet.be/assets/favicon/favicon.ico
href: https://www2.telenet.be/nl/klantenservice/raadpleeg-je-internetverbruik/
- Telenet Modem:
- icon: https://static.telenet.be/assets/favicon/favicon.ico
# href: https://mijn.telenet.be/mijntelenet/rgw/settings.do?identifier=u381160&action=showAdvancedSettings
href: https://www2.telenet.be/residential/nl/mijn-telenet/je-thuisnetwerk#/mainnavitem=hgw/mainnavitemid=item-1/subnavitem=modem_general
- TransIP:
- icon: https://www.transip.eu/cache-60c9b25f/img/transip-new/favicons/favicon.png
href: https://www.transip.eu/cp/
- Homemade:
- AI-Transparency:
- href: https://ai-transparency.depeuter.dev
icon: https://ai-transparency.depeuter.dev/img/transparency.png
- Down-message:
- href: https://down.depeuter.dev
icon: https://down.depeuter.dev/assets/icon.jpg
- Portfolio:
- href: https://tibo.depeuter.dev
icon: https://tibo.depeuter.dev/assets/owl_circuit.png

View file

@ -0,0 +1,30 @@
- Networking:
- AXE5400 Tri-Band Wi-Fi 6E Router:
description: Router
href: https://tplinkwifi.net
ping: http://192.168.0.1
icon: tp-link
- Traefik Isabel:
description: Reverse proxy manager
href: https://traefik.isabel.depeuter.dev/dashboard/#
ping: https://traefik.isabel.depeuter.dev/dashboard/#
icon: traefik
widget:
type: traefik
url: https://traefik.isabel.depeuter.dev
- Traefik Niko:
description: Reverse proxy manager
href: https://traefik.niko.depeuter.dev/dashboard/#
ping: https://traefik.niko.depeuter.dev/dashboard/#
- Technitium DNS Isabel:
description: DNS server
href: https://dns.Isabel.depeuter.dev
ping: http://192.168.0.13:53
icon: technitium
- Technitium DNS Niko:
description: DNS server
href: https://dns.niko.depeuter.dev
ping: http://192.168.0.30:53
icon: technitium

255
hosts/Isabel/default.nix Normal file
View file

@ -0,0 +1,255 @@
{ config, pkgs, ... }:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# Use the systemd-boot EFI boot loader.
boot.loader = {
systemd-boot.enable = true;
efi = {
canTouchEfiVariables = true;
efiSysMountPoint = "/boot/efi";
};
};
console = {
font = "Lat2-Terminus16";
keyMap = "us";
};
# List packages installed in the system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
];
environment.etc = {
"homepage/bookmarks.yaml".text = ''
- Office:
- Zoho Mail:
- icon: zohomail
href: https://mail.zoho.eu
- Network:
- Cloudlfare:
- icon: cloudflare
href: https://dash.cloudflare.com
- TransIP:
- icon: https://www.transip.eu/cache-60c9b25f/img/transip-new/favicons/favicon.png
href: https://www.transip.eu/cp/
- Telenet Internet usage:
- icon: https://static.telenet.be/assets/favicon/favicon.ico
href: https://www2.telenet.be/nl/klantenservice/raadpleeg-je-internetverbruik/
- Telenet Modem:
- icon: https://static.telenet.be/assets/favicon/favicon.ico
# href: https://mijn.telenet.be/mijntelenet/rgw/settings.do?identifier=u381160&action=showAdvancedSettings
href: https://www2.telenet.be/residential/nl/mijn-telenet/je-thuisnetwerk#/mainnavitem=hgw/mainnavitemid=item-1/subnavitem=modem_general
- Pulsetic:
- href: https://status.depeuter.dev
icon: https://pulsetic.com/favicon-196x196.png
- Homemade:
- AI-Transparency:
- href: https://ai-transparency.depeuter.dev
icon: https://ai-transparency.depeuter.dev/img/transparency.png
- Down-message:
- href: https://down.depeuter.dev
icon: https://down.depeuter.dev/assets/icon.jpg
- Portfolio:
- href: https://tibo.depeuter.dev
icon: https://tibo.depeuter.dev/assets/owl_circuit.png
'';
"homepage/services.yaml".text = ''
- Networking:
- Traefik Isabel:
description: Reverse proxy manager
href: https://traefik.isabel.depeuter.dev/dashboard/#
ping: https://traefik.isabel.depeuter.dev/dashboard/#
icon: traefik
widget:
type: traefik
url: https://traefik.isabel.depeuter.dev
- Traefik Niko:
description: Reverse proxy manager
href: https://traefik.niko.depeuter.dev/dashboard/#
ping: https://traefik.niko.depeuter.dev/dashboard/#
icon: traefik
widget:
type: traefik
url: https://traefik.niko.depeuter.dev
'';
"homepage/settings.yaml".text = ''
---
# For configuration options and examples, please see:
# https://gethomepage.dev/en/configs/settings
providers:
openweathermap: openweathermapapikey
weatherapi: weatherapiapikey
'';
};
homelab.apps.technitiumDNS.enable = true;
# Select internationalisation properties.
i18n.defaultLocale = "en_GB.utf8";
networking = {
hostName = "Hugo-Isabel";
domain = "depeuter.dev";
enableIPv6 = true;
# Open ports in the firewall.
firewall = {
enable = true;
};
networkmanager.enable = true;
};
# List services that you want to enable:
services = {
tailscale = {
enable = true;
useRoutingFeatures = "server";
authKeyFile = "/etc/nixos/tailscale-authkey";
extraUpFlags = [
"--advertise-routes=192.168.0.0/24"
"--exit-node"
];
};
# Fix DNS issues. See:
# https://github.com/tailscale/tailscale/issues/4254
# resolved.enable = true;
};
system.stateVersion = "24.05";
security.sudo = {
enable = true;
};
virtualisation = {
docker = {
enable = true;
autoPrune.enable = true;
};
oci-containers = {
backend = "docker";
containers = {
reverse-proxy = {
hostname = "traefik";
image = "traefik:v3.0";
cmd = [
"--api.insecure=true"
# Add Docker provider
"--providers.docker=true"
"--providers.docker.exposedByDefault=false"
# Add web entrypoint
"--entrypoints.web.address=:80/tcp"
"--entrypoints.web.http.redirections.entrypoint.to=websecure"
"--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Add websecure entrypoint
"--entrypoints.websecure.address=:443/tcp"
"--entrypoints.websecure.http.tls=true"
"--entrypoints.websecure.http.tls.certResolver=letsencrypt"
"--entrypoints.websecure.http.tls.domains[0].main=depeuter.dev"
"--entrypoints.websecure.http.tls.domains[0].sans=*.depeuter.dev"
"--entrypoints.websecure.http.tls.domains[1].sans=*.isabel.depeuter.dev"
"--entrypoints.websecure.http.tls.domains[2].sans=*.jelly.depeuter.dev"
# Certificates
"--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
"--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
"--certificatesresolvers.letsencrypt.acme.email=tibo.depeuter@telenet.be"
"--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
# Additional routes
];
ports = [
"80:80/tcp"
"443:443/tcp"
# "8080:8080/tcp" # The Web UI (enabled by --api.insecure=true)
];
environment = {
# TODO Hide this!
"CLOUDFLARE_DNS_API_TOKEN" = "6Vz64Op_a6Ls1ljGeBxFoOVfQ-yB-svRbf6OyPv2";
};
environmentFiles = [
];
volumes = [
"/var/run/docker.sock:/var/run/docker.sock:ro" # So that Traefik can listen to the Docker events
"letsencrypt:/letsencrypt"
];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.traefik.rule" = "Host(`traefik.isabel.depeuter.dev`)";
"traefik.http.services.traefik.loadbalancer.server.port" = "8080";
};
autoStart = true;
};
feishin = {
hostname = "feishin";
image = "ghcr.io/jeffvli/feishin:0.7.1";
ports = [
# "9180:9180/tcp" # Web player (HTTP)
];
environment = {
# pre defined server name
SERVER_NAME = "Hugo";
# When true AND name/type/url are set, only username/password can be toggled
SERVER_LOCK = "true";
# navidrome also works
SERVER_TYPE = "jellyfin";
# http://address:port
SERVER_URL= "https://jelly.depeuter.dev";
TZ = config.time.timeZone;
};
labels = {
"traefik.enable" = "true";
"traefik.http.routers.feishin.rule" = "Host(`music.depeuter.dev`)";
"traefik.http.services.feishin.loadbalancer.server.port" = "9180";
"traefik.tls.options.default.minVersion" = "VersionTLS13";
};
autoStart = true;
};
dashboard = {
hostname = "dashboard";
image = "ghcr.io/gethomepage/homepage:v0.9.3";
ports = [
# "3000:3000/tcp"
];
volumes = [
"/etc/homepage:/app/config" # Make sure your local config directory exists
"/var/run/docker.sock:/var/run/docker.sock:ro" # optional, for docker integrations
];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.dashboard.rule" = "Host(`dash.depeuter.dev`)";
"traefik.http.services.dashboard.loadbalancer.server.port" = "3000";
"traefik.tls.options.default.minVersion" = "VersionTLS13";
};
autoStart = true;
};
prometheus = {
hostname = "prometheus";
image = "prom/prometheus:v2.45.6";
ports = [
# "127.0.0.1:9090:9090/tcp"
];
labels = {
"traefik.enable" = "true";
"traefik.http.routers.prometheus.rule" = "Host(`prometheus.isabel.depeuter.dev`)";
"traefik.http.services.prometheus.loadbalancer.server.port" = "9090";
"traefik.tls.options.default.minVersion" = "VersionTLS13";
};
autoStart = true;
};
};
};
};
}

View file

@ -0,0 +1,39 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ata_piix" "xhci_pci" "ahci" "sd_mod" "sr_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-label/NIX-ROOT";
fsType = "ext4";
};
fileSystems."/boot/efi" =
{ device = "/dev/disk/by-label/NIX-BOOT";
fsType = "vfat";
};
swapDevices =
[ { device = "/dev/disk/by-label/SWAP"; }
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens3.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

119
hosts/Niko/default.nix Normal file
View file

@ -0,0 +1,119 @@
{ config, pkgs, ... }:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
homelab = {
apps = {
technitiumDNS.enable = true;
traefik.enable = true;
};
users.deploy.enable = true;
};
# Use the systemd-boot EFI boot loader.
boot.loader = {
systemd-boot.enable = true;
efi = {
canTouchEfiVariables = true;
efiSysMountPoint = "/boot/efi";
};
};
console = {
font = "Lat2-Terminus16";
keyMap = "us";
};
# List packages installed in the system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
cifs-utils
];
hardware = {
enableRedistributableFirmware = true;
enableAllFirmware = true;
graphics.enable = true;
};
# Select internationalisation properties.
i18n.defaultLocale = "en_GB.UTF-8";
networking = {
hostName = "Niko";
domain = "depeuter.dev";
enableIPv6 = true;
# Open ports in the firewall.
firewall = {
enable = true;
};
networkmanager.enable = true;
extraHosts = ''
192.168.0.11 jelly.depeuter.dev
'';
};
nixpkgs.config.allowUnfree = true;
# List services that you want to enable:
services = {
# Cage, a wayland kiosk service
cage = {
enable = true;
environment = {
# Do not fail when there are no input devices.
# WLR_LIBINPUT_NO_DEVICES = "1";
};
extraArguments = [
"-d" # Don't draw client side decorations, when possible
# "-m" "last" # Use only the last connected output
"-s" # Allow VT switching
];
program = "/home/jellyfin-mpv-shim/start.sh";
user = config.users.users.jellyfin-mpv-shim.name;
};
pulseaudio.enable = true;
tailscale = {
enable = true;
useRoutingFeatures = "server";
authKeyFile = "/etc/nixos/tailscale-authkey";
extraUpFlags = [
"--advertise-routes=192.168.0.0/24"
"--exit-node"
];
};
# Fix DNS issues. See:
# https://github.com/tailscale/tailscale/issues/4254
# resolved.enable = true;
};
# Define a user account. Don't forget to set a password with 'passwd'.
users.users.jellyfin-mpv-shim = {
description = "Jellyfin MPV Shim User";
isNormalUser = true;
extraGroups = [
config.users.groups.audio.name
config.users.groups.video.name
];
packages = with pkgs; [
jellyfin-mpv-shim
mpv
socat
];
};
systemd.services."cage-tty1".serviceConfig.Restart = "always";
system.stateVersion = "24.05";
}

View file

@ -0,0 +1,53 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd = {
availableKernelModules = [
"xhci_pci"
"ahci"
"usb_storage"
"sd_mod"
];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
fileSystems = {
"/" = {
device = "/dev/disk/by-uuid/20b7eff3-fca5-4b60-a5a9-13219f70ce23";
fsType = "ext4";
};
"/boot/efi" = {
device = "/dev/disk/by-uuid/0B6D-0DCD";
fsType = "vfat";
};
"/media/photos" = {
device = "//192.168.0.11/CANVAS";
fsType = "cifs";
options = let
# This line prevents hanging on network split
automount_opts = "x-systemd.automount,noauto,x-systemd.idle-timeout=60,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s,user,users";
in ["${automount_opts},credentials=/etc/nixos/smb-secrets,uid=1002,gid=100"];
};
};
swapDevices = [
{ device = "/dev/disk/by-uuid/f3679da0-45b3-45c0-a1d0-af8d771a7dbf"; }
];
networking = {
hostId = "7a139e16";
useDHCP = lib.mkDefault true;
};
powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View file

@ -0,0 +1,48 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
calibre.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa2100";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.31";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,48 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
arr.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa2300";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.33";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,98 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps.jellyfin.enable = true;
virtualisation.guest.enable = true;
};
networking = {
hostId = "aaaa2200";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.94";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.11";
### Nvidia GPU support ###
services.xserver.videoDrivers = [ "nvidia" ];
# virtualisation.docker.package = pkgs.nvidia-docker;
nixpkgs.config = {
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"nvidia-x11"
"nvidia-settings"
"nvidia-persistenced"
];
# enable vaapi on OS-level
# packageOverrides = pkgs: {
# vaapiIntel = pkgs.vaapiIntel.override {
# enableHybridCodec = true;
# };
# };
};
hardware = {
graphics = {
enable = true;
# driSupport = true;
# driSupport32Bit = true;
extraPackages = with pkgs; [
# intel-media-driver
# intel-vaapi-driver # previously vaapiIntel
# vaapiVdpau
# intel-compute-runtime # OpenCL filter support (hardware tonemapping and subtitle burn-in)
# unstable.vpl-gpu-rt # QSV on 11th gen or newer
# intel-media-sdk # QSV up to 11th gen
];
};
nvidia = {
package = config.boot.kernelPackages.nvidiaPackages.stable;
# Whether to enable kernel modesetting when using the NVIDIA proprietary driver.
modesetting.enable = true;
# powerManagement = {
# enable = false;
# finegrained = false;
# };
open = false;
nvidiaSettings = false;
# Whether to enable nvidia-persistenced a update for NVIDIA GPU headless mode, i.e. It ensures all GPUs stay awake even during headless mode .
# nvidiaPersistenced = true;
};
nvidia-container-toolkit.enable = true;
};
};
}

View file

@ -0,0 +1,36 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab.virtualisation.guest.enable = true;
networking = {
# TODO hostName = "nixos";
# TODO hostId = "aaaa9000";
domain = "depeuter.dev";
enableIPv6 = true;
useDHCP = false;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.90";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

49
hosts/Testing/default.nix Normal file
View file

@ -0,0 +1,49 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps = {
freshrss.enable = true;
traefik.enable = true;
};
virtualisation.guest.enable = true;
};
networking = {
hostName = "Testing";
hostId = "aaaa9200";
domain = "roxanne.depeuter.dev";
useDHCP = false;
enableIPv6 = true;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
# Open ports in the firewall.
firewall = {
enable = true;
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.92";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -0,0 +1,49 @@
{ config, pkgs, lib, system, ... }:
{
config = {
homelab = {
apps.vaultwarden = {
enable = true;
domain = "https://vault.depeuter.dev";
name = "Hugo's Vault";
};
virtualisation.guest.enable = true;
users.admin = {
enable = true;
authorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJnihoyozOCnm6T9OzL2xoMeMZckBYR2w43us68ABA93"
];
};
};
networking = {
hostId = "aaaa1300";
domain = "depeuter.dev";
enableIPv6 = true;
useDHCP = false;
defaultGateway = {
address = "192.168.0.1";
interface = "ens18";
};
interfaces.ens18 = {
ipv4.addresses = [
{
address = "192.168.0.22";
prefixLength = 24;
}
];
};
nameservers = [
"1.1.1.1" # Cloudflare
"1.0.0.1" # Cloudflare
];
};
system.stateVersion = "24.05";
};
}

View file

@ -1,6 +1,7 @@
{
imports = [
./docker.nix
./gitops.nix
./monitoring.nix
./nfs.nix
./traefik.nix
@ -11,6 +12,7 @@
homelab = {
services.openssh.enable = true;
users.admin.enable = true;
gitops.enable = true;
};
nix.settings.experimental-features = [

141
modules/common/gitops.nix Normal file
View file

@ -0,0 +1,141 @@
{ config, lib, pkgs, ... }:
let
cfg = config.homelab.gitops;
updateScript = pkgs.writeShellApplication {
name = "homelab-gitops-update";
runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ];
text = ''
set -euo pipefail
REMOTE_URL="${cfg.repoUrl}"
BRANCH="${cfg.branch}"
echo "Checking remote hash for $REMOTE_URL branch $BRANCH..."
# Fetch remote hash, fallback to unknown if it fails
REMOTE_HASH=$(git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true)
if [ -z "$REMOTE_HASH" ]; then
echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe."
REMOTE_HASH="unknown_remote"
fi
LOCAL_HASH="unknown_local"
if [ -f /run/current-system/configurationRevision ]; then
LOCAL_HASH=$(cat /run/current-system/configurationRevision)
fi
echo "Remote hash: $REMOTE_HASH"
echo "Local hash: $LOCAL_HASH"
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then
echo "Hashes match. No update needed."
exit 0
fi
echo "Hashes differ or unknown. Triggering nixos-rebuild..."
# Trigger the build and switch
nixos-rebuild switch --flake "git+$REMOTE_URL?ref=$BRANCH"
echo "Update successful."
'';
};
in {
options.homelab.gitops = {
enable = lib.mkEnableOption "Custom GitOps native deployment system";
repoUrl = lib.mkOption {
type = lib.types.str;
default = "https://git.depeuter.dev/Bos55/nix-config.git";
description = "The repository URL to pull configurations from.";
};
branch = lib.mkOption {
type = lib.types.str;
default = "v2";
description = "The branch to deploy.";
};
useBuilder = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to use the central Builder host to compile packages.";
};
};
config = lib.mkIf cfg.enable {
# 1. Systemd Service and Timer for polling
systemd.services.homelab-gitops = {
description = "Homelab GitOps Update Service";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${updateScript}/bin/homelab-gitops-update";
# Must run as root to rebuild the system
User = "root";
};
};
systemd.timers.homelab-gitops = {
description = "Timer for Homelab GitOps Update Service";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnUnitActiveSec = "5m";
RandomizedDelaySec = "30s";
};
};
# 2. Webhook listener for instant trigger
sops.secrets."webhook-secret" = {};
services.webhook = {
enable = true;
port = 9000;
hooks = {
gitops = {
execute-command = "${pkgs.systemd}/bin/systemctl";
pass-arguments-to-command = [
{ source = "string"; name = "start"; }
{ source = "string"; name = "homelab-gitops.service"; }
];
trigger-rule = {
match = {
type = "payload-hash-sha256";
secret = "{{ getenv \"WEBHOOK_SECRET\" }}";
parameter = {
source = "header";
name = "X-Forgejo-Signature";
};
};
};
};
};
};
# Inject the secret as an environment variable into the webhook service
systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path;
# 3. Builder Configuration
sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {};
nix.buildMachines = lib.mkIf cfg.useBuilder [
{
hostName = "builder.depeuter.dev"; # Must be routable from nodes
system = "x86_64-linux";
sshUser = "builder";
sshKey = config.sops.secrets."builder-ssh-key".path;
maxJobs = 4;
speedFactor = 2;
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
}
];
nix.distributedBuilds = lib.mkIf cfg.useBuilder true;
};
}