Commit Graph
9 Commits
Author SHA1 Message Date
Hermes cdf9e8d3ef [grade=A] DC-082+DC-064: eliminate command injection surface + add Docker resource limits
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
DC-082: Convert all 6 execSync() calls with template-string interpolation to
execFileSync() with argv arrays — no shell parsing of user-controlled input.
Files: routes/ca.js (5 calls), src/docker/self-updater.js (1 call).
Also removed stale execSync imports (Codex LOW finding).

DC-064: Add --memory=512m --memory-swap=1g --cpus=1.5 to docker run in start.sh
to prevent container OOM from taking down the host.

Codex grade: A (30,783 tokens). All 1539 tests pass.
2026-08-12 04:35:15 -07:00
Hermes Agent 56f1a001f2 start.sh auto-sync dashboard bundle into /var/www/dashcaddy-status/
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
After every rebuild the freshly-baked dashboard bundle lives in
/opt/dashcaddy/status/dist/ + sw.js. DNS2 also serves files from
/var/www/dashcaddy-status/dist/ + sw.js (the original Windows-installer
mirror path). Without an explicit copy step between build and start.sh,
the served bundle stays on whatever hash was there before, while the API
responds with new code. That mismatch is what shows up in the dashboard
as "version unavailable" + "no data" widgets — saw it in the deploy
that followed DC-046/047 (fixed by a manual cp this time, never again).

Sync block runs before docker run:
  cp /opt/dashcaddy/status/dist/*.js /var/www/dashcaddy-status/dist/
  cp /opt/dashcaddy/status/sw.js   /var/www/dashcaddy-status/
  cp /opt/dashcaddy/status/index.html /var/www/dashcaddy-status/

All  guarded so set -e doesn't kill the container start on a
single per-file failure (e.g. read-only mount, missing dir). Missing
source dir is a WARN + no-op rather than a fatal — fresh installs
without status/dist/ don't get a stale-bundle problem, just a log line.

Test: scripts/test-start-sh-sync.sh — 7 assertions across 4 cases
(fresh-copy, missing-source, idempotent-re-sync, set-e-survives-permission-
denied). All pass.
2026-07-20 02:07:25 -07:00
Hermes Agent 894e091335 DC-050 harden dataDir + add image-layer migration
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Three-part fix for the silent data-loss failure mode that survives DC-039:
If SERVICES_FILE env was unset, platformPaths.dataDir resolved to /etc/dashcaddy
(image-layer path), and audit/license/error logs would silently land there and
vanish on every container recreate.

1. platform-paths.assertSafe({mode:'production'}) — throws FATAL on forbidden
   zones (/app/src,routes,scripts,utils,managers,security + /etc/* + /usr + /var).
   Bypassed with SKIP_DATA_DIR_GUARD=1.
2. server.js calls assertSafe() before any runtime work.
3. start.sh one-time migration: scans 6 known image-layer zombie paths,
   copies non-empty content to bind mount with 'migrated-' prefix,
   gated by sentinel file. Survives set -e per-file failures.

19/19 platform-paths tests + 5/5 shell migration tests.
Suite: 1066/1067 (1 pre-existing public-routes-drift failure from in-flight
auth refactor, untouched by this commit).

Verified live on DNS2: live audit log at /app/data/audit-log.json (315KB,
active) is unaffected; vestigial 2-byte /app/src/security/audit-log.json +
140KB /app/src/utils/error.log (pre-DC-039 era) will be recovered on next
container recreate.
2026-07-20 00:53:05 -07:00
Krystie 0f04bb3638 DC-044: mount Sami CA + fix ca.sami /etc/hosts in DashCaddy container
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Two related fixes from the dashboard 11/15 false-negatives:

1. The Sami Home Network CA cert (/etc/ssl/sami-ca/root.crt) was not
   mounted into the container, so the health-checker's HTTPS probe to
   *.sami hosts failed with "certificate verify failed". Added a bind
   mount + CA_CERT_PATH env var so the app's httpsAgent picks it up
   (verified at startup: "HTTPS agent configured with CA certificate").

2. The --add-host=ca.sami:127.0.0.1 line pinned ca.sami to the
   container's loopback, but nothing listens on 443 inside the
   container. Probe failed with ECONNREFUSED 127.0.0.1:443. Removed
   the override so ca.sami resolves via DNS to 100.121.150.22 (Caddy
   on DNS2) and the probe reaches the real service.

After both fixes: 15/15 services healthy, 0 429s on the health checker,
caddy.ok=true on /health/ready.
2026-07-08 22:00:25 -07:00
Krystie d04238621f DC-042: implement real Tailscale manager — replace null stub
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
The previous getTailscaleStatus() in src/app.js was a hard-coded
`return null` stub with a TODO saying it would be populated by context.
The context had a tailscale.* namespace declared with null function
stubs (routes/context.js:71), but nothing ever set them to real
functions. routes/tailscale.js has been calling ctx.tailscale.getStatus()
/ getLocalIP() / isTailscaleIP() and getting undefined back, silently
returning empty device lists. The tailscaleAuthMiddleware's allowedTailnet
check (DC-121, device-not-in-tailnet 403) was dead code for the same reason.

This commit replaces the stub with a real implementation:

- New src/managers/tailscale-manager.js shells out to the host's
  `tailscale status --json` (cached 5 minutes), parses the result, and
  exposes getStatus / getLocalIP / getSummary / getDevices / isTailscaleIP /
  invalidateCache / getAccessToken (stub) / startSyncTimer / stopSyncTimer
  / syncAPI (stub). All failure modes (CLI missing, tailscaled down,
  malformed JSON, EACCES) are handled gracefully — return null with no
  cache poisoning.
- src/context/index.js now wires the manager into ctx.tailscale.* so
  routes/tailscale.js and middleware.js's allowedTailnet gate get the
  real functions.
- src/app.js:189 getTailscaleStatus() now delegates to the manager
  instead of returning null.
- The duplicate isTailscaleIP() in src/app.js:179 (no malformed-input
  guards) is removed in favor of the canonical version in
  src/utilities/network-detector.js (DC-031) which the manager also uses.
- start.sh now bind-mounts /usr/bin/tailscale (statically linked Go binary
  — works under Alpine libc) and /var/run/tailscale/ into the container,
  read-only. Lets the container invoke the CLI without needing its own
  tailscale install.
- 41 new unit tests in __tests__/tailscale-manager.test.js cover: CLI
  success/missing/daemon-down/malformed-JSON paths, 5-min cache hit/miss,
  1-hour installed-cache hit/miss, getLocalIP IPv4/IPv6/missing-choices,
  getSummary shape, getDevices shape with full + minimal peer fields,
  startSyncTimer/stopSyncTimer interval + idempotency, TAILSCALE_BIN env
  override.

Total: 1138 tests pass (was 1097, +41 new), 0 new ESLint warnings.

What this unlocks:
- /api/v1/tailscale/status → real installed/connected/hostname/ip/
  peerCount/onlinePeerCount summary instead of empty
- /api/v1/tailscale/devices → real device list (was returning [])
- /api/v1/tailscale/check-connection → works (uses real isTailscaleIP)
- tailscaleAuthMiddleware allowedTailnet check (DC-121) is no longer
  dead code — a request from a Tailscale IP not in the allowed tailnet
  now actually gets 403 instead of being silently allowed.
2026-07-06 18:55:16 -07:00
Krystie 95f558c49f DC-030: bake /etc/hosts overrides into start.sh (fix git.sami resolution in container)
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
The container's health-checker runs against Caddy via /etc/hosts resolution.
The node:20-alpine base image has no entries for *.sami, so without explicit
--add-host flags every *.sami probe resolves via the configured DNS server
(100.121.150.22 Technitium or 8.8.8.8) — both of which DO resolve *.sami but
return the WAN/Tailscale IP. That works for most services because Caddy on
DNS2:443 handles them.

BUT: a previous container run passed --add-host=git.sami:100.81.59.99
(DNS3's Tailscale IP). DNS3 does NOT serve HTTPS on 443 — Gitea listens on
:3030 only. So git.sami health checks inside the container hit DNS3:443,
get ECONNREFUSED, and the dashboard shows git.sami as down even though Caddy
on DNS2:443 correctly routes git.sami → 100.81.59.99:3030.

Fix: inject the correct --add-host flags from start.sh (the source of truth
for container setup) so future recreates get consistent resolution. git.sami
is intentionally left OUT — Caddy on DNS2:443 is the only correct ingress
for git.sami traffic.

Also documents the rationale so the next person doesn't reintroduce the
git.sami override by accident.

Live verified:
- container /etc/hosts has all needed entries except git.sami
- curl https://git.sami/ from inside container → 200 (via Caddy on :443)
- curl https://sync.sami/ from inside container → 302 (upstream redirect)
- curl https://router.sami/ from inside container → 302 (upstream redirect)
2026-07-03 00:08:55 -07:00
Krystie fef7e07b49 DC-026/027/028: close 3 more auth security holes + rate limit /auth/* + audit credential exposures
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
[DC-026] routes/auth/sso-gate.js — fix sessionDuration='never' bypass
  Both /auth/gate/:serviceId and /auth/app-token/:serviceId had a session
  check gated on `sessionDuration !== 'never'`. An admin setting TOTP to
  never-expire accidentally created an authentication-free path to credential
  injection (Basic Auth, X-Api-Key, Plex/Prowlarr tokens). Patched: session
  required whenever TOTP is enabled, period. Added 8 regression tests.

[DC-027] src/utilities/middleware.js — rate limit /auth/*
  New authLimiter (20 req / 15 min) on /auth/keys, /auth/jwt, /auth/gate,
  /auth/app-token. These endpoints expose credentials and were unmetered.
  Without this, an attacker with a guessed session cookie could burn through
  every credential-touching endpoint. Added 5 tests.

[DC-028] src/security/audit-logger.js — log credential exposures
  /auth/gate and /auth/app-token were in SKIP_PATHS, silently dropping
  every credential-exposure event from the audit log. Combined with the
  GET-skip rule, NONE of these events were being recorded. Now logged
  with named actions: auth.credential-injection, auth.app-token-issue,
  auth.api-key-generate, auth.api-key-revoke, auth.jwt-mint. Added 9 tests.

[start.sh] Disable in-container self-updater
  DASHCADDY_UPDATE_ENABLED=false. Without this, the container kept writing
  trigger.json every 30 min and clobbered my in-progress host edits. The
  path unit on the host is still active for manual triggers, but the
  container won't auto-update itself — only when an admin clicks the
  update button or a new release is manually published.

[package.json] Bump to 1.14.7

Test results: 1066/1066 passing across 39 suites (added 22 new tests).
2026-07-01 04:20:57 -07:00
Krystie a5f51e4a0c DC-023: operational fixes — DNS, rate limiter, version sync
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- VERSION: bump from 1.14.4 to 1.14.6 to match package.json (HEAD had stale value)
- middleware.js: apply existing totpLimiter (10/15min) to /totp/setup endpoint
  (was previously unmetered, allowing secret enumeration)
- dashcaddy-update.sh: hook post-deploy-patches.sh into the update flow
  so the container can survive transitions between broken → fixed tarballs
- start.sh: add --add-host flags for get.dashcaddy.net and get2.dashcaddy.net
  so the container can resolve the release server (was failing with ENOTFOUND)
2026-07-01 03:10:53 -07:00
Krystie 44af47d344 feat: add Sami Files logPath to template + mount in start.sh
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-19 18:16:34 -07:00