diff --git a/nixos/packages/gs1200-exporter.nix b/nixos/packages/gs1200-exporter.nix index 35e95e7..4d7c858 100644 --- a/nixos/packages/gs1200-exporter.nix +++ b/nixos/packages/gs1200-exporter.nix @@ -6,13 +6,18 @@ buildGoModule rec { pname = "gs1200-exporter"; version = "2.11.12"; + + __structuredAttrs = true; + src = fetchFromGitHub { owner = "robinelfrink"; repo = "gs1200-exporter"; rev = "v${version}"; hash = "sha256-8s2VgaqYXp9PN2oNU/sWpjQjDPSWolbWEVSZcx9Lh3M="; }; + vendorHash = "sha256-204bFaywOolKVNoeH/w72Ba1PYAVgQawEmlaEXgRaRY="; + meta = { description = "Prometheus exporter for Zyxel GS1200 switches"; homepage = "https://github.com/robinelfrink/gs1200-exporter"; diff --git a/nixos/packages/oidcwarden.nix b/nixos/packages/oidcwarden.nix index d12d151..73bd9ea 100644 --- a/nixos/packages/oidcwarden.nix +++ b/nixos/packages/oidcwarden.nix @@ -1,5 +1,6 @@ { - pkgs, + vaultwarden, + rustPlatform, fetchFromGitHub, ... }: @@ -11,10 +12,10 @@ let hash = "sha256-tHacn9RtoByWpqnWX2/gWwODDSeXJa4mk4MfxHiiJ8A="; }; in -pkgs.vaultwarden.overrideAttrs (old: { +vaultwarden.overrideAttrs (old: { pname = "oidcwarden"; inherit src; - cargoDeps = pkgs.rustPlatform.importCargoLock { + cargoDeps = rustPlatform.importCargoLock { lockFile = "${src}/Cargo.lock"; }; postInstall = (old.postInstall or "") + '' diff --git a/nixos/packages/overlays.nix b/nixos/packages/overlays.nix index 99aac1d..d0bfe58 100644 --- a/nixos/packages/overlays.nix +++ b/nixos/packages/overlays.nix @@ -1,4 +1,4 @@ final: prev: { gs1200-exporter = final.callPackage ./gs1200-exporter.nix { }; - oidcwarden = final.callPackage ./oidcwarden.nix { }; + oidcwarden = final.callPackage ./oidcwarden.nix { inherit (prev) vaultwarden; }; } diff --git a/nixos/roles/gs-exporter.nix b/nixos/roles/gs-exporter.nix new file mode 100644 index 0000000..7d6a6ef --- /dev/null +++ b/nixos/roles/gs-exporter.nix @@ -0,0 +1,177 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.gs1200-exporter; + + instanceOpts = + { name, ... }: + { + options = { + address = lib.mkOption { + type = lib.types.str; + description = "IP address or hostname of the GS1200 switch."; + example = "192.168.1.3"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9934; + description = "Port on which to expose Prometheus metrics."; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Password to log in to the GS1200 web interface. + Use passwordFile instead to avoid storing the password in the Nix store. + ''; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a file containing the password to log in to the GS1200 web interface. + This is the recommended option as it avoids storing the password in the Nix store. + Compatible with sops-nix and agenix. + ''; + example = "/run/secrets/gs1200-password"; + }; + + debug = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable debug logging. Logs are accessible via journalctl -u gs1200-exporter-."; + }; + + verbose = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable verbose logging. Logs are accessible via journalctl -u gs1200-exporter-."; + }; + + json = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Output logs in JSON format. Logs are accessible via journalctl -u gs1200-exporter-."; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "gs1200-exporter-${name}"; + description = "User under which the service runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "gs1200-exporter-${name}"; + description = "Group under which the service runs."; + }; + }; + }; + + mkService = name: icfg: { + "gs1200-exporter-${name}" = { + description = "Prometheus exporter for Zyxel GS1200 switch '${name}'"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + User = icfg.user; + Group = icfg.group; + Restart = "always"; + RestartSec = "10s"; + + # Hardening + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + CapabilityBoundingSet = ""; + }; + + script = + let + args = lib.concatStringsSep " " ( + [ + "--address ${icfg.address}" + "--port ${toString icfg.port}" + ] + ++ lib.optional icfg.debug "--debug" + ++ lib.optional icfg.verbose "--verbose" + ++ lib.optional icfg.json "--json" + ); + in + if icfg.passwordFile != null then + '' + export GS1200_PASSWORD=$(cat ${icfg.passwordFile}) + exec ${lib.getExe pkgs.gs1200-exporter} ${args} + '' + else + '' + export GS1200_PASSWORD=${lib.escapeShellArg icfg.password} + exec ${lib.getExe pkgs.gs1200-exporter} ${args} + ''; + }; + }; + +in +{ + options.services.gs1200-exporter = { + instances = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule instanceOpts); + default = { }; + description = '' + Each attribute defines a separate gs1200-exporter instance for one switch. + Use this to monitor multiple GS1200 switches on the same host. + ''; + example = lib.literalExpression '' + { + switch-1 = { + address = "192.168.1.3"; + port = 9934; + passwordFile = "/run/secrets/gs1200-password"; + }; + switch-2 = { + address = "192.168.1.4"; + port = 9935; + passwordFile = "/run/secrets/gs1200-password"; + }; + } + ''; + }; + }; + + config = lib.mkIf (cfg.instances != { }) { + assertions = lib.flatten ( + lib.mapAttrsToList (name: icfg: [ + { + assertion = icfg.address != ""; + message = "services.gs1200-exporter.instances.${name}: address must be set."; + } + { + assertion = (icfg.password == null) != (icfg.passwordFile == null); + message = "services.gs1200-exporter.instances.${name}: exactly one of password or passwordFile must be set."; + } + ]) cfg.instances + ); + + users.users = lib.mapAttrs' ( + name: icfg: + lib.nameValuePair icfg.user { + isSystemUser = true; + inherit (icfg) group; + description = "gs1200-exporter '${name}' service user"; + } + ) cfg.instances; + + users.groups = lib.mapAttrs' (_name: icfg: lib.nameValuePair icfg.group { }) cfg.instances; + + systemd.services = lib.mkMerge (lib.mapAttrsToList mkService cfg.instances); + }; +} diff --git a/nixos/roles/monitoring.nix b/nixos/roles/monitoring.nix index a6409e6..2f24a38 100644 --- a/nixos/roles/monitoring.nix +++ b/nixos/roles/monitoring.nix @@ -23,6 +23,9 @@ let }; in { + + imports = [ ./gs-exporter.nix ]; + sops.secrets = { grafana_secret_key = { owner = "grafana"; @@ -32,8 +35,14 @@ in owner = "grafana"; group = "grafana"; }; + zyxel_pass = { + group = "gs1200-exporter"; + mode = "0440"; + }; }; + users.groups.gs1200-exporter = { }; + services = { grafana = { enable = true; @@ -141,9 +150,32 @@ in } ]; } + { + job_name = "gs1200-zyxel1"; + static_configs = [ { targets = [ "localhost:9934" ]; } ]; + } + { + job_name = "gs1200-zyxel2"; + static_configs = [ { targets = [ "localhost:9935" ]; } ]; + } ] ++ (lib.mapAttrsToList mkNodeJob extraNodes); }; + + gs1200-exporter.instances = { + zyxel1 = { + address = "192.168.2.3"; + port = 9934; + passwordFile = config.sops.secrets.zyxel_pass.path; + group = "gs1200-exporter"; + }; + zyxel2 = { + address = "192.168.2.4"; + port = 9935; + passwordFile = config.sops.secrets.zyxel_pass.path; + group = "gs1200-exporter"; + }; + }; }; networking.firewall.allowedTCPPorts = [ diff --git a/nixos/roles/vaultwarden.nix b/nixos/roles/vaultwarden.nix index 71dc39e..7359b25 100644 --- a/nixos/roles/vaultwarden.nix +++ b/nixos/roles/vaultwarden.nix @@ -1,29 +1,21 @@ { config, pkgs, - inputs, ... }: - let port = 8222; - oidcwarden = import ../packages/oidcwarden.nix { - inherit pkgs; - oidcwarden-src = inputs.oidcwarden; - }; in { sops.secrets.vaultwarden_env = { owner = "vaultwarden"; group = "vaultwarden"; }; - services.vaultwarden = { enable = true; - package = oidcwarden; + package = pkgs.oidcwarden; environmentFile = config.sops.secrets.vaultwarden_env.path; backupDir = "/var/local/vaultwarden/backup"; - config = { DOMAIN = "https://vault.cyperpunk.de"; ROCKET_ADDRESS = "0.0.0.0"; @@ -38,9 +30,7 @@ in SSO_PKCE = false; }; }; - networking.firewall.allowedTCPPorts = [ port ]; - systemd = { services.vaultwarden-backup-rotate = { description = "Rotate old Vaultwarden backups"; @@ -49,7 +39,6 @@ in ExecStart = "${pkgs.findutils}/bin/find /var/lib/vaultwarden/backup -mtime +30 -delete"; }; }; - timers.vaultwarden-backup-rotate = { wantedBy = [ "timers.target" ]; timerConfig = { diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index e0d8b2c..72395be 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -1,4 +1,4 @@ -test: ENC[AES256_GCM,data:MDd0sD9oGirw,iv:jhnNjMTqD6lyXckiuuySXnuxMf0N9w1AlsJvbt0GB+8=,tag:DssZmmRbErs99dds89Cr4A==,type:str] +zyxel_pass: ENC[AES256_GCM,data:yEqvWjeII7Uk7w==,iv:j7Lj2HCzlPRnVlvd1c0dTd41gdJAGRi+U6jrH0Vm71U=,tag:AvcPhHJVTCubu/YD9RBivg==,type:str] cachix_auth_token: ENC[AES256_GCM,data:nR7e2ZOA3q5DmkrqFEzINpKFEHVD5nyzc3DQ3QgD42fdyABV+r1Ela3iEcbU8SWj5JMRq8T1r7QxqcYW+VSMsT2cjQV2e4ZrpUmkX2QnhfmLqQBdJLhgNKBnu+x8QGJpQ3j7mG23atJ3BDTYBEKlI8y6wLEgpTX8GIVzHJVwfbqewTX4EfFyh3mVMtxAK9II/w==,iv:CSMcUdsqC97fmu1Po3cRrUj9h51Wv+KaUPfEToE7qVs=,tag:s1XHG2eyZYJJ5xd9CZb+Pw==,type:str] GROQ_API_KEY: ENC[AES256_GCM,data:OyuC4jfw67sCDa0XBGr78S6pzPV1ruy7KiIqPMgWWcOCVm3Y/khXEYPMjUTGrq9YLOw1MLso0OE=,iv:0y9klMYVtGsqAaLc2JidjZYSLhhbcbWbnBf8sZiC3rM=,tag:r6G2pzZn2d9JIaS+ozKnmg==,type:str] OPENWEATHER_API_KEY: ENC[AES256_GCM,data:bcuLz70u40nZfNgPTaeNRXdR/zjx0SQjwMbMNNFqROI=,iv:VCzse1a1/k1ZDIpFPL1QhjuS6YaDyohWi61JZaoc0Ws=,tag:UJSNyniNNLfGGRY/uiJcRA==,type:str] @@ -38,7 +38,7 @@ sops: N3I5dzUwc3JtYzczMUhyT04vSHlZamMKT+FzYcDLmlEFYxm/XoBpJb8XaZzBH1v9 6fuez+zApathZfl14w41kAUojPWBznnxDqYtNvzVVLXwnpp3BMx+7w== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-05-19T17:36:24Z" - mac: ENC[AES256_GCM,data:1tWUPr3njHiQeTH8jdVTIPRIIhOvQSJthGTgjT0P+WvOSZlRgwPK/vM6beweK6xfxdDk4dzRNKxZv3QzKiCwJTaP91MZaRVOly3u2ST0tE1FANo3ZqM34ePBVtlR9q+pDO05JczIjWBTlUcBsVqGKMSpAhn1jZDSY45r18HNwqQ=,iv:6X5FoNb7vl4BYniUE3D4e8wGZVGHbqgsFC1Wv70dCKc=,tag:bRxXg2UfXBhqeXZqdR9Unw==,type:str] + lastmodified: "2026-05-20T08:25:30Z" + mac: ENC[AES256_GCM,data:rh4FcdDtUVvEvv/0XR/J62SgRlv/c0Wve4IIjlr3jItdPkIIkncX+ychxwSIqQEzcQD4BO6MJ7Ex1HXcOP0+5pg3Qvysj+J8y5JGpoIi2dAGh9A7uzMG/cOQD4TuUAQl+HsO6U9b/hrJg6qwyqxrvsupEkH4c7zCb7WbpZfn0o0=,iv:ZQ59dQXJqvLIqlyJmHCByF12Oi6e9vp9ikGGIERIyQE=,tag:Mgbxhu7rOdiHFv+EoYAPuA==,type:str] unencrypted_suffix: _unencrypted version: 3.12.2