5 Commits
Author SHA1 Message Date
Juan Denis b02dd53eaa Compress responses for every site
Nothing configured gzip beyond Debian's stock text/html-only line,
and the .htaccess converter claimed compression was global. A conf.d
snippet now adds gzip (and brotli when the module is loaded), holding
only the directives the running config lacks: a repeated http-level
gzip directive fails nginx -t. A snippet nginx rejects is removed, so
compression can't block a vhost write.
2026-09-27 18:18:39 -04:00
Juan Denis 7a8f436dbb Log request timing in every generated vhost
Per-app latency, error rate and cache status were invisible: the
vhosts logged plain combined lines. The serverkit_timed format keeps
the combined fields first so bandwidth accounting and the fail2ban
filters still match, and appends request/upstream time, cache status
and host. Legacy access_log lines read as equivalent for drift so an
upgrade doesn't flag every site.
2026-09-27 17:50:32 -04:00
Juan Denis 76c57c19d6 fix(security): harden nginx write path and certbot issuance trust bugs
- run wildcard/SAN certbot privileged and fail on non-zero exit instead
  of reporting unissued cert paths as success
- cloudflare DNS-01 credentials: unpredictable mkstemp file, 0600,
  always deleted (was predictable /tmp path, never cleaned up)
- route53 credentials via sudo env A=B (sudo env_reset strips env= kwarg)
- validate domains before they reach certbot argv or cert file paths
- validate reverse-proxy builder input (domains, upstreams, headers,
  locations, rate-limit/cache values) against nginx config injection
- write_vhost: refuse non-plain-filename site names and roll back the
  vhost when the reload itself fails, not only when nginx -t fails
- create_proxy API returns 400 on service errors instead of always 201
2026-08-27 23:54:01 -04:00
Juan Denis ded7d54039 refactor(nginx): consolidate vhost writes 2026-08-23 15:50:53 -04:00
Juan DenisandClaude Opus 5 6cd1950fc9 refactor(nginx): one privileged-write door, one vhost lifecycle
Plan 75 §G2 + §G4, together because they are the same physical files and
G4 is built on G2's helper.

§G2 — write_privileged_file(). The write-a-root-owned-file sequence was
pasted 15+ times in two competing forms: run_privileged(['tee', path],
input=...) and a raw subprocess.run(['sudo', 'tee', ...]). The second
hardcodes sudo instead of asking _needs_sudo(), so it fails on a panel
already running as root in a container with no sudo binary — plan 74's
outage shape. All core-service tee sites now go through one helper that
owns the sudo decision, the result shape, and honest failure: a missing
tee or a timeout reports the failure instead of being swallowed (§A).

§G4 — NginxService.write_vhost()/read_vhost(). environment_domain_service
re-implemented write → symlink → nginx -t → reload with raw sudo calls,
and nginx_advanced_service wrote the vhost with a plain unprivileged
open() — PermissionError against a root-owned sites-available, with no
config test and no reload. Both now go through NginxService.

The rollback is the part no copy had: a vhost that fails nginx -t used to
stay on disk and symlinked, so the next unrelated reload (a cert renewal,
another site going live) failed and the blame landed on whatever
triggered it. write_vhost restores the previous content, or removes a
file that did not exist before, and never reloads a config that failed
the test.

SITES_AVAILABLE/SITES_ENABLED were independently declared in four files,
so an NGINX_CONF_DIR override — the env var NginxService honours, and what
tests redirect — silently did not apply to the other three. One
declaration now.

Mutation-checked: disabling the rollback fails two tests.

Also lands unit_is_active() in app/utils/system.py; its adopters are in
the following commit (§G6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:26:23 -04:00