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:
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user