Skip to content

fix(domain): unblock domain add for static apps on the control plane - #184

Open
samo-agent wants to merge 3 commits into
NikolayS:mainfrom
samo-agent:fix/custom-domain-vhost-git-ownership
Open

fix(domain): unblock domain add for static apps on the control plane#184
samo-agent wants to merge 3 commits into
NikolayS:mainfrom
samo-agent:fix/custom-domain-vhost-git-ownership

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Two independent root-cause fixes that together block samohost domain add for a static app on the control plane, plus a docs note. Discovered while attaching a customer custom domain to a live static site; each fix is TDD'd and independently reviewed (correctness + security/safety lenses).

1. Ownership-safe git rev-parse in the static custom-domain guard (c3716a0)

  • Symptom: domain add failed with active static release checkout does not match its recorded sha.
  • Cause: the custom-domain vhost script is pushed over SSH as the admin user samo, but static release checkouts are owned by the app user. git -C <release> rev-parse HEAD aborts with dubious ownership; the 2>/dev/null masked it → empty string → never equals the recorded sha. (The deploy path is unaffected — it SSHes as the app user, Issue SSH previews as the app user on shared-web VMs (mirror #161) #163.)
  • Fix: scope -c safe.directory="$SAMOHOST_CHECKOUT_REAL" to the already-realpath-validated canonical checkout path (per-invocation, non-persistent; matches the fix(deploy): emit safe.directory before first git call (unblocks dubious-ownership deploy failure) #160 trust model but narrower) and drop 2>/dev/null so real git errors surface. Same guard in bootstrap.ts (root-run) fixed identically.
  • Integrity preserved: the HEAD == recorded-sha check is unchanged; only git's ownership refusal is lifted for the one validated path. Verified live: as samo, old form → empty, new form → correct sha (release is a linked worktree; git 2.43).

2. Resilient control-plane Caddy reload (9195df7)

  • Symptom: after fix feat: scaffold samohost VM/app control plane #1, domain add failed at the control-plane step: Job for caddy.service failed.
  • Cause: the CP caddy.service (PrivateTmp=true, running 88 days) lost its systemd-private /tmp staging dir to the D /tmp … 30d tmpfiles cleaner, so systemctl reload caddy fails with status=226/NAMESPACE. Caddy keeps serving, but config changes silently don't apply. Host-wide for long-running PrivateTmp services.
  • Fix (CP-local sites only): systemctl reload caddy || caddy reload --config /etc/caddy/Caddyfile --force. The admin-API fallback is zero-downtime (verified live on all 41 hosts) and immune to the namespace issue; fails closed on an invalid config. App-VM reloads left as systemctl-only (their sudoers grant only that).

3. Docs (fc6894f)

docs/stack/routing.md: the reload failure mode + fallback + host remedy.

Host follow-up (NOT in this PR — needs a maintenance window)

systemctl reload/restart caddy on the control plane still fails until caddy.service is restarted once (recreates the namespace dir). Also fix the /tmp tmpfiles rule so it stops evicting live systemd-private-* dirs — this affects every long-running PrivateTmp service, not just Caddy.

Tests

  • Full suite 2059 pass / 0 fail across 92 files; tsc --noEmit clean.
  • New regression tests: ownership-safe git form + bash -n; resilient CP reload + fail-closed lock (not.toContain("reload caddy || true")) + app-VM scope guard.

🤖 Generated with Claude Code

samo-agent and others added 3 commits July 18, 2026 19:09
The custom-domain vhost script runs as the admin SSH user 'samo', but static
release checkouts are owned by the app user, so 'git rev-parse HEAD' aborted
with dubious-ownership and the guard falsely reported release drift, blocking
'domain add' for every static app. Scope git safe.directory to the already
realpath-validated checkout path so the integrity check runs regardless of
checkout ownership. Deploy path is unaffected (it SSHes as the app user).

Apply the same fix to src/app/bootstrap.ts:762 (re-bootstrap of a host with
an active static deployment runs as root, which also trips dubious-ownership
on app-user-owned release dirs). Drop 2>/dev/null in both sites so real git
errors reach the operator instead of being masked by the drift message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'systemctl reload caddy' can fail on the control plane with systemd
status=226/NAMESPACE when the long-running caddy service's PrivateTmp
namespace goes stale, which blocked 'domain add' at the control-plane
vhost step. Fall back to 'caddy reload --config ... --force' (the admin
API, zero-downtime, Caddy's recommended reload) when systemctl reload
fails, so control-plane routing changes still apply. Fail-closed on a
genuinely invalid config is preserved (both paths validate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teTmp namespace failure

Records the systemd status=226/NAMESPACE failure mode (stale PrivateTmp
staging dir on long-running caddy.service) and the admin-API reload
fallback that samohost now uses on the control plane, plus the host-side
maintenance remedy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant