From 8ba6decc1fd5542ddd9adf058fb104aef085aac8 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Tue, 5 May 2026 02:05:16 -0400 Subject: [PATCH] firefly-iii-data-importer: init --- hosts/muffin/default.nix | 1 + hosts/muffin/service-configs.nix | 9 ++ modules/server-age-secrets.nix | 18 +++ secrets/secrets.nix | Bin 3546 -> 3675 bytes .../firefly-iii-data-importer-app-key.age | Bin 0 -> 286 bytes secrets/server/firefly-iii-fidi-token.age | Bin 0 -> 272 bytes services/firefly-iii-data-importer.nix | 128 ++++++++++++++++++ 7 files changed, 156 insertions(+) create mode 100644 secrets/server/firefly-iii-data-importer-app-key.age create mode 100644 secrets/server/firefly-iii-fidi-token.age create mode 100644 services/firefly-iii-data-importer.nix diff --git a/hosts/muffin/default.nix b/hosts/muffin/default.nix index c0cd5ae..d5f15b9 100644 --- a/hosts/muffin/default.nix +++ b/hosts/muffin/default.nix @@ -54,6 +54,7 @@ # ../../services/llama-cpp.nix ../../services/trilium.nix ../../services/firefly-iii.nix + ../../services/firefly-iii-data-importer.nix ../../services/ups.nix diff --git a/hosts/muffin/service-configs.nix b/hosts/muffin/service-configs.nix index 2043d85..53a11d2 100644 --- a/hosts/muffin/service-configs.nix +++ b/hosts/muffin/service-configs.nix @@ -340,6 +340,15 @@ rec { domain = "firefly.${site_config.domain}"; }; + firefly_iii_data_importer = rec { + dataDir = services_dir + "/firefly-iii-data-importer"; + domain = "firefly-import.${site_config.domain}"; + # SimpleFIN config json the user uploads after first interactive setup. + # `JSON_CONFIGURATION_DIR` defaults to `/configurations` per the + # data-importer module's tmpfiles rules; we reuse that path. + simplefinConfigPath = "${dataDir}/storage/configurations/simplefin.json"; + }; + media = { moviesDir = torrents_path + "/media/movies"; tvDir = torrents_path + "/media/tv"; diff --git a/modules/server-age-secrets.nix b/modules/server-age-secrets.nix index 9eaec0f..0f09962 100644 --- a/modules/server-age-secrets.nix +++ b/modules/server-age-secrets.nix @@ -199,5 +199,23 @@ owner = "firefly-iii"; group = "caddy"; }; + + # Firefly III Data Importer Laravel APP_KEY (base64:<32 random bytes>) + firefly-iii-data-importer-app-key = { + file = ../secrets/server/firefly-iii-data-importer-app-key.age; + mode = "0400"; + owner = "firefly-iii-data-importer"; + group = "caddy"; + }; + + # Firefly III Personal Access Token used by the data importer (FIREFLY_III_ACCESS_TOKEN). + # First-deploy ciphertext is a placeholder string; rotate after creating + # the PAT in the Firefly UI (Profile → OAuth → Personal Access Tokens). + firefly-iii-fidi-token = { + file = ../secrets/server/firefly-iii-fidi-token.age; + mode = "0400"; + owner = "firefly-iii-data-importer"; + group = "caddy"; + }; }; } diff --git a/secrets/secrets.nix b/secrets/secrets.nix index afc5dd5fd84718d9310e02423f5e38aa8e6c5be8..1b3cfb51245827912ea9b698a79e70b6f38975eb 100644 GIT binary patch literal 3675 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 x>&IX9lzH~@RQ{gUKQrX2j!b!&alz`z zrNp(7PDdx7{G>BaVy?eA^Rz>*Gj~nfl3jOc@y6EjPZsQHBKM{~TUq_3GSX4<8gtUV zgr}!?4D=@|M9+TqCGXJAV3nyyPdt@sx_j5vt@7}M^BhifIogjV7&qO#bp8I9b^i8o z8?I>F*=}#By+Zx3V-l13I~U_84)^RsGufssulljQ)pF`oziTS5qa;7PI_9#M`D&Zx iT%L=nri)L`)!?6=$(WaSE$i)G=ICmPA}>Z`@qGa9$cS42 literal 0 HcmV?d00001 diff --git a/services/firefly-iii-data-importer.nix b/services/firefly-iii-data-importer.nix new file mode 100644 index 0000000..25dcec4 --- /dev/null +++ b/services/firefly-iii-data-importer.nix @@ -0,0 +1,128 @@ +{ + config, + lib, + service_configs, + ... +}: +let + fidi = service_configs.firefly_iii_data_importer; +in +{ + imports = [ + # Same chicanery as services/firefly-iii.nix: the upstream module's + # actual systemd units are firefly-iii-data-importer-setup.service and + # phpfpm-firefly-iii-data-importer.service. Hook the zfs mount into the + # `-setup` unit; the upstream `requiredBy` chain pulls phpfpm forward. + (lib.serviceMountWithZpool "firefly-iii-data-importer-setup" service_configs.zpool_ssds [ + fidi.dataDir + ]) + ]; + + services.firefly-iii-data-importer = { + enable = true; + dataDir = fidi.dataDir; + # Same trick as firefly-iii: run as group caddy so caddy can read the + # php-fpm unix socket without us having to override `listen.group`. + group = "caddy"; + virtualHost = fidi.domain; + settings = { + APP_ENV = "production"; + APP_KEY_FILE = config.age.secrets.firefly-iii-data-importer-app-key.path; + LOG_CHANNEL = "syslog"; + LOG_LEVEL = "info"; + + # Importer → Firefly III. The /etc/hosts override below pins this hostname + # to 127.0.0.1 on muffin so the importer's outbound HTTPS lands on the + # local Caddy without hairpinning through the WAN gateway. The TLS cert + # is the same wildcard Caddy serves externally, so verification works. + FIREFLY_III_URL = "https://${service_configs.firefly_iii.domain}"; + FIREFLY_III_ACCESS_TOKEN_FILE = config.age.secrets.firefly-iii-fidi-token.path; + + TRUSTED_PROXIES = "127.0.0.1,::1"; + + # SimpleFIN polls the last 90d every run; without this, every daily run + # logs hundreds of "duplicate transaction" warnings as Firefly's server- + # side dedup catches them. Firefly still rejects the duplicates; we just + # don't see the noise. + IGNORE_DUPLICATE_ERRORS = true; + + # CLI-driven imports only. Leave the /autoimport HTTP endpoint disabled + # (default) — the systemd timer below uses `artisan importer:import` + # against a static config file, which avoids exposing another web- + # reachable surface that needs its own shared secret. + CAN_POST_AUTOIMPORT = false; + CAN_POST_FILES = false; + }; + }; + + # Pin the firefly-iii hostname to loopback on muffin so the importer's + # outbound API calls hit local Caddy directly. No effect on any other host. + networking.extraHosts = "127.0.0.1 ${service_configs.firefly_iii.domain}"; + + services.caddy.virtualHosts.${fidi.domain}.extraConfig = '' + encode zstd gzip + import ${config.age.secrets.caddy_auth.path} + + root * ${config.services.firefly-iii-data-importer.package}/public + php_fastcgi unix/${config.services.phpfpm.pools.firefly-iii-data-importer.socket} + file_server + ''; + + # Daily SimpleFIN import via the importer's CLI. The unit is gated by + # ConditionPathExists so it silently no-ops until the user has uploaded + # `simplefin.json` (per the post-deploy operational steps). + systemd.services.firefly-iii-data-importer-import = { + description = "Run scheduled Firefly III data importer config (SimpleFIN)"; + after = [ + "phpfpm-firefly-iii-data-importer.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + unitConfig.ConditionPathExists = fidi.simplefinConfigPath; + serviceConfig = { + Type = "oneshot"; + User = config.services.firefly-iii-data-importer.user; + Group = config.services.firefly-iii-data-importer.group; + WorkingDirectory = config.services.firefly-iii-data-importer.package; + ExecStart = "${config.services.firefly-iii-data-importer.package}/artisan importer:import ${fidi.simplefinConfigPath}"; + # Same hardening posture as the upstream commonServiceConfig. + ReadWritePaths = [ fidi.dataDir ]; + PrivateTmp = true; + PrivateDevices = true; + ProtectSystem = "strict"; + ProtectHome = "tmpfs"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectClock = true; + ProtectHostname = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + NoNewPrivileges = true; + RemoveIPC = true; + CapabilityBoundingSet = ""; + AmbientCapabilities = ""; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service @resources" + "~@obsolete @privileged" + ]; + }; + }; + + systemd.timers.firefly-iii-data-importer-import = { + description = "Daily Firefly III SimpleFIN import"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + RandomizedDelaySec = "1h"; + Persistent = true; + }; + }; +}