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 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 14:14:05 -07:00
parent ad5da2ffa5
commit c6c9e6c5d3
5 changed files with 73 additions and 10 deletions

View File

@@ -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) | | 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) | | 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) | | 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) | | 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) | | 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 - 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 - **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 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 - 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 - 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

View File

@@ -787,7 +787,7 @@ prepared database, so it is not that short: accepted.
documented; delete it or fix it before anyone uses it. 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). **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 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`, droplet, stores them in `/root/ayanova-secrets.txt` (600), makes `config.json` 640 `root:www-data`,

View File

@@ -8,7 +8,11 @@ If no override is specified AyaNova will use the following default value:
`"http://*:7575"` `"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 ## Overriding

View File

@@ -215,16 +215,22 @@ Give www-data ownership of the ayanova directory:
sudo chown -vR :www-data /var/ayanova 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 ```bash
sudo chmod -R g+rw /var/ayanova/data 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 ```bash
chmod g+rw /var/ayanova/ sudo chmod 640 /var/ayanova/config.json
``` ```
### 10\. Create and install a service to run AyaNova automatically ### 10\. Create and install a service to run AyaNova automatically
@@ -254,6 +260,12 @@ SyslogIdentifier=ayanova-server
User=www-data User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false 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] [Install]
WantedBy=multi-user.target 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. 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: Enable the service:
```bash ```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. 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 ## Getting started and using AyaNova

View File

@@ -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. **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: 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 update && sudo apt-get upgrade && \
sudo apt-get install -y aspnetcore-runtime-10.0 && \ sudo apt-get install -y aspnetcore-runtime-10.0 && \
sudo chown -vR :www-data /var/ayanova && \ 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`. 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. 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. 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. After this step has completed you can continue with the normal upgrade process in the next step.
### 2\. Upgrade ### 2\. Upgrade
@@ -48,7 +66,7 @@ Run the following shell commands:
```bash ```bash
cd /var/ayanova && \ cd /var/ayanova && \
sudo systemctl stop ayanova.service && \ 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 *.dll && \
sudo rm -r ./resource && \ sudo rm -r ./resource && \
sudo rm -r ./wwwroot && \ sudo rm -r ./wwwroot && \