Files
joulenap/SECURITY.md
T
Catubba 5ed03db20c feat(demo): route-aware demo replay and 1.0 documentation
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.
2026-08-04 14:09:19 +02:00

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 a restricted authorized_keys line 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_tls is 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), and app.session controls its lifetime (max_age_days, default 14) and the Secure flag (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.bak beside your config as a rollback parachute; it contains every token, password hash and secret from the old configuration. Joulenap sets it to 0600, 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.