hortusfox-setup and the cron jobs previously ran with no systemd
hardening at all; phpfpm-hortusfox only had whatever services.phpfpm
sets by default (ProtectSystem = "full"). Add a shared conservative
baseline (NoNewPrivileges, Protect*, Restrict*, LockPersonality,
RemoveIPC, RestrictAddressFamilies scoped to AF_UNIX/AF_INET/AF_INET6)
to hortusfox-setup and the cron services, and tighten phpfpm's own
ProtectSystem to "strict" plus an explicit ReadWritePaths for stateDir.
Deliberately no UMask override: a tighter umask silently reintroduces
the "Permission denied" static-asset bug already fixed once, since it
changes the mode cp/mkdir produce for files nginx/caddy (a different
user) need to read directly. Also no MemoryDenyWriteExecute or
SystemCallFilter for now -- broader surface, easy to break something
subtle in migrations/mysql/zip handling without much more testing.
Verified against both vm-tests (full boot + cookie-jar login) for both
proxy backends before landing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hortusfox-setup reads both files as cfg.user, not root. Pointing either
at an agenix/sops-nix secret without setting its owner/group to match
cfg.user/cfg.group fails the service with a permission error rather
than anything obviously related -- note it on both options.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HortusFox writes export/auto-backup zips to public/backup using a
fully predictable hf_backup_<timestamp>.zip name, serves that
directory as real static files (ahead of its own auth-gated routes),
and never deletes them itself unless an external backup_path is
separately configured. That leaves full data exports downloadable by
anyone who knows or guesses the timestamp, indefinitely. This can't be
fixed from packaging alone, but giving public/backup's tmpfiles rule
an age (new backupMaxAge option, default "1d", null to disable) lets
systemd-tmpfiles-clean.timer prune old exports and bound the exposure
window. Every other writableDirs entry is unaffected (age stays "-").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cron previously curled appUrl (the externally-reachable domain, with
cfg.port appended when set). That meant every cron tick depended on
the host being able to route back to its own public domain (hairpin
NAT, not guaranteed), and whenever cfg.port was set to an
externally-forwarded port nginx/caddy don't themselves listen on (e.g.
the QEMU test VM's hostfwd), cron would curl a port nothing is
listening on at all and fail outright.
curl --resolve now pins the connection to 127.0.0.1 on the port
nginx/caddy actually listen on (80, or 443 under proxy.ssl), while
keeping the Host header/SNI as cfg.domain so vhost routing and cert
validation still work normally. Also fixes the cron services'
"after" dependency, which was hardcoded to nginx.service even when
proxy.server = "caddy".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
proxy.ssl = true (the default) requests a real ACME certificate for
services.hortusfox-web.domain. Left at its own default of "localhost",
that request can never succeed -- Let's Encrypt won't issue for it --
but previously this only surfaced as an ACME failure at activation
time. Catch it at eval time instead with a clear assertion message.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both fields were spliced directly into a single-quoted SQL string. An
ordinary value containing an apostrophe (e.g. adminAccount.username =
"O'Brien") broke the INSERT's SQL syntax outright, failing
hortusfox-setup on first boot under set -euo pipefail; a crafted value
could inject arbitrary SQL. Escape backslashes and quotes to match
MySQL/MariaDB's default string literal rules.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The seeded admin account's UserModel "name" was hardcoded to "admin".
Make it a separate, overridable option (still defaulting to "admin"
for compatibility) -- it's purely a display name, since HortusFox logs
in by email rather than this field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The seeded admin account's email was hardcoded to admin@<domain>, which
isn't necessarily a mailbox the deployer can actually receive mail at --
notably breaking HortusFox's own "Restore password" flow, which emails
that address. Make it a separate, overridable option (still defaulting
to admin@<domain> for compatibility).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Asatru's own url()/asset() helpers build absolute URLs from
$_SERVER['SERVER_PORT'] rather than APP_URL, and nginx/caddy populate
that from the port they're actually listening on -- not an externally
forwarded one (e.g. the QEMU test VM's hostfwd to 8080). That silently
dropped the port from every generated asset/redirect URL, breaking all
static assets when accessed through a forwarded port. Override
SERVER_PORT at the fastcgi layer to cfg.port when set, for both
backends.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Packages HortusFox from source via php.buildComposerProject and provides
a NixOS module (services.hortusfox-web) to deploy it as a full service
(mariadb + phpfpm + nginx/caddy + ACME + cron timers).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>