diff --git a/hosts/harbor/configuration.nix b/hosts/harbor/configuration.nix index 5c1fc46..2b16219 100644 --- a/hosts/harbor/configuration.nix +++ b/hosts/harbor/configuration.nix @@ -23,6 +23,11 @@ owner = config.services.gitea.user; key = "databasePassword"; }; + sops.secrets."gotify-admin-pass" = { + sopsFile = ../../secrets/harbor/gotify.yaml; + owner = config.hive.gotify-instance.user; + key = "adminPassword"; + }; # Configure nix and garbage collection nix = { @@ -58,6 +63,9 @@ hive.gitea-instance.enable = true; hive.gitea-instance.instanceFQDN = "git.jroeger.de"; hive.gitea-instance.databasePasswordFile = config.sops.secrets.gitea-db-pass.path; + hive.gotify-instance.enable = true; + hive.gotify-instance.instanceFQDN = "gotify.jroeger.de"; + hive.gotify-instance.adminPasswordSopsKey = config.sops.secrets.gotify-admin-pass.name; hive.nextcloud-instance.enable = true; hive.nextcloud-instance.ssl = true; hive.nextcloud-instance.adminPasswordFile = config.sops.secrets.nextcloud-admin-pass.path; diff --git a/modules/default.nix b/modules/default.nix index 347c458..94d4ef0 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -28,6 +28,7 @@ ./programs/spotify-shortcuts.nix ./services/borg-server.nix ./services/gitea-instance.nix + ./services/gotify-instance.nix ./services/kdeconnect.nix ./services/nextcloud-instance.nix ./services/virt-manager.nix diff --git a/modules/services/gotify-instance.nix b/modules/services/gotify-instance.nix new file mode 100644 index 0000000..453a341 --- /dev/null +++ b/modules/services/gotify-instance.nix @@ -0,0 +1,131 @@ +{ + config, + lib, + ... +}: let + cfg = config.hive.gotify-instance; + server-config = { + server = { + listenaddr = "localhost"; + port = 54545; + ssl.enabled = false; + ssl.redirecttohttps = false; + cors.alloworigins = ["${cfg.instanceFQDN}"]; + stream.allowedorigins = ["${cfg.instanceFQDN}"]; + }; + database = { + dialect = "postgres"; + connection = "host=/run/postgresql dbname=${cfg.user} user=${cfg.user}"; + }; + defaultuser = { + name = "admin"; + pass = config.sops.placeholder.${cfg.adminPasswordSopsKey}; + }; + registration = false; + }; + server-config-yaml = lib.generators.toYAML {} server-config; +in { + options.hive.gotify-instance = { + enable = lib.mkEnableOption "Enable the Gotify instance"; + instanceFQDN = lib.mkOption { + type = lib.types.singleLineStr; + example = "gotify.example.com"; + description = "Fully qualified domain name of the Gotify instance"; + }; + user = lib.mkOption { + type = lib.types.singleLineStr; + default = "gotify"; + description = "The user to run the service as"; + }; + group = lib.mkOption { + type = lib.types.singleLineStr; + default = "gotify"; + description = "The group to run the service as"; + }; + adminPasswordSopsKey = lib.mkOption { + type = lib.types.singleLineStr; + description = "The SOPS key for the default admin user"; + }; + }; + + config = lib.mkIf cfg.enable { + services.gotify.enable = true; + + # Config setup + sops.templates."gotify-server-config.yml" = { + owner = cfg.user; + content = server-config-yaml; + }; + environment.etc."gotify/config.yml".source = config.sops.templates."gotify-server-config.yml".path; + + # User setup + users.users = lib.mkIf (cfg.user == "gotify") { + gotify = { + description = "Gotify service"; + useDefaultShell = true; + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = lib.mkIf (cfg.group == "gotify") { + gotify = {}; + }; + + # Configure gotify to run as the specified user (for postgres authentication) + systemd.services.gotify-server = { + serviceConfig = { + DynamicUser = lib.mkForce false; + User = cfg.user; + RuntimeDirectory = "gotify"; + }; + }; + + # Fallback server with only 403 + services.nginx.virtualHosts.${config.networking.domain} = lib.mkDefault { + default = true; + locations."/".return = 403; + forceSSL = true; + enableACME = true; + }; + + # Virtual host for gotify + services.nginx.virtualHosts."${cfg.instanceFQDN}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://${server-config.server.listenaddr}:${toString server-config.server.port}"; + extraConfig = '' + # Ensuring it can use websockets + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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 http; + proxy_redirect http:// $scheme://; + + # The proxy must preserve the host because gotify verifies the host with the origin + # for WebSocket connections + proxy_set_header Host $host; + + # These sets the timeout so that the websocket can stay alive + proxy_connect_timeout 1m; + proxy_send_timeout 1m; + proxy_read_timeout 1m; + ''; + }; + }; + + # Database setup + services.postgresql = { + enable = true; + ensureDatabases = [cfg.user]; + ensureUsers = [ + { + name = cfg.user; + ensureDBOwnership = true; + } + ]; + }; + }; +} diff --git a/secrets/harbor/gotify.yaml b/secrets/harbor/gotify.yaml new file mode 100644 index 0000000..82b95ef --- /dev/null +++ b/secrets/harbor/gotify.yaml @@ -0,0 +1,25 @@ +adminPassword: ENC[AES256_GCM,data:I4P2Aujt8xv1ZQeHBbNYDoSxVa08,iv:tYWYQiWRu13HmxEaMuxmdSBgY0F0d6NOl3xEVdNdFCA=,tag:y8C8Fotgut90yo4giLDf3w==,type:str] +sops: + age: + - recipient: age1wf0rq27v0n27zfy0es8ns3n25e2fdt063dgn68tt3f89rgrtu9csq4yhsp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVWC9JN1gvN1FTem5JWCt6 + UzVyU1FoUVlNVzViT1dmYis5bzNXWDBFaEF3CkZOcTFGZERoMnNnTFNEWXl0blNC + dXJLUDAyR1p2VVowQXM3Sk43Sk90K2sKLS0tIFd4RE52QjFhTSswa0JTNHd4dnhs + SWQ2YVhVUjFBOEJLWFhVMjZmTlFVUU0KzU2A9OrsbvmciS1tHbSz4kPo3N3lzquK + z3yINH+Hff/qvMuiKpngyVl/hzxJXUmtuA1W7xJc38zkSTbGqZ7jcg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1expg8vyduf290pz7l4f3mjzvk9f0azfdn48pyjzs3m6p7v4qjq0qwtn36z + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOdklaZUZBWnpYbmRzR0Ns + YlNQdDZvNG0ybE9leEZlaHJIZ1pqUjc4a3owCjJGWWxlUFcxTi84ZEtadkN3VHlE + Tzh1dDZ4cDF2dDZVajZTTzE1bmVXUU0KLS0tIEQ1dGF3bmlTQU0xdGJBWEhlNGtY + eG5mRkhaZjk0VDVxNG8wZVZnZ1ZPbGsKWfsNKT98X19b1ipgk8YPJmXjmmpLmXYL + 7e4ViRsetxSQGsELY3Cw5Bg2+rQW64fjajk0cloChN12l0q1FifJuw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-10-30T14:07:46Z" + mac: ENC[AES256_GCM,data:08oJt55ARwK5eSUpf2oBvw8f/cYrgsAFJS7DTN09mLzeGurh3LyzoL43Cq6GrtLaIry3Hzu6Zn0MmIY+GDXG1vdnvdgXF0JPffsLIFLg1n951RWiuZlpDbbhIdncErM4X/9EpAGqsko8UXa+X2u2Yl6b08HS/6Hz6s0lfuRTovo=,iv:TENht5StVCtsgI67mT/yGKYxHXLIrYE6ufj7sffcvW4=,tag:WhdWzNZNHm3EQZHEq1rXxQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.11.0