Commit Graph
6 Commits
Author SHA1 Message Date
emily 2c3607f17c fix: prevent HAProxy from reusing stale keep-alive conns to nginx/Nextcloud
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
DAVx5 (CalDAV/CardDAV) on greg's phone was intermittently failing every
sync type (CONTACTS/EVENTS/TASKS/RefreshCollectionsWorker) against
next.thehellings.com with:

  java.io.IOException: unexpected end of stream
  Caused by: java.io.EOFException: \n not found: limit=0

This is the classic OkHttp/HTTP client signature of the far end
silently closing a pooled keep-alive connection: the client reuses a
socket it still believes is open, gets zero bytes back while reading
response headers, and throws exactly this exception.

Root cause: HAProxy's 'next' backend proxies to nginx on
127.0.0.1:8080, and HAProxy defaults to end-to-end keep-alive (both
client- and server-side) unless told otherwise. nginx's
keepalive_timeout is 65s, so any HAProxy<->nginx connection idle past
that gets closed by nginx without HAProxy's knowledge. A request that
lands on that now-dead pooled connection right after gets nothing back
- surfacing to the client as a bare socket EOF while reading headers.
The frontend's existing 'option http-server-close'/'http-keep-alive'
pair only governs the client-facing side of HAProxy and does nothing
for the HAProxy->nginx leg.

Fix:
- backend next: add 'option http-server-close' so HAProxy opens a
  fresh connection to nginx per request instead of pooling/reusing
  one. The backend is localhost, so the extra TCP handshake cost is
  negligible, and this removes the whole class of stale-connection EOF
  errors.
- defaults: add 'timeout http-keep-alive 30s' to bound how long an
  idle client-facing keep-alive connection is held open. Previously
  unset, it fell back to 'timeout client' (500s) - unnecessarily long
  given maxconn is only 80, and tightens client-side connection churn
  to be more predictable too.

Diagnosed by pulling the nginx_access journal (enabled in #37/#38) for
the failing sync window and cross-referencing nginx's
services.nginx.appendHttpConfig / generated nginx.conf keepalive
settings against HAProxy's request-level defaults. Could not run
'haproxy -c'/'nginx -t' locally (no toolchain in the agent sandbox) -
recommend confirming via CI/garnix before merge, same as #38.
2026-08-09 22:21:20 -05:00
emily e4fe9e84bd fix: nginx syslog access_log tag must not contain a hyphen
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
The nginx_access syslog tag added in #37 (feat/emily-incident-logging)
used tag=nginx-access. nginx's syslog sink only accepts alphanumeric
characters and underscores in the tag field, so the generated
nginx.conf failed its config test on linode:

  nginx: [emerg] syslog "tag" only allows alphanumeric characters
  and underscore in .../nginx.conf:114

Because nginx-pre-start failed, nginx.service crash-looped until it
hit systemd's start-limit-hit and gave up entirely. Since Nextcloud is
proxied through nginx (127.0.0.1:8080, fronted by haproxy's 'next'
backend), this took next.thehellings.com down with a 503 from haproxy
(phpfpm-nextcloud/postgresql/redis backends were all healthy and
unaffected - purely an nginx config parse failure).

Fix: use an underscore (nginx_access) instead of a hyphen.
2026-08-09 21:38:53 -05:00
emily 10cdf9408d feat: enable request-level logging for bandwidth/traffic incident tracing
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
Triggered by investigating a several-hour >10Mbps traffic spike to
linode. HAProxy's own IPAccounting confirmed ~121GB moved over ~19.6h
before it crash-looped, but with 'option httplog' commented out and no
per-backend request logs, there was no way to attribute that traffic
to a specific backend, host, or client.

- linode: enable HAProxy httplog + defaults 'log global' (was
  commented out) so every proxied HTTP request is now logged with
  timing/status/bytes.
- linode: add a haproxy 'stats' listener on 127.0.0.1:8404 for live
  per-backend/per-server connection and byte counters.
- linode: route nginx (Nextcloud's local vhost) access logs to
  journald via syslog, since the read-only monitoring account has no
  access to /var/log/nginx/*.
- linode: enable vnstat for historical per-interface bandwidth
  tracking (5-min granularity) so a reported 'traffic was high for N
  hours' can be confirmed/timestamped immediately instead of
  reconstructed after the fact from journal timestamps.
- k3s manifests: enable Traefik access logging (JSON) — this is the
  ingress layer HAProxy forwards :80 traffic to (git/matrix/immich),
  and lacked any per-request visibility.
- hosts/baseline.nix (fleet-wide): add a journald rate limit
  (2000 lines / 30s per unit). Found live while investigating that
  uptime-kuma on 'kuma' was logging a Prometheus label-validation
  error on every monitor beat (~100k lines/hour), which was itself
  degrading journalctl responsiveness on that host during the
  cross-host traffic scan.

Related but not otherwise addressed here: Nebula relay/handshake
churn on kuma's tunnel and the etcd read-latency warnings seen on
isaiah/zeke around the same incident window — noted for a future
investigation, not fixed by this PR.
2026-08-09 16:15:35 -05:00
emily 3cba4cbd86 feat: add read-only emily monitoring account
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
Adds a new NixOS module (greg.monitoring-access) that provisions a
dedicated, SSH-key-only 'emily' user account across all managed hosts.

The account is intentionally minimal-privilege:
- No password set (SSH key auth only)
- Not a member of wheel, no sudo/sudo-rs rules
- Only extra group membership is systemd-journal, granting read access
  to system logs for monitoring/analysis tasks
- Authorized key lives in home/ssh/emily_authorized_keys, mirroring the
  existing pattern used for the greg account's authorized_keys

This lets the Hermes agent (emily) log in read-only to inspect logs and
system state when asked, without any ability to modify configuration,
escalate privileges, or run destructive commands.

Module is imported unconditionally in modules/nixos/default.nix like
the other nixos modules, and defaults to enabled; it can be disabled
per-host via greg.monitoring-access.enable = false if ever needed.
2026-08-09 07:38:01 -05:00
emily 28977235a1 chore: clean up builder2, Ceph module, normalize Darwin host symlinks
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
- Remove builder2 (retired host): dangling network.json entry and
  empty home/hosts/builder2 stub. Its old IP (10.42.1.17) is already
  correctly owned by pve4.
- Remove the abandoned Ceph module (modules/nixos/ceph.nix) and its
  unencrypted plaintext keyring files under secrets/. No host ever
  enabled services.ceph-benaco; the keyrings were dead, unencrypted
  credentials sitting in the repo.
- Normalize Darwin host identity: IVR and Lithic are the only two
  physical Darwin machines. All other darwin/hosts/* names are now
  symlinks to whichever of the two they represent, matching the DHCP
  name variations nix-darwin sees depending on network:
    gregory -> ivr
    gregory.hellings-mbp -> ivr
    MacBook-Prolocal -> ivr
    gregs-MacBook-Pro-16-inch-Nov-2024 -> lithic
    li -> lithic (pre-existing)
  This lets each machine's config be maintained once regardless of
  what hostname it currently advertises.
2026-08-08 02:49:50 -05:00
emily fcb5a89727 fix: correct pve1 IP to 10.42.0.4, rename stale joel/opnsense refs
buildbot/nix-eval Build done. (1 warning)
buildbot/nix-build Build done.
pve1 is a static/DHCP-reserved Proxmox host at 10.42.0.4 (previously
mislabeled 'joel' in some places). 10.42.1.1 is the UDM Pro gateway
IP, not pve1 -- OPNsense was retired in favor of Ubiquiti. Removes
the stale duplicate PVE1 DHCP reservation at 10.42.1.1 and drops the
now-redundant 'joel' entry from network.json (consolidated into
pve1).
2026-08-08 00:44:21 -05:00