Compare commits
3
Commits
8ec061bede
...
2b1797770f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1797770f | ||
|
|
e210e344c8 | ||
|
|
6f16735747 |
@@ -42,6 +42,7 @@ The module (`nixos-module/default.nix`, `config.services.hortusfox-web`) wires H
|
||||
- **`cp` carries the store's read-only mode onto the copy.** Every directory `cp -r` creates inherits the store's `555` (no write) mode, so the script reopens them for writing (`find "${runDir}" -type d -exec chmod u+w {} +`, directories only, never individual files) before it can `rm`/relink anything under them.
|
||||
- **Writable subdirs are seeded, not just replaced.** `cache`, `app/logs`, `public/img`, `public/attachments`, `public/backup`, `public/themes` get swapped for symlinks into persistent state (`stateDir = /var/lib/hortusfox`). Two of these (`public/img`, `public/themes`) ship real default assets in the package itself (login background, default theme) — naively replacing them with an empty persistent dir on first boot silently deletes those defaults and breaks page rendering (`Exception: Resource "img/pattern.jpg" not found`). The fix: before symlinking, `cp -rn` (no-clobber) the package's copy of each writable dir into `stateDir/<dir>` first, so shipped defaults get seeded in (and any *new* ones added by a future upstream bump get picked up too) without ever touching a file the user/app has already written.
|
||||
- **Nested writable dirs need their own tmpfiles rule for the parent, too.** `systemd.tmpfiles.rules` declares a `d` line per entry in `writableDirs`, but `app/logs` and the four `public/*` entries are nested under `app`/`public`, which have no rule of their own. `systemd-tmpfiles` auto-vivifies a missing parent for a nested `d` line, but does so with default `root:root 0755` ownership and does **not** reliably go on to create the requested leaf — so `app/logs` (and all of `public/*`) silently never got created, and the hortusfox-setup service's own `mkdir -p "${stateDir}/${d}"` would fail with `Permission denied` trying to create the leaf inside a root-owned, not-writable-by-`cfg.user` parent. Fixed by computing `writableDirParents` (the unique set of intermediate dirs, e.g. `app`, `public`) and giving each its own explicit tmpfiles `d` rule, ordered before the leaves. If you add a new *nested* entry to `writableDirs`, make sure its parent is covered by this too.
|
||||
- **`public/backup` gets auto-cleaned; nothing else does.** HortusFox's own export/auto-backup feature (`BackupModule`/`BackupController`) writes to `public/backup/hf_backup_<timestamp>.zip` — a fully predictable filename — and never deletes it itself (the auto-backup cron only does so when an external `backup_path` is also configured in the admin panel; the manual "Export" flow never does). Since `public/backup` is served as a real static file by nginx/caddy before ever reaching the app's own auth-gated routes, that zip — a full data export — is downloadable by anyone who knows or guesses the timestamp, indefinitely. This is upstream's own design and can't be fixed from here, but `writableDirAges` gives `public/backup`'s tmpfiles `d` rule an `age` field driven by the `backupMaxAge` option (default `"1d"`, `null` disables), so `systemd-tmpfiles-clean.timer` prunes old exports and bounds the exposure window. Every other `writableDirs` entry keeps `age = "-"` (never auto-cleaned) via `writableDirAges.${d} or "-"`.
|
||||
- **`RuntimeDirectoryMode` and `stateDir`'s tmpfiles rules must both stay world-readable (`0755`).** nginx/caddy run as their own user, not `cfg.user`, and need to `stat`/read everything under `runDir` *and* into `stateDir`'s writable subdirs (`public/img` etc., reached through symlinks from `runDir`) to serve `public/` directly. At `0750` the proxy can't even traverse the directory, `tryFiles`/`file_server` stats fail with `Permission denied`, and — this is the trap — the site can still *appear* to work, because PHP-FPM (running as `cfg.user`) serves pages fine over fastcgi regardless of the proxy's own filesystem access; only requests for *static* files (not routed through PHP) actually exercise this path. A plain `curl` on `/` won't catch it; the VM tests fetch a real static asset and grep the proxy's own log for `Permission denied` specifically because of it. Nothing secret is exposed by the world-readable mode: `.env` is a symlink to `stateDir/.env`, which stays `600` independently.
|
||||
- **`.env` rendering.** A base attrset of env vars (`envVars`) is merged with the freeform `cfg.settings` attrset (`envVars // cfg.settings`, so `settings` always wins) and serialized to KEY=VALUE lines via `pkgs.writeText`. `envVars` isn't just our curated options (`domain`/`timezone`/`database.name`/`smtp.*`) — it mirrors the *entire* default set from upstream's `.env.example` (app metadata, session duration, photo-resize factors, `SMTP_AUTH`, `APP_BASEDIR`, ...). Leaving any of these out isn't cosmetic: several app code paths read `$_ENV[...]` directly with no `isset()` guard (`APP_BASEDIR` in particular), so an omitted key produces a `PHP Warning: Undefined array key` on *every single request*, not just a missing feature. `APP_VERSION` derives from `package.version` rather than being duplicated by hand, so it can't drift from `package.nix` on a version bump. Secrets are handled separately: if `cfg.secretFile` is set, its contents are `cat`'d verbatim onto the end of the rendered file at runtime — it is *not* parsed or filtered, so it can carry arbitrary keys (`APP_KEY`, `SMTP_PASSWORD`, custom API keys, …). The rendered file lands in `stateDir/.env`, then is symlinked into `runDir`.
|
||||
- **`settings.APP_DEBUG` matters more than it looks.** Upstream's `asatru` CLI (`app root`/`asatru` file) refuses to run at all unless `APP_DEBUG` is true — it just prints a warning and exits 0. That means the `hortusfox-setup` service's `php asatru migrate:fresh`/`migrate:upgrade` calls are silent no-ops unless `settings.APP_DEBUG = true` (or it's set via `secretFile`). The module's own default is `APP_DEBUG = false`, so **migrations do not run out of the box** — this is why `tests/default.nix` and both `vm-test`s explicitly set `settings.APP_DEBUG = true`.
|
||||
@@ -49,6 +50,7 @@ The module (`nixos-module/default.nix`, `config.services.hortusfox-web`) wires H
|
||||
- **`adminAccount.passwordFile` actually seeds an admin account.** This was previously a documented no-op; it isn't anymore, because there is no other way to log in at all. `migrate:fresh` creates schema only, no rows. HortusFox has no CLI/seed hook of its own, and its only "create user" flow lives in the admin panel — which requires already being logged in as an admin. So on first boot only (same `.installed` guard as migrations), if this is set, the setup script bcrypt-hashes the file's contents via `php -r '...password_hash(...)...'` (matching `UserModel`'s own hashing) and `INSERT`s a row directly into the `UserModel` table (name `adminAccount.username`, default `"admin"`, purely a display name since HortusFox logs in by email; email `adminAccount.email` — defaults to `admin@<domain>` but is independently overridable, since that default isn't necessarily a mailbox anyone controls and HortusFox's own "Restore password" flow emails it — admin flag set), bypassing the app's normal create-then-promote two-step flow since neither step is reachable yet. Both `vm-test`s exercise a full cookie-jar login against this seeded account, including scraping the CSRF token from `/auth` and checking the final URL isn't `/auth` again — page content alone isn't reliable here, since the dashboard's own (hidden) change-password form makes "does the response contain a password field" a false-positive-prone check.
|
||||
- **Serving.** `proxy.server` selects the reverse-proxy backend: `"nginx"` (default) or `"caddy"`, each gated by `lib.mkIf (cfg.proxy.server == "...")`. `proxy.ssl` (default `true`) toggles HTTPS/ACME on the vhost — nginx via `forceSSL`/`enableACME`, caddy by prefixing the site address with `http://` (which disables its automatic HTTPS for that vhost specifically) — set it to `false` for local/test deployments with no real domain or internet access to reach Let's Encrypt (both `vm-test`s and `tests/default.nix` do this). `services.phpfpm.pools.hortusfox` runs as `cfg.user`/`cfg.group` regardless of proxy backend, but its fastcgi socket's `listen.owner`/`listen.group` must match whichever proxy is actually enabled (`config.services.caddy.user`/`.group` vs `config.services.nginx.user`/`.group`) — this was hardcoded to nginx before caddy support existed. nginx uses explicit `tryFiles`/location blocks (an Asatru-framework-style rewrite-to-`index.php` rule, mirroring upstream's `.htaccess`); caddy's `php_fastcgi` directive covers the same rewrite-then-serve behavior natively. Dotfiles are denied on both. When `proxy.ssl = true`, the deploying host config must set nginx's `security.acme.{acceptTerms,defaults.email}` or caddy's `services.caddy.email` globally (see comment at the bottom of the module).
|
||||
- **`port` also has to override `SERVER_PORT` at the fastcgi layer, not just `APP_URL`.** Asatru's own `url()`/`asset()` helpers (`vendor/danielbrendel/asatru-php-framework/src/helper.php`) never read `APP_URL` at all — they build every absolute URL straight from `$_SERVER['SERVER_NAME']`/`SERVER_PORT`. Nginx's/Caddy's default fastcgi env populates `SERVER_PORT` from the port the proxy itself is listening on (e.g. the QEMU test VM's unforwarded guest port 80), not an externally-reachable/forwarded one (e.g. the hostfwd'd 8080) — so with only `APP_URL` fixed, page HTML still loaded fine (that request itself hits the forwarded port directly) but every asset/CSS/JS URL the app *generates* silently dropped the port, breaking every static asset (`net::ERR_CONNECTION_REFUSED` in the browser, since nothing listens on host port 80). Fixed by explicitly overriding the `SERVER_PORT` fastcgi param/env var to `cfg.port` whenever it's set — nginx via an extra `fastcgi_param SERVER_PORT "...";` line after `include fastcgi_params` (later same-name `fastcgi_param` wins), caddy via `php_fastcgi`'s `env SERVER_PORT ...` sub-directive. Verified by booting `nixosConfigurations.test` manually and confirming assets load over the forwarded port.
|
||||
- **Systemd sandboxing, and the umask trap.** `hortusfox-setup` and the cron services share a `commonHardening` serviceConfig attrset (NoNewPrivileges, Protect*, Restrict*, LockPersonality, RemoveIPC, RestrictAddressFamilies = `AF_UNIX`/`AF_INET`/`AF_INET6` for the mysql socket and SMTP/cron's loopback curl); `phpfpm-hortusfox` instead tightens `services.phpfpm`'s own baseline (`ProtectSystem` "full" → "strict" via `lib.mkForce`, since the phpfpm module sets it as a plain assignment) plus `ReadWritePaths = [ stateDir ]`. `hortusfox-setup`'s `ProtectSystem = "strict"` needs no extra `ReadWritePaths` since its declared `StateDirectory`/`RuntimeDirectory` are auto-exempted. Deliberately excluded: `UMask` — a tighter umask (e.g. `0027`) looks like obvious hardening, but silently reintroduces the exact "Permission denied" static-asset bug fixed above, since it changes what mode `cp -r`/`mkdir` produce for files hortusfox-setup writes to `runDir`/`stateDir` and phpfpm writes to (e.g. uploaded `public/attachments`) — from world-readable 0755/0644 down to 0750/0640 — and the later `find ... chmod u+w` only adds the owner-write bit back, it can't restore group/other bits umask already stripped at creation time. Also excluded: `MemoryDenyWriteExecute` (PHP/Zend may want executable heap for opcache) and `SystemCallFilter` (broad surface, easy to break something subtle in migrations/mysql/zip handling) — this is a first pass at blast-radius reduction, not maximal hardening, and was verified against both `vm-test`s (full boot + login) before landing.
|
||||
- **Cron.** HortusFox's cron endpoints (task overdue/tomorrow/recurring reminders, calendar reminders, auto-backup) are driven by `curl`ing the app's own HTTP cron routes from systemd timers (`hortusfox-cron-<name>` service+timer pairs), rather than an in-process scheduler. These live in the *same* `systemd.services` attrset as `hortusfox-setup`/`phpfpm-hortusfox` (merged in with `//`, generated via `lib.mapAttrs'` over `cronjobs`) — keep it that way; splitting the cron services into a second top-level `systemd.services = ...;` binding is a Nix duplicate-attribute error (partial-path assignments like `systemd.services.hortusfox-setup = ...` can't coexist with a later full `systemd.services = ...` reassignment in the same module).
|
||||
|
||||
## Current state / known issues
|
||||
|
||||
@@ -67,8 +67,9 @@ and a `services.hortusfox-web` NixOS module that wires the built package into a
|
||||
| `user` / `group` | string | `"hortusfox"` | System user/group the app and its phpfpm pool run as. |
|
||||
| `timezone` | string | `"UTC"` | App timezone (`APP_TIMEZONE`). |
|
||||
| `database.name` | string | `"hortusfox"` | MariaDB database name. |
|
||||
| `secretFile` | null or path | `null` | File of `KEY=VALUE` lines (`APP_KEY`, `SMTP_PASSWORD`, API keys, ...) appended verbatim onto the generated `.env` at runtime — not parsed, so it can carry arbitrary keys. Keep it out of the Nix store in production (agenix/sops-nix); a store path is fine for throwaway test configs. |
|
||||
| `adminAccount.passwordFile` | null or path | `null` | Plaintext password file for a seeded initial admin account. HortusFox has no CLI/seed hook of its own and its only "create user" flow lives behind the admin panel, so without this a fresh deployment has no way to log in at all. Only takes effect on first boot; changing it later has no effect on an existing account. |
|
||||
| `secretFile` | null or path | `null` | File of `KEY=VALUE` lines (`APP_KEY`, `SMTP_PASSWORD`, API keys, ...) appended verbatim onto the generated `.env` at runtime — not parsed, so it can carry arbitrary keys. Keep it out of the Nix store in production (agenix/sops-nix); a store path is fine for throwaway test configs. Read by `hortusfox-setup`, which runs as the module's system user, not root — make sure its owner/group lets that user read it. |
|
||||
| `backupMaxAge` | null or string | `"1d"` | How long HortusFox's own export/auto-backup zips (`public/backup`, systemd-tmpfiles "age" syntax, e.g. `"1d"`, `"12h"`) are kept before auto-deletion. HortusFox names these with a fully predictable `hf_backup_<timestamp>.zip` pattern and serves them from the public webroot without ever cleaning them up itself, so anyone who knows or guesses the timestamp can otherwise download a full data export indefinitely. `null` disables cleanup entirely (not recommended for internet-facing deployments). |
|
||||
| `adminAccount.passwordFile` | null or path | `null` | Plaintext password file for a seeded initial admin account. HortusFox has no CLI/seed hook of its own and its only "create user" flow lives behind the admin panel, so without this a fresh deployment has no way to log in at all. Only takes effect on first boot; changing it later has no effect on an existing account. Read by `hortusfox-setup` under the same non-root user as `secretFile` above. |
|
||||
| `adminAccount.username` | string | `"admin"` | Name for the seeded admin account — purely a display name, since HortusFox logs in by email rather than this field. |
|
||||
| `adminAccount.email` | string | `"admin@${domain}"` | Email for the seeded admin account. The default is just a guess at a real mailbox — set it to an address you actually control if you intend to use HortusFox's "Restore password" flow, which emails it. |
|
||||
| `settings` | attrs of str/int/bool | `{ }` | Extra/overriding `.env` entries, merged over the module's generated defaults (`settings` always wins). Note `php asatru` (used for migrations) only runs when `APP_DEBUG` is true, so migrations silently no-op unless it's set here or via `secretFile`. |
|
||||
|
||||
+101
-5
@@ -29,6 +29,23 @@ let
|
||||
"public/img" "public/attachments" "public/backup" "public/themes"
|
||||
];
|
||||
|
||||
# systemd-tmpfiles "age" overrides for specific writableDirs (default: "-",
|
||||
# never auto-cleaned). public/backup is HortusFox's own export/auto-backup
|
||||
# destination, named with a fully predictable `hf_backup_<timestamp>.zip`
|
||||
# pattern (see BackupModule/BackupController upstream) and -- since it
|
||||
# lives under public/, served as a real static file by nginx/caddy before
|
||||
# ever reaching the app's own auth-gated routes -- effectively downloadable
|
||||
# by anyone who knows or guesses that timestamp, indefinitely, since
|
||||
# upstream never deletes it (the auto-backup cron only does so when an
|
||||
# external backup_path is also configured in the admin panel; the manual
|
||||
# "Export" flow never does). We can't fix upstream's public-webroot
|
||||
# placement or predictable naming from here, but pruning anything older
|
||||
# than backupMaxAge bounds how long a given export stays fetchable, via
|
||||
# systemd's own periodic systemd-tmpfiles-clean.timer.
|
||||
writableDirAges = {
|
||||
"public/backup" = if cfg.backupMaxAge == null then "-" else cfg.backupMaxAge;
|
||||
};
|
||||
|
||||
# Intermediate parent dirs of the above (e.g. "app", "public") that need
|
||||
# their own explicit tmpfiles rule too: systemd-tmpfiles auto-vivifies a
|
||||
# missing parent when processing a nested `d` line, but does so with
|
||||
@@ -135,6 +152,43 @@ let
|
||||
cronPort = if cfg.proxy.ssl then 443 else 80;
|
||||
cronUrl = path: "${scheme}://${cfg.domain}:${toString cronPort}${path}";
|
||||
|
||||
# Baseline systemd sandboxing shared by this module's own units
|
||||
# (hortusfox-setup and the cron jobs; phpfpm-hortusfox gets its own,
|
||||
# layered on top of what services.phpfpm already sets). Deliberately
|
||||
# conservative: no MemoryDenyWriteExecute (PHP/Zend may want executable
|
||||
# heap for opcache) and no SystemCallFilter (broad surface, easy to break
|
||||
# something subtle in migrations/mysql/zip handling) -- this is a first
|
||||
# pass at blast-radius reduction, not maximal hardening. RestrictAddress-
|
||||
# Families keeps AF_INET/AF_INET6 (SMTP, cron's loopback curl) alongside
|
||||
# AF_UNIX (the mysql socket). Deliberately no UMask override: this
|
||||
# module's whole design (see the "world-readable 0755" notes elsewhere)
|
||||
# depends on hortusfox-setup's and phpfpm's default umask producing
|
||||
# world-readable files/dirs (0755/0644) for nginx/caddy, a different
|
||||
# user, to traverse and serve statically -- a tighter umask (e.g. 0027)
|
||||
# silently reintroduces the exact "Permission denied" static-asset bug
|
||||
# that was already fixed once, since a later `chmod u+w` only adds the
|
||||
# owner-write bit back and can't restore group/other bits umask stripped
|
||||
# at creation time.
|
||||
commonHardening = {
|
||||
NoNewPrivileges = true;
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||
};
|
||||
|
||||
# curl'd cron endpoints -> (name, path, calendar spec)
|
||||
cronjobs = {
|
||||
"tasks-overdue" = { path = "/cronjob/tasks/overdue"; when = "*-*-* 06,18:00:00"; };
|
||||
@@ -212,7 +266,27 @@ in {
|
||||
File with secret KEY=VALUE lines (APP_KEY, SMTP_PASSWORD, API keys,
|
||||
...) appended verbatim to the generated .env at runtime. Keep it out
|
||||
of the Nix store in production (e.g. agenix/sops-nix); a store path
|
||||
is fine for throwaway test configs.
|
||||
is fine for throwaway test configs. Read by hortusfox-setup, which
|
||||
runs as cfg.user rather than root -- if you point this at an
|
||||
agenix/sops-nix secret, make sure its owner/group is set to
|
||||
cfg.user/cfg.group (or make it world-readable), or the service fails
|
||||
with a permission error.
|
||||
'';
|
||||
};
|
||||
|
||||
backupMaxAge = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = "1d";
|
||||
example = "7d";
|
||||
description = ''
|
||||
How long HortusFox's own export/auto-backup zips (written to
|
||||
public/backup, using systemd-tmpfiles "age" syntax, e.g. "1d",
|
||||
"12h") are kept before being auto-deleted. HortusFox names these
|
||||
files with a fully predictable hf_backup_<timestamp>.zip pattern and
|
||||
serves them from the public webroot without ever cleaning them up
|
||||
itself, so anyone who knows or guesses the timestamp can download a
|
||||
full data export indefinitely otherwise. Set to null to disable
|
||||
cleanup entirely (not recommended for internet-facing deployments).
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -228,7 +302,9 @@ in {
|
||||
a fresh deployment has no way to log in at all. Only used on first
|
||||
boot (guarded by the same stateDir/.installed marker as
|
||||
migrate:fresh); changing it afterwards has no effect on an
|
||||
existing account.
|
||||
existing account. Read by hortusfox-setup, which runs as cfg.user
|
||||
rather than root -- as with secretFile, make sure this file's
|
||||
owner/group lets cfg.user read it.
|
||||
|
||||
Seeds a user (name set via adminAccount.username, email via
|
||||
adminAccount.email), bcrypt-hashed via
|
||||
@@ -328,7 +404,7 @@ in {
|
||||
systemd.tmpfiles.rules =
|
||||
[ "d ${stateDir} 0755 ${cfg.user} ${cfg.group} - -" ]
|
||||
++ map (d: "d ${stateDir}/${d} 0755 ${cfg.user} ${cfg.group} - -") writableDirParents
|
||||
++ map (d: "d ${stateDir}/${d} 0755 ${cfg.user} ${cfg.group} - -") writableDirs;
|
||||
++ map (d: "d ${stateDir}/${d} 0755 ${cfg.user} ${cfg.group} ${writableDirAges.${d} or "-"} -") writableDirs;
|
||||
|
||||
# Kept as one attrset: hortusfox-setup/phpfpm-hortusfox plus the
|
||||
# cron-generated services all live under systemd.services, and a
|
||||
@@ -341,7 +417,7 @@ in {
|
||||
requires = [ "mysql.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
serviceConfig = commonHardening // {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = cfg.user;
|
||||
@@ -353,6 +429,11 @@ in {
|
||||
# which stays mode 600 regardless of this.
|
||||
RuntimeDirectory = "hortusfox";
|
||||
RuntimeDirectoryMode = "0755";
|
||||
# Whole filesystem read-only except /dev, /proc, /sys and the
|
||||
# StateDirectory/RuntimeDirectory declared above, which systemd
|
||||
# auto-exempts -- everything this script writes to (stateDir,
|
||||
# runDir) is covered by those, so no extra ReadWritePaths needed.
|
||||
ProtectSystem = "strict";
|
||||
};
|
||||
|
||||
path = [ phpPackage config.services.mysql.package pkgs.coreutils pkgs.gnused ];
|
||||
@@ -434,17 +515,32 @@ in {
|
||||
phpfpm-hortusfox = {
|
||||
after = [ "hortusfox-setup.service" ];
|
||||
requires = [ "hortusfox-setup.service" ];
|
||||
# services.phpfpm already sets a reasonable baseline (PrivateTmp,
|
||||
# ProtectHome, ProtectSystem = "full", RestrictAddressFamilies).
|
||||
# Tighten ProtectSystem to "strict" (whole filesystem read-only
|
||||
# except /dev, /proc, /sys and explicit exceptions) -- stateDir is
|
||||
# the only path phpfpm actually needs to write to, since every
|
||||
# writable subdir under runDir is a symlink resolving there; runDir
|
||||
# itself (the app code) only needs to be read, which ProtectSystem
|
||||
# doesn't restrict. mkForce is needed since services.phpfpm sets
|
||||
# ProtectSystem = "full" as a plain (non-mkDefault) assignment.
|
||||
serviceConfig.ProtectSystem = lib.mkForce "strict";
|
||||
serviceConfig.ReadWritePaths = [ stateDir ];
|
||||
};
|
||||
} // (lib.mapAttrs' (name: job:
|
||||
lib.nameValuePair "hortusfox-cron-${name}" {
|
||||
description = "HortusFox cronjob: ${name}";
|
||||
after = [ "${cfg.proxy.server}.service" ];
|
||||
serviceConfig = {
|
||||
serviceConfig = commonHardening // {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
ExecStart = "${pkgs.curl}/bin/curl -fsS --max-time 300 "
|
||||
+ "--resolve ${cfg.domain}:${toString cronPort}:127.0.0.1 "
|
||||
+ "${cronUrl job.path}";
|
||||
# A single outbound curl to the app's own loopback address --
|
||||
# doesn't touch the filesystem at all, so lock it down fully.
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = "tmpfs";
|
||||
};
|
||||
}) cronjobs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user