Rewrite demoTimeline.ts around the three-route scenario and rebuild the scripted replay inside devStub.ts on top of the 1.0 endpoints, then bring the shipped documentation in line with what actually ships. Demo: - demoTimeline.ts keys its online windows by PBS id, so one field covers a single-box backup and a sync route's two; steps carry the per-device names the backend really emits, plus their detail. - The demo auto-plays: it opens mid-backup on Nightly and the queued Lab route starts by itself when that lands, with the target left awake between them and the skipped power-off recording why. The queue and the power lease are visible without a click. - The clock ticks and the fixture calendar shifts by whole weeks, so weekdays and times survive and the schedules stay self-consistent. Dev stub mode keeps its frozen clock; every replay mutation sits behind the demo flag. - Restore the "fake data" banner and make logout reload rather than strand the visitor on a login form. - build:demo now type-checks first, which it never did. Fix the expanded run history row refetching its detail only once, so a run in flight showed a frozen step timeline while its task log kept streaming. Docs: - ARCHITECTURE: the route model, the queue and lease, a cycle per kind, the migration, and REST tables rebuilt from the shipped routers. - CONFIG-WIZARD: the two device flows, and the /remote grant a sync route needs on a peer configured before 1.0. - INTEGRATIONS: the new dashboard payload, snippets matching the ones the app generates, the labelled metric names, and a 0.9 mapping table. - README, INSTALL: routes, the five settings tabs, upgrading from 0.9, and the Node version CI and the image actually build with. - SECURITY: transport pinning, auth hardening, the two API-key endpoints outside the session, and what Joulenap deliberately does not do. - CONTRIBUTING: npm test is a separate CI step, and the demo section now describes the demo that exists. - CHANGELOG: the 1.0.0 entry, including the breaking dashboard and metrics shapes and the exclude guest mode widening to all.
6.5 KiB
Security Policy
Joulenap can trigger backups and power machines on and off, and it holds Proxmox API tokens and an SSH key. We take security reports seriously and appreciate responsible disclosure.
Supported versions
Only the latest release line receives security fixes.
| Version | Supported |
|---|---|
| 1.0.x | ✅ |
| < 1.0 | ❌ |
Reporting a vulnerability
Please do not open a public issue for security problems.
Report privately through GitHub's Report a vulnerability button (Security tab → Report a vulnerability). This opens a private advisory visible only to the maintainers.
Please include:
- affected version and how Joulenap is deployed (Docker / LXC / source),
- a description of the issue and its impact,
- steps to reproduce or a proof of concept,
- any relevant logs or config — with secrets redacted (tokens, SSH keys, bot tokens).
You can expect an acknowledgement within a few days. We'll work with you on a fix and coordinate disclosure; we're happy to credit you unless you'd prefer to stay anonymous.
Security model
Joulenap is designed to run on a trusted LAN/VPN, not on the public internet.
Credentials
- Scoped API tokens over root passwords, one per Proxmox host and one per backup server. If root
credentials are supplied during setup, they're used transiently to provision a scoped token and
install the SSH key, then discarded — never written to
config.yaml. - One SSH key, shared by every backup server Joulenap powers off. It's generated by the app at
data/id_ed25519, and the wizard offers arestrictedauthorized_keysline with a forced command so the key can only trigger the power-off. Because every managed server trusts the same key, treat it as protecting all of them: the wizard deliberately reuses an existing key rather than regenerating one, since a new key would silently lock Joulenap out of the servers already configured.
Transport
- Each backup server's API is TLS-pinned to the certificate fingerprint captured for that device at setup, so a swapped or MITM certificate is rejected rather than trusted. A legitimately renewed certificate is accepted once you re-run that device's connect step.
- Each backup server's SSH host key is verified. It's shown to you for confirmation during
setup, stored in
data/known_hosts, and checked on every later connection. The wizard refuses to send a root password over SSH before the host key has been confirmed. - A Proxmox host's
verify_tlsis off by default, because a stock PVE serves a self-signed certificate. Where a PVE has a valid certificate, turn it on — it is the only protection for a root password used during provisioning, which is otherwise exposed to anyone who can intercept that connection.
Access to the interface
- The UI is login-protected with a single admin account. The session cookie is signed with a
per-install random key (
app.secret_key), andapp.sessioncontrols its lifetime (max_age_days, default 14) and theSecureflag (https_only— turn it on when serving over HTTPS or behind a TLS-terminating proxy). - Failed logins are rate-limited: 5 failures from one IP address locks that address out for 5 minutes. Admin passwords must be at least 8 characters.
- Changing the password revokes every other session immediately — sessions carry a version derived from the password hash, so only the session that made the change stays signed in. Changing the username or password also requires re-entering the current password, so a hijacked session alone can't silently take the account over.
- The first-run setup endpoint stays open until an account exists (and is rate-limited). Complete the initial registration promptly.
The two endpoints that aren't behind the login
GET /api/dashboard (dashboard widgets) and GET /metrics (Prometheus) are polled by machines that
can't hold a login session, so they authenticate with a shared read-only API key instead — sent
as an X-API-Key header or a ?key= query parameter. Worth knowing:
- Both are disabled until you generate a key under Settings → Integrations, and deleting the key
disables both again. A request with no key configured is refused with
403. - The key is read-only: these endpoints expose backup status, schedules and datastore usage. They cannot start, stop or change anything.
- A
?key=in a URL can end up in proxy and browser logs. Prefer the header where your client supports it.
Data at rest, and outbound traffic
- Secrets live in
config.yaml(git-ignored), and are redacted in every API response — including the Advanced tab's export, which is therefore a readable copy and not a restorable backup. - Upgrading from 0.9 leaves a second file holding the same secrets. The migration writes
config.yaml.pre-overhaul.bakbeside your config as a rollback parachute; it contains every token, password hash and secret from the old configuration. Joulenap sets it to0600, but it is a real second copy — delete it once you're satisfied with the upgrade. - Joulenap makes no outbound internet connection unless you ask it to. The update check is off by default; notifications only reach the services you configure.
What Joulenap does not do
So you know what to expect, and what isn't worth reporting as a vulnerability:
- One admin account, no roles. There is no multi-user support, no RBAC and no per-user permissions. Everyone with the password can do everything.
- No audit trail of who did what. The run history records what happened, not which person triggered it — with a single account there is nobody to distinguish.
- Not hardened for the public internet. There's no built-in TLS, no MFA and no brute-force
protection beyond the per-IP login lockout. Put it on your LAN or behind a VPN. If you must expose
it, terminate TLS in front of it, set
session.https_only: true, and restrict who can reach it. - It is as privileged as you make it. Joulenap can start backups and power machines off; a compromise of the host it runs on is a compromise of those capabilities. Scope its tokens as tightly as your setup allows.
See the Security section of the README for operator guidance, and
docs/CONFIG-WIZARD.md for how pinning and host-key verification
are established during setup. If you find a gap between this model and the code, that's exactly the
kind of report we want.