Pre-fix: /api/v1/ws checked cookies.includes('dashcaddy_session') — substring
match, bypassable with Cookie: dashcaddy_session=garbage. Production also
accepted any 11+ char ?token= query string. Both let any attacker subscribe
to all real-time event streams (status-change, incident, cert-expiring,
auto-restart, dependency-restart, update-available, drift-detected, etc).
Fix (3 files, +404/-81):
(1) server.js:80-99 wires ctx.session.isValid (HMAC-verifying isSessionValid
from middleware.js:265-279) into deps.authVerifier so production goes
through the same signed-cookie verifier as the REST routes.
(2) dashboard-ws.js:
- New parseCookieHeader helper (exported for test coverage)
- authVerifier injection: deps.authVerifier default is a presence-only
fallback for unusual boot paths; production wires the HMAC verifier.
- Upgrade handler replaces substring check with authVerifier(request).
401 includes Connection: close so browsers don't retry. Logs WS upgrade
rejections at WARN with ip + path.
- Removes ?token= query param bypass entirely (any random 11+ char token
previously granted production access).
- 16 KB message size cap defense-in-depth in the message handler.
(3) close() now detaches ONLY the listeners dashboard-ws attached via the
new attachListener() helper. The previous code called
resourceMonitor.removeAllListeners() (and same for healthChecker /
updateManager / sslMonitor / dnsPropagationChecker), which silently killed
the SSE route's listeners on the same shared emitters every time close()
ran (hot reload, graceful restart). The new test proves the SSE listener
survives dashboard-ws.close() and the resourceMonitor still emits to it.
Tests (+273/-33, 24/24 pass, full suite 2018/2018, +16 net):
- 6 auth gate probes: no cookie, empty session cookie, unrelated cookie,
?token= bypass rejected, token+empty-cookie combo rejected, valid
cookie grants 101
- 2 listener-isolation: close() detaches only OUR listeners; close() is
idempotent
- 8 parseCookieHeader unit tests (undefined, empty, single, multi,
whitespace, HMAC-shaped value preservation, malformed pair, empty name)
- Existing DC-076 tests updated to send Cookie header
Refs: codex-as-judge SKILL.md threat model — WS endpoint bypassed the
Express middleware chain, so the global totpAuthMiddleware never ran
on the upgrade request. Auth must be re-asserted at the upgrade handler.
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.
Introduces a unified security event store and HTTP API that ingests events
from any of the configured sources (API audit, Caddy access log, fail2ban,
shared_bans, future remote agents) and surfaces them in the dashboard.
New files:
src/security/event-store.js JSONL-backed store + in-memory query index
src/security/host-registry.js Registered hosts with per-host API keys
src/security/event-workers.js Tail-followers for Caddy/fail2ban/shared_bans logs
routes/security.js Events, hosts, ingest, SSE stream endpoints
status/js/security-center.js Dashboard modal with Overview/Events/Hosts tabs
SECURITY-FEATURE.md Full feature documentation
DEAD-CODE.md, DUP-CODE.md, HARDENING.md Prior audits
Modified:
src/app.js Mount /api/v1/security/*
src/utilities/middleware.js Add ingest endpoints to PUBLIC_ROUTES
src/security/audit-logger.js Mirror audit events into security store
server.js Start security workers on boot
status/build.js Bundle security-center.js
status/index.html Add Security button to nav
The DC-020 require-path sweep fixed every '../src/...' -> './src/...' in
server.js, but missed one: line 73 still had .
From the production entry point (/app/server.js) this resolves to
/app/state-manager.js — a file that does NOT exist (the module lives at
src/managers/state-manager.js). Unlike the optional modules below it,
this require is bare (not wrapped in try/catch), so a MODULE_NOT_FOUND
here throws out of the top-level startup IIFE and crash-loops the
container — the exact same failure mode as the deleted license-keygen.js.
Fix: ./state-manager -> ./src/managers/state-manager (matches line 146).
Also hardens the DC-020 regression guard (app-startup-smoke.test.js):
adds a static check that EVERY relative require() in server.js resolves
to a real file on disk. server.js cannot be require()'d at test time
(its IIFE binds port 3001 + starts interval modules, leaking workers),
so the static scan is what catches this class of entry-point path bug.
This test would have failed on the original ./state-manager line.
1067/1067 tests pass (was 1066 baseline + 1 new). Zero new ESLint warnings.
v1.14.4 (commit d2a48b1) shipped with broken relative paths and missing
license-keygen.js module. This commit:
- server.js: 26 '../src/...' requires rewritten to './src/...' (server is
at API root, must use ./src for files in src/)
- src/managers/license-manager.js: './license-keygen' rewritten to
'../../license-keygen' (license-keygen.js lives at API root)
- src/docker/self-updater.js: './platform-paths' rewritten to
'../../platform-paths' (platform-paths.js lives at API root)
- license-keygen.js: restored to root (was missing from v1.14.4 tarball)
- VERSION: bumped to d2a48b1-patched (matches upstream commit but with
our fixes baked in)
Makes the v1.14.4 source buildable and runnable without external patches.
Companion to scripts/dashcaddy-post-deploy-patches.sh which applies these
fixes automatically during the host-side update flow.
Three cascading bugs in server.js's workflow engine init block:
1. fetchT was referenced but never imported from ./src/utils/http
2. notification-manager was called as factory function but the module
now exports a class (NotificationManager) - need 'new'
3. servicesStateManager was referenced in workflowCtx but only created
later inside an async IIFE (out of scope at workflow init time)
Result: every container start logged
Workflow engine failed to initialize - fetchT is not defined
and the workflow engine never actually wired to resourceMonitor/
updateManager event sources. The 'app' context workflow engine
still ran but didn't get those connections.
Fix:
- Import fetchT at top of file
- Use 'new' for NotificationManager instantiation
- Hoist servicesStateManager creation before workflow init and
remove the duplicate inside the health-checker async IIFE
Verified: container restart shows
[server] Workflow engine initialized
[ResourceMonitor] Workflow engine configured
[UpdateManager] Workflow engine configured
in the log, no more errors at startup.
Also bumps VERSION to current SHA (bump from c64bbe2).
After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
Cross-platform hardening — removes all hardcoded /app/ paths from route files
and routes them through platform-paths.js so the app works the same way
regardless of Docker layout (single-file mount vs consolidated data dir).
Changes:
- platform-paths.js: add generatedCertsDir, pkiDir, containerUpdatesDir,
containerFrontendDir, containerAssetsDir, resolveAssetsPath()
- self-updater.js: UPDATE_URL/MIRROR_URL/CHANNEL env var overrides
- routes/ca.js: use platformPaths for cert paths and generated certs dir
- routes/services.js: use platformPaths.pkiRootCert
- routes/themes.js: derive THEMES_DIR from platformPaths.servicesFile
- routes/config/assets.js + backup.js: use resolveAssetsPath() fallback
- routes/services.js + src/app.js: use platformPaths.pkiRootCert
- server.js: HOST env var support, parse PORT as int
- src/app.js: GET /api/v1/version (public, no auth), global request timeout,
disable x-powered-by, trust proxy
- pylon/dashcaddy-pylon.js: PYLON_HOST env var, graceful shutdown on SIGTERM/SIGINT
A fresh user can now deploy with a custom Docker layout (e.g. /opt/dc/data/
as a single volume mount) and the app finds its files automatically, no env
var configuration required.
1. WebSocket exec auth bypass (exec.js): Require valid JWT or API key
before accepting WebSocket upgrade. Reject unauthenticated requests
with 401 before the upgrade completes.
2. Shell injection in router auto-login (session-handlers.js): Validate
baseUrl against safe hostname pattern before embedding in wget shell
command. Reject with null session if invalid.
3. Path traversal in credentials routes (services.js): Add explicit
serviceId validation (alphanumeric + dash/underscore/dot, max 100
chars) to all three credential endpoints. Removed redundant
try/catch wrapper.
4. execSync injection in CA CSR generation (ca.js): Add sanitize step
replacing any non-alphanumeric domain chars with underscore before
interpolation into shell subj argument. Redundant with existing
validation but provides defense-in-depth.
5. Auth bypass when TOTP disabled (middleware.js): Split the logic
cleanly — disabled TOTP means no auth (initial setup state), enabled
TOTP means all auth methods checked (session/JWT/API key). Removed
the sessionDuration:never conflating shortcut.
- Container exec/shell via WebSocket + xterm.js (subtle >_ button on cards)
- Live dashboard updates via SSE (resource alerts, health changes, update notices)
- Docker Compose import with YAML parsing, preview, and dependency-ordered deploy
- Volume & network management modal with disk usage overview
- CPU/memory resource limits on deploy and live update
- Email SMTP notifications (nodemailer) alongside Discord/Telegram/ntfy
- Scheduled auto-update scheduler with maintenance windows (daily/weekly/monthly)
New deps: ws, js-yaml, nodemailer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Consolidated all error classes into single errors.js
- Removed duplicate error definitions (NotFoundError, etc.)
- Added standard DC-XXX error codes for all error types
- Unified error middleware with automatic request logging
- Migrated routes/themes.js to throw-based error pattern
- Updated routes/services.js to use ConflictError
- Cleaner server.js error handler registration
- 40% less error handling boilerplate in routes
- Consistent error response format across all endpoints
- Replaced god object ctx with explicit dependency injection
- Added JSDoc documenting required dependencies (8 deps vs 50+)
- Updated response calls to use response-helpers (success/error)
- Self-documenting: you can see exactly what this route needs
- Health checks, pylon relay, CA cert validation all preserved
- Replaced god object ctx with explicit dependency injection
- Added JSDoc documenting all required dependencies
- Updated response calls to use response-helpers (success/error)
- Maintained all existing functionality
- Self-documenting: you can see exactly what this route needs
- Easier testing: mock only what's actually used (14 deps vs 50+ ctx properties)
DashCaddy Pylon is a lightweight probe agent that runs on remote
networks to relay health checks for services the main DashCaddy
instance can't reach directly (e.g., .sami domains, LAN IPs).
- Standalone zero-dependency Node.js script (pylon/dashcaddy-pylon.js)
- Optional API key auth, HEAD→GET fallback, batch probe support
- Health routes now try direct check first, fall back to pylon relay
- New endpoints: /health/probe (act as pylon), /health/pylon (status)
- Config: add "pylon": { "url": "...", "key": "..." } to config.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add url-resolver.js with single resolveServiceUrl() used by all 5 consumers
(probes, health routes, health checker auto-config)
- Health checker now does full sync (add/update/remove) instead of add-only,
and re-syncs automatically after every services.json mutation
- docker-maintenance and log-digest are now optional imports with try/catch,
preventing container crashes when these files are absent
- Add null guards in routes/logs.js for graceful 503 responses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mount Tailscale router at /tailscale prefix so all 10 routes resolve
to /api/tailscale/* as expected by middleware, audit logger, and
frontend. Previously 5 routes (status, config, check-connection,
devices, protect-service) resolved to /api/* instead, with config
colliding with the settings route. Strip redundant /tailscale/ prefix
from OAuth routes that were compensating for the missing mount prefix.
Increase default health check timeout from 10s to 20s to reduce false
positives on slower services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents Docker disk bloat by adding log rotation (10MB max, 3 files)
to all container creation and update paths, auto-pruning dangling
images after deploy/remove/update, and a daily maintenance module
that cleans build cache and warns on disk thresholds.
Saves a deployment manifest in services.json at deploy time so users
can restore all their apps after a Docker purge. Adds restore-all
and restore-single endpoints that recreate containers, Caddy config,
and DNS records from the saved manifests.
Adds an hourly log collector and daily digest generator that
summarizes errors, warnings, and events across all services into
a single human-readable report with guidance on where to investigate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- self-updater.js: polls for new versions, downloads/verifies tarballs,
triggers host-side rebuild via systemd path unit
- dashcaddy-update.sh + systemd units: host-side container rebuild with
automatic rollback on health check failure
- 7 new /api/v1/system/* endpoints for version info, update check/apply,
rollback, and update history
- Frontend: DashCaddy tab in Updates modal with version display,
changelog, update button, rollback, and notification dot
- install.sh: updater service installation, volume mounts, env vars
- build-release.sh + webhook-handler.js: release server pipeline
(Gitea webhook → build tarball → deploy to get.dashcaddy.net)
- Dockerfile: DASHCADDY_COMMIT build arg → VERSION file
- Version bump to 1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Deploy endpoint: validate appId, config, and subdomain before use (prevents 500 crash on empty body)
- Container ops: return 404 instead of 500 for non-existent containers
- Update-subdomain: require oldSubdomain/newSubdomain fields (prevents false 200 with undefined values)
- Global error handler: catch-all that never leaks stack traces or internal paths
- API 404 catch-all: return JSON instead of HTML for unmatched /api/* routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apps can now be served at domain.com/appname/ instead of requiring
subdomain DNS records (appname.domain.com). Supports three subpath
modes per template: native (URL base env var), strip (handle_path),
and none (incompatible warning). Tested on Linux with deploy/removal
lifecycle verified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.