commit 523019b00d93b8aa4503c180ce43776eb6f1c39a Author: padreug Date: Thu Jan 15 12:59:16 2026 +0100 Initial commit: Fava NixOS module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/fava.nix b/fava.nix new file mode 100644 index 0000000..d2efee7 --- /dev/null +++ b/fava.nix @@ -0,0 +1,156 @@ +{ config, lib, pkgs, domain ? null, ... }: + +with lib; + +let + cfg = config.services.fava; + +in { + options.services.fava = { + enable = mkEnableOption "Fava web interface for Beancount"; + + ledgerFile = mkOption { + type = types.path; + default = "/var/lib/fava/ledger.beancount"; + description = "Path to the beancount ledger file"; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Host address to bind to"; + }; + + port = mkOption { + type = types.port; + default = 5000; + description = "Port to run Fava on"; + }; + + user = mkOption { + type = types.str; + default = "fava"; + description = "User account under which Fava runs"; + }; + + group = mkOption { + type = types.str; + default = "fava"; + description = "Group under which Fava runs"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/fava"; + description = "Directory for Fava data and ledger files"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open the firewall port for Fava"; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = []; + description = "Additional command-line options to pass to Fava"; + example = [ "--read-only" "--incognito" ]; + }; + + nginx = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable nginx reverse proxy for Fava"; + }; + + subdomain = mkOption { + type = types.str; + default = "fava"; + description = "Subdomain for Fava (e.g., 'fava' for fava.domain.com)"; + }; + }; + }; + + config = mkIf cfg.enable { + # Create system user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + description = "Fava service user"; + }; + + users.groups.${cfg.group} = {}; + + # Ensure data directory and ledger file exist with correct permissions + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" + "f ${cfg.ledgerFile} 0640 ${cfg.user} ${cfg.group} - -" + ]; + + # Fava systemd service + systemd.services.fava = { + description = "Fava - Web interface for Beancount"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + + ExecStart = "${pkgs.fava}/bin/fava ${cfg.ledgerFile} --host ${cfg.host} --port ${toString cfg.port} ${concatStringsSep " " cfg.extraOptions}"; + + Restart = "on-failure"; + RestartSec = "5s"; + + # Security hardening + NoNewPrivileges = true; + PrivateHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHome = true; + ProtectSystem = "full"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + + # Allow read/write access to data directory + ReadWritePaths = [ cfg.dataDir ]; + }; + }; + + # Nginx reverse proxy configuration (optional) + services.nginx.virtualHosts = mkIf (cfg.nginx.enable && domain != null) { + "${cfg.nginx.subdomain}.${domain}" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://${cfg.host}:${toString cfg.port}"; + proxyWebsockets = true; + extraConfig = '' + 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_buffering off; + ''; + }; + }; + }; + + # Open firewall port if requested + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..85fcb55 --- /dev/null +++ b/flake.nix @@ -0,0 +1,10 @@ +{ + description = "Fava (Beancount web UI) NixOS module"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: { + nixosModules.fava = import ./fava.nix; + nixosModules.default = self.nixosModules.fava; + }; +}