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 |
| 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 |
| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Next |
| 11 | Sockeye public routes (licence fetch, trial requests) | |
| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Done 2026-10-23 |
| 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**.
@@ -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 |
| 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 |
| 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 |
| 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 |
@@ -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 |
| 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 |
| 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 |
**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
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.
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
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;
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)
`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
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
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
@@ -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 | 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 | 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 |