Security review pass 10: deployment

From code: AyaNova listens on all interfaces on plain HTTP 7575 in every
template, hosted droplets included, with no firewall from the setup script (F36);
standalone PostgreSQL trusts local connections (F37); Linux hardening (F38).
F2 confirmed and extended; F9 extended (hosted droplets start with the default
SuperUser password). Review only, no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:40:46 -07:00
parent fc3342df48
commit 40eb36080b

View File

@@ -20,8 +20,8 @@ keep it that way while findings are open.
| 7 | Staff roles: the authenticated routes without a role check in the action (list below) | Done 2026-10-23 | | 7 | Staff roles: the authenticated routes without a role check in the action (list below) | Done 2026-10-23 |
| 8 | Email and notifications (`direct-smtp`, templates, delivery) | Done 2026-10-23 | | 8 | Email and notifications (`direct-smtp`, templates, delivery) | Done 2026-10-23 |
| 9 | Client (XSS, token storage, `npm audit` triage), CORS, headers, Swagger UI | Done 2026-10-23 | | 9 | Client (XSS, token storage, `npm audit` triage), CORS, headers, Swagger UI | Done 2026-10-23 |
| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Next | | 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Done 2026-10-23 |
| 11 | Sockeye public routes (licence fetch, trial requests) | | | 11 | Sockeye public routes (licence fetch, trial requests) | Next |
The order is by risk to internet-facing servers. Findings from any pass go in **Findings**. The order is by risk to internet-facing servers. Findings from any pass go in **Findings**.
@@ -210,7 +210,9 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca
| F34 | Medium | confirmed | Shipped client libraries behind on security fixes: DOMPurify 2.3.8 (wiki sanitiser), Vuetify 2.6.6 | | F34 | Medium | confirmed | Shipped client libraries behind on security fixes: DOMPurify 2.3.8 (wiki sanitiser), Vuetify 2.6.6 |
| F6 | Medium | confirmed | No lockout or rate limit on login or the 2FA step; no password rules | | F6 | Medium | confirmed | No lockout or rate limit on login or the 2FA step; no password rules |
| F9 | Medium | confirmed | Default SuperUser password, and an anonymous route that says whether it is still set | | F9 | Medium | confirmed | Default SuperUser password, and an anonymous route that says whether it is still set |
| F2 | Medium | candidate | Windows installers give all local users modify rights on the program and data folders | | F36 | Medium | confirmed (code) | AyaNova listens on all interfaces on plain HTTP port 7575 in every template, hosted droplets included, and the setup script adds no firewall |
| F37 | Medium | confirmed (code) | Standalone PostgreSQL accepts any local connection without a password (`trust`) |
| F2 | Medium | confirmed (code) | Windows installers give all local users modify rights on the program and data folders, `config.json` included |
| F7 | Low | confirmed | Password change or reset does not end existing sessions, and makes the download key non-expiring | | F7 | Low | confirmed | Password change or reset does not end existing sessions, and makes the download key non-expiring |
| F8 | Low | confirmed | Latest-token check skipped for any URL ending in `/auth` or `notify/hello` | | F8 | Low | confirmed | Latest-token check skipped for any URL ending in `/auth` or `notify/hello` |
| F4 | Low | confirmed (code) | Any logged-in user can send an in-app notification to any user | | F4 | Low | confirmed (code) | Any logged-in user can send an in-app notification to any user |
@@ -220,6 +222,7 @@ Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in ca
| F25 | Low | confirmed (code) | Attachment and export gaps: upload stored before the rights check, "attached by" can be set to anyone, exports not tied to their user | | F25 | Low | confirmed (code) | Attachment and export gaps: upload stored before the rights check, "attached by" can be set to anyone, exports not tied to their user |
| F29 | Low | confirmed (code) | `server-info` gives every login, customer contacts included, the server DB ID and full licence details | | F29 | Low | confirmed (code) | `server-info` gives every login, customer contacts included, the server DB ID and full licence details |
| F35 | Low | confirmed | No security headers from the server itself (self-hosted), no CSP anywhere, CORS any origin, public Swagger UI, download keys in URLs | | F35 | Low | confirmed | No security headers from the server itself (self-hosted), no CSP anywhere, CORS any origin, public Swagger UI, download keys in URLs |
| F38 | Low | confirmed (code) | Linux hardening: no systemd sandboxing, service user can replace program files, desktop runs as root, unused Docker files with a fixed database password |
| F22 | Low | confirmed | Minor report and job route gaps: template export by any download key, job status, logs and cancel by GUID without an owner check, browser without sandbox | | F22 | Low | confirmed | Minor report and job route gaps: template export by any download key, job status, logs and cancel by GUID without an owner check, browser without sandbox |
**Release decisions.** John, 2026-10-23: **F13 to F17 are urgent and are fixed in 8.3.0**, as **Release decisions.** John, 2026-10-23: **F13 to F17 are urgent and are fixed in 8.3.0**, as
@@ -323,7 +326,7 @@ A new database has SuperUser `superuser` / `l3tm3in` (`PrimeData.cs`). Anonymous
`GET notify/hello` returns `sudf: true` while that is still the password, and the login form `GET notify/hello` returns `sudf: true` while that is still the password, and the login form
then fills it in. Anyone can therefore check an internet-facing server for it in one request then fills it in. Anyone can therefore check an internet-facing server for it in one request
and log in as SuperUser. Each call also runs a PBKDF2 hash, a cheap way to load the server. and log in as SuperUser. Each call also runs a PBKDF2 hash, a cheap way to load the server.
Whether hosted droplets set their own SuperUser password at setup: to check in pass 10. Whether hosted droplets set their own SuperUser password at setup: to check in pass 10. **Pass 10:** they do not. The setup script generates the database password and JWT secret but not a SuperUser password, so every new hosted droplet starts internet-facing with `l3tm3in` until someone changes it. Fix in the script: generate one, set it once with `AYANOVA_SET_SUPERUSER_PW`, and write it to `/root/ayanova-secrets.txt` with the others.
Fix options: only return `sudf` from local or LAN addresses, or force a password change on Fix options: only return `sudf` from local or LAN addresses, or force a password change on
first SuperUser login; at least warn in the UI and the manual. first SuperUser login; at least warn in the UI and the manual.
@@ -709,6 +712,48 @@ Fix: add the headers in the app (so every deployment gets them), with a CSP that
client's own scripts; limit CORS to the server's own origin unless API users need more; client's own scripts; limit CORS to the server's own origin unless API users need more;
consider Swagger UI for logged-in users only. consider Swagger UI for logged-in users only.
### F36 Listening on all interfaces, no firewall (pass 10, from code)
Every `config.json` template (`lan-install-config.json`, `linux-server`, `linux-desktop`, the
standalone launcher's) sets `AYANOVA_USE_URLS` to `http://*:7575`. On hosted droplets nginx
proxies HTTPS to `127.0.0.1:7575`, but Kestrel also listens on the public address, and the setup
script configures no host firewall. Unless a DigitalOcean cloud firewall blocks it (**John to
check**), anyone can reach AyaNova directly on port 7575 over plain HTTP, without TLS and
without nginx's headers, and logins sent there travel in clear. The standalone (single-PC)
install also accepts connections from the whole network. Fix: hosted and Linux server
templates bind `http://127.0.0.1:7575` when behind a proxy; the setup script enables `ufw`
allowing only 22, 80 and 443; standalone binds to localhost; LAN stays on all interfaces (that
is its purpose) and the manual recommends HTTPS through IIS for anything beyond the LAN.
### F37 Standalone PostgreSQL trusts local connections (pass 10, from code)
`dist/win-x64/postgres-empty-database/pg_hba.conf` uses `trust` for local, 127.0.0.1 and ::1, and
`listen_addresses` is the default (localhost). Any user or program on that PC can connect as any
database user, the `postgres` superuser included, without a password. Standalone is a
single-PC (mostly evaluation) install, so the exposure is local only; with F2 the database
files are writable by every local user anyway. Fix: `scram-sha-256` with a password generated
at install and written to the launcher's `config.json`; or accept it for evaluation installs
(John's decision).
### F38 Linux hardening (pass 10, from code)
- The systemd unit written by the setup script runs as `www-data` with no sandboxing options
(`NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`,
`PrivateTmp`, `ProtectHome`).
- `chmod g+rw /var/ayanova/` gives `www-data` (which nginx also runs as) write access to the
program folder itself, so the service account can replace program files by renaming. Only
`data` needs write access.
- The Linux desktop install runs as root (`sudo ./AyaNova`, see Deferred), so the report browser,
already started without Chrome's sandbox, runs as root too.
- `dist/docker/linux-x64/docker-compose.yml` publishes PostgreSQL on port 5432 with a fixed
superuser password. It is not built (`build-release.bat` lines are commented out) or
documented; delete it or fix it before anyone uses it.
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`,
runs AyaNova as `www-data`, installs unattended upgrades, uses PGDG PostgreSQL on its default
localhost listener, and gets a Let's Encrypt certificate with HSTS and the headers in F35.
### F2 Windows folder permissions (from the 8.3.0 installer test) ### F2 Windows folder permissions (from the 8.3.0 installer test)
`lan.iss` and `standalone.iss` set `Permissions: users-modify` on `{app}` (the program `lan.iss` and `standalone.iss` set `Permissions: users-modify` on `{app}` (the program
@@ -717,7 +762,7 @@ attachments, logs). Any local Windows user can therefore replace program files t
the user who starts AyaNova or as the IIS application pool, and can read the database files the user who starts AyaNova or as the IIS application pool, and can read the database files
and backups. Matters on shared or terminal-server machines. The program-folder permission and backups. Matters on shared or terminal-server machines. The program-folder permission
originally let the report browser download beside the app; since 8.3.0 it downloads under originally let the report browser download beside the app; since 8.3.0 it downloads under
the data folder, so `{app}` may no longer need it. To confirm in pass 10. the data folder, so `{app}` may no longer need it. To confirm in pass 10. **Pass 10:** confirmed from `lan.iss` and `standalone.iss`. `config.json`, with the database password and the JWT secret, is installed in `{app}`, so any local user can read those secrets and change the configuration or the program files; standalone also grants `users-modify` on `{app}\local-postgres` (the PostgreSQL binaries) and the database folder. Fix: drop `users-modify` from `{app}` and `local-postgres`; keep write access only where the running account needs it (logs, backups, temp, attachments, report browser), granted to that account or to Administrators rather than all Users; give `config.json` an ACL for Administrators and the service account.
### F3 `notify/direct-smtp` open to every logged-in user ### F3 `notify/direct-smtp` open to every logged-in user
@@ -788,3 +833,4 @@ roles that should not see the schedule (pass 7).
| 2026-10-23 | 7 Staff roles | Went through the remaining logged-in routes without a role check, the restricted-role filtering in `WorkOrderBiz`, and user and contact administration in `UserBiz`. Tested on the local dev server, read-only: **F27 confirmed** (`SubContractorRestricted` through `report/data`). F28 from code (live test not done). New: F29. The rest of the list found OK (see "Pass 7: routes ... found OK") | | 2026-10-23 | 7 Staff roles | Went through the remaining logged-in routes without a role check, the restricted-role filtering in `WorkOrderBiz`, and user and contact administration in `UserBiz`. Tested on the local dev server, read-only: **F27 confirmed** (`SubContractorRestricted` through `report/data`). F28 from code (live test not done). New: F29. The rest of the list found OK (see "Pass 7: routes ... found OK") |
| 2026-10-23 | 8 Email and notifications | Code reading only, nothing sent. Read `NotifyController` (direct SMTP and message), `Mailer`, `GlobalOpsNotificationSettings` controller, biz and model, `NotifyEventHelper`, `CoreJobNotify`, `CoreJobCustomerNotify`, `NotifySubscriptionBiz`. F3 and F4 confirmed by code; F17 confirmed and re-rated Medium (names and links, no reports). New: F30, F31, F32. **Action for John: rotate the FastMail app password in F32** | | 2026-10-23 | 8 Email and notifications | Code reading only, nothing sent. Read `NotifyController` (direct SMTP and message), `Mailer`, `GlobalOpsNotificationSettings` controller, biz and model, `NotifyEventHelper`, `CoreJobNotify`, `CoreJobCustomerNotify`, `NotifySubscriptionBiz`. F3 and F4 confirmed by code; F17 confirmed and re-rated Medium (names and links, no reports). New: F30, F31, F32. **Action for John: rotate the FastMail app password in F32** |
| 2026-10-23 | 9 Client, headers, CORS, Swagger | Code reading of every `v-html` use in `raven-client` and its data source; `npm audit` read-only against the lockfile (no install, no change), triaged to shipped packages; server and nginx headers, CORS, Swagger. New: F33, F34 (needs John's decision on two front-end packages), F35; F14 extended (`unit/customerunit`) | | 2026-10-23 | 9 Client, headers, CORS, Swagger | Code reading of every `v-html` use in `raven-client` and its data source; `npm audit` read-only against the lockfile (no install, no change), triaged to shipped packages; server and nginx headers, CORS, Swagger. New: F33, F34 (needs John's decision on two front-end packages), F35; F14 extended (`unit/customerunit`) |
| 2026-10-23 | 10 Deployment | Code reading: `lan.iss`, `standalone.iss`, the four `config.json` templates, the standalone PostgreSQL configuration, the hosted setup script in `sockeye-client` (secrets, permissions, systemd unit, nginx, certbot), `dist/docker`. F2 confirmed and extended (`config.json` readable by all users); F9 extended (hosted droplets start with the default SuperUser password). New: F36, F37, F38. **John to check:** whether a DigitalOcean cloud firewall blocks port 7575 on hosted droplets |