From a2799f5e542cad5630965d3f1406528d3d5a28b4 Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Tue, 27 Jan 2026 21:41:46 -0500 Subject: [PATCH] Build a new monitoring stack --- modules/hosts/common/secrets.yaml | 7 +- .../nixos/hetznix01/post-install/default.nix | 1 + .../hetznix01/post-install/monitoring.nix | 129 ++++++ .../nixos/hetznix02/post-install/default.nix | 1 + .../hetznix02/post-install/monitoring.nix | 129 ++++++ .../hosts/nixos/kiosk-entryway/default.nix | 1 + .../hosts/nixos/kiosk-entryway/monitoring.nix | 129 ++++++ .../nixos/nixnuc/containers/nginx-proxy.nix | 181 --------- modules/hosts/nixos/nixnuc/default.nix | 203 ++-------- .../nixnuc/grafana-files/alert-rules.yaml | 298 ++++++++++++++ .../nixos/nixnuc/hardware-configuration.nix | 5 - .../hosts/nixos/nixnuc/monitoring-stack.nix | 381 ++++++++++++++++++ modules/hosts/nixos/nixnuc/secrets.yaml | 7 +- 13 files changed, 1104 insertions(+), 368 deletions(-) create mode 100644 modules/hosts/nixos/hetznix01/post-install/monitoring.nix create mode 100644 modules/hosts/nixos/hetznix02/post-install/monitoring.nix create mode 100644 modules/hosts/nixos/kiosk-entryway/monitoring.nix delete mode 100644 modules/hosts/nixos/nixnuc/containers/nginx-proxy.nix create mode 100644 modules/hosts/nixos/nixnuc/grafana-files/alert-rules.yaml create mode 100644 modules/hosts/nixos/nixnuc/monitoring-stack.nix diff --git a/modules/hosts/common/secrets.yaml b/modules/hosts/common/secrets.yaml index 98375e1..05dfce3 100644 --- a/modules/hosts/common/secrets.yaml +++ b/modules/hosts/common/secrets.yaml @@ -5,7 +5,8 @@ hetzner_lego_env: ENC[AES256_GCM,data:xRADnkMC/mTq8/oRpZ+NYTStB9qX2N6V0GNIpGsXNe restic_env: ENC[AES256_GCM,data:FCYR8tkClRwfcjUotcr28D6uRz7sNihn50nw38CaYnqOD/U9+5kU0iAPSvqAbeuw+xUoKKKAPAfMHI12dPTYt17Wz1N7i4a+MRkiIR9pjyv5KZTK59G+,iv:jStc8GMbZUQUgooZiRdImSZskdckYN1cRm2gsKbUyYY=,tag:HpQQIj1j7fjCmxkSeY/k4g==,type:str] restic_repo: ENC[AES256_GCM,data:kCoNYVKwB87W4h5doa3IXj4n,iv:jKEw/Hki/tp3RSTsRB4dlg593I5B4pCLBav84ADCh70=,tag:+GFF5vHOVw0r/G8BbhcCjw==,type:str] restic_password: ENC[AES256_GCM,data:PfQsxJul1Qpt3WQoUEI941l+yng3lVjhDd8=,iv:U5KjhcVqyksN2ay19RBjNhYIB31tUbfNRIqCEx/+Wbc=,tag:jsoU+B1mjAprPK+M5I0pAQ==,type:str] -uptimekuma_grafana_api_key: ENC[AES256_GCM,data:irDlzCrDO5n/pMRHagueBTrXmm3herjOSioAz9E5O4a9dJD4ALhp5LPpmTs=,iv:oJhm8eZWSEvqF3Whv/UrS6sgIRdrZ0eqWF98vdwD0V8=,tag:HvOBFo4CJ9NLSz9FwMCP2w==,type:str] +vmagent_push_pw: ENC[AES256_GCM,data:Ey82+FQWOdTn8iTcCbn73A2pYIh8dfD6Dw==,iv:0xEtakEOzZd2wbg407aA0rLeZqPo5NpqWdTSD3VQ+yw=,tag:wINlAjbCYnw9L4odxKJFAg==,type:str] +uptimekuma_grafana_api_key: ENC[AES256_GCM,data:cXdbdiEa4dqigcojFgE8Wf4esjbF7wmrx5BuEQltfwM5fNJQG6LP/8ZcopI=,iv:dUftPkEyy1DMq5fJTQiJlDab2UMAbwJt+81p35eBrAU=,tag:Nh+X6lTKB5EboADaSVK00g==,type:str] wifi_creds: ENC[AES256_GCM,data:9lgTtI8YHyCHrvqss4W7coLnqfOAoQzrCQne6dLv0x66pt7jLo4Y6YSd3TklRTurS9usvNk3sg==,iv:6g86hOmpnOxf4p4C+wPit7EP0DD+xb+cINiWRJnTRDM=,tag:ZW336IhXtrf5l5n/RJecoQ==,type:str] sops: age: @@ -108,7 +109,7 @@ sops: ODFjcWxtRjkweGJvdzdWSEphMHRCdm8Kx0amHgaZZR26c+VRVTyBEnm+w5c5nA7R txHj1U349LbfEsovTqZAL1o2WuX+gmXSj1aeXPKW+S0bIagC6dDacA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-01-25T02:27:36Z" - mac: ENC[AES256_GCM,data:83QyyqQqy9dkW1PIjo6fFDV1oLv3GCrfV9xiq5pZJwW9uhvNTi1LvR4bX55foK7lPBRmtW9xCRIGWCm1nORTJH1ae3cdRuZ+moUictpn0AuqJY/E/4+Nlr7TMdJUj3NI9bfGkJ+BoizGvPg+43ubobnl/+PH7/3Tu0omXkhUB9A=,iv:tcBMolA2hcVKfUhPnK6P9H1Xl1n5jMH+Qo0vEe5CgkE=,tag:L9gpXtvYIYN/gRl7rBPqtA==,type:str] + lastmodified: "2026-02-01T03:15:26Z" + mac: ENC[AES256_GCM,data:gGk69AnZNAlA5fxViXZQMpGStn8v2L/IyIUAshyrlAKu8dcTDQfCDfC2kTlo0Q/gwhqchSKYFFKWWze7EUwUWmlYjJu5MEEK4aQ0HEObff27AKQloOln1X3jiIR7bnsKoakbAKJWYpGf6PWClbH64mjoJhBroG5amAwE25bevDE=,iv:bO2kFuGcq/1j2uo2y5/UwOHNQ5A8orStI190NMdAr1E=,tag:7xwClQ28I695qDICc0GS+A==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/modules/hosts/nixos/hetznix01/post-install/default.nix b/modules/hosts/nixos/hetznix01/post-install/default.nix index 8741d3f..1e49b0d 100644 --- a/modules/hosts/nixos/hetznix01/post-install/default.nix +++ b/modules/hosts/nixos/hetznix01/post-install/default.nix @@ -7,6 +7,7 @@ in { ../../../common/linux/restic.nix ./containers/emqx.nix ./matrix-synapse.nix + ./monitoring.nix #./mosquitto.nix ./nginx.nix ]; diff --git a/modules/hosts/nixos/hetznix01/post-install/monitoring.nix b/modules/hosts/nixos/hetznix01/post-install/monitoring.nix new file mode 100644 index 0000000..c9af29a --- /dev/null +++ b/modules/hosts/nixos/hetznix01/post-install/monitoring.nix @@ -0,0 +1,129 @@ +{ config, pkgs, ... }: let + metrics_server = "https://monitoring.home.technicalissues.us/remotewrite"; +in { + services = { + vmagent = { + enable = true; + package = pkgs.victoriametrics; + + # Prometheus-style scrape configuration + prometheusConfig = { + global.scrape_interval = "15s"; + + scrape_configs = [ + { + job_name = "node"; + static_configs = [ + { targets = ["127.0.0.1:9100"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + regex = "127.0.0.1.*"; + replacement = "${config.networking.hostName}"; + } + ]; + } + + # Nginx exporter + { + job_name = "nginx"; + static_configs = [ + { targets = ["127.0.0.1:9113"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + replacement = "${config.networking.hostName}"; + } + ]; + } + ]; + }; + + # Remote write to VictoriaMetrics + remoteWrite = { + basicAuthUsername = "metricsshipper"; + basicAuthPasswordFile = config.sops.secrets.vmagent_push_pw.path; + url = metrics_server; + }; + + extraArgs = [ + # Pass other remote write flags the module does not expose natively: + "-remoteWrite.flushInterval=10s" + "-remoteWrite.maxDiskUsagePerURL=1GB" + + # Prevent vmagent from failing the entire scrape if a target is down: + "-promscrape.suppressScrapeErrors" + + # Enable some debugging info suggested by the interface on port 8429 + "-promscrape.dropOriginalLabels=false" + ]; + }; + + # ---------------------------- + # Exporters (using built-in NixOS modules) + # ---------------------------- + + # Node exporter - using the built-in module + prometheus.exporters.node = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9100; + enabledCollectors = [ + "systemd" + ]; + extraFlags = [ + "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|run|tmp|var/lib/docker/.+)($|/)" + "--collector.diskstats.device-exclude=^(loop|ram|fd|sr|dm-|nvme[0-9]n[0-9]p[0-9]+_crypt)$" + ]; + }; + + # Nginx exporter - using the built-in module + prometheus.exporters.nginx = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9113; + scrapeUri = "https://127.0.0.1/server_status"; + sslVerify = false; + }; + }; + + # ---------------------------- + # Users and groups for service accounts + # ---------------------------- + users.users.vmagent = { + isSystemUser = true; + group = "vmagent"; + }; + + users.groups.vmagent = {}; + + # ---------------------------- + # SOPS secrets configuration + # ---------------------------- + sops = { + secrets = { + vmagent_push_pw = { + owner = "vmagent"; + restartUnits = ["vmagent.service"]; + sopsFile = ../../../common/secrets.yaml; + }; + }; + }; +} + diff --git a/modules/hosts/nixos/hetznix02/post-install/default.nix b/modules/hosts/nixos/hetznix02/post-install/default.nix index 4545616..2500481 100644 --- a/modules/hosts/nixos/hetznix02/post-install/default.nix +++ b/modules/hosts/nixos/hetznix02/post-install/default.nix @@ -1,6 +1,7 @@ { config, username, ... }: { imports = [ ../../../common/linux/lets-encrypt.nix + ./monitoring.nix ./nginx.nix ]; diff --git a/modules/hosts/nixos/hetznix02/post-install/monitoring.nix b/modules/hosts/nixos/hetznix02/post-install/monitoring.nix new file mode 100644 index 0000000..c9af29a --- /dev/null +++ b/modules/hosts/nixos/hetznix02/post-install/monitoring.nix @@ -0,0 +1,129 @@ +{ config, pkgs, ... }: let + metrics_server = "https://monitoring.home.technicalissues.us/remotewrite"; +in { + services = { + vmagent = { + enable = true; + package = pkgs.victoriametrics; + + # Prometheus-style scrape configuration + prometheusConfig = { + global.scrape_interval = "15s"; + + scrape_configs = [ + { + job_name = "node"; + static_configs = [ + { targets = ["127.0.0.1:9100"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + regex = "127.0.0.1.*"; + replacement = "${config.networking.hostName}"; + } + ]; + } + + # Nginx exporter + { + job_name = "nginx"; + static_configs = [ + { targets = ["127.0.0.1:9113"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + replacement = "${config.networking.hostName}"; + } + ]; + } + ]; + }; + + # Remote write to VictoriaMetrics + remoteWrite = { + basicAuthUsername = "metricsshipper"; + basicAuthPasswordFile = config.sops.secrets.vmagent_push_pw.path; + url = metrics_server; + }; + + extraArgs = [ + # Pass other remote write flags the module does not expose natively: + "-remoteWrite.flushInterval=10s" + "-remoteWrite.maxDiskUsagePerURL=1GB" + + # Prevent vmagent from failing the entire scrape if a target is down: + "-promscrape.suppressScrapeErrors" + + # Enable some debugging info suggested by the interface on port 8429 + "-promscrape.dropOriginalLabels=false" + ]; + }; + + # ---------------------------- + # Exporters (using built-in NixOS modules) + # ---------------------------- + + # Node exporter - using the built-in module + prometheus.exporters.node = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9100; + enabledCollectors = [ + "systemd" + ]; + extraFlags = [ + "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|run|tmp|var/lib/docker/.+)($|/)" + "--collector.diskstats.device-exclude=^(loop|ram|fd|sr|dm-|nvme[0-9]n[0-9]p[0-9]+_crypt)$" + ]; + }; + + # Nginx exporter - using the built-in module + prometheus.exporters.nginx = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9113; + scrapeUri = "https://127.0.0.1/server_status"; + sslVerify = false; + }; + }; + + # ---------------------------- + # Users and groups for service accounts + # ---------------------------- + users.users.vmagent = { + isSystemUser = true; + group = "vmagent"; + }; + + users.groups.vmagent = {}; + + # ---------------------------- + # SOPS secrets configuration + # ---------------------------- + sops = { + secrets = { + vmagent_push_pw = { + owner = "vmagent"; + restartUnits = ["vmagent.service"]; + sopsFile = ../../../common/secrets.yaml; + }; + }; + }; +} + diff --git a/modules/hosts/nixos/kiosk-entryway/default.nix b/modules/hosts/nixos/kiosk-entryway/default.nix index f3cb9cf..a28e1eb 100644 --- a/modules/hosts/nixos/kiosk-entryway/default.nix +++ b/modules/hosts/nixos/kiosk-entryway/default.nix @@ -2,6 +2,7 @@ imports = [ ./disk-config.nix ./hardware-configuration.nix + ./monitoring.nix ]; system.stateVersion = "24.11"; diff --git a/modules/hosts/nixos/kiosk-entryway/monitoring.nix b/modules/hosts/nixos/kiosk-entryway/monitoring.nix new file mode 100644 index 0000000..c9af29a --- /dev/null +++ b/modules/hosts/nixos/kiosk-entryway/monitoring.nix @@ -0,0 +1,129 @@ +{ config, pkgs, ... }: let + metrics_server = "https://monitoring.home.technicalissues.us/remotewrite"; +in { + services = { + vmagent = { + enable = true; + package = pkgs.victoriametrics; + + # Prometheus-style scrape configuration + prometheusConfig = { + global.scrape_interval = "15s"; + + scrape_configs = [ + { + job_name = "node"; + static_configs = [ + { targets = ["127.0.0.1:9100"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + regex = "127.0.0.1.*"; + replacement = "${config.networking.hostName}"; + } + ]; + } + + # Nginx exporter + { + job_name = "nginx"; + static_configs = [ + { targets = ["127.0.0.1:9113"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + replacement = "${config.networking.hostName}"; + } + ]; + } + ]; + }; + + # Remote write to VictoriaMetrics + remoteWrite = { + basicAuthUsername = "metricsshipper"; + basicAuthPasswordFile = config.sops.secrets.vmagent_push_pw.path; + url = metrics_server; + }; + + extraArgs = [ + # Pass other remote write flags the module does not expose natively: + "-remoteWrite.flushInterval=10s" + "-remoteWrite.maxDiskUsagePerURL=1GB" + + # Prevent vmagent from failing the entire scrape if a target is down: + "-promscrape.suppressScrapeErrors" + + # Enable some debugging info suggested by the interface on port 8429 + "-promscrape.dropOriginalLabels=false" + ]; + }; + + # ---------------------------- + # Exporters (using built-in NixOS modules) + # ---------------------------- + + # Node exporter - using the built-in module + prometheus.exporters.node = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9100; + enabledCollectors = [ + "systemd" + ]; + extraFlags = [ + "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|run|tmp|var/lib/docker/.+)($|/)" + "--collector.diskstats.device-exclude=^(loop|ram|fd|sr|dm-|nvme[0-9]n[0-9]p[0-9]+_crypt)$" + ]; + }; + + # Nginx exporter - using the built-in module + prometheus.exporters.nginx = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9113; + scrapeUri = "https://127.0.0.1/server_status"; + sslVerify = false; + }; + }; + + # ---------------------------- + # Users and groups for service accounts + # ---------------------------- + users.users.vmagent = { + isSystemUser = true; + group = "vmagent"; + }; + + users.groups.vmagent = {}; + + # ---------------------------- + # SOPS secrets configuration + # ---------------------------- + sops = { + secrets = { + vmagent_push_pw = { + owner = "vmagent"; + restartUnits = ["vmagent.service"]; + sopsFile = ../../../common/secrets.yaml; + }; + }; + }; +} + diff --git a/modules/hosts/nixos/nixnuc/containers/nginx-proxy.nix b/modules/hosts/nixos/nixnuc/containers/nginx-proxy.nix deleted file mode 100644 index 0a78727..0000000 --- a/modules/hosts/nixos/nixnuc/containers/nginx-proxy.nix +++ /dev/null @@ -1,181 +0,0 @@ -{ config, ... }: let - http_port = 80; - https_port = 443; - gandi_api = "${config.sops.secrets.gandi_api.path}"; - #gandi_dns_pat = "${config.sops.secrets.gandi_dns_pat.path}"; - home_domain = "home.technicalissues.us"; - backend_ip = "192.168.20.190"; - mini_watcher = "192.168.23.20"; -in { - sops.secrets.gandi_api = { - sopsFile = ../../../../system/common/secrets.yaml; - restartUnits = [ - "container@nginx-proxy.service" - ]; - }; - #sops.secrets.gandi_dns_pat = { - # sopsFile = ../../../../system/common/secrets.yaml; - # restartUnits = [ - # "container@nginx-proxy.service" - # ]; - #}; - - ## - ## Gandi (gandi.net) - ## - ## Single host update - # protocol=gandi - # zone=example.com - # password=my-gandi-access-token - # use-personal-access-token=yes - # ttl=10800 # optional - # myhost.example.com - services.ddclient = { - enable = true; - protocol = "gandi"; - zone = "technicalissues.us"; - domains = [ home_domain ]; - username = "unused"; - extraConfig = '' - usev4=webv4 - #usev6=webv6 - #use-personal-access-token=yes - ttl=300 - ''; - passwordFile = gandi_api; }; - - containers.nginx-proxy = { - bindMounts."${gandi_api}".isReadOnly = true; - #bindMounts."${gandi_dns_pat}".isReadOnly = true; - autoStart = true; - timeoutStartSec = "5min"; - privateNetwork = true; - hostBridge = "br1-23"; - localAddress = "192.168.23.21/24"; - config = { config, pkgs, lib, ... }: { - system.stateVersion = "23.11"; - - programs.traceroute.enable = true; - - services.nginx = { - enable = true; - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - appendHttpConfig = '' - # Add HSTS header with preloading to HTTPS requests. - # Adding this header to HTTP requests is discouraged - map $scheme $hsts_header { - https "max-age=31536000;"; - } - add_header Strict-Transport-Security $hsts_header; - ''; - - virtualHosts = { - "${home_domain}" = { - serverAliases = [ "nix-tester.${home_domain}" ]; - default = true; - listen = [ - { port = http_port; addr = "0.0.0.0"; } - { port = https_port; addr = "0.0.0.0"; ssl = true; } - ]; - enableACME = true; - acmeRoot = null; - addSSL = true; - forceSSL = false; - locations."/" = { - return = "200 '

Hello world ;)

'"; - extraConfig = '' - add_header Content-Type text/html; - ''; - }; - }; - "ab.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyWebsockets = true; - locations."/".proxyPass = "http://${backend_ip}:13378"; - }; - "atuin.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyPass = "http://${mini_watcher}:9999"; - }; - "nc.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - extraConfig = '' - client_max_body_size 0; - underscores_in_headers on; - ''; - locations."/".proxyWebsockets = true; - locations."/".proxyPass = "http://${mini_watcher}:8081"; - locations."/".extraConfig = '' - # these are added per https://www.nicemicro.com/tutorials/debian-snap-nextcloud.html - add_header Front-End-Https on; - proxy_headers_hash_max_size 512; - proxy_headers_hash_bucket_size 64; - proxy_buffering off; - proxy_max_temp_file_size 0; - ''; - }; - "onlyoffice.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyWebsockets = true; - locations."/".proxyPass = "http://${mini_watcher}:8888"; - }; - "readit.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyPass = "http://${backend_ip}:8090"; - }; - "tandoor.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyPass = "http://${backend_ip}:8080"; - }; - }; - }; - - security.acme = { - acceptTerms = true; - defaults = { - email = "lets-encrypt@technicalissues.us"; - credentialFiles = { "GANDIV5_API_KEY_FILE" = gandi_api; }; - #credentialFiles = { "GANDIV5_PERSONAL_ACCESS_TOKEN_FILE" = gandi_dns_pat; }; - dnsProvider = "gandiv5"; - dnsResolver = "ns1.gandi.net"; - # uncomment below for testing - #server = "https://acme-staging-v02.api.letsencrypt.org/directory"; - }; - }; - - networking = { - firewall = { - enable = true; - allowedTCPPorts = [ http_port https_port ]; - }; - defaultGateway = "192.168.23.1"; - # Use systemd-resolved inside the container - # Workaround for bug https://github.com/NixOS/nixpkgs/issues/162686 - useHostResolvConf = lib.mkForce false; - }; - - services.resolved.enable = true; - }; - }; -} diff --git a/modules/hosts/nixos/nixnuc/default.nix b/modules/hosts/nixos/nixnuc/default.nix index 3921f53..be8ff03 100644 --- a/modules/hosts/nixos/nixnuc/default.nix +++ b/modules/hosts/nixos/nixnuc/default.nix @@ -3,7 +3,6 @@ https_port = 443; home_domain = "home.technicalissues.us"; backend_ip = "127.0.0.1"; - mini_watcher = "192.168.23.20"; restic_backup_time = "02:00"; in { imports = [ @@ -11,6 +10,7 @@ in { ./containers/audiobookshelf.nix ./containers/mountain-mesh-bot-discord.nix ./containers/psitransfer.nix + ./monitoring-stack.nix ../../common/linux/lets-encrypt.nix ../../common/linux/restic.nix ]; @@ -96,16 +96,11 @@ in { 8888 # Atuin 8090 # Wallabag in docker compose 8945 # Pinchflat - 9090 # Prometheus Server - 9273 # Telegraf's Prometheus endpoint 13378 # Audiobookshelf in oci-container - 22000 # Syncthing transfers ]; allowedUDPPorts = [ 1900 # Jellyfin service auto-discovery 7359 # Jellyfin auto-discovery - 21027 # Syncthing discovery - 22000 # Syncthing transfers ]; }; # Or disable the firewall altogether. @@ -135,7 +130,9 @@ in { }; services.pulseaudio.enable = false; - programs.mtr.enable = true; + programs = { + mtr.enable = true; + }; # List services that you want to enable: services = { @@ -219,22 +216,6 @@ in { stateDir = "/orico/forgejo"; }; fwupd.enable = true; - grafana = { - enable = true; - settings = { - auth = { - disable_login_form = true; - oauth_auto_login = true; - }; - server = { - domain = "monitoring.${home_domain}"; - http_addr = "0.0.0.0"; - http_port = 3002; - root_url = "https://monitoring.${home_domain}/grafana/"; # Not needed if it is `https://your.domain/` - serve_from_sub_path = true; - }; - }; - }; jellyfin = { enable = true; openFirewall = true; @@ -461,27 +442,14 @@ in { enableACME = true; acmeRoot = null; forceSSL = true; - locations."/grafana/".proxyPass = "http://${backend_ip}:3002/grafana/"; - }; - "nc.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - extraConfig = '' - client_max_body_size 0; - underscores_in_headers on; - ''; - locations."/".proxyWebsockets = true; - locations."/".proxyPass = "http://${mini_watcher}:8081"; - locations."/".extraConfig = '' - # these are added per https://www.nicemicro.com/tutorials/debian-snap-nextcloud.html - add_header Front-End-Https on; - proxy_headers_hash_max_size 512; - proxy_headers_hash_bucket_size 64; - proxy_buffering off; - proxy_max_temp_file_size 0; - ''; + locations = { + "/grafana/".proxyPass = "http://${backend_ip}:3002/grafana/"; + "/remotewrite" = { + basicAuthFile = config.sops.secrets.nginx_basic_auth.path; + proxyPass = "http://127.0.0.1:8428/api/v1/write"; + proxyWebsockets = true; + }; + }; }; "nextcloud.${home_domain}" = { enableACME = true; @@ -500,14 +468,6 @@ in { deny all; ''; }; - "onlyoffice.${home_domain}" = { - listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; - enableACME = true; - acmeRoot = null; - forceSSL = true; - locations."/".proxyWebsockets = true; - locations."/".proxyPass = "http://${mini_watcher}:8888"; - }; "readit.${home_domain}" = { listen = [{ port = https_port; addr = "0.0.0.0"; ssl = true; }]; enableACME = true; @@ -520,6 +480,9 @@ in { nominatim = { enable = true; hostName = "nominatim.home.technicalissues.us"; + settings = { + NOMINATIM_PROJECT_DIR = "/var/lib/nominatim/project"; + }; ui.config = '' Nominatim_Config.Page_Title="Beantown's Nominatim"; Nominatim_Config.Nominatim_API_Endpoint='https://${config.services.nominatim.hostName}/'; @@ -542,52 +505,23 @@ in { postgresql = { enable = true; package = pkgs.postgresql_16; + ensureUsers = [ + { + # Required by Nominatim + name = "www-data"; + ensureDBOwnership = false; + } + ]; }; postgresqlBackup = { enable = true; backupAll = true; startAt = "*-*-* 23:00:00"; }; - prometheus = { - enable = true; - checkConfig = "syntax-only"; - globalConfig.scrape_interval = "10s"; # "1m" - scrapeConfigs = [ - { - job_name = "hass"; - scrape_interval = "30s"; - metrics_path = "/api/prometheus"; - static_configs = [{ - targets = [ "192.168.22.22:8123" ]; - }]; - bearer_token_file = config.sops.secrets.home_assistant_token.path; - } - { - job_name = "telegraf"; - scrape_interval = "5s"; - static_configs = [{ - targets = [ "localhost:9273" ]; - }]; - } - { - job_name = "uptimekuma"; - scheme = "https"; - scrape_interval = "30s"; - static_configs = [{ - targets = [ "utk.technicalissues.us" ]; - }]; - basic_auth = { - password_file = config.sops.secrets.uptimekuma_grafana_api_key.path; - username = ""; - }; - } - ]; - }; resolved.enable = true; restic.backups.daily = { paths = [ config.services.forgejo.stateDir - config.services.grafana.dataDir config.services.mealie.settings.DATA_DIR config.services.nextcloud.home "${config.users.users.${username}.home}/compose-files/wallabag" @@ -595,7 +529,6 @@ in { "/orico/jellyfin/data" "/orico/jellyfin/staging/downloaded-files" "/var/backup/postgresql" - "/var/lib/prometheus2" ]; timerConfig = { OnCalendar = restic_backup_time; @@ -615,83 +548,6 @@ in { ]; useRoutingFeatures = "both"; }; - telegraf = { - enable = true; - extraConfig = { - agent = { - interval = "15s"; - round_interval = true; - collection_jitter = "0s"; - debug = false; - }; - inputs = { - cpu = { - percpu = true; - totalcpu = true; - report_active = true; - collect_cpu_time = true; - }; - disk = { - ignore_fs = [ - "aufs" - "devfs" - "devtmpfs" - "iso9660" - "overlay" - "squashfs" - "tmpfs" - ]; - }; - diskio = { - skip_serial_number = true; - }; - docker = { - endpoint = "unix:///run/docker.sock"; - container_name_include = []; - storage_objects = [ - "container" - "volume" - ]; - perdevice_include = [ - "cpu" - "blkio" - "network" - ]; - total_include = [ - "cpu" - "blkio" - "network" - ]; - timeout = "5s"; - gather_services = false; - }; - mem = {}; - net = { - ignore_protocol_stats = true; - }; - nginx = { - insecure_skip_verify = true; - urls = [ "https://127.0.0.1/server_status" ]; - }; - #smart = {}; - system = {}; - #systemd_units = {}; - zfs = { - datasetMetrics = true; - poolMetrics = true; - }; - }; - outputs = { - prometheus_client = { - collectors_exclude = [ - "gocollector" - "process" - ]; - listen = ":9273"; - }; - }; - }; - }; zfs.autoScrub.enable = true; }; @@ -724,10 +580,6 @@ in { "phpfpm-firefly-iii-data-importer.service" ]; }; - home_assistant_token = { - owner = config.users.users.prometheus.name; - restartUnits = ["prometheus.service"]; - }; immich_kiosk_basic_auth = { owner = config.users.users.nginx.name; restartUnits = ["nginx.service"]; @@ -745,14 +597,13 @@ in { restartUnits = ["mealie.service"]; }; nextcloud_admin_pass.owner = config.users.users.nextcloud.name; + nginx_basic_auth = { + owner = "nginx"; + restartUnits = ["nginx.service"]; + }; tailscale_key = { restartUnits = [ "tailscaled-autoconnect.service" ]; }; - uptimekuma_grafana_api_key = { - owner = config.users.users.prometheus.name; - restartUnits = ["prometheus.service"]; - sopsFile = ../../common/secrets.yaml; - }; }; }; @@ -780,8 +631,6 @@ in { ]; }; - users.users.telegraf.extraGroups = [ "docker" ]; - # Enable common container config files in /etc/containers virtualisation.containers.enable = true; diff --git a/modules/hosts/nixos/nixnuc/grafana-files/alert-rules.yaml b/modules/hosts/nixos/nixnuc/grafana-files/alert-rules.yaml new file mode 100644 index 0000000..6008fe7 --- /dev/null +++ b/modules/hosts/nixos/nixnuc/grafana-files/alert-rules.yaml @@ -0,0 +1,298 @@ +apiVersion: 1 +groups: + - orgId: 1 + name: infrastructure_alerts + folder: Monitoring + interval: 1m + rules: + - uid: node_exporter_down + title: Node Exporter Down + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: VictoriaMetrics + model: + expr: up{job="node"} + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: B + type: reduce + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + conditions: + - evaluator: + params: + - 1 + type: lt + operator: + type: and + query: + params: + - C + type: query + expression: B + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: "" + panelId: 0 + noDataState: NoData + execErrState: Error + for: 2m + annotations: + summary: Node exporter has been down for more than 2 minutes + labels: + severity: critical + isPaused: false + - uid: high_cpu_usage + title: High CPU Usage + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: VictoriaMetrics + model: + expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: B + type: reduce + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + conditions: + - evaluator: + params: + - 80 + type: gt + operator: + type: and + query: + params: + - C + type: query + expression: B + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: "" + panelId: 0 + noDataState: NoData + execErrState: Error + for: 5m + annotations: + summary: CPU usage is above 80% for more than 5 minutes + labels: + severity: warning + isPaused: false + - uid: high_memory_usage + title: High Memory Usage + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: VictoriaMetrics + model: + expr: 100 * (1 - ((node_memory_MemAvailable_bytes) / (node_memory_MemTotal_bytes))) + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: B + type: reduce + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + conditions: + - evaluator: + params: + - 85 + type: gt + operator: + type: and + query: + params: + - C + type: query + expression: B + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: "" + panelId: 0 + noDataState: NoData + execErrState: Error + for: 5m + annotations: + summary: Memory usage is above 85% for more than 5 minutes + labels: + severity: warning + isPaused: false + - uid: low_disk_space + title: Low Disk Space + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: VictoriaMetrics + model: + expr: 100 - ((node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|fuse.lxcfs"} * 100) / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|fuse.lxcfs"}) + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: B + type: reduce + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + conditions: + - evaluator: + params: + - 85 + type: gt + operator: + type: and + query: + params: + - C + type: query + expression: B + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: "" + panelId: 0 + noDataState: NoData + execErrState: Error + for: 5m + annotations: + summary: Disk usage on root filesystem is above 85% + labels: + severity: warning + isPaused: false + - uid: cadvisor_down + title: cAdvisor Down + condition: C + data: + - refId: A + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: VictoriaMetrics + model: + expr: up{job="cadvisor"} + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + - refId: B + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + expression: A + intervalMs: 1000 + maxDataPoints: 43200 + reducer: last + refId: B + type: reduce + - refId: C + relativeTimeRange: + from: 600 + to: 0 + datasourceUid: "-100" + model: + conditions: + - evaluator: + params: + - 1 + type: lt + operator: + type: and + query: + params: + - C + type: query + expression: B + intervalMs: 1000 + maxDataPoints: 43200 + refId: C + type: threshold + dashboardUid: "" + panelId: 0 + noDataState: NoData + execErrState: Error + for: 2m + annotations: + summary: cAdvisor (container metrics) has been down for more than 2 minutes + labels: + severity: warning + isPaused: false + diff --git a/modules/hosts/nixos/nixnuc/hardware-configuration.nix b/modules/hosts/nixos/nixnuc/hardware-configuration.nix index 4af6582..17b0c38 100644 --- a/modules/hosts/nixos/nixnuc/hardware-configuration.nix +++ b/modules/hosts/nixos/nixnuc/hardware-configuration.nix @@ -28,11 +28,6 @@ fsType = "zfs"; }; - fileSystems."/var/lib/prometheus2" = - { device = "orico/prometheus"; - fsType = "zfs"; - }; - swapDevices = [ ]; # Enables DHCP on each ethernet and wireless interface. In case of scripted networking diff --git a/modules/hosts/nixos/nixnuc/monitoring-stack.nix b/modules/hosts/nixos/nixnuc/monitoring-stack.nix new file mode 100644 index 0000000..e486512 --- /dev/null +++ b/modules/hosts/nixos/nixnuc/monitoring-stack.nix @@ -0,0 +1,381 @@ +{ config, pkgs, ... }: let + home_domain = "home.technicalissues.us"; +in { + environment.systemPackages = with pkgs; [ + # Keeping empty for manual testing if needed + ]; + + services = { + # ---------------------------- + # PostgreSQL database + # ---------------------------- + postgresql = { + enable = true; + ensureDatabases = [ "grafana" ]; + ensureUsers = [ + { + name = "grafana"; + ensureDBOwnership = true; + } + ]; + }; + + # ---------------------------- + # VictoriaMetrics storage + # ---------------------------- + victoriametrics = { + enable = true; + stateDir = "victoriametrics"; # Just the directory name, module adds /var/lib/ prefix + package = pkgs.victoriametrics; + }; + + # ---------------------------- + # vmagent: scrape exporters + # ---------------------------- + vmagent = { + enable = true; + package = pkgs.victoriametrics; + + # Prometheus-style scrape configuration + prometheusConfig = { + global.scrape_interval = "15s"; + + scrape_configs = [ + # Node exporter: CPU, memory, disk, diskio, network, system, ZFS + { + job_name = "node"; + static_configs = [ + { + targets = [ + "127.0.0.1:9100" # nixnuc + "192.168.22.22:9100" # home assistant + "umbrel:9100" + ]; + } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__" "nodename"]; + regex = "node_uname_info;0d869efa-prometheus-node-exporter"; + target_label = "nodename"; + replacement = "homeassistant"; + } + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + regex = "127.0.0.1.*"; + replacement = "${config.networking.hostName}"; + } + { + target_label = "instance"; + regex = "192.168.22.22.*"; + replacement = "homeassistant"; + } + ]; + } + + # cAdvisor: Docker containers + { + job_name = "cadvisor"; + static_configs = [ + { targets = ["127.0.0.1:8081"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + replacement = "${config.networking.hostName}"; + } + ]; + } + + # Nginx exporter + { + job_name = "nginx"; + static_configs = [ + { targets = ["127.0.0.1:9113"]; } + ]; + metric_relabel_configs = [ + { + source_labels = ["__name__"]; + regex = "go_.*"; + action = "drop"; + } + ]; + relabel_configs = [ + { + target_label = "instance"; + replacement = "${config.networking.hostName}"; + } + ]; + } + + # Home Assistant metrics + { + job_name = "homeassistant"; # built in endpoint + scrape_interval = "30s"; + metrics_path = "/api/prometheus"; + static_configs = [ + { targets = ["192.168.22.22:8123"]; } + ]; + bearer_token_file = config.sops.secrets.home_assistant_token.path; + relabel_configs = [ + { + target_label = "instance"; + replacement = "homeassistant"; + } + ]; + } + + # Uptime Kuma metrics + { + job_name = "uptimekuma"; + scheme = "https"; + scrape_interval = "30s"; + static_configs = [ + { targets = ["utk.technicalissues.us"]; } + ]; + basic_auth = { + password_file = config.sops.secrets.uptimekuma_grafana_api_key.path; + username = "unused"; + }; + metric_relabel_configs = [ + { + source_labels = ["monitor_hostname"]; + regex = "^null$"; + replacement = ""; + target_label = "monitor_hostname"; + } + { + source_labels = ["monitor_port"]; + regex = "^null$"; + replacement = ""; + target_label = "monitor_port"; + } + { + source_labels = ["monitor_url"]; + regex = "https:\/\/"; + replacement = ""; + target_label = "monitor_url"; + } + ]; + } + ]; + }; + + # Remote write to VictoriaMetrics + remoteWrite.url = "http://127.0.0.1:8428/api/v1/write"; + + extraArgs = [ + # Pass other remote write flags the module does not expose natively: + "-remoteWrite.flushInterval=10s" + "-remoteWrite.maxDiskUsagePerURL=1GB" + + # Prevent vmagent from failing the entire scrape if a target is down: + "-promscrape.suppressScrapeErrors" + + # Enable some debugging info suggested by the interface on port 8429 + "-promscrape.dropOriginalLabels=false" + ]; + }; + + # ---------------------------- + # Grafana with VictoriaMetrics datasource + # ---------------------------- + grafana = { + enable = true; + + # Install VictoriaMetrics plugin declaratively + declarativePlugins = [ + pkgs.grafanaPlugins.victoriametrics-metrics-datasource + ]; + + provision = { + # Alert rules provisioning + # To add more rules: create them in Grafana UI, then export via: + # Alerting -> Alert rules -> Export rules (YAML format) + # Copy the exported rules into ./alert-rules.nix + alerting.rules.path = ./grafana-files/alert-rules.yaml; + + datasources.settings.datasources = [ + { + name = "VictoriaMetrics"; + type = "victoriametrics-metrics-datasource"; + access = "proxy"; + url = "http://127.0.0.1:8428"; + isDefault = true; + uid = "VictoriaMetrics"; # Set explicit UID for use in alert rules + } + ]; + }; + + + settings = { + auth = { + # Set to true to disable (hide) the login form, useful if you use OAuth + disable_login_form = false; + }; + + "auth.generic_oauth" = { + name = "Pocket ID"; + enabled = true; + + # Use Grafana's file reference syntax for secrets + client_id = "$__file{${config.sops.secrets.grafana_oauth_client_id.path}}"; + client_secret = "$__file{${config.sops.secrets.grafana_oauth_client_secret.path}}"; + + auth_style = "AutoDetect"; + scopes = "openid email profile groups"; + auth_url = "${config.services.pocket-id.settings.APP_URL}/authorize"; + token_url = "${config.services.pocket-id.settings.APP_URL}/api/oidc/token"; + allow_sign_up = true; + auto_login = true; + name_attribute_path = "display_name"; + login_attribute_path = "preferred_username"; + email_attribute_name = "email:primary"; + email_attribute_path = "email"; + role_attribute_path = "contains(groups[*], 'grafana_super_admin') && 'GrafanaAdmin' || contains(groups[*], 'grafana_admin') && 'Admin' || contains(groups[*], 'grafana_editor') && 'Editor' || 'Viewer'"; + role_attribute_strict = false; + allow_assign_grafana_admin = true; + skip_org_role_sync = false; + use_pkce = true; + use_refresh_token = false; + tls_skip_verify_insecure = false; + }; + + # Database configuration - use PostgreSQL with peer authentication + database = { + type = "postgres"; + host = "/run/postgresql"; # Use Unix socket instead of TCP + name = "grafana"; + user = "grafana"; + # No password needed - using peer authentication via Unix socket + }; + + # Server configuration + server = { + domain = "monitoring.${home_domain}"; + http_addr = "0.0.0.0"; + http_port = 3002; + root_url = "https://monitoring.${home_domain}/grafana/"; + serve_from_sub_path = true; + }; + + # Enable unified alerting (Grafana's built-in alerting) + "unified_alerting" = { + enabled = true; + }; + + # Disable legacy alerting + alerting.enabled = false; + }; + }; + + # ---------------------------- + # Exporters (using built-in NixOS modules) + # ---------------------------- + + # Node exporter - using the built-in module + prometheus.exporters.node = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9100; + enabledCollectors = [ + "zfs" + "systemd" + ]; + extraFlags = [ + "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|run|tmp|var/lib/docker/.+)($|/)" + "--collector.diskstats.device-exclude=^(loop|ram|fd|sr|dm-|nvme[0-9]n[0-9]p[0-9]+_crypt)$" + ]; + }; + + # Nginx exporter - using the built-in module + prometheus.exporters.nginx = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9113; + scrapeUri = "https://127.0.0.1/server_status"; + sslVerify = false; + }; + + # cAdvisor for Docker containers + cadvisor = { + enable = true; + listenAddress = "127.0.0.1"; + port = 8081; + extraOptions = [ + "--docker_only=true" + "--housekeeping_interval=30s" + "--disable_metrics=hugetlb" + ]; + }; + }; + + # ---------------------------- + # Users and groups for service accounts + # ---------------------------- + users.users.vmagent = { + isSystemUser = true; + group = "vmagent"; + }; + + users.groups.vmagent = {}; + + # ---------------------------- + # Systemd service dependencies + # ---------------------------- + systemd.services.grafana = { + after = [ "postgresql.service" ]; + requires = [ "postgresql.service" ]; + }; + + # ---------------------------- + # SOPS secrets configuration + # ---------------------------- + sops = { + defaultSopsFile = ./secrets.yaml; + secrets = { + grafana_oauth_client_id = { + owner = "grafana"; + restartUnits = ["grafana.service"]; + }; + grafana_oauth_client_secret = { + owner = "grafana"; + restartUnits = ["grafana.service"]; + }; + home_assistant_token = { + owner = "vmagent"; + restartUnits = ["vmagent.service"]; + }; + uptimekuma_grafana_api_key = { + owner = "vmagent"; + restartUnits = ["vmagent.service"]; + sopsFile = ../../common/secrets.yaml; + }; + }; + }; + + # ----------------------------- + # Backups of all this + # ----------------------------- + services.restic.backups.daily = { + paths = [ + config.services.grafana.dataDir + config.services.victoriametrics.stateDir + ]; + }; +} + diff --git a/modules/hosts/nixos/nixnuc/secrets.yaml b/modules/hosts/nixos/nixnuc/secrets.yaml index 7f8ab72..407fbf3 100644 --- a/modules/hosts/nixos/nixnuc/secrets.yaml +++ b/modules/hosts/nixos/nixnuc/secrets.yaml @@ -5,11 +5,14 @@ firefly_app_key: ENC[AES256_GCM,data:sNaqRgFOSmdSS0lCmEG8Nxy/3N7F/hQyS6iPnwau3sQ firefly_pat_data_import: ENC[AES256_GCM,data:mZ32gVaDZGEnpsu4HBHeIZ8UtkTQMrHNWp6iv1jOxsUNJlswWS0kU3IN/sVRWT734KcBGUnriWcXIFvZTx6KXhOjWdEYXyO+g9ws88CWCAguCssiPJI23XaDwMhaqjWRvauzPgjzGXdvg4pFs+myxui+MkLyqcv/ZOBoMt85Nn/M6xuMyyzzmalvTtovCgkja3LJDgxCK7AZoGkXK3X9v15ShHaAjM6mEYkSpONeprew+a7PfmPkM8ynMMOgdCQbRLf/34tLXnTer9qL/kz/Xt14PZsQ5+Fj+fxVwt08xv4FWi3VqUR9wL4Fs4G/yJQHUkDQXDcCk67+TgHxFnsYcxZw6k0ue8u1Ab1aDPyEhUD/UwyzTKyIjzCCIc8NBnst6o8kV62d6ei7dG4PWvNk8m2yNhZVlvoKEjmvZg0Bxv7xGniSimXpvTcMs18+mB0nOcYFUZJf6S0tolcClEDBEegwf0kD+p2F7jXsUjcJsaxqh9Xk215qbTi9iRVtQMWfPO8dCo4EC9vs7MFNmtJsGEp0WR2nwGFh7eIRbzug5+HC5OtUEGbmIUhLmTYnrohkJUKauMYALeYUS7j8GwFdfN0qT0N5r1d5OC/uOih2W5jWiMsOkNWKlaxZ/D1TOTYjLdcHCmIDlQHv9XVFUketsy6KPyg3XkmicKYq18xMi2pkWwGwDV0rd0aqHqRLTNfj3BoXI53eWSsZo8CKuUgWyXM4mIRy7yle2s52a2Idsc8/Zx42MmbWov3c5CNaQDjaseTSO8bv57U30GBY43UBJ4n+p6AIBnXQAN9d/YQ2Wvphe80ZdqyfHzTgj+6PEQKFXzs4KJDE83es/1+HVZdKehpUksenPeQ8UAxoIFtgFjdz6p9JBdXcuFhkWs6oAT96VQtkS6aHgOOUSTN2+H0Q/l6yvIvQB/QcI88r4BiU2vroGqzuVRCbgaXZqjUU06lS608gJOGqIe2a2bHbxwKU+SHEglTdDzRP4n02v8Ua5+BpcWVmhamEzd+8qhLEp/awYi9rd2Kc38T/vowsumLQAvfuZHUt8ySS1JEsyjrWsXQ/cfAiKMeTWkL7b2vEmvx30D8Acz68wDmXwxI/pWDKSe+jagegkmPWVnGQz7MATfApjIl4ZksDQH/O88WrYVF71YYfhA2Qpxr+2FHJ0F2kTPV6tv3QPcteJH4s8pVXvgpYZ050FL9/gJ+gB4rt23Lp0pGW+WEo6YenZ0plbWe4ixf8+U4he5eecGAlAYFEVYZnl6WKLLhX9/xMG9vNJODnUxF97csMPDYiGBhupT8dVk6+1ww=,iv:L0Ff7RYYOPqPeR81LJuTMZ5dsmeQrJtfO1e7Aei+tc4=,tag:wK5s7gRQNpk2aOnsIhtr2A==,type:str] firefly_simplefin_token: ENC[AES256_GCM,data:xJGII+ChIinSpJt+DNU8k8xtgxZkQPmr+02MpO39SylPI6qLHUiXib8xHGrxSSoux19dVdUVXsGO3jRsdIfrX4R6vt/zrIi7yK2gSTZaaoYI1nRg125aino1tIu1Xc+btbro26gDUL8ExlGOg+xvCdPrsmEPbuxVEKm1XxSOOjfvHFjhaMr8XV8/ewqcf5fYlUXEOtNyb6TCrNnp1eSp8+/Uii9GU5O8dlCt1Rof2wNeGQD4lV+lRZqOD0QX2aL5fyXxNUm5W8qFXLiscS8XCl0x2MM9X/mSQ6PC9MlY4WOHXw9/1S3jiVpUyM0U7wJO,iv:+/EwhdcfEwDm+1nhMuIFMiRvT/wbH+vIsmiOLP8bX8g=,tag:RjBomarf49a3xT2TPSTV+A==,type:str] firefly_vanity_url: ENC[AES256_GCM,data:ypIv2VA/B8RStW5Ifj5J20n/MI+jevd0vYrJPchERpt+txPJ0fo=,iv:16/hQyr/TJgLlNjJSUirEpth7jSHP/ooXQ/Vr97wBiA=,tag:p4WiNKm1zkizQCOFVKcW0Q==,type:str] +grafana_oauth_client_id: ENC[AES256_GCM,data:AyqM4bV/9yiWyi0/wqu/SUt5cXYgpsvDcsOgCWENcLeFgGbV,iv:jbUSZQwGOc9V0RON9nAvI86HjGCDuh4wrnpgWcWNG0I=,tag:V6Xe1/r0srITkPDQZ2G+wA==,type:str] +grafana_oauth_client_secret: ENC[AES256_GCM,data:As+m4hLWXy2uMq3KHWPtKzsRY4faYjGUrCdCSzP+TuI=,iv:3UAl8vuf7gUc5voyA2yuY8iiBaoI0iF3U7AFv0siskE=,tag:oD3B5SCNkIfqn88gBxy3Fw==,type:str] home_assistant_token: ENC[AES256_GCM,data:fNpoH60rXAsoVx/NBoDobDw/e6IoeoZfef1uDR7HbmnHNI101b+kQKkB8wBXEDLf7MlqlXVc1ExlgYFUo7+h2msx4WZUCGzuHtp1cMP0O9s2PZoQU8KQM2Frd69vOUcOT5y5ShJZPRrf6H2UKrm//3jE9zDOxxy3cQj6Q+jQLXX4AfZ12JAKzSiee9URWdU8eCHbnquUl1RNHF7zZQ8Mr9m41sBOrpBXt7ErsrhxxRhwlk7qprLt,iv:1j/QmOkLYd7nA+wXS49drBHU09HzMP3XxPbPdaErV6E=,tag:ftyOeNaN5PwNebeMPNAmTg==,type:str] immich_kiosk_basic_auth: ENC[AES256_GCM,data:R/vaYXe0SRwSUg8zC367vA/OYhjIcgfbHxR3OhLt1YYUhPNXVuUGmn199zLD,iv:GBEjJKvbwHAS75negE2whTlI4wS8K2nQYNm3015DFoY=,tag:zs+bJEr9JlIrJtikHycN4g==,type:str] mealie: ENC[AES256_GCM,data:fZFBWlh/nbxK0GA6+fb1FK6aFfbiV/GsBYKYRPgavcsB9h4HwRSZN94gPQ3AfGnk1q08ORPBYShNUIaVdsf4+t3taDC7bNTwPqFGRxoeKOU15BPj6Fpw7SHlplApUg7RDmuTWZuXDH0F49N+09GU/Lc26ewHMAzL8CtGg/H7s3CQfFEqCYYcXJeUcnNDu6PRnKFIAppflSScREp29CUf/IfTYQXidQC25upZgcvGJ+dCTDyNFftlhQiFQh8nFRFVJfKk4iBqnig3fOYsTD32ohAotfDGsSUWb9yjoYQNnNn0V14/,iv:WRFiBtavTP3iP5YSaCMkd7ag8ccx1BO76Vlw6axPkJU=,tag:4BhAxbF8JUB0MV12yx86uQ==,type:str] mtnmesh_bot_dot_env: ENC[AES256_GCM,data:jKz0voG/a7Eq+zHI2fsejTpu5H+ic5ZE8VmtOQDQEOTvYT8GWr2oCGwh68ql7g+nU5hlNN7uLl9LreW59nTgmvWNCVjVNKchxJeydktMUoq2FlgyoC2I354bi9gFEL0X5JYRR7jG9iwy4WGyEFfhhUJn1MSwOurRZPw/tdWTUKQVwN5oqjMxkXrZhFLDzq6BrTs0hBpzRYTe,iv:ZBxHkW8VyU+8v4GiDJMEaeqm3Fdtuwm7M/7YXkfkMnw=,tag:S8hTOSicQNIAj/Gdxzw5+A==,type:str] nextcloud_admin_pass: ENC[AES256_GCM,data:KztB3Tkqlt73PEO41lthGYElrbwVdfqQgT6f,iv:kRwXqGJO4AUOMq+uYzndGhscaJiyvG4ANKabHHd78YM=,tag:dP3PgKafDTv8x7huKJGDqA==,type:str] +nginx_basic_auth: ENC[AES256_GCM,data:9qy8ccV9yWOrQtagf4D4fTp4v8GuY5ORxsE8hUv/vKngOhlG1ikvuIJacIfTfZj9JCoVWnQ=,iv:UGh/KGqT93t91rXu0JN1Hi4/Hb0ICnGiKkymm/5VLU0=,tag:8X6+bKk9zqggRXQhXrhJAg==,type:str] psitransfer_dot_env: ENC[AES256_GCM,data:bhvU0AOCjecZ62BtLw4H1DdkLeatI+uUl6L7UkdDRkBF3sayO45Z1eR4q60tflXucyTGhT8WgKFz53I+C2dn265wzojIRc3Xr4TBLyWpfJ7/dct40SckgUiRvOnrefiriWQ=,iv:DGMhDkzgeupzzTJnCdVWDPUSo2wxI3MAypKQwVfHExE=,tag:KbteGqrkqgj2XB1lvlk/yQ==,type:str] pinchflat_dot_env: ENC[AES256_GCM,data:8DLiFXThG5PGJ0ymW5bMVy5A8dM=,iv:BGkVvxaNwFIMSaA3F6h4ZsgkC9tm1lohA0lg2pgZhpw=,tag:qQNrluP5exdKG3NXgQHM8g==,type:str] tandoor_db_pass: ENC[AES256_GCM,data:X0unx5jquLsUXadbF6xLjjeGY+f8Ec4kdc15JQ==,iv:XptlJHfAkF+3jbgJTqxhVReYjuVVdk3NzfPepP78DRI=,tag:3RG5P9QGCJ/fjdxWpY1xWA==,type:str] @@ -25,7 +28,7 @@ sops: bHZlNTZDV2NYU1hQQy9mem80SFF6TFkKfmjkJBfTdh0vTtGaVx1t3tHJvSsAwdYD PF025X9U+yG2oIopwXEVBkxcD70eyuJn3OqH0xoVLBkbhNM9i8LHrA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-10T23:26:34Z" - mac: ENC[AES256_GCM,data:pwgPKBBp2oNu2RRZDprbpVmWxCbsReS3l5/alUfJ7KrMWU6tWiKXNbE2HY/40HYAGyJQ4Jal+u4tKEnPReVubEIe8NwuMlXaDh4ynEN1DId9FaRuCjKatkySWPOGO6DFd4Ajr1Xt1XtWVRqhlCAf5nE+nG1ZBQbloWgOEQZ9m6s=,iv:D0l5yu4lFwrboHbTN26ECPBM6MluPHGR2x6JAwbAUoE=,tag:ctlUVc2CFglsYuTYyCuuYQ==,type:str] + lastmodified: "2026-02-01T03:12:35Z" + mac: ENC[AES256_GCM,data:2PCSk5RQfgsDkQwlujmrBw4yDOIypKBeW/MAF339OR2o77Dz4+YHbUjxoPHt84bpZDMNeUDAifQUoBrKqq66gBJU7CcF/A/dRGCw5xxkdGGEqIjOX+SpC4I+j0zfJ34Pc1BvmTtY32Ivb9njqKZtTj21KJGMB/NDdkgYrDkqY+g=,iv:TSh4Xlmu840HVPBRw+2D2NoDURkEusjwhUEVoL0YWvs=,tag:4K6sHya1LEOziB4zBo0QIg==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0