fava-module/fava.nix

156 lines
4.2 KiB
Nix

{ 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 ];
};
}