From c6c9e6c5d37afe3d08484c2392b76d56debcc00c Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Mon, 26 Oct 2026 14:14:05 -0700 Subject: [PATCH] case 4727 - docs: Linux server permissions, service security settings, 127.0.0.1 behind NGINX Install: program files read only for the service account, config.json 640, systemd security settings in the service file, and a final step binding AyaNova to 127.0.0.1 once NGINX works. Upgrade: the pre-8.3.0 step sets the same permissions instead of re-adding group write on the program folder, offers the security settings as a drop-in, and the upgrade commands use curl -fO so a failed download stops before the program files are deleted. Co-Authored-By: Claude Opus 5.5 --- devdocs/modernization/NET10-PORT.md | 3 +- devdocs/modernization/SECURITY-REVIEW.md | 2 +- docs/8.0/ayanova/docs/ops-config-use-urls.md | 6 ++- .../ayanova/docs/ops-install-linux-server.md | 48 +++++++++++++++++-- .../ayanova/docs/ops-upgrade-linux-server.md | 24 ++++++++-- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index 07b8ca7e..8bac5811 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -194,7 +194,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4724 | SMTP: certificate checked, password never returned | F30, F31. **Done** (certificate checked, opt-out config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` with manual page; password masked as `********` and kept on save; test `SmtpPasswordHidden`; self-signed listener proof; changelog lines for both) | | 4725 | Sessions: password change ends sessions, exact-path token exemption, default SuperUser refused when licensed | F7, F8, F9. **Done** (sessions end on reset, administrator password change and login disabled; exact route exemption, inactive users refused; default-password SuperUser limited to changing it on a purchased licence, option A; test `Sessions`; John's click-through with a temporary licence override; manual and changelog) | | 4726 | Login throttling/lockout (raven and sockeye) | F6. **Done**; the Sockeye half (`6d6c74f`), like 4710's, takes effect when Sockeye is deployed to its droplet (10 failures per login or 50 per address in 15 minutes, then 429 for the rest of the window; same in Sockeye, whose login is public; test `LoginLockout`; per-address limit proved with a forwarded documentation address) | -| 4727 | Low severity hardening | F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously). In steps: 1 logo (F23) **done**; 2 API root page (F11, F39) **done**; 3 `server-info` (F29) **done**; 4 template export (F22) **done**, job owner check accepted; 5 `OverrideUserId` and export download (F25) **done**; 6 safe headers (F35) **done**; 7 remove `dist/docker` (F38) **done**. All steps done; `sockeye-client` setup script: bind `127.0.0.1`, the `chmod`, the systemd options (F36, F38) **done** (`9f300a6`, tested on `devtestn10` 2026-10-26; existing droplets get `harden-830.sh` at the rollout). **Case done.** F10 moved to case 4736, after 8.3.0 (John, 2026-10-26) | +| 4727 | Low severity hardening | F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously). In steps: 1 logo (F23) **done**; 2 API root page (F11, F39) **done**; 3 `server-info` (F29) **done**; 4 template export (F22) **done**, job owner check accepted; 5 `OverrideUserId` and export download (F25) **done**; 6 safe headers (F35) **done**; 7 remove `dist/docker` (F38) **done**. All steps done; `sockeye-client` setup script: bind `127.0.0.1`, the `chmod`, the systemd options (F36, F38) **done** (`9f300a6`, tested on `devtestn10` 2026-10-26; existing droplets get `harden-830.sh` at the rollout); the Linux server install and upgrade manual pages updated to match (permissions, service settings, `127.0.0.1` behind NGINX, `curl -fO`). **Case done.** F10 moved to case 4736, after 8.3.0 (John, 2026-10-26) | | 4681 | Sockeye: licence/trial request hardening (tagged sockeye, rockfish) | F40. **Done** (sockeye `cd3ebed`: rate limits, email check, cryptographic codes; rvf unchanged) | | 4732 | Trusted reverse proxies setting (from the Sockeye migration runbook, Appendix A) | F6 follow-up. **Done** (`AYANOVA_TRUSTED_PROXIES`: addresses or CIDR networks, chain followed, invalid entries warned at boot; one log warning per start when `X-Forwarded-For` comes from an untrusted proxy; manual page and changelog. Proved by hand through the LAN address: not set, logins recorded as the proxy and the warning logged; set, the forwarded client address used, a forged extra entry ignored, a two-proxy chain followed; localhost unchanged. Not needed in Sockeye: its nginx connects on loopback) | @@ -278,6 +278,7 @@ Found during the port, out of scope. One line each. - Pre-existing, same in 8.2.4 (found in the 4579 sandbox test, John: Deferred): rendering a report for a record that does not exist (e.g. work order ids 1 and 2 in a trial without sample data) fails the job with a generic `ArgumentNullException` ("Value cannot be null. (Parameter 'o')") from `JObject.FromObject` in `WorkOrderBiz.GetReportData` (`WorkOrderBiz.cs:1030`), instead of skipping the missing record or saying it is not found. Other object types' `GetReportData` likely do the same - **Roles audit, case 4735 (John, 2026-10-25, this release; found in 4719):** check every object's rights as documented in the manual (each page's "Authorization Roles required" section and `ay-biz-admin-roles.md`) against `biz/BizRoles.cs`. Report templates were changeable by Business administration - restricted although the roles page uses exactly that case as its example of a view-only restricted role (fixed in 4719). If that slipped through there could be others; for each mismatch decide whether the code or the page is wrong - From case 4697 (John: Deferred): (1) a relative date range that crosses a daylight saving change can be an hour off at its far end, because the client sends only its current offset with list requests; exact would need the client to send its IANA time zone name too (front-end change). (2) Dashboard widgets group by day, week or month with `date_trunc` on the stored UTC times, so for a user far from UTC an item near midnight lands in the neighbouring bar; same need for the time zone name +- From case 4727 (John: later hardening case, 2026-10-26): AyaNova runs as `www-data` on droplets and in the Linux manual (as in Microsoft's ASP.NET Core with NGINX guide), the same account as NGINX's workers, so NGINX can read `config.json` (database password) and write `data`. A dedicated `ayanova` system account isolates them: service `User=`, ownership, the setup script and the manual, plus a migration for existing droplets and self-hosted installs - Found in the case 4727 droplet test (2026-10-26): stopping the service logs `ObjectDisposedException` from `JobsBiz.ProcessJobsAsync` (`JobsBiz.cs:328`), because the job loop runs once more after the host has disposed its services; the server-problem notification it tries to raise fails the same way, so nothing is sent. Seen at the first 8.3.0 stop on `devtestn10`; not yet checked whether 8.2.4 does the same. Every weekly reboot would log it - Pre-existing (found in the 4632 IIS test, John: Deferred): emailed report attachments have the content type `application/pdf/pdf`, because `Mailer.cs:51` builds `new MimePart("application/pdf", "pdf")` (media type and subtype) instead of `new MimePart("application", "pdf")`. Mail programs still open the file by its `.pdf` name diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index 4d1944e6..07307d0c 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -787,7 +787,7 @@ prepared database, so it is not that short: accepted. documented; delete it or fix it before anyone uses it. **John, 2026-10-24:** the `chmod` and the systemd sandboxing options go into the `sockeye-client` setup script (to do there; the systemd options need testing on a droplet so nothing that writes, such as logs, backups or the report browser download, is blocked). `dist/docker`: early development option, never offered: removed in case 4727, step 7 (2026-10-24; with its commented-out lines in `build-release.bat`; git history keeps it). -**Fixed for hosted in case 4727 (2026-10-26):** the setup script (`sockeye-client` `9f300a6`) no longer gives the service group write access to the program folder (`chmod -R go-w /var/ayanova`, then group write on `data` only) and adds `NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`, `PrivateTmp` and `ProtectHome` to the unit. Existing droplets get the same from a one-time `harden-830.sh`, run right before each droplet's 8.3.0 upgrade (in `devdocs/build/RELEASE-PROCEDURE.md`; the sandbox options as a drop-in, `ayanova.service.d/hardening.conf`; also `config.json` 640 and the old `Chrome` folders removed). Tested 2026-10-26 on `devtestn10` (Ubuntu 24.04.5), both paths, with the subscription zip of 2026-10-23: existing droplet with `harden-830.sh`, then a fresh install with the updated script (droplet rebuilt). Each time: AyaNova only on `127.0.0.1:7575`, the public address refused, HTTPS 200; `www-data` cannot write the program folder, can write `data`; a report render (including the first-use browser download into `data/report-browser`), a manual backup (pg_dump) and an attachment upload, download and delete all completed with no read-only or denied-access errors in the journal or the AyaNova log. `systemd-analyze security`: 9.2 to 8.3. Further systemd restrictions (kernel, address families, system calls) would each need the same test with the report browser and pg_dump: a later case if wanted. The desktop install running as root stays in Deferred. +**Fixed for hosted in case 4727 (2026-10-26):** the setup script (`sockeye-client` `9f300a6`) no longer gives the service group write access to the program folder (`chmod -R go-w /var/ayanova`, then group write on `data` only) and adds `NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`, `PrivateTmp` and `ProtectHome` to the unit. Existing droplets get the same from a one-time `harden-830.sh`, run right before each droplet's 8.3.0 upgrade (in `devdocs/build/RELEASE-PROCEDURE.md`; the sandbox options as a drop-in, `ayanova.service.d/hardening.conf`; also `config.json` 640 and the old `Chrome` folders removed). Tested 2026-10-26 on `devtestn10` (Ubuntu 24.04.5), both paths, with the subscription zip of 2026-10-23: existing droplet with `harden-830.sh`, then a fresh install with the updated script (droplet rebuilt). Each time: AyaNova only on `127.0.0.1:7575`, the public address refused, HTTPS 200; `www-data` cannot write the program folder, can write `data`; a report render (including the first-use browser download into `data/report-browser`), a manual backup (pg_dump) and an attachment upload, download and delete all completed with no read-only or denied-access errors in the journal or the AyaNova log. `systemd-analyze security`: 9.2 to 8.3. Self-hosted Linux servers (case 4727, 2026-10-26): the manual's install steps now set the same permissions and service settings and, once NGINX works, bind `127.0.0.1` (`ops-install-linux-server.md`, `ops-config-use-urls.md`); the 8.3.0 upgrade step no longer re-adds group write on the program folder and offers the settings as a drop-in for existing installs; its upgrade commands use `curl -fO`, so a failed download stops before the program files are deleted (`ops-upgrade-linux-server.md`). Further systemd restrictions (kernel, address families, system calls) would each need the same test with the report browser and pg_dump: a later case if wanted. The desktop install running as root stays in Deferred. OK in pass 10: the setup script generates a unique database password and JWT secret per droplet, stores them in `/root/ayanova-secrets.txt` (600), makes `config.json` 640 `root:www-data`, diff --git a/docs/8.0/ayanova/docs/ops-config-use-urls.md b/docs/8.0/ayanova/docs/ops-config-use-urls.md index 4e71db9c..c5bb9cbf 100644 --- a/docs/8.0/ayanova/docs/ops-config-use-urls.md +++ b/docs/8.0/ayanova/docs/ops-config-use-urls.md @@ -8,7 +8,11 @@ If no override is specified AyaNova will use the following default value: `"http://*:7575"` -This means AyaNova will listen on port 7575 +This means AyaNova will listen on port 7575 on all of the computer's network addresses. + +## Behind a reverse proxy + +When all users connect through a reverse proxy (such as NGINX) on the same computer, use `127.0.0.1` in place of `*`, for example `"http://127.0.0.1:7575"`. AyaNova then accepts connections only from that computer, so it can not be reached directly over unencrypted HTTP, bypassing the reverse proxy. See the [Linux server installation](ops-install-linux-server.md#f-accept-connections-only-through-nginx) instructions. ## Overriding diff --git a/docs/8.0/ayanova/docs/ops-install-linux-server.md b/docs/8.0/ayanova/docs/ops-install-linux-server.md index d9256c73..0ab514d9 100644 --- a/docs/8.0/ayanova/docs/ops-install-linux-server.md +++ b/docs/8.0/ayanova/docs/ops-install-linux-server.md @@ -215,16 +215,22 @@ Give www-data ownership of the ayanova directory: sudo chown -vR :www-data /var/ayanova ``` -Set permissions to read / write for the AyaNova data directory: +Make the AyaNova program files read only for everyone but root, so the account AyaNova runs as can not change them: + +```bash +sudo chmod -R go-w /var/ayanova +``` + +Set permissions to read / write for the AyaNova data directory, the only place AyaNova needs to write to: ```bash sudo chmod -R g+rw /var/ayanova/data ``` -Set permissions for the AyaNova folder +The config.json file holds the database password, make it readable only by root and AyaNova: ```bash -chmod g+rw /var/ayanova/ +sudo chmod 640 /var/ayanova/config.json ``` ### 10\. Create and install a service to run AyaNova automatically @@ -254,6 +260,12 @@ SyslogIdentifier=ayanova-server User=www-data Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false +# Security: AyaNova can write only to its data directory +NoNewPrivileges=true +ProtectSystem=strict +ReadWritePaths=/var/ayanova/data +PrivateTmp=true +ProtectHome=true [Install] WantedBy=multi-user.target @@ -261,6 +273,8 @@ WantedBy=multi-user.target Make the changes and save with `ctr-x` and type `y` to answer "Save modified buffer?" prompt. +The lines under `# Security` limit what the AyaNova service can do on the server: it can not gain extra privileges, sees the rest of the system as read only and can write only to its data directory. AyaNova keeps all of its files (logs, attachments, backups, temporary files and the report rendering browser) in the data directory by default so nothing else is needed. If you have set a [log](ops-log.md#log-path), [attachments](ops-config-folder-user-files.md), [backup](ops-config-folder-backup-files.md) or [temporary files](ops-config-folder-temporary-files.md) path outside of the data directory, add each of those folders to the `ReadWritePaths` line separated by spaces, otherwise AyaNova will not be able to write to them. + Enable the service: ```bash @@ -614,7 +628,33 @@ Certbot will also install a systemd timer event which will auto-renew the SSL ce Confirm that you can now navigate to AyaNova web app login page through encrypted HTTPS via the domain name configured earlier. i.e. `https://ayanova.yourdomain.com` or `https://yourdomain.com` if not using a subdomain. -You should now see the login (or license agreement) page through an encrypted SSL connection and AyaNova is ready for use. +You should now see the login (or license agreement) page through an encrypted SSL connection. + +#### F\. Accept connections only through NGINX + +AyaNova still listens on port 7575 on all of the server's network addresses, where it can be reached directly over unencrypted HTTP, bypassing NGINX and its encryption. Now that NGINX is working, set AyaNova to accept connections only from this server itself so all users connect through NGINX. + +Edit the [AYANOVA_USE_URLS](ops-config-use-urls.md) setting in config.json: + +```bash +sudo nano /var/ayanova/config.json +``` + +and change `http://*:7575` to `http://127.0.0.1:7575` so the line reads: + +```json + "AYANOVA_USE_URLS": "http://127.0.0.1:7575", +``` + +Save, then restart AyaNova: + +```bash +sudo systemctl restart ayanova.service +``` + +Confirm again that the login page opens through `https://` as in the previous step. AyaNova is now ready for use. + +Only make this change if all users connect through NGINX: users connecting directly to port 7575 on the local network will no longer be able to connect and must use the `https://` address instead. ## Getting started and using AyaNova diff --git a/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md b/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md index 10d5b672..f67a703b 100644 --- a/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md +++ b/docs/8.0/ayanova/docs/ops-upgrade-linux-server.md @@ -18,7 +18,7 @@ If you do not have a fresh backup there will be no way to recover from potential **Only if** your current AyaNova version is older than 8.3.0 there is an extra step before the normal upgrade process. -8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS (8.2.0 to 8.2.4), so the .NET 10 runtime must be installed first. If you are upgrading from a version older than 8.2.0 this script also accommodates that release's changes to folder ownership and permissions. +8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS (8.2.0 to 8.2.4), so the .NET 10 runtime must be installed first. This script also sets the folder ownership and permissions the [installation instructions](ops-install-linux-server.md#9-set-ownership-and-permissions) now use: the program files read only for the account AyaNova runs as, which only needs to write to the data directory, and config.json, which holds the database password, readable only by root and AyaNova. From a terminal window run the following commands: @@ -26,15 +26,33 @@ From a terminal window run the following commands: sudo apt-get update && sudo apt-get upgrade && \ sudo apt-get install -y aspnetcore-runtime-10.0 && \ sudo chown -vR :www-data /var/ayanova && \ -sudo chmod g+rw /var/ayanova/ +sudo chmod -R go-w /var/ayanova && \ +sudo chmod -R g+rw /var/ayanova/data && \ +sudo chmod 640 /var/ayanova/config.json ``` +These commands assume AyaNova runs as `www-data`, as in the installation instructions. If you chose another account, use its group in the `chown` line. + On Ubuntu 24.04 and 26.04 the .NET 10 runtime comes from Ubuntu's own package archive. On Ubuntu 22.04 first add the Ubuntu .NET backports package repository with `sudo add-apt-repository ppa:dotnet/backports`. Installing the .NET 10 runtime replaces the shared `dotnet` command, the .NET 8 runtime stays installed and can be removed later if nothing else on the server uses it. If there is an issue or error break apart the commands and execute each one individually to find the error. +#### Recommended: security settings for the AyaNova service + +The [installation instructions](ops-install-linux-server.md#10-create-and-install-a-service-to-run-ayanova-automatically) now add security settings to the AyaNova service so it can write only to its data directory. To add them to an existing installation without editing the service file, run: + +```bash +sudo mkdir -p /etc/systemd/system/ayanova.service.d && \ +printf '[Service]\nNoNewPrivileges=true\nProtectSystem=strict\nReadWritePaths=/var/ayanova/data\nPrivateTmp=true\nProtectHome=true\n' | sudo tee /etc/systemd/system/ayanova.service.d/hardening.conf && \ +sudo systemctl daemon-reload +``` + +They take effect when the AyaNova service next starts, in the upgrade step below. If you have set a log, attachments, backup or temporary files path outside of the data directory, first read the note about `ReadWritePaths` in the [installation instructions](ops-install-linux-server.md#10-create-and-install-a-service-to-run-ayanova-automatically). + +If users connect through NGINX as in the [internet access](ops-install-linux-server.md#internet-access) instructions, also set AyaNova to [accept connections only through NGINX](ops-install-linux-server.md#f-accept-connections-only-through-nginx). + After this step has completed you can continue with the normal upgrade process in the next step. ### 2\. Upgrade @@ -48,7 +66,7 @@ Run the following shell commands: ```bash cd /var/ayanova && \ sudo systemctl stop ayanova.service && \ -sudo curl -O https://www.ayanova.com/download/ayanova-linux-x64-server.zip && \ +sudo curl -fO https://www.ayanova.com/download/ayanova-linux-x64-server.zip && \ sudo rm *.dll && \ sudo rm -r ./resource && \ sudo rm -r ./wwwroot && \